# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Automation Feature Container - Main Module. Handles feature initialization and RBAC catalog registration. """ import logging from typing import Dict, List, Any logger = logging.getLogger(__name__) # Feature metadata FEATURE_CODE = "automation" FEATURE_LABEL = {"en": "Automation", "de": "Automatisierung", "fr": "Automatisation"} FEATURE_ICON = "mdi-cog-clockwise" # UI Objects for RBAC catalog UI_OBJECTS = [ { "objectKey": "ui.feature.automation.definitions", "label": {"en": "Automation Definitions", "de": "Automatisierungs-Definitionen", "fr": "Définitions d'automatisation"}, "meta": {"area": "definitions"} }, { "objectKey": "ui.feature.automation.templates", "label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"}, "meta": {"area": "templates"} }, { "objectKey": "ui.feature.automation.logs", "label": {"en": "Execution Logs", "de": "Ausführungsprotokolle", "fr": "Journaux d'exécution"}, "meta": {"area": "logs"} }, ] # Resource Objects for RBAC catalog RESOURCE_OBJECTS = [ { "objectKey": "resource.feature.automation.create", "label": {"en": "Create Automation", "de": "Automatisierung erstellen", "fr": "Créer automatisation"}, "meta": {"endpoint": "/api/automations", "method": "POST"} }, { "objectKey": "resource.feature.automation.update", "label": {"en": "Update Automation", "de": "Automatisierung aktualisieren", "fr": "Modifier automatisation"}, "meta": {"endpoint": "/api/automations/{automationId}", "method": "PUT"} }, { "objectKey": "resource.feature.automation.delete", "label": {"en": "Delete Automation", "de": "Automatisierung löschen", "fr": "Supprimer automatisation"}, "meta": {"endpoint": "/api/automations/{automationId}", "method": "DELETE"} }, { "objectKey": "resource.feature.automation.execute", "label": {"en": "Execute Automation", "de": "Automatisierung ausführen", "fr": "Exécuter automatisation"}, "meta": {"endpoint": "/api/automations/{automationId}/execute", "method": "POST"} }, ] # Template roles for this feature TEMPLATE_ROLES = [ { "roleLabel": "automation-admin", "description": { "en": "Automation Administrator - Full access to automation configuration and execution", "de": "Automatisierungs-Administrator - Vollzugriff auf Automatisierungs-Konfiguration und Ausführung", "fr": "Administrateur automatisation - Accès complet à la configuration et exécution" }, "accessRules": [ # Full UI access {"context": "UI", "item": None, "view": True}, # Full DATA access {"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"}, ] }, { "roleLabel": "automation-editor", "description": { "en": "Automation Editor - Create and modify automations", "de": "Automatisierungs-Editor - Automatisierungen erstellen und bearbeiten", "fr": "Éditeur automatisation - Créer et modifier les automatisations" }, "accessRules": [ # UI access to definitions and templates - vollqualifizierte ObjectKeys {"context": "UI", "item": "ui.feature.automation.definitions", "view": True}, {"context": "UI", "item": "ui.feature.automation.templates", "view": True}, {"context": "UI", "item": "ui.feature.automation.logs", "view": True}, # Group-level DATA access {"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "n"}, ] }, { "roleLabel": "automation-user", "description": { "en": "Automation User - Create and manage own automations", "de": "Automatisierungs-Benutzer - Eigene Automatisierungen erstellen und verwalten", "fr": "Utilisateur automatisation - Créer et gérer ses propres automatisations" }, "accessRules": [ {"context": "UI", "item": "ui.feature.automation.definitions", "view": True}, {"context": "UI", "item": "ui.feature.automation.templates", "view": True}, {"context": "UI", "item": "ui.feature.automation.logs", "view": True}, {"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"}, ] }, { "roleLabel": "automation-viewer", "description": { "en": "Automation Viewer - View automations and execution results", "de": "Automatisierungs-Betrachter - Automatisierungen und Ausführungsergebnisse einsehen", "fr": "Visualiseur automatisation - Consulter les automatisations et résultats" }, "accessRules": [ # UI access to view only {"context": "UI", "item": "ui.feature.automation.definitions", "view": True}, {"context": "UI", "item": "ui.feature.automation.logs", "view": True}, # Read-only DATA access (my level) {"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"}, ] }, ] def getFeatureDefinition() -> Dict[str, Any]: """Return the feature definition for registration.""" return { "code": FEATURE_CODE, "label": FEATURE_LABEL, "icon": FEATURE_ICON, "autoCreateInstance": True, # Automatically create instance in root mandate during bootstrap } def getUiObjects() -> List[Dict[str, Any]]: """Return UI objects for RBAC catalog registration.""" return UI_OBJECTS def getResourceObjects() -> List[Dict[str, Any]]: """Return resource objects for RBAC catalog registration.""" return RESOURCE_OBJECTS def getTemplateRoles() -> List[Dict[str, Any]]: """Return template roles for this feature.""" return TEMPLATE_ROLES def registerFeature(catalogService) -> bool: """ Register this feature's RBAC objects in the catalog. Args: catalogService: The RBAC catalog service instance Returns: True if registration was successful """ try: # Register UI objects for uiObj in UI_OBJECTS: catalogService.registerUiObject( featureCode=FEATURE_CODE, objectKey=uiObj["objectKey"], label=uiObj["label"], meta=uiObj.get("meta") ) # Register Resource objects for resObj in RESOURCE_OBJECTS: catalogService.registerResourceObject( featureCode=FEATURE_CODE, objectKey=resObj["objectKey"], label=resObj["label"], meta=resObj.get("meta") ) # Sync template roles to database _syncTemplateRolesToDb() # Mark existing templates without isSystem field as system templates (migration) _migrateExistingTemplates() logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects") return True except Exception as e: logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}") return False def _syncTemplateRolesToDb() -> int: """ Sync template roles and their AccessRules to the database. Creates global template roles (mandateId=None) if they don't exist. Returns: Number of roles created/updated """ try: from modules.interfaces.interfaceDbApp import getRootInterface from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext rootInterface = getRootInterface() # Get existing template roles for this feature (Pydantic models) existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE) # Filter to template roles (mandateId is None) templateRoles = [r for r in existingRoles if r.mandateId is None] existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles} createdCount = 0 for roleTemplate in TEMPLATE_ROLES: roleLabel = roleTemplate["roleLabel"] if roleLabel in existingRoleLabels: roleId = existingRoleLabels[roleLabel] # Ensure AccessRules exist for this role _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) else: # Create new template role newRole = Role( roleLabel=roleLabel, description=roleTemplate.get("description", {}), featureCode=FEATURE_CODE, mandateId=None, # Global template featureInstanceId=None, isSystemRole=False ) createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump()) roleId = createdRole.get("id") # Create AccessRules for this role _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) logger.info(f"Created template role '{roleLabel}' with ID {roleId}") createdCount += 1 if createdCount > 0: logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles") return createdCount except Exception as e: logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}") return 0 def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int: """ Ensure AccessRules exist for a role based on templates. Args: rootInterface: Root interface instance roleId: Role ID ruleTemplates: List of rule templates Returns: Number of rules created """ from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext # Get existing rules for this role (Pydantic models) existingRules = rootInterface.getAccessRulesByRole(roleId) # Create a set of existing rule signatures to avoid duplicates # IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+ existingSignatures = set() for rule in existingRules: sig = (rule.context.value if rule.context else None, rule.item) existingSignatures.add(sig) createdCount = 0 for template in ruleTemplates: context = template.get("context", "UI") item = template.get("item") sig = (context, item) if sig in existingSignatures: continue # Map context string to enum if context == "UI": contextEnum = AccessRuleContext.UI elif context == "DATA": contextEnum = AccessRuleContext.DATA elif context == "RESOURCE": contextEnum = AccessRuleContext.RESOURCE else: contextEnum = context newRule = AccessRule( roleId=roleId, context=contextEnum, item=item, view=template.get("view", False), read=template.get("read"), create=template.get("create"), update=template.get("update"), delete=template.get("delete"), ) rootInterface.db.recordCreate(AccessRule, newRule.model_dump()) createdCount += 1 if createdCount > 0: logger.debug(f"Created {createdCount} AccessRules for role {roleId}") return createdCount def _migrateExistingTemplates() -> None: """ Migration: Mark existing templates that have no isSystem/featureInstanceId fields as system templates (isSystem=True). This runs idempotently during feature registration. """ try: from modules.features.automation.interfaceFeatureAutomation import getInterface from modules.security.rootAccess import getRootUser from modules.features.automation.datamodelFeatureAutomation import AutomationTemplate rootUser = getRootUser() automationInterface = getInterface(rootUser) # Get all templates from DB allTemplates = automationInterface.db.getRecordset(AutomationTemplate) migratedCount = 0 for template in allTemplates: templateId = template.get("id") isSystem = template.get("isSystem") featureInstanceId = template.get("featureInstanceId") # Templates without isSystem set (old templates) → mark as system if isSystem is None and featureInstanceId is None: automationInterface.db.recordModify( AutomationTemplate, templateId, {"isSystem": True, "featureInstanceId": None} ) migratedCount += 1 if migratedCount > 0: logger.info(f"Migrated {migratedCount} existing templates to isSystem=True") except Exception as e: logger.warning(f"Template migration check failed (non-critical): {e}")