# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ RBAC models: AccessRule, AccessRuleContext, Role. Multi-Tenant Design: - Role hat einen unveränderlichen Kontext (mandateId, featureInstanceId, featureCode) - AccessRule referenziert Role via roleId (FK), nicht via roleLabel - Kontext-Felder sind IMMUTABLE nach Erstellung """ import uuid from typing import Optional from enum import Enum from pydantic import BaseModel, Field from modules.datamodels.datamodelBase import PowerOnModel from modules.shared.i18nRegistry import i18nModel from modules.datamodels.datamodelUtils import TextMultilingual from modules.datamodels.datamodelUam import AccessLevel class AccessRuleContext(str, Enum): """Context type enumeration""" DATA = "DATA" # Database tables and fields UI = "UI" # UI elements and features RESOURCE = "RESOURCE" # System resources (AI models, actions, etc.) @i18nModel("Rolle") class Role(PowerOnModel): """ Data model for RBAC roles. Kernkonzept: Eine Rolle existiert immer in einem spezifischen Kontext. Der Kontext ist IMMUTABLE nach Erstellung. Kontext-Typen: - mandateId=None, featureInstanceId=None → GLOBAL (Template-Rolle) - mandateId=X, featureInstanceId=None → MANDATE-Rolle - mandateId=X, featureInstanceId=Y → INSTANCE-Rolle """ id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the role", json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False} ) roleLabel: str = Field( description="Unique role label identifier (e.g., 'admin', 'user', 'viewer')", json_schema_extra={"label": "Rollen-Label", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True} ) description: TextMultilingual = Field( description="Role description in multiple languages", json_schema_extra={"label": "Beschreibung", "frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True} ) # KONTEXT - IMMUTABLE nach Create (nur Create/Delete, kein Update!) mandateId: Optional[str] = Field( default=None, description="FK → Mandate.id (CASCADE DELETE). Null = Global/Template role.", json_schema_extra={ "label": "Mandant", "frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "fk_target": {"db": "poweron_app", "table": "Mandate", "labelField": "label"}, }, ) featureInstanceId: Optional[str] = Field( default=None, description="FK → FeatureInstance.id (CASCADE DELETE). Null = Mandate-level or Global role.", json_schema_extra={ "label": "Feature-Instanz", "frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "fk_target": {"db": "poweron_app", "table": "FeatureInstance", "labelField": "label"}, }, ) featureCode: Optional[str] = Field( default=None, description="Feature code (z.B. 'trustee') - für Template-Rollen", json_schema_extra={"label": "Feature-Code", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False} ) isSystemRole: bool = Field( default=False, description="Whether this is a system role that cannot be deleted", json_schema_extra={"label": "System-Rolle", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False} ) @i18nModel("Zugriffsregel") class AccessRule(PowerOnModel): """ Data model for access control rules. WICHTIG: roleId referenziert die Role via FK (nicht mehr roleLabel!) Die Felder 'context' und 'roleId' sind IMMUTABLE nach Erstellung. """ id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the access rule", json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False} ) roleId: str = Field( description="FK → Role.id (CASCADE DELETE!)", json_schema_extra={ "label": "Rolle", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "fk_target": {"db": "poweron_app", "table": "Role", "labelField": "roleLabel"}, }, ) context: AccessRuleContext = Field( description="Context type: DATA (database), UI (interface), RESOURCE (system resources). IMMUTABLE!", json_schema_extra={"label": "Kontext", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_options": [ {"value": "DATA", "label": "Daten"}, {"value": "UI", "label": "Oberfläche"}, {"value": "RESOURCE", "label": "Ressource"} ]} ) item: Optional[str] = Field( default=None, description="Item identifier (null = all items in context). Format: DATA: '' or '
.', UI: cascading string (e.g., 'playground.voice.settings'), RESOURCE: cascading string (e.g., 'ai.model.anthropic')", json_schema_extra={"label": "Element", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False} ) view: bool = Field( default=False, description="View permission: if true, item is visible/enabled. Only objects with view=true are shown.", json_schema_extra={"label": "Anzeigen", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": True} ) read: Optional[AccessLevel] = Field( default=None, description="Read permission level (only for DATA context)", json_schema_extra={"label": "Lesen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [ {"value": "a", "label": "Alle Datensätze"}, {"value": "m", "label": "Meine Datensätze"}, {"value": "g", "label": "Gruppen-Datensätze"}, {"value": "n", "label": "Kein Zugriff"} ]} ) create: Optional[AccessLevel] = Field( default=None, description="Create permission level (only for DATA context)", json_schema_extra={"label": "Erstellen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [ {"value": "a", "label": "Alle Datensätze"}, {"value": "m", "label": "Meine Datensätze"}, {"value": "g", "label": "Gruppen-Datensätze"}, {"value": "n", "label": "Kein Zugriff"} ]} ) update: Optional[AccessLevel] = Field( default=None, description="Update permission level (only for DATA context)", json_schema_extra={"label": "Aktualisieren", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [ {"value": "a", "label": "Alle Datensätze"}, {"value": "m", "label": "Meine Datensätze"}, {"value": "g", "label": "Gruppen-Datensätze"}, {"value": "n", "label": "Kein Zugriff"} ]} ) delete: Optional[AccessLevel] = Field( default=None, description="Delete permission level (only for DATA context)", json_schema_extra={"label": "Loeschen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [ {"value": "a", "label": "Alle Datensätze"}, {"value": "m", "label": "Meine Datensätze"}, {"value": "g", "label": "Gruppen-Datensätze"}, {"value": "n", "label": "Kein Zugriff"} ]} ) # IMMUTABLE Fields Definition - für Enforcement auf Application-Level IMMUTABLE_FIELDS = { "Role": ["mandateId", "featureInstanceId", "featureCode"], "AccessRule": ["context", "roleId"] } def validateUpdateNotImmutable(model: str, updateData: dict): """ Blockiert Updates auf immutable Felder. Wirft ValueError wenn versucht wird, Kontext-Felder zu ändern. Args: model: Model name (z.B. "Role", "AccessRule") updateData: Dictionary mit Update-Daten Raises: ValueError: Wenn immutable Felder im Update enthalten sind """ forbidden = IMMUTABLE_FIELDS.get(model, []) violations = [f for f in forbidden if f in updateData] if violations: raise ValueError( f"Cannot update immutable fields on {model}: {violations}. " f"Delete and recreate instead." )