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

15 KiB

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

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:

    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