diff --git a/modules/features/automation/interfaceFeatureAutomation.py b/modules/features/automation/interfaceFeatureAutomation.py index 34102f5e..2bbf56e0 100644 --- a/modules/features/automation/interfaceFeatureAutomation.py +++ b/modules/features/automation/interfaceFeatureAutomation.py @@ -42,8 +42,10 @@ class AutomationObjects: # Initialize database with proper configuration self._initializeDatabase() - # Initialize RBAC (dbApp = self.db since we use poweron_app) - self.rbac = RbacClass(self.db, dbApp=self.db) + # Initialize RBAC - AccessRules are in poweron_app, not poweron_automation! + from modules.security.rootAccess import getRootDbAppConnector + dbApp = getRootDbAppConnector() + self.rbac = RbacClass(self.db, dbApp=dbApp) # Update database context self.db.updateContext(self.userId) @@ -100,7 +102,9 @@ class AutomationObjects: record = self.db.getRecordset(model, {"id": recordId}) if record: return record[0].get("_createdBy") == self.userId - return True + else: + return False # Record not found = no access + return True # No recordId needed (e.g., for CREATE) return False # ========================================================================= diff --git a/modules/features/chatbot/streaming/events.py b/modules/features/chatbot/streaming/events.py index 732752ec..7a65205e 100644 --- a/modules/features/chatbot/streaming/events.py +++ b/modules/features/chatbot/streaming/events.py @@ -85,7 +85,7 @@ class EventManager: """ queue = self._queues.get(context_id) if not queue: - logger.warning(f"No queue found for workflow {context_id}, event not emitted") + # DEBUG level: This is normal for background workflows without active SSE listener return event = { diff --git a/modules/security/rbac.py b/modules/security/rbac.py index 5d20d1fb..11d6e11d 100644 --- a/modules/security/rbac.py +++ b/modules/security/rbac.py @@ -102,21 +102,30 @@ class RbacClass: allRulesWithPriority = self._getRulesForRoleIds(roleIds, context, mandateId, featureInstanceId) # Für jede Rolle die spezifischste Regel finden + # Priority order: 1) Role priority (instance > mandate > global), 2) Item specificity (exact > prefix > generic) rolePermissions = {} for priority, rule in allRulesWithPriority: # Find most specific rule for this item if self._ruleMatchesItem(rule, item): roleId = rule.roleId - # Speichere mit Priorität (höhere Priorität überschreibt) - if roleId not in rolePermissions or priority > rolePermissions[roleId][0]: - rolePermissions[roleId] = (priority, rule) + # Calculate item specificity: 0=generic (null), 1=prefix match, 2=exact match + itemSpecificity = self._getItemSpecificity(rule, item) + + # Compare: first by role priority, then by item specificity + if roleId not in rolePermissions: + rolePermissions[roleId] = (priority, itemSpecificity, rule) + else: + existingPriority, existingSpecificity, _ = rolePermissions[roleId] + # Higher role priority wins, or same priority but higher specificity + if priority > existingPriority or (priority == existingPriority and itemSpecificity > existingSpecificity): + rolePermissions[roleId] = (priority, itemSpecificity, rule) - # Find highest priority among matching rules - highestPriority = max((p for p, _ in rolePermissions.values()), default=0) + # Find highest priority among matching rules (role priority only, specificity already handled per role) + highestPriority = max((p for p, _, _ in rolePermissions.values()), default=0) # Combine permissions ONLY from rules with highest priority # This ensures instance-specific rules (Priority 3) override global rules (Priority 1) - for roleId, (priority, rule) in rolePermissions.items(): + for roleId, (priority, specificity, rule) in rolePermissions.items(): # Only use rules with highest priority if priority < highestPriority: continue @@ -433,6 +442,23 @@ class RbacClass: return False + def _getItemSpecificity(self, rule: AccessRule, item: str) -> int: + """ + Calculate the specificity of a rule's item match. + + Returns: + 0 = generic rule (item=None) + 1 = prefix match + 2 = exact match + """ + if rule.item is None: + return 0 # Generic rule - lowest specificity + if rule.item == item: + return 2 # Exact match - highest specificity + if item and item.startswith(rule.item + "."): + return 1 # Prefix match - medium specificity + return 0 # No match (shouldn't happen if _ruleMatchesItem was true) + def findMostSpecificRule(self, rules: List[AccessRule], item: str) -> Optional[AccessRule]: """ Find the most specific rule for an item (longest matching prefix wins). diff --git a/modules/workflows/automation/mainWorkflow.py b/modules/workflows/automation/mainWorkflow.py index 291f3120..e7c6839e 100644 --- a/modules/workflows/automation/mainWorkflow.py +++ b/modules/workflows/automation/mainWorkflow.py @@ -306,6 +306,9 @@ def createAutomationEventHandler(automationId: str, eventUser): logger.error(f"Automation {automationId} has no creator user") return + # Get mandate context from automation definition + automationMandateId = getattr(automation, "mandateId", None) + # Get creator user from database using services eventServices = getServices(eventUser, None) creatorUser = eventServices.interfaceDbApp.getUser(creatorUserId) @@ -313,10 +316,10 @@ def createAutomationEventHandler(automationId: str, eventUser): logger.error(f"Creator user {creatorUserId} not found for automation {automationId}") return - # Get services for creator user (provides access to interfaces) - creatorServices = getServices(creatorUser, None) + # Get services for creator user WITH mandate context from automation + creatorServices = getServices(creatorUser, automationMandateId) - # Execute automation with creator user's context + # Execute automation with creator user's context and mandate # executeAutomation is in same module, so we can call it directly await executeAutomation(automationId, creatorServices) logger.info(f"Successfully executed automation {automationId} as user {creatorUserId}")