183 lines
8.4 KiB
Python
183 lines
8.4 KiB
Python
# 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, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "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, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
|
|
)
|
|
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, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "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: '<table>' or '<table>.<field>', 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."
|
|
)
|