# Plan: Flexible Role & Permission Management with Platform Controls ## Status: READY FOR APPROVAL ## Summary Design a flexible role/permission management system that: 1. **Modules define permissions** - Each module declares its available permissions 2. **Platforms control availability** - Platforms can restrict which permissions stores can use 3. **Stores customize roles** - Stores create custom roles within platform constraints 4. **Multi-tier hierarchy** - Platform → Store → User permission inheritance --- ## Current State Analysis ### What Exists Today | Component | Location | Description | |-----------|----------|-------------| | **Role Model** | `app/modules/tenancy/models/store.py` | `store_id`, `name`, `permissions` (JSON array) | | **StoreUser Model** | Same file | Links user → store with `role_id` | | **PermissionDiscoveryService** | `app/modules/tenancy/services/permission_discovery_service.py` | Discovers permissions from modules | | **StoreTeamService** | `app/modules/tenancy/services/store_team_service.py` | Manages team invitations, role assignment | | **Role Presets** | In discovery service code | Hardcoded `ROLE_PRESETS` dict | | **Platform Model** | `models/database/platform.py` | Multi-platform support | | **PlatformModule** | `models/database/platform_module.py` | Controls which modules are enabled per platform | | **StorePlatform** | `models/database/store_platform.py` | Store-platform relationship with `tier_id` | ### Current Gaps 1. **No platform-level permission control** - Platforms cannot restrict which permissions stores can assign 2. **No custom role CRUD API** - Roles are created implicitly when inviting team members 3. **Presets are code-only** - Cannot customize role templates per platform 4. **No role templates table** - Platform admins cannot define default roles for their stores --- ## Proposed Architecture ### Tier 1: Module-Defined Permissions (Exists) Each module declares permissions in `definition.py`: ```python permissions=[ PermissionDefinition( id="products.view", label_key="catalog.permissions.products_view", category="products", ), PermissionDefinition( id="products.create", label_key="catalog.permissions.products_create", category="products", ), ] ``` **Discovery Service** aggregates all permissions at runtime. ### Tier 2: Platform Permission Control (New) New `PlatformPermissionConfig` model to control: - Which permissions are available to stores on this platform - Default role templates for store onboarding - Permission bundles based on subscription tier ``` ┌─────────────────────────────────────────────────────────────────┐ │ PLATFORM │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ PlatformPermissionConfig │ │ │ │ - platform_id │ │ │ │ - allowed_permissions: ["products.*", "orders.*"] │ │ │ │ - blocked_permissions: ["team.manage"] │ │ │ │ - tier_restrictions: {free: [...], pro: [...]} │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ PlatformRoleTemplate │ │ │ │ - platform_id │ │ │ │ - name: "Manager", "Staff", etc. │ │ │ │ - permissions: [...] │ │ │ │ - is_default: bool (create for new stores) │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Tier 3: Store Role Customization (Enhanced) Stores can: - View roles available (from platform templates or custom) - Create custom roles (within platform constraints) - Edit role permissions (within allowed set) - Assign roles to team members ``` ┌─────────────────────────────────────────────────────────────────┐ │ STORE │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Role (existing model, enhanced) │ │ │ │ - store_id │ │ │ │ - name │ │ │ │ - permissions: [...] (validated against platform) │ │ │ │ - is_from_template: bool │ │ │ │ - source_template_id: FK (nullable) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ StoreUser (existing, unchanged) │ │ │ │ - user_id │ │ │ │ - store_id │ │ │ │ - role_id │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Data Model Changes ### New Models #### 1. PlatformPermissionConfig ```python # app/modules/tenancy/models/platform_permission_config.py class PlatformPermissionConfig(Base): """Platform-level permission configuration""" __tablename__ = "platform_permission_configs" id: Mapped[int] = mapped_column(primary_key=True) platform_id: Mapped[int] = mapped_column(ForeignKey("platforms.id"), unique=True) # Permissions this platform allows stores to use # Empty = all discovered permissions allowed allowed_permissions: Mapped[list[str]] = mapped_column(JSON, default=list) # Explicit blocklist (takes precedence over allowed) blocked_permissions: Mapped[list[str]] = mapped_column(JSON, default=list) # Tier-based restrictions: {"free": ["products.view"], "pro": ["products.*"]} tier_permissions: Mapped[dict] = mapped_column(JSON, default=dict) created_at: Mapped[datetime] = mapped_column(default=func.now()) updated_at: Mapped[datetime] = mapped_column(onupdate=func.now()) # Relationships platform: Mapped["Platform"] = relationship(back_populates="permission_config") ``` #### 2. PlatformRoleTemplate ```python # app/modules/tenancy/models/platform_role_template.py class PlatformRoleTemplate(Base): """Role templates defined at platform level""" __tablename__ = "platform_role_templates" id: Mapped[int] = mapped_column(primary_key=True) platform_id: Mapped[int] = mapped_column(ForeignKey("platforms.id")) name: Mapped[str] = mapped_column(String(50)) # "Manager", "Staff", etc. display_name: Mapped[str] = mapped_column(String(100)) # i18n key or display name description: Mapped[str | None] = mapped_column(String(255)) # Permissions for this template permissions: Mapped[list[str]] = mapped_column(JSON, default=list) # Configuration is_default: Mapped[bool] = mapped_column(default=False) # Auto-create for new stores is_system: Mapped[bool] = mapped_column(default=False) # Cannot be deleted order: Mapped[int] = mapped_column(default=100) # Display order created_at: Mapped[datetime] = mapped_column(default=func.now()) updated_at: Mapped[datetime] = mapped_column(onupdate=func.now()) # Relationships platform: Mapped["Platform"] = relationship(back_populates="role_templates") __table_args__ = ( UniqueConstraint("platform_id", "name", name="uq_platform_role_template"), ) ``` ### Enhanced Existing Models #### Role Model Enhancement ```python # Add to existing Role model in app/modules/tenancy/models/store.py class Role(Base): # ... existing fields ... # NEW: Track template origin source_template_id: Mapped[int | None] = mapped_column( ForeignKey("platform_role_templates.id"), nullable=True ) is_custom: Mapped[bool] = mapped_column(default=False) # Store-created custom role # Relationship source_template: Mapped["PlatformRoleTemplate"] = relationship() ``` --- ## Service Layer Changes ### 1. PlatformPermissionService (New) ```python # app/modules/tenancy/services/platform_permission_service.py class PlatformPermissionService: """Manages platform-level permission configuration""" def get_allowed_permissions( self, db: Session, platform_id: int, tier_id: int | None = None ) -> set[str]: """ Get permissions allowed for a platform/tier combination. 1. Start with all discovered permissions 2. Filter by platform's allowed_permissions (if set) 3. Remove blocked_permissions 4. Apply tier restrictions (if tier_id provided) """ pass def validate_permissions( self, db: Session, platform_id: int, tier_id: int | None, permissions: list[str] ) -> tuple[list[str], list[str]]: """ Validate permissions against platform constraints. Returns (valid_permissions, invalid_permissions) """ pass def update_platform_config( self, db: Session, platform_id: int, allowed_permissions: list[str] | None = None, blocked_permissions: list[str] | None = None, tier_permissions: dict | None = None ) -> PlatformPermissionConfig: """Update platform permission configuration""" pass ``` ### 2. PlatformRoleTemplateService (New) ```python # app/modules/tenancy/services/platform_role_template_service.py class PlatformRoleTemplateService: """Manages platform role templates""" def get_templates(self, db: Session, platform_id: int) -> list[PlatformRoleTemplate]: """Get all role templates for a platform""" pass def create_template( self, db: Session, platform_id: int, name: str, permissions: list[str], is_default: bool = False ) -> PlatformRoleTemplate: """Create a new role template (validates permissions)""" pass def create_default_roles_for_store( self, db: Session, store: Store ) -> list[Role]: """ Create store roles from platform's default templates. Called during store onboarding. """ pass def seed_default_templates(self, db: Session, platform_id: int): """Seed platform with standard role templates (Manager, Staff, etc.)""" pass ``` ### 3. Enhanced StoreTeamService ```python # Updates to app/modules/tenancy/services/store_team_service.py class StoreTeamService: def get_available_permissions( self, db: Session, store: Store ) -> list[PermissionDefinition]: """ Get permissions available to this store based on: 1. Platform constraints 2. Store's subscription tier """ platform_perm_service = PlatformPermissionService() store_platform = db.query(StorePlatform).filter(...).first() allowed = platform_perm_service.get_allowed_permissions( db, store_platform.platform_id, store_platform.tier_id ) # Return PermissionDefinitions filtered to allowed set all_perms = permission_discovery_service.get_all_permissions() return [p for p in all_perms if p.id in allowed] def create_custom_role( self, db: Session, store: Store, name: str, permissions: list[str] ) -> Role: """ Create a custom role for the store. Validates permissions against platform constraints. """ # Validate permissions valid, invalid = self.platform_permission_service.validate_permissions( db, store.platform_id, store.tier_id, permissions ) if invalid: raise InvalidPermissionsException(invalid) role = Role( store_id=store.id, name=name, permissions=valid, is_custom=True ) db.add(role) return role def update_role( self, db: Session, store: Store, role_id: int, name: str | None = None, permissions: list[str] | None = None ) -> Role: """Update an existing role (validates permissions)""" pass def delete_role( self, db: Session, store: Store, role_id: int ) -> bool: """Delete a custom role (cannot delete if in use)""" pass ``` --- ## API Endpoints ### Platform Admin Endpoints (Admin Panel) ```python # app/modules/tenancy/routes/admin/platform_permissions.py @router.get("/platforms/{platform_id}/permissions") def get_platform_permission_config(platform_id: int): """Get platform permission configuration""" @router.put("/platforms/{platform_id}/permissions") def update_platform_permission_config(platform_id: int, config: PermissionConfigUpdate): """Update platform permission configuration""" @router.get("/platforms/{platform_id}/role-templates") def list_role_templates(platform_id: int): """List role templates for a platform""" @router.post("/platforms/{platform_id}/role-templates") def create_role_template(platform_id: int, template: RoleTemplateCreate): """Create a new role template""" @router.put("/platforms/{platform_id}/role-templates/{template_id}") def update_role_template(platform_id: int, template_id: int, template: RoleTemplateUpdate): """Update a role template""" @router.delete("/platforms/{platform_id}/role-templates/{template_id}") def delete_role_template(platform_id: int, template_id: int): """Delete a role template""" ``` ### Store Dashboard Endpoints ```python # app/modules/tenancy/routes/api/store_roles.py @router.get("/roles") def list_store_roles(): """List all roles for current store""" @router.post("/roles") def create_custom_role(role: RoleCreate): """Create a custom role (validates permissions against platform)""" @router.put("/roles/{role_id}") def update_role(role_id: int, role: RoleUpdate): """Update a role""" @router.delete("/roles/{role_id}") def delete_role(role_id: int): """Delete a custom role""" @router.get("/available-permissions") def get_available_permissions(): """Get permissions available to this store (filtered by platform/tier)""" ``` --- ## Permission Flow Diagram ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ PERMISSION FLOW │ └─────────────────────────────────────────────────────────────────────────────┘ 1. MODULE DEFINES PERMISSIONS ┌──────────────┐ │ catalog │ → products.view, products.create, products.edit, ... │ orders │ → orders.view, orders.manage, orders.refund, ... │ team │ → team.view, team.manage, team.invite, ... └──────────────┘ ↓ 2. DISCOVERY SERVICE AGGREGATES ┌────────────────────────────────────────────────┐ │ PermissionDiscoveryService │ │ get_all_permissions() → 50+ permissions │ └────────────────────────────────────────────────┘ ↓ 3. PLATFORM FILTERS PERMISSIONS ┌────────────────────────────────────────────────┐ │ PlatformPermissionConfig │ │ allowed: ["products.*", "orders.view"] │ │ blocked: ["orders.refund"] │ │ tier_permissions: │ │ free: ["products.view", "orders.view"] │ │ pro: ["products.*", "orders.*"] │ └────────────────────────────────────────────────┘ ↓ 4. STORE CREATES/USES ROLES ┌────────────────────────────────────────────────┐ │ Role (store-specific) │ │ Manager: [products.*, orders.view] │ │ Staff: [products.view, orders.view] │ └────────────────────────────────────────────────┘ ↓ 5. USER GETS PERMISSIONS VIA ROLE ┌────────────────────────────────────────────────┐ │ StoreUser │ │ user_id: 123 │ │ role_id: 5 (Staff) │ │ → permissions: [products.view, orders.view] │ └────────────────────────────────────────────────┘ ``` --- ## Database Migrations ### Migration 1: Add Platform Permission Config ```sql CREATE TABLE platform_permission_configs ( id SERIAL PRIMARY KEY, platform_id INTEGER NOT NULL UNIQUE REFERENCES platforms(id), allowed_permissions JSONB DEFAULT '[]', blocked_permissions JSONB DEFAULT '[]', tier_permissions JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP ); ``` ### Migration 2: Add Platform Role Templates ```sql CREATE TABLE platform_role_templates ( id SERIAL PRIMARY KEY, platform_id INTEGER NOT NULL REFERENCES platforms(id), name VARCHAR(50) NOT NULL, display_name VARCHAR(100) NOT NULL, description VARCHAR(255), permissions JSONB DEFAULT '[]', is_default BOOLEAN DEFAULT FALSE, is_system BOOLEAN DEFAULT FALSE, "order" INTEGER DEFAULT 100, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP, UNIQUE (platform_id, name) ); ``` ### Migration 3: Enhance Roles Table ```sql ALTER TABLE roles ADD COLUMN source_template_id INTEGER REFERENCES platform_role_templates(id), ADD COLUMN is_custom BOOLEAN DEFAULT FALSE; ``` --- ## Implementation Phases ### Phase 1: Data Models (Foundation) **Files to create:** - `app/modules/tenancy/models/platform_permission_config.py` - `app/modules/tenancy/models/platform_role_template.py` - `migrations/versions/xxx_add_platform_permission_tables.py` - `migrations/versions/xxx_enhance_roles_table.py` **Files to modify:** - `app/modules/tenancy/models/__init__.py` - Export new models - `app/modules/tenancy/models/store.py` - Add Role enhancement ### Phase 2: Service Layer **Files to create:** - `app/modules/tenancy/services/platform_permission_service.py` - `app/modules/tenancy/services/platform_role_template_service.py` **Files to modify:** - `app/modules/tenancy/services/store_team_service.py` - Add role CRUD, permission validation ### Phase 3: API Endpoints **Files to create:** - `app/modules/tenancy/routes/admin/platform_permissions.py` - `app/modules/tenancy/routes/api/store_roles.py` - `app/modules/tenancy/schemas/platform_permissions.py` - `app/modules/tenancy/schemas/roles.py` **Files to modify:** - `app/modules/tenancy/routes/__init__.py` - Register new routers ### Phase 4: Store Onboarding Integration **Files to modify:** - `app/modules/tenancy/services/store_service.py` - Create default roles from templates during store creation ### Phase 5: Admin UI (Optional, Future) **Files to create/modify:** - Admin panel for platform permission configuration - Admin panel for role template management - Store dashboard for custom role management --- ## Verification 1. **App loads:** `python -c "from main import app; print('OK')"` 2. **Migrations run:** `make migrate-up` 3. **Architecture validation:** `python scripts/validate/validate_architecture.py -v` 4. **Unit tests:** Test permission filtering logic - Platform with no config → all permissions allowed - Platform with allowed list → only those permissions - Platform with blocked list → all except blocked - Tier restrictions → correct subset per tier 5. **Integration tests:** - Create store → gets default roles from platform templates - Create custom role → validates against platform constraints - Assign role → user gets correct permissions - Change tier → available permissions update 6. **API tests:** - Platform admin can configure permissions - Store owner can create/edit custom roles - Invalid permissions are rejected --- ## Files Summary ### New Files (9) | File | Purpose | |------|---------| | `app/modules/tenancy/models/platform_permission_config.py` | Platform permission config model | | `app/modules/tenancy/models/platform_role_template.py` | Platform role template model | | `app/modules/tenancy/services/platform_permission_service.py` | Platform permission logic | | `app/modules/tenancy/services/platform_role_template_service.py` | Role template logic | | `app/modules/tenancy/routes/admin/platform_permissions.py` | Admin API endpoints | | `app/modules/tenancy/routes/api/store_roles.py` | Store API endpoints | | `app/modules/tenancy/schemas/platform_permissions.py` | Pydantic schemas | | `app/modules/tenancy/schemas/roles.py` | Role schemas | | `migrations/versions/xxx_platform_permission_tables.py` | Database migration | ### Modified Files (5) | File | Changes | |------|---------| | `app/modules/tenancy/models/__init__.py` | Export new models | | `app/modules/tenancy/models/store.py` | Enhance Role model | | `app/modules/tenancy/services/store_team_service.py` | Add role CRUD, validation | | `app/modules/tenancy/services/store_service.py` | Create default roles on store creation | | `app/modules/tenancy/routes/__init__.py` | Register new routers | --- ## Key Design Decisions 1. **Wildcard support in permissions** - `products.*` matches `products.view`, `products.create`, etc. 2. **Tier inheritance** - Higher tiers include all permissions of lower tiers 3. **Template-based store roles** - Default roles created from platform templates, but store can customize 4. **Soft validation** - Invalid permissions in existing roles are not automatically removed (audit trail) 5. **Backward compatible** - Existing roles without `source_template_id` continue to work