platform-core/modules/datamodels/datamodelRbac.py
ValueOn AG 4a60086c80
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 15s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped
cp adapted to 2026 poweron
2026-06-09 09:53:31 +02:00

217 lines
9.1 KiB
Python

# Copyright (c) 2026 PowerOn AG
# 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, Dict, List, Protocol, runtime_checkable
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: '<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"}
]}
)
@runtime_checkable
class RbacProtocol(Protocol):
"""Structural type for RBAC checkers — allows aicore (L3) to reference
the RBAC contract without importing from security (L4)."""
def checkResourceAccessBulk(
self,
user: "User",
resourcePaths: List[str],
mandateId: Optional[str] = None,
featureInstanceId: Optional[str] = None,
) -> Dict[str, bool]: ...
# 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."
)