Files
orion/app/modules/tenancy/services/permission_discovery_service.py
Samir Boulahtit 03395a9dfa refactor: implement module-driven permissions and relocate business logic
File Relocations:
- Delete app/config/ folder (empty after menu_registry removal)
- Move feature_gate.py → app/modules/billing/dependencies/
- Move theme_presets.py → app/modules/cms/services/

Module-Driven Permissions System:
- Add PermissionDefinition dataclass to app/modules/base.py
- Create PermissionDiscoveryService in tenancy module
- Update module definitions to declare their own permissions:
  - core: dashboard.view, settings.*
  - catalog: products.*
  - orders: orders.*
  - inventory: stock.*
  - customers: customers.*
  - tenancy: team.*
- Update app/core/permissions.py to use discovery service
- Role presets (owner, manager, staff, etc.) now use module permissions

This follows the same pattern as module-driven menus:
- Each module defines its permissions in definition.py
- PermissionDiscoveryService aggregates all permissions at runtime
- Tenancy module handles role-to-permission assignment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:42:13 +01:00

408 lines
12 KiB
Python

# 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
from app.modules.base import PermissionDefinition
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",
# 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",
]