# 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
