23 KiB
Workflow Actions RBAC Integration Concept
Übersicht
Dieses Dokument beschreibt das Konzept für die Umstrukturierung der Workflow Actions, um:
- RBAC-Integration zu ermöglichen (Schutz von Actions über RESOURCE-Context)
- Strukturierte Parameter-Definitionen statt Docstrings zu verwenden
- UI-Rendering-Typen für Parameter zu definieren
- Keine Duplikation von Parameter-Definitionen zu haben
- Plug-and-Play Funktionalität beizubehalten
Architektur-Konzept
Grundprinzip: Deklarative Action-Definition
Ähnlich wie bei aicore Models, wo eine Struktur definiert wird und die Funktion ein Attribut ist, werden Actions jetzt deklarativ definiert:
class MethodOutlook(MethodBase):
def __init__(self, services):
super().__init__(services)
self.name = "outlook"
self.description = "Handle Microsoft Outlook email operations"
# Actions werden deklarativ definiert
self._actions = {
"readEmails": ActionDefinition(
actionId="outlook.readEmails", # Für RBAC: RESOURCE context
description="Read emails from Outlook mailbox",
parameters={
"connectionReference": ActionParameter(
name="connectionReference",
type=str,
frontendType="select",
frontendOptions="user.connection",
required=True,
description="Microsoft connection label"
),
"query": ActionParameter(
name="query",
type=str,
frontendType="text",
required=False,
description="Search query for emails"
),
"folder": ActionParameter(
name="folder",
type=str,
frontendType="select",
frontendOptions="outlook.folder",
required=False,
description="Folder name (e.g., 'Inbox', 'Drafts')"
),
"limit": ActionParameter(
name="limit",
type=int,
frontendType="number",
required=False,
default=50,
description="Maximum number of emails to return"
)
},
execute=self._executeReadEmails # Funktion als Attribut
),
"sendEmail": ActionDefinition(
actionId="outlook.sendEmail",
description="Send email via Outlook",
parameters={...},
execute=self._executeSendEmail
)
}
async def _executeReadEmails(self, parameters: Dict[str, Any]) -> ActionResult:
"""Execute function - keine Parameter-Definition mehr hier"""
# Implementation...
Globale Frontend-Type-Definition
WICHTIG: Frontend-Types werden zentral in modules/shared/frontendTypes.py definiert, nicht redundant pro Action.
Die globale FrontendType Enum enthält:
- Standard Types:
text,textarea,number,select,multiselect,checkbox,date,datetime,email,timestamp,json,multilingual,file - Custom Types für Actions:
userConnection,documentReference,workflowAction
Custom-Types unterstützen dynamische Option-Listen über API-Endpoints:
userConnection→/api/options/user.connection(Connections des aktuellen Users)documentReference→/api/options/workflow.documentReference(Document-Referenzen aus Workflow-Context)workflowAction→/api/options/workflow.action(Verfügbare Actions aus Workflow-Context)
Datenmodelle
ActionParameter
WICHTIG: Frontend-Types werden global definiert in modules/shared/frontendTypes.py und nicht redundant in Actions.
from typing import Optional, Any, Union, List, Dict
from pydantic import BaseModel, Field
from modules.shared.frontendTypes import FrontendType # Globale Definition
class ActionParameter(BaseModel):
"""Parameter definition for an action"""
name: str = Field(description="Parameter name")
type: str = Field(description="Python type as string (e.g., 'str', 'int', 'bool', 'List[str]')")
frontendType: FrontendType = Field(description="UI rendering type (from global FrontendType enum)")
frontendOptions: Optional[Union[str, List[Dict[str, Any]]]] = Field(
None,
description="Options for select/multiselect/custom types. String reference (e.g., 'user.connection') or static list. For custom types like userConnection, this is automatically set to the API endpoint."
)
required: bool = Field(False, description="Whether parameter is required")
default: Optional[Any] = Field(None, description="Default value")
description: str = Field("", description="Parameter description")
validation: Optional[Dict[str, Any]] = Field(
None,
description="Validation rules (e.g., {'min': 1, 'max': 100})"
)
Custom Frontend Types:
FrontendType.USER_CONNECTION: User connection selector - dynamische Options von/api/options/user.connectionFrontendType.DOCUMENT_REFERENCE: Document reference selector - dynamische Options aus Workflow-ContextFrontendType.WORKFLOW_ACTION: Workflow action selector - dynamische Options aus verfügbaren Actions
Für Custom-Types wird frontendOptions automatisch auf den entsprechenden API-Endpoint gesetzt (z.B. "user.connection").
ActionDefinition
from typing import Dict, Callable, Awaitable
from pydantic import BaseModel, Field
class ActionDefinition(BaseModel):
"""Complete definition of an action"""
actionId: str = Field(
description="Unique action identifier for RBAC (format: 'module.actionName', e.g., 'outlook.readEmails')"
)
description: str = Field(description="Action description")
parameters: Dict[str, ActionParameter] = Field(
default_factory=dict,
description="Parameter definitions"
)
execute: Callable[[Dict[str, Any]], Awaitable[ActionResult]] = Field(
description="Execution function - async function that takes parameters dict and returns ActionResult"
)
# Optional metadata
category: Optional[str] = Field(None, description="Action category for grouping")
tags: List[str] = Field(default_factory=list, description="Tags for search/filtering")
MethodBase Erweiterung
Neue MethodBase Struktur
class MethodBase:
"""Base class for all methods"""
def __init__(self, services: Any):
self.services = services
self.name: str
self.description: str
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
# Actions werden als Dictionary definiert
self._actions: Dict[str, ActionDefinition] = {}
# Nach Initialisierung: Actions registrieren
self._registerActions()
def _registerActions(self):
"""Register all actions defined in _actions"""
# Kann überschrieben werden für dynamische Registrierung
pass
@property
def actions(self) -> Dict[str, Dict[str, Any]]:
"""
Dynamically collect all actions from _actions dictionary.
Returns format compatible with existing system.
"""
result = {}
for actionName, actionDef in self._actions.items():
# RBAC-Check: Prüfe ob Action für aktuellen User verfügbar ist
if not self._checkActionPermission(actionDef.actionId):
continue # Skip if user doesn't have permission
# Konvertiere ActionDefinition zu altem Format für Kompatibilität
result[actionName] = {
'description': actionDef.description,
'parameters': self._convertParametersToOldFormat(actionDef.parameters),
'method': self._createActionWrapper(actionDef)
}
return result
def _checkActionPermission(self, actionId: str) -> bool:
"""
Check if current user has permission to execute this action.
Uses RBAC RESOURCE context.
"""
if not hasattr(self.services, 'rbac') or not self.services.rbac:
# Fallback: Allow if RBAC not available (backward compatibility)
return True
currentUser = self.services.chat.getCurrentUser()
if not currentUser:
return False
# RBAC-Check: RESOURCE context, item = actionId
permissions = self.services.rbac.getUserPermissions(
user=currentUser,
context=AccessRuleContext.RESOURCE,
item=actionId
)
return permissions.view
def _convertParametersToOldFormat(self, parameters: Dict[str, ActionParameter]) -> Dict[str, Dict[str, Any]]:
"""Convert ActionParameter dict to old format for compatibility"""
result = {}
for paramName, param in parameters.items():
result[paramName] = {
'type': param.type,
'required': param.required,
'description': param.description,
'default': param.default,
'frontendType': param.frontendType.value,
'frontendOptions': param.frontendOptions
}
return result
def _createActionWrapper(self, actionDef: ActionDefinition):
"""Create wrapper function that matches old action signature"""
async def wrapper(parameters: Dict[str, Any], *args, **kwargs):
# Parameter-Validierung basierend auf ActionParameter definitions
validatedParams = self._validateParameters(parameters, actionDef.parameters)
# Execute action
return await actionDef.execute(validatedParams, *args, **kwargs)
wrapper.is_action = True
return wrapper
def _validateParameters(self, parameters: Dict[str, Any], paramDefs: Dict[str, ActionParameter]) -> Dict[str, Any]:
"""Validate parameters against definitions"""
validated = {}
for paramName, paramDef in paramDefs.items():
value = parameters.get(paramName)
# Check required
if paramDef.required and value is None:
raise ValueError(f"Required parameter '{paramName}' is missing")
# Use default if not provided
if value is None and paramDef.default is not None:
value = paramDef.default
# Type validation
if value is not None:
value = self._validateType(value, paramDef.type)
# Custom validation rules
if paramDef.validation and value is not None:
self._applyValidationRules(value, paramDef.validation)
validated[paramName] = value
return validated
def _validateType(self, value: Any, expectedType: type) -> Any:
"""Validate and convert value to expected type"""
# Type validation logic...
if expectedType == int:
return int(value)
elif expectedType == str:
return str(value)
# ... weitere Typen
return value
def _applyValidationRules(self, value: Any, rules: Dict[str, Any]):
"""Apply custom validation rules"""
if 'min' in rules and value < rules['min']:
raise ValueError(f"Value must be >= {rules['min']}")
if 'max' in rules and value > rules['max']:
raise ValueError(f"Value must be <= {rules['max']}")
# ... weitere Validierungsregeln
Migrationsstrategie
Schritt 1: Neue Datenmodelle erstellen
Datei: gateway/modules/datamodels/datamodelWorkflowActions.py
from typing import Optional, Any, Union, List, Dict, Callable, Awaitable
from enum import Enum
from pydantic import BaseModel, Field
from modules.datamodels.datamodelChat import ActionResult
from modules.shared.frontendTypes import FrontendType # Globale Definition verwenden
class ActionParameter(BaseModel):
"""Parameter definition for an action"""
name: str
type: str # String representation of type: "str", "int", "bool", "List[str]", etc.
frontendType: FrontendType
frontendOptions: Optional[Union[str, List[Dict[str, Any]]]] = None
required: bool = False
default: Optional[Any] = None
description: str = ""
validation: Optional[Dict[str, Any]] = None
class ActionDefinition(BaseModel):
"""Complete definition of an action"""
actionId: str # Format: "module.actionName" (e.g., "outlook.readEmails")
description: str
parameters: Dict[str, ActionParameter] = Field(default_factory=dict)
execute: Optional[Callable] = None # Will be set dynamically
category: Optional[str] = None
tags: List[str] = Field(default_factory=list)
Schritt 2: MethodBase erweitern
Datei: gateway/modules/workflows/methods/methodBase.py
- Neue
_actionsDictionary Property - RBAC-Check Integration
- Parameter-Validierung
- Kompatibilität mit bestehendem System
Schritt 3: Beispiel-Migration
Vorher (methodOutlook.py):
@action
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
"""
GENERAL:
- Purpose: Read emails from Outlook mailbox
Parameters:
- connectionReference (str, required): Microsoft connection label.
- query (str, optional): Search query for emails.
- folder (str, optional): Folder name.
- limit (int, optional): Maximum number of emails. Default: 50.
"""
# Implementation...
Nachher (methodOutlook.py):
def __init__(self, services):
super().__init__(services)
self.name = "outlook"
self.description = "Handle Microsoft Outlook email operations"
self._actions = {
"readEmails": ActionDefinition(
actionId="outlook.readEmails",
description="Read emails from Outlook mailbox",
parameters={
"connectionReference": ActionParameter(
name="connectionReference",
type="str",
frontendType=FrontendType.USER_CONNECTION, # Custom type - automatisch API-Endpoint
required=True,
description="Microsoft connection label"
),
"query": ActionParameter(
name="query",
type="str",
frontendType=FrontendType.TEXT,
required=False,
description="Search query for emails"
),
"folder": ActionParameter(
name="folder",
type="str",
frontendType=FrontendType.SELECT,
frontendOptions="outlook.folder",
required=False,
description="Folder name (e.g., 'Inbox', 'Drafts')"
),
"limit": ActionParameter(
name="limit",
type="int",
frontendType=FrontendType.NUMBER,
required=False,
default=50,
description="Maximum number of emails to return",
validation={"min": 1, "max": 1000}
)
},
execute=self._executeReadEmails
)
}
async def _executeReadEmails(self, parameters: Dict[str, Any]) -> ActionResult:
"""Execute function - keine Parameter-Definition mehr hier"""
# Implementation bleibt gleich...
RBAC-Integration
Action-IDs Format
Actions werden im RBAC-System als RESOURCE-Context Items behandelt:
- Format:
{moduleName}.{actionName} - Beispiele:
outlook.readEmailsoutlook.sendEmailsharepoint.uploadDocumentai.process
RBAC-Regeln für Actions
{
"roleLabel": "user",
"context": "RESOURCE",
"item": "outlook.readEmails",
"view": true
}
{
"roleLabel": "admin",
"context": "RESOURCE",
"item": "outlook",
"view": true
}
Hierarchie: Spezifische Action-Regeln überschreiben generische Module-Regeln.
Bootstrap: Default RBAC Rules für Actions
In interfaceBootstrap.py:
def initRbacRules(db: DatabaseConnector) -> None:
# ... existing rules ...
# Action Rules (RESOURCE context)
createActionRules(db)
def createActionRules(db: DatabaseConnector):
"""Create default RBAC rules for workflow actions"""
# SysAdmin: Access to all actions
db.recordCreate(AccessRule(
roleLabel="sysadmin",
context=AccessRuleContext.RESOURCE,
item=None, # All resources
view=True
))
# Admin: Access to all actions
db.recordCreate(AccessRule(
roleLabel="admin",
context=AccessRuleContext.RESOURCE,
item=None,
view=True
))
# User: Access to specific actions only
userActions = [
"outlook.readEmails",
"outlook.sendEmail",
"sharepoint.readDocuments",
"ai.process"
]
for actionId in userActions:
db.recordCreate(AccessRule(
roleLabel="user",
context=AccessRuleContext.RESOURCE,
item=actionId,
view=True
))
# Viewer: Read-only actions
viewerActions = [
"outlook.readEmails",
"sharepoint.readDocuments"
]
for actionId in viewerActions:
db.recordCreate(AccessRule(
roleLabel="viewer",
context=AccessRuleContext.RESOURCE,
item=actionId,
view=True
))
Vorteile
1. Keine Duplikation
- Parameter werden nur einmal definiert (in
ActionDefinition) - Keine Docstring-Parsing mehr nötig
- Type-Safety durch Pydantic Models
2. RBAC-Integration
- Jede Action hat eine eindeutige ID für RBAC
- Granulare Kontrolle pro Action möglich
- Hierarchische Regeln (Module → Action)
3. UI-Rendering
- Frontend-Typen explizit definiert
- Options-Referenzen für dynamische Optionen
- Validierung auf Backend-Ebene
4. Plug-and-Play
- Actions bleiben als separate Method-Klassen
- Einfache Erweiterung durch neue Method-Klassen
- Kompatibilität mit bestehendem System
5. Type Safety
- Pydantic Models für Validierung
- Type-Hints für bessere IDE-Unterstützung
- Runtime-Validierung
Migration Timeline
Phase 1: Foundation (Woche 1)
- ✅ Datenmodelle erstellen (
datamodelWorkflowActions.py) - ✅ MethodBase erweitern
- ✅ RBAC-Integration in MethodBase
Phase 2: Beispiel-Migration (Woche 2)
- 📝 Ein Method-Beispiel migrieren (z.B.
methodAi.py) - 📝 Tests schreiben
- 📝 Dokumentation aktualisieren
Phase 3: Vollständige Migration (Woche 3-4)
- 📝 Alle Methods migrieren
- 📝 RBAC-Regeln in Bootstrap erstellen
- 📝 Frontend-Integration
Phase 4: Testing & Cleanup (Woche 5)
- 📝 Unit Tests
- 📝 Integration Tests
- 📝 Performance Tests
- 📝 Alte Docstring-Parsing-Logik entfernen
Offene Fragen
-
Backward Compatibility: Sollen alte Actions ohne
_actionsDictionary weiterhin funktionieren?- Antwort: Ja, MethodBase prüft zuerst
_actions, dann fallback auf@actionDecorator
- Antwort: Ja, MethodBase prüft zuerst
-
Parameter-Validierung: Soll Validierung strikt sein oder tolerant?
- Antwort: Konfigurierbar pro Action
-
Action-Discovery: Sollen Actions zur Laufzeit registriert werden können?
- Antwort: Ja, über
_registerActions()Methode
- Antwort: Ja, über
-
Frontend-Integration: Wie werden Actions im Frontend angezeigt?
- Antwort: API-Endpoint
/api/workflows/actionsliefert strukturierte Action-Definitionen
- Antwort: API-Endpoint
API-Endpunkte
GET /api/workflows/actions
Liefert alle verfügbaren Actions für den aktuellen User (gefiltert nach RBAC):
{
"actions": [
{
"module": "outlook",
"actionId": "outlook.readEmails",
"name": "readEmails",
"description": "Read emails from Outlook mailbox",
"parameters": {
"connectionReference": {
"type": "str",
"frontendType": "userConnection",
"frontendOptions": "user.connection", # Automatisch für Custom-Types
"required": true,
"description": "Microsoft connection label"
},
"documentList": {
"type": "List[str]",
"frontendType": "documentReference",
"frontendOptions": "workflow.documentReference", # Automatisch für Custom-Types
"required": false,
"description": "Document list reference(s) from previous actions"
},
...
}
},
...
]
}
GET /api/workflows/actions/{module}
Liefert Actions für ein spezifisches Modul.
POST /api/workflows/actions/{module}/{action}/execute
Führt eine Action aus (mit RBAC-Check).
Custom Frontend Types für Actions
Verfügbare Custom Types
-
FrontendType.USER_CONNECTION- API-Endpoint:
/api/options/user.connection - Beschreibung: Zeigt alle aktiven Connections des aktuellen Users
- Verwendung: Für Parameter wie
connectionReferencein Outlook/SharePoint Actions - Beispiel:
ActionParameter( name="connectionReference", type="str", frontendType=FrontendType.USER_CONNECTION, required=True, description="Microsoft connection label" )
- API-Endpoint:
-
FrontendType.DOCUMENT_REFERENCE- API-Endpoint:
/api/options/workflow.documentReference(zu implementieren) - Beschreibung: Zeigt verfügbare Document-Referenzen aus dem aktuellen Workflow-Context
- Verwendung: Für Parameter wie
documentListin Actions, die auf vorherige Action-Ergebnisse verweisen - Beispiel:
ActionParameter( name="documentList", type="List[str]", frontendType=FrontendType.DOCUMENT_REFERENCE, required=False, description="Document list reference(s) from previous actions" )
- API-Endpoint:
-
FrontendType.WORKFLOW_ACTION- API-Endpoint:
/api/options/workflow.action(zu implementieren) - Beschreibung: Zeigt verfügbare Actions aus dem Workflow-Context
- Verwendung: Für Parameter, die auf andere Actions verweisen
- API-Endpoint:
Custom Types erweitern
Neue Custom-Types können über frontendTypes.py registriert werden:
from modules.shared.frontendTypes import FrontendType, registerCustomType
# Neuer Custom-Type hinzufügen
FrontendType.SHAREPOINT_FOLDER = "sharepointFolder"
# Registrieren
registerCustomType(
frontendType=FrontendType.SHAREPOINT_FOLDER,
optionsApiEndpoint="sharepoint.folder",
description={
"en": "SharePoint Folder",
"fr": "Dossier SharePoint",
"de": "SharePoint-Ordner"
}
)
Frontend-Integration
Das Frontend muss:
- Custom-Types erkennen (z.B.
frontendType === "userConnection") - Automatisch Options von
/api/options/{optionsName}laden - Die Options als Select/Multiselect rendern
Beispiel Frontend-Logik:
if (param.frontendType === 'userConnection') {
// Automatisch Options von /api/options/user.connection laden
const options = await fetch(`/api/options/${param.frontendOptions}`);
// Als Select rendern
}