# app/modules/tenancy/services/permission_discovery_service.py """ Permission Discovery Service - Discovers and aggregates permissions from all modules. This service implements the module-driven permission system where each module defines its own permissions through PermissionDefinition in its definition.py file. Key Features: - Discovers permission definitions from all loaded modules - Groups permissions by category for UI organization - Provides role preset mappings (owner, manager, staff, etc.) - Supports permission checking and validation Usage: from app.modules.tenancy.services.permission_discovery_service import ( permission_discovery_service ) # Get all permissions all_perms = permission_discovery_service.get_all_permissions() # Get permissions grouped by category grouped = permission_discovery_service.get_permissions_by_category() # Get permission IDs for a role preset staff_perms = permission_discovery_service.get_preset_permissions("staff") # Check if a permission ID is valid is_valid = permission_discovery_service.is_valid_permission("products.view") """ import logging from dataclasses import dataclass, field logger = logging.getLogger(__name__) @dataclass class DiscoveredPermission: """ A permission discovered from a module, enriched with module info. Extends PermissionDefinition with module context for tracking which module provides each permission. """ id: str label_key: str description_key: str category: str is_owner_only: bool module_code: str @dataclass class PermissionCategory: """ A category grouping related permissions for UI organization. """ id: str label_key: str permissions: list[DiscoveredPermission] = field(default_factory=list) class PermissionDiscoveryService: """ Service to discover and aggregate permissions from all modules. This service: 1. Collects permission definitions from all module definition.py files 2. Groups permissions by category for UI display 3. Provides role preset definitions (owner, manager, staff, etc.) 4. Validates permission IDs """ # ========================================================================= # Role Presets # ========================================================================= # Role presets define default permission sets. These reference permission IDs # that should be defined by modules. The service validates these at runtime. ROLE_PRESETS = { "owner": None, # Special: owners get ALL permissions "manager": { # Dashboard "dashboard.view", # Products "products.view", "products.create", "products.edit", "products.delete", "products.import", "products.export", # Stock/Inventory "stock.view", "stock.edit", "stock.transfer", # Orders "orders.view", "orders.edit", "orders.cancel", "orders.refund", # Customers "customers.view", "customers.edit", "customers.export", # Marketing "marketing.view", "marketing.create", "marketing.send", # Reports "reports.view", "reports.financial", "reports.export", # Settings (limited) "settings.view", "settings.theme", # Team (view only) "team.view", # Imports "imports.view", "imports.create", "imports.cancel", }, "staff": { # Dashboard "dashboard.view", # Products "products.view", "products.create", "products.edit", # Stock "stock.view", "stock.edit", # Orders "orders.view", "orders.edit", # Customers "customers.view", "customers.edit", }, "support": { # Dashboard "dashboard.view", # Products (view only) "products.view", # Orders "orders.view", "orders.edit", # Customers "customers.view", "customers.edit", }, "viewer": { # Read-only access "dashboard.view", "products.view", "stock.view", "orders.view", "customers.view", "reports.view", }, "marketing": { # Marketing-focused role "dashboard.view", "customers.view", "customers.export", "marketing.view", "marketing.create", "marketing.send", "reports.view", }, } # Category labels for UI grouping CATEGORY_LABELS = { "dashboard": "tenancy.permissions.category.dashboard", "products": "tenancy.permissions.category.products", "stock": "tenancy.permissions.category.stock", "orders": "tenancy.permissions.category.orders", "customers": "tenancy.permissions.category.customers", "marketing": "tenancy.permissions.category.marketing", "reports": "tenancy.permissions.category.reports", "settings": "tenancy.permissions.category.settings", "team": "tenancy.permissions.category.team", "imports": "tenancy.permissions.category.imports", "general": "tenancy.permissions.category.general", } def get_all_permissions(self) -> list[DiscoveredPermission]: """ Discover all permissions from all loaded modules. Returns: List of DiscoveredPermission objects from all modules """ from app.modules.registry import MODULES permissions = [] for module_code, module_def in MODULES.items(): for perm in module_def.permissions: discovered = DiscoveredPermission( id=perm.id, label_key=perm.label_key, description_key=perm.description_key, category=perm.category, is_owner_only=perm.is_owner_only, module_code=module_code, ) permissions.append(discovered) return sorted(permissions, key=lambda p: (p.category, p.id)) def get_all_permission_ids(self) -> set[str]: """ Get set of all permission IDs from all modules. Returns: Set of permission ID strings """ from app.modules.registry import MODULES ids = set() for module_def in MODULES.values(): ids.update(module_def.get_permission_ids()) return ids def get_permission(self, permission_id: str) -> DiscoveredPermission | None: """ Get a specific permission by ID. Args: permission_id: Permission ID to look up Returns: DiscoveredPermission if found, None otherwise """ from app.modules.registry import MODULES for module_code, module_def in MODULES.items(): for perm in module_def.permissions: if perm.id == permission_id: return DiscoveredPermission( id=perm.id, label_key=perm.label_key, description_key=perm.description_key, category=perm.category, is_owner_only=perm.is_owner_only, module_code=module_code, ) return None def get_permissions_by_category(self) -> list[PermissionCategory]: """ Get all permissions grouped by category. Returns: List of PermissionCategory objects with permissions """ all_perms = self.get_all_permissions() # Group by category categories_map: dict[str, list[DiscoveredPermission]] = {} for perm in all_perms: if perm.category not in categories_map: categories_map[perm.category] = [] categories_map[perm.category].append(perm) # Build category objects categories = [] for cat_id, perms in sorted(categories_map.items()): label_key = self.CATEGORY_LABELS.get( cat_id, f"tenancy.permissions.category.{cat_id}" ) categories.append( PermissionCategory( id=cat_id, label_key=label_key, permissions=perms, ) ) return categories def get_permissions_for_module( self, module_code: str ) -> list[DiscoveredPermission]: """ Get all permissions defined by a specific module. Args: module_code: Module code to get permissions for Returns: List of DiscoveredPermission from the specified module """ from app.modules.registry import MODULES module_def = MODULES.get(module_code) if not module_def: return [] return [ DiscoveredPermission( id=perm.id, label_key=perm.label_key, description_key=perm.description_key, category=perm.category, is_owner_only=perm.is_owner_only, module_code=module_code, ) for perm in module_def.permissions ] def is_valid_permission(self, permission_id: str) -> bool: """ Check if a permission ID is valid (defined by some module). Args: permission_id: Permission ID to validate Returns: True if permission exists, False otherwise """ return permission_id in self.get_all_permission_ids() def get_preset_permissions(self, preset_name: str) -> set[str]: """ Get permission IDs for a role preset. Args: preset_name: Name of preset (owner, manager, staff, support, viewer, marketing) Returns: Set of permission IDs for the preset, or empty set if not found. For "owner", returns all permission IDs. """ preset = self.ROLE_PRESETS.get(preset_name.lower()) if preset_name.lower() == "owner": # Owners get all permissions return self.get_all_permission_ids() if preset is None: logger.warning(f"Unknown role preset: {preset_name}") return set() # Filter to only permissions that actually exist all_perms = self.get_all_permission_ids() return preset & all_perms def get_available_presets(self) -> list[str]: """ Get list of available role preset names. Returns: List of preset names """ return list(self.ROLE_PRESETS.keys()) def validate_permissions(self, permission_ids: list[str]) -> list[str]: """ Validate a list of permission IDs. Args: permission_ids: List of permission IDs to validate Returns: List of invalid permission IDs (empty if all valid) """ all_perms = self.get_all_permission_ids() return [pid for pid in permission_ids if pid not in all_perms] def get_owner_only_permissions(self) -> set[str]: """ Get permission IDs that are owner-only. These permissions cannot be assigned to team members. Returns: Set of owner-only permission IDs """ from app.modules.registry import MODULES owner_only = set() for module_def in MODULES.values(): for perm in module_def.permissions: if perm.is_owner_only: owner_only.add(perm.id) return owner_only # Singleton instance permission_discovery_service = PermissionDiscoveryService() __all__ = [ "permission_discovery_service", "PermissionDiscoveryService", "DiscoveredPermission", "PermissionCategory", ]