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>
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
# app/config/__init__.py
|
|
||||||
"""Configuration modules for the application."""
|
|
||||||
|
|
||||||
# Note: menu_registry.py has been deprecated in favor of module-driven menus.
|
|
||||||
# Menu items are now defined in each module's definition.py file and aggregated
|
|
||||||
# by MenuDiscoveryService (app/modules/core/services/menu_discovery_service.py).
|
|
||||||
|
|
||||||
__all__ = []
|
|
||||||
@@ -2,26 +2,38 @@
|
|||||||
"""
|
"""
|
||||||
Permission constants and checking logic for RBAC.
|
Permission constants and checking logic for RBAC.
|
||||||
|
|
||||||
This module defines:
|
NOTE: This module now uses the module-driven permission system.
|
||||||
- Vendor-specific permissions
|
Permissions are defined in each module's definition.py file and
|
||||||
- Permission groups (for easier role creation)
|
discovered by PermissionDiscoveryService.
|
||||||
- Permission checking utilities
|
|
||||||
|
This file provides backward-compatible exports for existing code.
|
||||||
|
New code should use:
|
||||||
|
from app.modules.tenancy.services.permission_discovery_service import (
|
||||||
|
permission_discovery_service
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from app.modules.tenancy.services.permission_discovery_service import (
|
||||||
|
permission_discovery_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VendorPermissions(str, Enum):
|
class VendorPermissions(str, Enum):
|
||||||
"""
|
"""
|
||||||
All available permissions within a vendor context.
|
All available permissions within a vendor context.
|
||||||
|
|
||||||
|
NOTE: This enum is maintained for backward compatibility.
|
||||||
|
Permissions are now defined in module definition.py files.
|
||||||
|
|
||||||
Naming convention: RESOURCE_ACTION
|
Naming convention: RESOURCE_ACTION
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Dashboard
|
# Dashboard (from core module)
|
||||||
DASHBOARD_VIEW = "dashboard.view"
|
DASHBOARD_VIEW = "dashboard.view"
|
||||||
|
|
||||||
# Products
|
# Products (from catalog module)
|
||||||
PRODUCTS_VIEW = "products.view"
|
PRODUCTS_VIEW = "products.view"
|
||||||
PRODUCTS_CREATE = "products.create"
|
PRODUCTS_CREATE = "products.create"
|
||||||
PRODUCTS_EDIT = "products.edit"
|
PRODUCTS_EDIT = "products.edit"
|
||||||
@@ -29,133 +41,91 @@ class VendorPermissions(str, Enum):
|
|||||||
PRODUCTS_IMPORT = "products.import"
|
PRODUCTS_IMPORT = "products.import"
|
||||||
PRODUCTS_EXPORT = "products.export"
|
PRODUCTS_EXPORT = "products.export"
|
||||||
|
|
||||||
# Stock/Inventory
|
# Stock/Inventory (from inventory module)
|
||||||
STOCK_VIEW = "stock.view"
|
STOCK_VIEW = "stock.view"
|
||||||
STOCK_EDIT = "stock.edit"
|
STOCK_EDIT = "stock.edit"
|
||||||
STOCK_TRANSFER = "stock.transfer"
|
STOCK_TRANSFER = "stock.transfer"
|
||||||
|
|
||||||
# Orders
|
# Orders (from orders module)
|
||||||
ORDERS_VIEW = "orders.view"
|
ORDERS_VIEW = "orders.view"
|
||||||
ORDERS_EDIT = "orders.edit"
|
ORDERS_EDIT = "orders.edit"
|
||||||
ORDERS_CANCEL = "orders.cancel"
|
ORDERS_CANCEL = "orders.cancel"
|
||||||
ORDERS_REFUND = "orders.refund"
|
ORDERS_REFUND = "orders.refund"
|
||||||
|
|
||||||
# Customers
|
# Customers (from customers module)
|
||||||
CUSTOMERS_VIEW = "customers.view"
|
CUSTOMERS_VIEW = "customers.view"
|
||||||
CUSTOMERS_EDIT = "customers.edit"
|
CUSTOMERS_EDIT = "customers.edit"
|
||||||
CUSTOMERS_DELETE = "customers.delete"
|
CUSTOMERS_DELETE = "customers.delete"
|
||||||
CUSTOMERS_EXPORT = "customers.export"
|
CUSTOMERS_EXPORT = "customers.export"
|
||||||
|
|
||||||
# Marketing
|
# Marketing (from messaging module - to be added)
|
||||||
MARKETING_VIEW = "marketing.view"
|
MARKETING_VIEW = "marketing.view"
|
||||||
MARKETING_CREATE = "marketing.create"
|
MARKETING_CREATE = "marketing.create"
|
||||||
MARKETING_SEND = "marketing.send"
|
MARKETING_SEND = "marketing.send"
|
||||||
|
|
||||||
# Reports
|
# Reports (from analytics module - to be added)
|
||||||
REPORTS_VIEW = "reports.view"
|
REPORTS_VIEW = "reports.view"
|
||||||
REPORTS_FINANCIAL = "reports.financial"
|
REPORTS_FINANCIAL = "reports.financial"
|
||||||
REPORTS_EXPORT = "reports.export"
|
REPORTS_EXPORT = "reports.export"
|
||||||
|
|
||||||
# Settings
|
# Settings (from core module)
|
||||||
SETTINGS_VIEW = "settings.view"
|
SETTINGS_VIEW = "settings.view"
|
||||||
SETTINGS_EDIT = "settings.edit"
|
SETTINGS_EDIT = "settings.edit"
|
||||||
SETTINGS_THEME = "settings.theme"
|
SETTINGS_THEME = "settings.theme"
|
||||||
SETTINGS_DOMAINS = "settings.domains"
|
SETTINGS_DOMAINS = "settings.domains"
|
||||||
|
|
||||||
# Team Management
|
# Team Management (from tenancy module)
|
||||||
TEAM_VIEW = "team.view"
|
TEAM_VIEW = "team.view"
|
||||||
TEAM_INVITE = "team.invite"
|
TEAM_INVITE = "team.invite"
|
||||||
TEAM_EDIT = "team.edit"
|
TEAM_EDIT = "team.edit"
|
||||||
TEAM_REMOVE = "team.remove"
|
TEAM_REMOVE = "team.remove"
|
||||||
|
|
||||||
# Marketplace Imports
|
# Marketplace Imports (from marketplace module - to be added)
|
||||||
IMPORTS_VIEW = "imports.view"
|
IMPORTS_VIEW = "imports.view"
|
||||||
IMPORTS_CREATE = "imports.create"
|
IMPORTS_CREATE = "imports.create"
|
||||||
IMPORTS_CANCEL = "imports.cancel"
|
IMPORTS_CANCEL = "imports.cancel"
|
||||||
|
|
||||||
|
|
||||||
class PermissionGroups:
|
class PermissionGroups:
|
||||||
"""Pre-defined permission groups for common roles."""
|
"""
|
||||||
|
Pre-defined permission groups for common roles.
|
||||||
|
|
||||||
# Full access (for owners)
|
NOTE: These now delegate to permission_discovery_service for consistency.
|
||||||
OWNER: set[str] = set(p.value for p in VendorPermissions)
|
"""
|
||||||
|
|
||||||
# Manager - Can do most things except team management and critical settings
|
@property
|
||||||
MANAGER: set[str] = {
|
def OWNER(self) -> set[str]:
|
||||||
VendorPermissions.DASHBOARD_VIEW.value,
|
"""Full access (for owners) - all permissions."""
|
||||||
VendorPermissions.PRODUCTS_VIEW.value,
|
return permission_discovery_service.get_preset_permissions("owner")
|
||||||
VendorPermissions.PRODUCTS_CREATE.value,
|
|
||||||
VendorPermissions.PRODUCTS_EDIT.value,
|
|
||||||
VendorPermissions.PRODUCTS_DELETE.value,
|
|
||||||
VendorPermissions.PRODUCTS_IMPORT.value,
|
|
||||||
VendorPermissions.PRODUCTS_EXPORT.value,
|
|
||||||
VendorPermissions.STOCK_VIEW.value,
|
|
||||||
VendorPermissions.STOCK_EDIT.value,
|
|
||||||
VendorPermissions.STOCK_TRANSFER.value,
|
|
||||||
VendorPermissions.ORDERS_VIEW.value,
|
|
||||||
VendorPermissions.ORDERS_EDIT.value,
|
|
||||||
VendorPermissions.ORDERS_CANCEL.value,
|
|
||||||
VendorPermissions.ORDERS_REFUND.value,
|
|
||||||
VendorPermissions.CUSTOMERS_VIEW.value,
|
|
||||||
VendorPermissions.CUSTOMERS_EDIT.value,
|
|
||||||
VendorPermissions.CUSTOMERS_EXPORT.value,
|
|
||||||
VendorPermissions.MARKETING_VIEW.value,
|
|
||||||
VendorPermissions.MARKETING_CREATE.value,
|
|
||||||
VendorPermissions.MARKETING_SEND.value,
|
|
||||||
VendorPermissions.REPORTS_VIEW.value,
|
|
||||||
VendorPermissions.REPORTS_FINANCIAL.value,
|
|
||||||
VendorPermissions.REPORTS_EXPORT.value,
|
|
||||||
VendorPermissions.SETTINGS_VIEW.value,
|
|
||||||
VendorPermissions.SETTINGS_THEME.value,
|
|
||||||
VendorPermissions.IMPORTS_VIEW.value,
|
|
||||||
VendorPermissions.IMPORTS_CREATE.value,
|
|
||||||
VendorPermissions.IMPORTS_CANCEL.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Staff - Can view and edit products/orders but limited access
|
@property
|
||||||
STAFF: set[str] = {
|
def MANAGER(self) -> set[str]:
|
||||||
VendorPermissions.DASHBOARD_VIEW.value,
|
"""Manager - Can do most things except team management."""
|
||||||
VendorPermissions.PRODUCTS_VIEW.value,
|
return permission_discovery_service.get_preset_permissions("manager")
|
||||||
VendorPermissions.PRODUCTS_CREATE.value,
|
|
||||||
VendorPermissions.PRODUCTS_EDIT.value,
|
|
||||||
VendorPermissions.STOCK_VIEW.value,
|
|
||||||
VendorPermissions.STOCK_EDIT.value,
|
|
||||||
VendorPermissions.ORDERS_VIEW.value,
|
|
||||||
VendorPermissions.ORDERS_EDIT.value,
|
|
||||||
VendorPermissions.CUSTOMERS_VIEW.value,
|
|
||||||
VendorPermissions.CUSTOMERS_EDIT.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Support - Can view and assist with orders/customers
|
@property
|
||||||
SUPPORT: set[str] = {
|
def STAFF(self) -> set[str]:
|
||||||
VendorPermissions.DASHBOARD_VIEW.value,
|
"""Staff - Can view and edit products/orders but limited access."""
|
||||||
VendorPermissions.PRODUCTS_VIEW.value,
|
return permission_discovery_service.get_preset_permissions("staff")
|
||||||
VendorPermissions.ORDERS_VIEW.value,
|
|
||||||
VendorPermissions.ORDERS_EDIT.value,
|
|
||||||
VendorPermissions.CUSTOMERS_VIEW.value,
|
|
||||||
VendorPermissions.CUSTOMERS_EDIT.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Viewer - Read-only access
|
@property
|
||||||
VIEWER: set[str] = {
|
def SUPPORT(self) -> set[str]:
|
||||||
VendorPermissions.DASHBOARD_VIEW.value,
|
"""Support - Can view and assist with orders/customers."""
|
||||||
VendorPermissions.PRODUCTS_VIEW.value,
|
return permission_discovery_service.get_preset_permissions("support")
|
||||||
VendorPermissions.STOCK_VIEW.value,
|
|
||||||
VendorPermissions.ORDERS_VIEW.value,
|
|
||||||
VendorPermissions.CUSTOMERS_VIEW.value,
|
|
||||||
VendorPermissions.REPORTS_VIEW.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Marketing - Focused on marketing and customer communication
|
@property
|
||||||
MARKETING: set[str] = {
|
def VIEWER(self) -> set[str]:
|
||||||
VendorPermissions.DASHBOARD_VIEW.value,
|
"""Viewer - Read-only access."""
|
||||||
VendorPermissions.CUSTOMERS_VIEW.value,
|
return permission_discovery_service.get_preset_permissions("viewer")
|
||||||
VendorPermissions.CUSTOMERS_EXPORT.value,
|
|
||||||
VendorPermissions.MARKETING_VIEW.value,
|
@property
|
||||||
VendorPermissions.MARKETING_CREATE.value,
|
def MARKETING(self) -> set[str]:
|
||||||
VendorPermissions.MARKETING_SEND.value,
|
"""Marketing - Focused on marketing and customer communication."""
|
||||||
VendorPermissions.REPORTS_VIEW.value,
|
return permission_discovery_service.get_preset_permissions("marketing")
|
||||||
}
|
|
||||||
|
|
||||||
|
# Singleton instance for backward compatibility
|
||||||
|
_permission_groups = PermissionGroups()
|
||||||
|
|
||||||
|
|
||||||
class PermissionChecker:
|
class PermissionChecker:
|
||||||
@@ -188,23 +158,14 @@ class PermissionChecker:
|
|||||||
return [perm for perm in required_permissions if perm not in permissions]
|
return [perm for perm in required_permissions if perm not in permissions]
|
||||||
|
|
||||||
|
|
||||||
# Helper function to get permissions for a role preset
|
|
||||||
def get_preset_permissions(preset_name: str) -> set[str]:
|
def get_preset_permissions(preset_name: str) -> set[str]:
|
||||||
"""
|
"""
|
||||||
Get permissions for a preset role.
|
Get permissions for a preset role.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
preset_name: Name of the preset (manager, staff, support, viewer, marketing)
|
preset_name: Name of the preset (owner, manager, staff, support, viewer, marketing)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Set of permission strings
|
Set of permission strings
|
||||||
"""
|
"""
|
||||||
presets = {
|
return permission_discovery_service.get_preset_permissions(preset_name)
|
||||||
"owner": PermissionGroups.OWNER,
|
|
||||||
"manager": PermissionGroups.MANAGER,
|
|
||||||
"staff": PermissionGroups.STAFF,
|
|
||||||
"support": PermissionGroups.SUPPORT,
|
|
||||||
"viewer": PermissionGroups.VIEWER,
|
|
||||||
"marketing": PermissionGroups.MARKETING,
|
|
||||||
}
|
|
||||||
return presets.get(preset_name.lower(), set())
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from fastapi import APIRouter, Depends, Query
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.deps import get_current_vendor_api, get_db, require_module_access
|
from app.api.deps import get_current_vendor_api, get_db, require_module_access
|
||||||
from app.core.feature_gate import RequireFeature
|
from app.modules.billing.dependencies.feature_gate import RequireFeature
|
||||||
from app.modules.analytics.services import stats_service
|
from app.modules.analytics.services import stats_service
|
||||||
from app.modules.analytics.schemas import (
|
from app.modules.analytics.schemas import (
|
||||||
VendorAnalyticsCatalog,
|
VendorAnalyticsCatalog,
|
||||||
|
|||||||
@@ -134,6 +134,48 @@ class MenuSectionDefinition:
|
|||||||
is_collapsible: bool = True
|
is_collapsible: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Permission Definitions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PermissionDefinition:
|
||||||
|
"""
|
||||||
|
Definition of a permission that a module exposes.
|
||||||
|
|
||||||
|
Permissions are granular capabilities that can be assigned to roles/users.
|
||||||
|
Each module defines its own permissions, which are then discovered and
|
||||||
|
aggregated by the tenancy module for role assignment.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier in format "resource.action" (e.g., "products.view")
|
||||||
|
label_key: i18n key for the permission label
|
||||||
|
description_key: i18n key for permission description
|
||||||
|
category: Grouping category for UI organization (e.g., "products", "orders")
|
||||||
|
is_owner_only: If True, only vendor owners can have this permission
|
||||||
|
|
||||||
|
Example:
|
||||||
|
PermissionDefinition(
|
||||||
|
id="products.view",
|
||||||
|
label_key="catalog.permissions.products_view",
|
||||||
|
description_key="catalog.permissions.products_view_desc",
|
||||||
|
category="products",
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
label_key: str
|
||||||
|
description_key: str = ""
|
||||||
|
category: str = "general"
|
||||||
|
is_owner_only: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Scheduled Task Definitions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ScheduledTask:
|
class ScheduledTask:
|
||||||
"""
|
"""
|
||||||
@@ -275,7 +317,7 @@ class ModuleDefinition:
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
features: list[str] = field(default_factory=list)
|
features: list[str] = field(default_factory=list)
|
||||||
menu_items: dict[FrontendType, list[str]] = field(default_factory=dict)
|
menu_items: dict[FrontendType, list[str]] = field(default_factory=dict)
|
||||||
permissions: list[str] = field(default_factory=list)
|
permissions: list[PermissionDefinition] = field(default_factory=list)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Menu Definitions (Module-Driven Menus)
|
# Menu Definitions (Module-Driven Menus)
|
||||||
@@ -400,9 +442,13 @@ class ModuleDefinition:
|
|||||||
"""Check if this module provides a specific feature."""
|
"""Check if this module provides a specific feature."""
|
||||||
return feature_code in self.features
|
return feature_code in self.features
|
||||||
|
|
||||||
def has_permission(self, permission_code: str) -> bool:
|
def has_permission(self, permission_id: str) -> bool:
|
||||||
"""Check if this module defines a specific permission."""
|
"""Check if this module defines a specific permission."""
|
||||||
return permission_code in self.permissions
|
return any(p.id == permission_id for p in self.permissions)
|
||||||
|
|
||||||
|
def get_permission_ids(self) -> set[str]:
|
||||||
|
"""Get all permission IDs defined by this module."""
|
||||||
|
return {p.id for p in self.permissions}
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Dependency Methods
|
# Dependency Methods
|
||||||
|
|||||||
14
app/modules/billing/dependencies/__init__.py
Normal file
14
app/modules/billing/dependencies/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# app/modules/billing/dependencies/__init__.py
|
||||||
|
"""FastAPI dependencies for the billing module."""
|
||||||
|
|
||||||
|
from .feature_gate import (
|
||||||
|
require_feature,
|
||||||
|
RequireFeature,
|
||||||
|
FeatureNotAvailableError,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"require_feature",
|
||||||
|
"RequireFeature",
|
||||||
|
"FeatureNotAvailableError",
|
||||||
|
]
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
# app/modules/catalog/definition.py
|
# app/modules/catalog/definition.py
|
||||||
"""Catalog module definition."""
|
"""Catalog module definition."""
|
||||||
|
|
||||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
from app.modules.base import (
|
||||||
|
MenuItemDefinition,
|
||||||
|
MenuSectionDefinition,
|
||||||
|
ModuleDefinition,
|
||||||
|
PermissionDefinition,
|
||||||
|
)
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
module = ModuleDefinition(
|
module = ModuleDefinition(
|
||||||
@@ -11,7 +16,46 @@ module = ModuleDefinition(
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
is_self_contained=True,
|
is_self_contained=True,
|
||||||
requires=["inventory"],
|
requires=["inventory"],
|
||||||
# New module-driven menu definitions
|
# Module-driven permissions
|
||||||
|
permissions=[
|
||||||
|
PermissionDefinition(
|
||||||
|
id="products.view",
|
||||||
|
label_key="catalog.permissions.products_view",
|
||||||
|
description_key="catalog.permissions.products_view_desc",
|
||||||
|
category="products",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="products.create",
|
||||||
|
label_key="catalog.permissions.products_create",
|
||||||
|
description_key="catalog.permissions.products_create_desc",
|
||||||
|
category="products",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="products.edit",
|
||||||
|
label_key="catalog.permissions.products_edit",
|
||||||
|
description_key="catalog.permissions.products_edit_desc",
|
||||||
|
category="products",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="products.delete",
|
||||||
|
label_key="catalog.permissions.products_delete",
|
||||||
|
description_key="catalog.permissions.products_delete_desc",
|
||||||
|
category="products",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="products.import",
|
||||||
|
label_key="catalog.permissions.products_import",
|
||||||
|
description_key="catalog.permissions.products_import_desc",
|
||||||
|
category="products",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="products.export",
|
||||||
|
label_key="catalog.permissions.products_export",
|
||||||
|
description_key="catalog.permissions.products_export_desc",
|
||||||
|
category="products",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# Module-driven menu definitions
|
||||||
menus={
|
menus={
|
||||||
FrontendType.VENDOR: [
|
FrontendType.VENDOR: [
|
||||||
MenuSectionDefinition(
|
MenuSectionDefinition(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import re
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.theme_presets import (
|
from app.modules.cms.services.theme_presets import (
|
||||||
THEME_PRESETS,
|
THEME_PRESETS,
|
||||||
apply_preset,
|
apply_preset,
|
||||||
get_available_presets,
|
get_available_presets,
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ Dashboard, settings, and profile management.
|
|||||||
Required for basic operation - cannot be disabled.
|
Required for basic operation - cannot be disabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
from app.modules.base import (
|
||||||
|
MenuItemDefinition,
|
||||||
|
MenuSectionDefinition,
|
||||||
|
ModuleDefinition,
|
||||||
|
PermissionDefinition,
|
||||||
|
)
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
core_module = ModuleDefinition(
|
core_module = ModuleDefinition(
|
||||||
@@ -16,6 +21,40 @@ core_module = ModuleDefinition(
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
is_core=True,
|
is_core=True,
|
||||||
is_self_contained=True,
|
is_self_contained=True,
|
||||||
|
# Module-driven permissions
|
||||||
|
permissions=[
|
||||||
|
PermissionDefinition(
|
||||||
|
id="dashboard.view",
|
||||||
|
label_key="core.permissions.dashboard_view",
|
||||||
|
description_key="core.permissions.dashboard_view_desc",
|
||||||
|
category="dashboard",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="settings.view",
|
||||||
|
label_key="core.permissions.settings_view",
|
||||||
|
description_key="core.permissions.settings_view_desc",
|
||||||
|
category="settings",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="settings.edit",
|
||||||
|
label_key="core.permissions.settings_edit",
|
||||||
|
description_key="core.permissions.settings_edit_desc",
|
||||||
|
category="settings",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="settings.theme",
|
||||||
|
label_key="core.permissions.settings_theme",
|
||||||
|
description_key="core.permissions.settings_theme_desc",
|
||||||
|
category="settings",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="settings.domains",
|
||||||
|
label_key="core.permissions.settings_domains",
|
||||||
|
description_key="core.permissions.settings_domains_desc",
|
||||||
|
category="settings",
|
||||||
|
is_owner_only=True, # Only owners can manage domains
|
||||||
|
),
|
||||||
|
],
|
||||||
features=[
|
features=[
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"settings",
|
"settings",
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ Defines the customers module including its features, menu items,
|
|||||||
route configurations, and self-contained module settings.
|
route configurations, and self-contained module settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
from app.modules.base import (
|
||||||
|
MenuItemDefinition,
|
||||||
|
MenuSectionDefinition,
|
||||||
|
ModuleDefinition,
|
||||||
|
PermissionDefinition,
|
||||||
|
)
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +35,33 @@ customers_module = ModuleDefinition(
|
|||||||
name="Customer Management",
|
name="Customer Management",
|
||||||
description="Customer database, profiles, addresses, and segmentation.",
|
description="Customer database, profiles, addresses, and segmentation.",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
|
# Module-driven permissions
|
||||||
|
permissions=[
|
||||||
|
PermissionDefinition(
|
||||||
|
id="customers.view",
|
||||||
|
label_key="customers.permissions.customers_view",
|
||||||
|
description_key="customers.permissions.customers_view_desc",
|
||||||
|
category="customers",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="customers.edit",
|
||||||
|
label_key="customers.permissions.customers_edit",
|
||||||
|
description_key="customers.permissions.customers_edit_desc",
|
||||||
|
category="customers",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="customers.delete",
|
||||||
|
label_key="customers.permissions.customers_delete",
|
||||||
|
description_key="customers.permissions.customers_delete_desc",
|
||||||
|
category="customers",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="customers.export",
|
||||||
|
label_key="customers.permissions.customers_export",
|
||||||
|
description_key="customers.permissions.customers_export_desc",
|
||||||
|
category="customers",
|
||||||
|
),
|
||||||
|
],
|
||||||
features=[
|
features=[
|
||||||
"customer_view", # View customer profiles
|
"customer_view", # View customer profiles
|
||||||
"customer_export", # Export customer data
|
"customer_export", # Export customer data
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ Defines the inventory module including its features, menu items,
|
|||||||
route configurations, and self-contained module settings.
|
route configurations, and self-contained module settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
from app.modules.base import (
|
||||||
|
MenuItemDefinition,
|
||||||
|
MenuSectionDefinition,
|
||||||
|
ModuleDefinition,
|
||||||
|
PermissionDefinition,
|
||||||
|
)
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +38,27 @@ inventory_module = ModuleDefinition(
|
|||||||
"transaction history, and bulk imports."
|
"transaction history, and bulk imports."
|
||||||
),
|
),
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
|
# Module-driven permissions
|
||||||
|
permissions=[
|
||||||
|
PermissionDefinition(
|
||||||
|
id="stock.view",
|
||||||
|
label_key="inventory.permissions.stock_view",
|
||||||
|
description_key="inventory.permissions.stock_view_desc",
|
||||||
|
category="stock",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="stock.edit",
|
||||||
|
label_key="inventory.permissions.stock_edit",
|
||||||
|
description_key="inventory.permissions.stock_edit_desc",
|
||||||
|
category="stock",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="stock.transfer",
|
||||||
|
label_key="inventory.permissions.stock_transfer",
|
||||||
|
description_key="inventory.permissions.stock_transfer_desc",
|
||||||
|
category="stock",
|
||||||
|
),
|
||||||
|
],
|
||||||
features=[
|
features=[
|
||||||
"inventory_basic", # Basic stock tracking
|
"inventory_basic", # Basic stock tracking
|
||||||
"inventory_locations", # Multiple warehouse locations
|
"inventory_locations", # Multiple warehouse locations
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ Defines the orders module including its features, menu items,
|
|||||||
route configurations, and self-contained module settings.
|
route configurations, and self-contained module settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
from app.modules.base import (
|
||||||
|
MenuItemDefinition,
|
||||||
|
MenuSectionDefinition,
|
||||||
|
ModuleDefinition,
|
||||||
|
PermissionDefinition,
|
||||||
|
)
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +39,33 @@ orders_module = ModuleDefinition(
|
|||||||
),
|
),
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
requires=["payments"], # Depends on payments module for checkout
|
requires=["payments"], # Depends on payments module for checkout
|
||||||
|
# Module-driven permissions
|
||||||
|
permissions=[
|
||||||
|
PermissionDefinition(
|
||||||
|
id="orders.view",
|
||||||
|
label_key="orders.permissions.orders_view",
|
||||||
|
description_key="orders.permissions.orders_view_desc",
|
||||||
|
category="orders",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="orders.edit",
|
||||||
|
label_key="orders.permissions.orders_edit",
|
||||||
|
description_key="orders.permissions.orders_edit_desc",
|
||||||
|
category="orders",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="orders.cancel",
|
||||||
|
label_key="orders.permissions.orders_cancel",
|
||||||
|
description_key="orders.permissions.orders_cancel_desc",
|
||||||
|
category="orders",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="orders.refund",
|
||||||
|
label_key="orders.permissions.orders_refund",
|
||||||
|
description_key="orders.permissions.orders_refund_desc",
|
||||||
|
category="orders",
|
||||||
|
),
|
||||||
|
],
|
||||||
features=[
|
features=[
|
||||||
"order_management", # Basic order CRUD
|
"order_management", # Basic order CRUD
|
||||||
"order_bulk_actions", # Bulk status updates
|
"order_bulk_actions", # Bulk status updates
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_vendor_api, require_module_access
|
from app.api.deps import get_current_vendor_api, require_module_access
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.feature_gate import RequireFeature
|
from app.modules.billing.dependencies.feature_gate import RequireFeature
|
||||||
from app.modules.orders.exceptions import (
|
from app.modules.orders.exceptions import (
|
||||||
InvoicePDFNotFoundException,
|
InvoicePDFNotFoundException,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ Platform, company, vendor, and admin user management.
|
|||||||
Required for multi-tenant operation - cannot be disabled.
|
Required for multi-tenant operation - cannot be disabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
from app.modules.base import (
|
||||||
|
MenuItemDefinition,
|
||||||
|
MenuSectionDefinition,
|
||||||
|
ModuleDefinition,
|
||||||
|
PermissionDefinition,
|
||||||
|
)
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
tenancy_module = ModuleDefinition(
|
tenancy_module = ModuleDefinition(
|
||||||
@@ -16,6 +21,36 @@ tenancy_module = ModuleDefinition(
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
is_core=True,
|
is_core=True,
|
||||||
is_self_contained=True,
|
is_self_contained=True,
|
||||||
|
# Module-driven permissions
|
||||||
|
permissions=[
|
||||||
|
PermissionDefinition(
|
||||||
|
id="team.view",
|
||||||
|
label_key="tenancy.permissions.team_view",
|
||||||
|
description_key="tenancy.permissions.team_view_desc",
|
||||||
|
category="team",
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="team.invite",
|
||||||
|
label_key="tenancy.permissions.team_invite",
|
||||||
|
description_key="tenancy.permissions.team_invite_desc",
|
||||||
|
category="team",
|
||||||
|
is_owner_only=True,
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="team.edit",
|
||||||
|
label_key="tenancy.permissions.team_edit",
|
||||||
|
description_key="tenancy.permissions.team_edit_desc",
|
||||||
|
category="team",
|
||||||
|
is_owner_only=True,
|
||||||
|
),
|
||||||
|
PermissionDefinition(
|
||||||
|
id="team.remove",
|
||||||
|
label_key="tenancy.permissions.team_remove",
|
||||||
|
description_key="tenancy.permissions.team_remove_desc",
|
||||||
|
category="team",
|
||||||
|
is_owner_only=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
features=[
|
features=[
|
||||||
"platform_management",
|
"platform_management",
|
||||||
"company_management",
|
"company_management",
|
||||||
|
|||||||
407
app/modules/tenancy/services/permission_discovery_service.py
Normal file
407
app/modules/tenancy/services/permission_discovery_service.py
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
# 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",
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user