- 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>
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
- Each subscription tier (Free, Starter, Pro, Enterprise) has a set of feature limits defined in the
TierFeatureLimittable. - Features are either binary (on/off) or quantitative (numeric cap).
- Modules declare their billable features via
FeatureProviderProtocol— a cross-module interface that lets each module own its feature definitions. - 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
- Modules are auto-discovered from
app/modules/*/definition.py. - Core modules (core, tenancy, cms, customers, billing, payments, messaging, contracts) are always enabled.
- Optional modules (catalog, orders, inventory, analytics, etc.) can be enabled/disabled per platform via the
PlatformModuletable. - 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
- Each module defines its menu items in
definition.pyusingMenuSectionDefinitionandMenuItemDefinition. MenuDiscoveryServiceaggregates items from all enabled modules and applies filtering.AdminMenuConfigstores visibility overrides using an opt-out model: all items visible by default, only hidden items stored in the database.- 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
-
Each module declares its permissions in
definition.pyusingPermissionDefinition:PermissionDefinition( id="products.view", label_key="catalog.permission.products_view", description_key="catalog.permission.products_view_desc", category="products", ) -
PermissionDiscoveryServiceaggregates permissions from all modules. -
Roles are collections of permission IDs, stored per-store in the
rolestable. -
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 — JWT auth, user roles, enforcement methods
- Store RBAC — Custom role CRUD, permission catalog API, admin role management
- Menu Management — Menu discovery, visibility config, AdminMenuConfig
- Module System — Module architecture, auto-discovery, classification
- Feature Gating — Tier-based feature limits