Files
orion/docs/backend/store-rbac.md
Samir Boulahtit 1dcb0e6c33
Some checks failed
CI / ruff (push) Successful in 11s
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 been cancelled
feat: RBAC Phase 1 — consolidate user roles into 4-value enum
Consolidate User.role (2-value: admin/store) + User.is_super_admin (boolean)
into a single 4-value UserRole enum: super_admin, platform_admin,
merchant_owner, store_member. Drop stale StoreUser.user_type column.
Fix role="user" bug in merchant creation.

Key changes:
- Expand UserRole enum from 2 to 4 values with computed properties
  (is_admin, is_super_admin, is_platform_admin, is_merchant_owner, is_store_user)
- Add Alembic migration (tenancy_003) for data migration + column drops
- Remove is_super_admin from JWT token payload
- Update all auth dependencies, services, routes, templates, JS, and tests
- Update all RBAC documentation

66 files changed, 1219 unit tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 22:44:29 +01:00

703 lines
16 KiB
Markdown

# Store RBAC System - Complete Guide
## Overview
The store dashboard implements a **Role-Based Access Control (RBAC)** system that distinguishes between **Owners** and **Team Members**, with granular permissions for team members.
---
## User Types
### 1. Merchant Owner
**Who:** The user who created/owns the store account.
**Characteristics:**
- Has **ALL permissions** automatically (no role needed)
- Cannot be removed or have permissions restricted
- Can invite team members
- Can create and manage roles
- Identified by `User.role = "merchant_owner"` and `Merchant.owner_user_id`
- `User.is_merchant_owner` property returns `True`
- `User.is_store_user` property returns `True`
- Ownership checked via `User.is_owner_of(store_id)`
**Database:**
```python
# User record for merchant owner
{
"id": 5,
"role": "merchant_owner", # Role on User model
}
# StoreUser record for owner
{
"store_id": 1,
"user_id": 5,
"role_id": None, # No role needed (owner has all perms)
"is_active": True
}
# Merchant record links ownership
{
"owner_user_id": 5 # Links to User.id
}
```
**Note:** The `user_type` column was removed from StoreUser. Ownership is determined by `User.role == "merchant_owner"` and `Merchant.owner_user_id`, not by a field on StoreUser.
**Permissions:**
-**All 75 permissions** (complete access)
- See full list below
---
### 2. Store Members (Team Members)
**Who:** Users invited by the merchant owner to help manage the store.
**Characteristics:**
- Have **limited permissions** based on assigned role
- Must be invited via email
- Invitation must be accepted before activation
- Can be assigned one of the pre-defined roles or custom role
- Identified by `User.role = "store_member"`
- `User.is_store_user` property returns `True`
- Permissions come from `StoreUser.role_id -> Role.permissions`
**Database:**
```python
# User record for store member
{
"id": 7,
"role": "store_member", # Role on User model
}
# StoreUser record for team member
{
"store_id": 1,
"user_id": 7,
"role_id": 3, # Role required for permission lookup
"is_active": True,
"invitation_token": None, # Accepted
"invitation_accepted_at": "2024-11-15 10:30:00"
}
# Role record
{
"id": 3,
"store_id": 1,
"name": "Manager",
"permissions": [
"dashboard.view",
"products.view",
"products.create",
"products.edit",
"orders.view",
...
]
}
```
**Permissions:**
- 🔒 **Limited** based on assigned role
- Can have between 0 and 75 permissions
- Common roles: Manager, Staff, Support, Viewer, Marketing
---
## Permission System
### All Available Permissions (75 total)
```python
class StorePermissions(str, Enum):
# Dashboard (1)
DASHBOARD_VIEW = "dashboard.view"
# Products (6)
PRODUCTS_VIEW = "products.view"
PRODUCTS_CREATE = "products.create"
PRODUCTS_EDIT = "products.edit"
PRODUCTS_DELETE = "products.delete"
PRODUCTS_IMPORT = "products.import"
PRODUCTS_EXPORT = "products.export"
# Stock/Inventory (3)
STOCK_VIEW = "stock.view"
STOCK_EDIT = "stock.edit"
STOCK_TRANSFER = "stock.transfer"
# Orders (4)
ORDERS_VIEW = "orders.view"
ORDERS_EDIT = "orders.edit"
ORDERS_CANCEL = "orders.cancel"
ORDERS_REFUND = "orders.refund"
# Customers (4)
CUSTOMERS_VIEW = "customers.view"
CUSTOMERS_EDIT = "customers.edit"
CUSTOMERS_DELETE = "customers.delete"
CUSTOMERS_EXPORT = "customers.export"
# Marketing (3)
MARKETING_VIEW = "marketing.view"
MARKETING_CREATE = "marketing.create"
MARKETING_SEND = "marketing.send"
# Reports (3)
REPORTS_VIEW = "reports.view"
REPORTS_FINANCIAL = "reports.financial"
REPORTS_EXPORT = "reports.export"
# Settings (4)
SETTINGS_VIEW = "settings.view"
SETTINGS_EDIT = "settings.edit"
SETTINGS_THEME = "settings.theme"
SETTINGS_DOMAINS = "settings.domains"
# Team Management (4)
TEAM_VIEW = "team.view"
TEAM_INVITE = "team.invite"
TEAM_EDIT = "team.edit"
TEAM_REMOVE = "team.remove"
# Marketplace Imports (3)
IMPORTS_VIEW = "imports.view"
IMPORTS_CREATE = "imports.create"
IMPORTS_CANCEL = "imports.cancel"
```
---
## Pre-Defined Roles
### 1. Owner (All 75 permissions)
**Use case:** Store owner (automatically assigned)
- ✅ Full access to everything
- ✅ Cannot be restricted
- ✅ No role record needed (permissions checked differently)
---
### 2. Manager (43 permissions)
**Use case:** Senior staff who manage most operations
**Has access to:**
- ✅ Dashboard, Products (all), Stock (all)
- ✅ Orders (all), Customers (view, edit, export)
- ✅ Marketing (all), Reports (all including financial)
- ✅ Settings (view, theme)
- ✅ Imports (all)
**Does NOT have:**
-`customers.delete` - Cannot delete customers
-`settings.edit` - Cannot change core settings
-`settings.domains` - Cannot manage domains
-`team.*` - Cannot manage team members
---
### 3. Staff (10 permissions)
**Use case:** Daily operations staff
**Has access to:**
- ✅ Dashboard view
- ✅ Products (view, create, edit)
- ✅ Stock (view, edit)
- ✅ Orders (view, edit)
- ✅ Customers (view, edit)
**Does NOT have:**
- ❌ Delete anything
- ❌ Import/export
- ❌ Marketing
- ❌ Financial reports
- ❌ Settings
- ❌ Team management
---
### 4. Support (6 permissions)
**Use case:** Customer support team
**Has access to:**
- ✅ Dashboard view
- ✅ Products (view only)
- ✅ Orders (view, edit)
- ✅ Customers (view, edit)
**Does NOT have:**
- ❌ Create/delete products
- ❌ Stock management
- ❌ Marketing
- ❌ Reports
- ❌ Settings
- ❌ Team management
---
### 5. Viewer (6 permissions)
**Use case:** Read-only access for reporting/audit
**Has access to:**
- ✅ Dashboard (view)
- ✅ Products (view)
- ✅ Stock (view)
- ✅ Orders (view)
- ✅ Customers (view)
- ✅ Reports (view)
**Does NOT have:**
- ❌ Edit anything
- ❌ Create/delete anything
- ❌ Marketing
- ❌ Financial reports
- ❌ Settings
- ❌ Team management
---
### 6. Marketing (7 permissions)
**Use case:** Marketing team focused on campaigns
**Has access to:**
- ✅ Dashboard (view)
- ✅ Customers (view, export)
- ✅ Marketing (all)
- ✅ Reports (view)
**Does NOT have:**
- ❌ Products management
- ❌ Orders management
- ❌ Stock management
- ❌ Financial reports
- ❌ Settings
- ❌ Team management
---
## Permission Checking Logic
### How Permissions Are Checked
```python
# In User model (models/database/user.py)
def has_store_permission(self, store_id: int, permission: str) -> bool:
"""Check if user has a specific permission in a store."""
# Step 1: Check if user is owner
if self.is_owner_of(store_id):
return True # ✅ Owners have ALL permissions
# Step 2: Check team member permissions
for vm in self.store_memberships:
if vm.store_id == store_id and vm.is_active:
if vm.role and permission in vm.role.permissions:
return True # ✅ Permission found in role
# No permission found
return False
```
### Permission Checking Flow
```
Request → Middleware → Extract store from URL
Check user authentication
Check if user is owner
├── YES → ✅ Allow (all permissions)
└── NO ↓
Check if user is team member
├── NO → ❌ Deny
└── YES ↓
Check if membership is active
├── NO → ❌ Deny
└── YES ↓
Check if role has required permission
├── NO → ❌ Deny (403 Forbidden)
└── YES → ✅ Allow
```
---
## Using Permissions in Code
### 1. Require Specific Permission
**When to use:** Endpoint needs one specific permission
```python
from fastapi import APIRouter, Depends
from app.api.deps import require_store_permission
from app.core.permissions import StorePermissions
from models.database.user import User
router = APIRouter()
@router.post("/products")
def create_product(
product_data: ProductCreate,
user: User = Depends(
require_store_permission(StorePermissions.PRODUCTS_CREATE.value)
)
):
"""
Create a product.
Required permission: products.create
✅ Owner: Always allowed
✅ Manager: Allowed (has products.create)
✅ Staff: Allowed (has products.create)
❌ Support: Denied (no products.create)
❌ Viewer: Denied (no products.create)
❌ Marketing: Denied (no products.create)
"""
# Create product...
pass
```
---
### 2. Require ANY Permission
**When to use:** Endpoint can be accessed with any of several permissions
```python
@router.get("/dashboard")
def view_dashboard(
user: User = Depends(
require_any_store_permission(
StorePermissions.DASHBOARD_VIEW.value,
StorePermissions.REPORTS_VIEW.value
)
)
):
"""
View dashboard.
Required: dashboard.view OR reports.view
✅ Owner: Always allowed
✅ Manager: Allowed (has both)
✅ Staff: Allowed (has dashboard.view)
✅ Support: Allowed (has dashboard.view)
✅ Viewer: Allowed (has both)
✅ Marketing: Allowed (has both)
"""
# Show dashboard...
pass
```
---
### 3. Require ALL Permissions
**When to use:** Endpoint needs multiple permissions
```python
@router.post("/products/bulk-delete")
def bulk_delete_products(
user: User = Depends(
require_all_store_permissions(
StorePermissions.PRODUCTS_VIEW.value,
StorePermissions.PRODUCTS_DELETE.value
)
)
):
"""
Bulk delete products.
Required: products.view AND products.delete
✅ Owner: Always allowed
✅ Manager: Allowed (has both)
❌ Staff: Denied (no products.delete)
❌ Support: Denied (no products.delete)
❌ Viewer: Denied (no products.delete)
❌ Marketing: Denied (no products.delete)
"""
# Delete products...
pass
```
---
### 4. Require Owner Only
**When to use:** Endpoint is owner-only (team management, critical settings)
```python
from app.api.deps import require_store_owner
@router.post("/team/invite")
def invite_team_member(
email: str,
role_id: int,
user: User = Depends(require_store_owner)
):
"""
Invite a team member.
Required: Must be store owner
✅ Owner: Allowed
❌ Manager: Denied (not owner)
❌ All team members: Denied (not owner)
"""
# Invite team member...
pass
```
---
### 5. Get User Permissions
**When to use:** Need to check permissions in business logic
```python
from app.api.deps import get_user_permissions
@router.get("/my-permissions")
def list_my_permissions(
permissions: list = Depends(get_user_permissions)
):
"""
Get all permissions for current user.
Returns:
- Owner: All 75 permissions
- Team Member: Permissions from their role
"""
return {"permissions": permissions}
```
---
## Database Schema
### StoreUser Table
```sql
CREATE TABLE store_users (
id SERIAL PRIMARY KEY,
store_id INTEGER NOT NULL REFERENCES stores(id),
user_id INTEGER NOT NULL REFERENCES users(id),
role_id INTEGER REFERENCES roles(id), -- NULL for merchant owners
invited_by INTEGER REFERENCES users(id),
invitation_token VARCHAR,
invitation_sent_at TIMESTAMP,
invitation_accepted_at TIMESTAMP,
is_active BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Note: user_type column removed. Ownership determined by
-- User.role = 'merchant_owner' and Merchant.owner_user_id.
```
### Role Table
```sql
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
store_id INTEGER NOT NULL REFERENCES stores(id),
name VARCHAR(100) NOT NULL,
permissions JSON DEFAULT '[]', -- Array of permission strings
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
---
## Team Member Lifecycle
### 1. Invitation
```
Merchant owner invites user:
→ User created with role="store_member"
→ StoreUser created:
{
"role_id": 3, -- Assigned role
"is_active": False,
"invitation_token": "abc123...",
"invitation_sent_at": "2024-11-29 10:00:00",
"invitation_accepted_at": null
}
```
### 2. Acceptance
```
User accepts invitation → StoreUser updated:
{
"is_active": True,
"invitation_token": null,
"invitation_accepted_at": "2024-11-29 10:30:00"
}
```
### 3. Active Member
```
Member can now access store dashboard with role permissions
```
### 4. Deactivation
```
Owner deactivates member → StoreUser updated:
{
"is_active": False
}
```
---
## Common Use Cases
### Use Case 1: Dashboard Access
**Q:** Can all users access the dashboard?
**A:** Yes, if they have `dashboard.view` permission.
- ✅ Owner: Always
- ✅ Manager, Staff, Support, Viewer, Marketing: All have it
- ❌ Custom role without `dashboard.view`: No
---
### Use Case 2: Product Management
**Q:** Who can create products?
**A:** Users with `products.create` permission.
- ✅ Owner: Always
- ✅ Manager: Yes (has permission)
- ✅ Staff: Yes (has permission)
- ❌ Support, Viewer, Marketing: No
---
### Use Case 3: Financial Reports
**Q:** Who can view financial reports?
**A:** Users with `reports.financial` permission.
- ✅ Owner: Always
- ✅ Manager: Yes (has permission)
- ❌ Staff, Support, Viewer, Marketing: No
---
### Use Case 4: Team Management
**Q:** Who can invite team members?
**A:** Only the store owner.
- ✅ Owner: Yes (owner-only operation)
- ❌ All team members (including Manager): No
---
### Use Case 5: Settings Changes
**Q:** Who can change store settings?
**A:** Users with `settings.edit` permission.
- ✅ Owner: Always
- ❌ Manager: No (doesn't have permission)
- ❌ All other roles: No
---
## Error Responses
### Missing Permission
```http
HTTP 403 Forbidden
{
"error_code": "INSUFFICIENT_STORE_PERMISSIONS",
"message": "You don't have permission to perform this action",
"details": {
"required_permission": "products.delete",
"store_code": "orion"
}
}
```
### Not Owner
```http
HTTP 403 Forbidden
{
"error_code": "STORE_OWNER_ONLY",
"message": "This operation requires store owner privileges",
"details": {
"operation": "team management",
"store_code": "orion"
}
}
```
### Inactive Membership
```http
HTTP 403 Forbidden
{
"error_code": "INACTIVE_STORE_MEMBERSHIP",
"message": "Your store membership is inactive"
}
```
---
## Summary
### Merchant Owner vs Store Member
| Feature | Merchant Owner (`role="merchant_owner"`) | Store Member (`role="store_member"`) |
|---------|-------|-------------|
| **Permissions** | All 75 (automatic) | Based on role (0-75) |
| **Role Required** | No | Yes |
| **Can Be Removed** | No | Yes |
| **Team Management** | Yes | No |
| **Critical Settings** | Yes | No (usually) |
| **Invitation Required** | No (creates store) | Yes |
| **Ownership Determined By** | `Merchant.owner_user_id` | N/A |
### Permission Hierarchy
```
Merchant Owner (75 permissions)
└─ Manager (43 permissions)
└─ Staff (10 permissions)
└─ Support (6 permissions)
└─ Viewer (6 permissions, read-only)
Marketing (7 permissions, specialized)
```
### Best Practices
1. **Use Constants:** Always use `StorePermissions.PERMISSION_NAME.value`
2. **Least Privilege:** Give team members minimum permissions needed
3. **Owner Only:** Keep sensitive operations owner-only
4. **Custom Roles:** Create custom roles for specific needs
5. **Regular Audit:** Review team member permissions regularly
---
This RBAC system provides flexible, secure access control for store dashboards with clear separation between owners and team members.