# 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 ~75 available permissions. ### 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 - [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