feat: implement complete RBAC access control with tests
Add 4-layer access control stack (subscription → module → menu → permissions): - P1: Wire requires_permission into menu sidebar filtering - P2: Expose window.USER_PERMISSIONS for Alpine.js client-side gating - P3: Add page-level permission guards on store routes - P4: Role CRUD API endpoints and role editor UI - P5: Audit trail for all role/permission changes Includes unit tests (menu permission filtering, role CRUD service) and integration tests (role API endpoints). All 404 core+tenancy tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
260
docs/architecture/access-control-stack.md
Normal file
260
docs/architecture/access-control-stack.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user