Files
orion/docs/architecture/access-control-stack.md
Samir Boulahtit f95db7c0b1
Some checks failed
CI / ruff (push) Successful in 9s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
feat(roles): add admin store roles page, permission i18n, and menu integration
- Add admin store roles page with merchant→store cascading for superadmin
  and store-only selection for platform admin
- Add permission catalog API with translated labels/descriptions (en/fr/de/lb)
- Add permission translations to all 15 module locale files (60 files total)
- Add info icon tooltips for permission descriptions in role editor
- Add store roles menu item and admin menu item in module definition
- Fix store-selector.js URL construction bug when apiEndpoint has query params
- Add admin store roles API (CRUD + platform scoping)
- Add integration tests for admin store roles and permission catalog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:31:27 +01:00

274 lines
15 KiB
Markdown

# Complete Access Control Stack
The Orion platform enforces access control through a **4-layer stack**. Each layer filters at a different level, and they compose together to determine what a user can see and do.
> **Think of it as:** subscription controls **WHAT** you can do, modules control **WHERE** it exists, menu config controls **WHAT's shown**, permissions control **WHO** can do it.
## Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ INCOMING REQUEST │
└──────────────────────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 1: SUBSCRIPTION GATING "What you can do" │
│ │
│ TierFeatureLimit → binary/quantitative caps per subscription tier │
│ MerchantFeatureOverride → per-merchant exceptions │
│ FeatureService.check_resource_limit() → enforcement point │
│ │
│ Example: Free tier → 50 products max. Pro tier → unlimited. │
│ Example: Binary feature "advanced_analytics" → on/off per tier. │
└──────────────────────────────────┬──────────────────────────────────────────┘
│ allowed
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 2: MODULE ENABLEMENT "Where it exists" │
│ │
│ PlatformModule table → per-platform module on/off │
│ Core modules always enabled; optional modules toggled per platform │
│ Auto-discovered from app/modules/*/definition.py │
│ │
│ Example: OMS platform has catalog + orders enabled. │
│ Example: Loyalty platform has loyalty + analytics but no inventory. │
└──────────────────────────────────┬──────────────────────────────────────────┘
│ module enabled
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 3: MENU VISIBILITY "What's shown" │
│ │
│ AdminMenuConfig → opt-out model (only hidden items stored) │
│ Platform scope → applies to all users on platform │
│ User scope → personal preference (super admins only) │
│ MenuDiscoveryService → filtering pipeline │
│ │
│ Example: Platform admin hides "code-quality" from store sidebar. │
│ Example: Mandatory items (dashboard) cannot be hidden. │
└──────────────────────────────────┬──────────────────────────────────────────┘
│ visible
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 4: ROLE PERMISSIONS "Who can do it" │
│ │
│ Module-declared permissions via PermissionDefinition │
│ Role presets: owner, manager, staff, support, viewer, marketing │
│ Per-store roles via StoreUser.role_id → Role.permissions │
│ Owner bypass: Merchant.owner_user_id gets all permissions │
│ │
│ Example: "viewer" role → can see products but not edit them. │
│ Example: Owner sees everything, no role record needed. │
└──────────────────────────────────┬──────────────────────────────────────────┘
│ permitted
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER SEES / DOES THE THING │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Layer 1: Subscription Gating
Subscription gating controls **what a store can do** based on its merchant's subscription tier.
### How It Works
1. Each subscription tier (Free, Starter, Pro, Enterprise) has a set of **feature limits** defined in the `TierFeatureLimit` table.
2. Features are either **binary** (on/off) or **quantitative** (numeric cap).
3. Modules declare their billable features via `FeatureProviderProtocol` — a cross-module interface that lets each module own its feature definitions.
4. Admins can override limits per-merchant using `MerchantFeatureOverride`.
### Enforcement
```python
from app.modules.billing.services.feature_service import feature_service
# Check before allowing an action
allowed, message = feature_service.check_resource_limit(
db=db,
feature_code="products",
store_id=store.id,
)
if not allowed:
raise TierLimitExceededException(message)
```
### Key Models
| Model | Table | Purpose |
|-------|-------|---------|
| `TierFeatureLimit` | `tier_feature_limits` | Per-tier feature caps (binary/quantitative) |
| `MerchantFeatureOverride` | `merchant_feature_overrides` | Per-merchant exceptions to tier limits |
### Resolution Order
```
MerchantFeatureOverride (if exists) → TierFeatureLimit → denied
```
If a merchant has an override for a feature, it takes precedence over the tier default.
## Layer 2: Module Enablement
Module enablement controls **where functionality exists** at the platform level.
### How It Works
1. Modules are auto-discovered from `app/modules/*/definition.py`.
2. **Core modules** (core, tenancy, cms, customers, billing, payments, messaging, contracts) are always enabled.
3. **Optional modules** (catalog, orders, inventory, analytics, etc.) can be enabled/disabled per platform via the `PlatformModule` table.
4. When a module is disabled, its routes, menu items, and features are excluded from the platform.
### Key Model
| Model | Table | Purpose |
|-------|-------|---------|
| `PlatformModule` | `platform_modules` | Junction table: which modules are enabled per platform |
### Impact on Other Layers
- **Menu items** from disabled modules are automatically excluded by `MenuDiscoveryService`
- **Features** from disabled modules are not available for subscription gating
- **Permissions** from disabled modules are not shown in role management
## Layer 3: Menu Visibility
Menu visibility controls **what's shown** in the sidebar for admin and store interfaces.
### How It Works
1. Each module defines its menu items in `definition.py` using `MenuSectionDefinition` and `MenuItemDefinition`.
2. `MenuDiscoveryService` aggregates items from all enabled modules and applies filtering.
3. `AdminMenuConfig` stores **visibility overrides** using an opt-out model: all items visible by default, only hidden items stored in the database.
4. Mandatory items (`is_mandatory=True`) cannot be hidden.
### Filtering Pipeline
```
All Module Menu Items
→ Remove items from disabled modules (Layer 2)
→ Remove super_admin_only items for non-super-admins
→ Remove items hidden via AdminMenuConfig
→ Remove items requiring permissions the user lacks (Layer 4)
= Final visible menu
```
### Scope
| Scope | Who it affects | Frontend |
|-------|----------------|----------|
| Platform | All platform admins and stores on that platform | Admin + Store |
| User | Individual super admin | Admin only |
### Key Model
| Model | Table | Purpose |
|-------|-------|---------|
| `AdminMenuConfig` | `admin_menu_configs` | Visibility overrides per platform or user |
## Layer 4: Role Permissions
Role permissions control **who can do what** at the store level.
### How It Works
1. Each module declares its permissions in `definition.py` using `PermissionDefinition`:
```python
PermissionDefinition(
id="products.view",
label_key="catalog.permission.products_view",
description_key="catalog.permission.products_view_desc",
category="products",
)
```
2. `PermissionDiscoveryService` aggregates permissions from all modules.
3. Roles are collections of permission IDs, stored per-store in the `roles` table.
4. Store team members are linked to roles via `StoreUser.role_id`.
### Owner Bypass
Store owners (`Merchant.owner_user_id`) automatically receive **all permissions** without needing a role record. Ownership is checked via `User.is_owner_of(store_id)`.
### Role Presets
The system provides 5 preset roles with predefined permission sets:
| Preset | Description | Permission Count |
|--------|-------------|-----------------|
| `manager` | Full operational access | ~23 |
| `staff` | Day-to-day operations | ~10 |
| `support` | Customer service focus | ~6 |
| `viewer` | Read-only access | ~6 |
| `marketing` | Marketing and customer data | ~7 |
Preset roles are created automatically on first access. Store owners can also create custom roles with any combination of the available permissions via the role editor UI at `/store/{store_code}/team/roles`.
### Custom Role Management
Store owners can create, edit, and delete custom roles via:
- **Store UI:** `/store/{store_code}/team/roles` (Alpine.js permission matrix)
- **Store API:** `POST/PUT/DELETE /api/v1/store/team/roles`
- **Admin UI:** `/admin/store-roles` (with Tom Select store picker)
- **Admin API:** `GET/POST/PUT/DELETE /api/v1/admin/store-roles?store_id=X`
The **Permission Catalog API** (`GET /api/v1/store/team/permissions/catalog`) returns all permissions grouped by category with labels and descriptions for the UI.
Admin access is scoped: **super admins** can manage any store, while **platform admins** can only manage stores within their assigned platforms (validated via `StorePlatform` table).
### Enforcement Points
| Where | How | Pattern |
|-------|-----|---------|
| **API routes** | `require_store_permission("products.view")` | FastAPI `Depends()` — returns 403 JSON |
| **Page routes** | `require_store_page_permission("products.view")` | FastAPI `Depends()` — redirects to no-access page |
| **Sidebar menu** | `MenuItemDefinition.requires_permission` | Items hidden if user lacks permission |
| **Template UI** | `window.USER_PERMISSIONS` | Alpine.js `x-show` for button/element hiding |
### Key Models
| Model | Table | Purpose |
|-------|-------|---------|
| `Role` | `roles` | Permission set per store (preset or custom) |
| `StoreUser` | `store_users` | Links user to store with role assignment |
### Per-Store Flexibility
A user can have **different roles in different stores**. For example, a user might be a `manager` in Store A but a `viewer` in Store B, because `StoreUser` records are per-store.
## How the Layers Interact
### Example: Store team member tries to view products
```
1. SUBSCRIPTION: Does the merchant's tier include "products" feature?
→ Yes (tier allows up to 200 products) → Continue
→ No → "Upgrade your plan to access products"
2. MODULE: Is the catalog module enabled on this platform?
→ Yes (OMS platform has catalog enabled) → Continue
→ No → Products section doesn't exist at all
3. MENU VISIBILITY: Is the products menu item visible?
→ Yes (not hidden in AdminMenuConfig) → Show in sidebar
→ No → Item hidden from sidebar (but URL still works)
4. PERMISSIONS: Does the user's role include "products.view"?
→ Yes (user has "staff" role with products.view) → Allow access
→ No → 403 Forbidden / redirect to no-access page
```
### Key Distinction
- **Layers 1-3** are about **platform/store configuration** — they define the environment
- **Layer 4** is about **individual user authorization** — it defines what each person can do within that environment
## Related Documentation
- [Authentication & RBAC](auth-rbac.md) — JWT auth, user roles, enforcement methods
- [Store RBAC](../backend/store-rbac.md) — Custom role CRUD, permission catalog API, admin role management
- [Menu Management](menu-management.md) — Menu discovery, visibility config, AdminMenuConfig
- [Module System](module-system.md) — Module architecture, auto-discovery, classification
- [Feature Gating](../implementation/feature-gating-system.md) — Tier-based feature limits