- 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>
21 KiB
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"andMerchant.owner_user_id User.is_merchant_ownerproperty returnsTrueUser.is_store_userproperty returnsTrue- Ownership checked via
User.is_owner_of(store_id)
Database:
# 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_userproperty returnsTrue- Permissions come from
StoreUser.role_id -> Role.permissions
Database:
# 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)
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
# 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
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
@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
@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)
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
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
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
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 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 403 Forbidden
{
"error_code": "STORE_OWNER_ONLY",
"message": "This operation requires store owner privileges",
"details": {
"operation": "team management",
"store_code": "orion"
}
}
Inactive Membership
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
- Use Constants: Always use
StorePermissions.PERMISSION_NAME.value - Least Privilege: Give team members minimum permissions needed
- Owner Only: Keep sensitive operations owner-only
- Custom Roles: Create custom roles for specific needs
- Regular Audit: Review team member permissions regularly
Custom Role Management
Overview
Store owners can create, edit, and delete custom roles with granular permission selection. Preset roles (manager, staff, support, viewer, marketing) cannot be deleted but can be edited.
Store Role CRUD API
All endpoints require store owner authentication.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/store/team/roles |
List all roles (creates defaults if none exist) |
POST |
/api/v1/store/team/roles |
Create a custom role |
PUT |
/api/v1/store/team/roles/{id} |
Update a role's name/permissions |
DELETE |
/api/v1/store/team/roles/{id} |
Delete a custom role |
Validation rules:
- Cannot create/rename a role to a preset name (manager, staff, etc.)
- Cannot delete preset roles
- Cannot delete a role with assigned team members
- All permission IDs are validated against the module-discovered permission catalog
Permission Catalog API
Returns all available permissions grouped by category, with human-readable labels and descriptions.
GET /api/v1/store/team/permissions/catalog
Required Permission: team.view
Response:
{
"categories": [
{
"id": "team",
"label": "tenancy.permissions.category.team",
"permissions": [
{
"id": "team.view",
"label": "tenancy.permissions.team_view",
"description": "tenancy.permissions.team_view_desc",
"is_owner_only": false
}
]
}
]
}
Permissions are discovered from all module definition.py files via PermissionDiscoveryService.get_permissions_by_category().
Role Editor UI
The role editor page at /store/{store_code}/team/roles:
- Lists all roles (preset + custom) with permission counts
- Modal for creating/editing roles with a permission matrix
- Permissions displayed with labels, IDs, and hover descriptions
- Category-level "Select All / Deselect All" toggles
- Owner-only permissions marked with an "Owner" badge
Alpine.js component: storeRoles() in app/modules/tenancy/static/store/js/roles.js
Menu location: Account section > Roles (requires team.view permission)
Admin Store Roles Management
Overview
Super admins and platform admins can manage roles for any store via the admin panel. Platform admins are scoped to stores within their assigned platforms.
Admin Role CRUD API
All endpoints require admin authentication (get_current_admin_api).
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/admin/store-roles?store_id=X |
List roles for a store |
GET |
/api/v1/admin/store-roles/permissions/catalog |
Permission catalog |
POST |
/api/v1/admin/store-roles?store_id=X |
Create a role |
PUT |
/api/v1/admin/store-roles/{id}?store_id=X |
Update a role |
DELETE |
/api/v1/admin/store-roles/{id}?store_id=X |
Delete a role |
Platform Admin Scoping
Platform admins can only access stores that belong to one of their assigned platforms:
# In StoreTeamService.validate_admin_store_access():
# 1. Super admin (accessible_platform_ids is None) → access all stores
# 2. Platform admin → store must exist in StorePlatform where
# platform_id is in the admin's accessible_platform_ids
The scoping is enforced at the service layer via validate_admin_store_access(), called by every admin endpoint before performing operations.
Admin UI
Page at /admin/store-roles:
- Tom Select store search/selector (shared
initStoreSelector()component) - Platform admins see only stores in their assigned platforms
- Same role CRUD and permission matrix as the store-side UI
- Located in the "User Management" admin menu section
Alpine.js component: adminStoreRoles() in app/modules/tenancy/static/admin/js/store-roles.js
Audit Trail
All role operations are logged via AuditAggregatorService:
| Action | Description |
|---|---|
role.create |
Custom role created |
role.update |
Role name or permissions modified |
role.delete |
Custom role deleted |
member.role_change |
Team member assigned a different role |
member.invite |
Team member invited |
member.remove |
Team member removed |
Key Files
| File | Purpose |
|---|---|
app/modules/tenancy/services/store_team_service.py |
Role CRUD, platform scoping, audit trail |
app/modules/tenancy/services/permission_discovery_service.py |
Permission catalog, role presets |
app/modules/tenancy/routes/api/store_team.py |
Store team & role API endpoints |
app/modules/tenancy/routes/api/admin_store_roles.py |
Admin store role API endpoints |
app/modules/tenancy/schemas/team.py |
Request/response schemas |
app/modules/tenancy/static/store/js/roles.js |
Store role editor Alpine.js component |
app/modules/tenancy/static/admin/js/store-roles.js |
Admin role editor Alpine.js component |
app/modules/tenancy/templates/tenancy/store/roles.html |
Store role editor template |
app/modules/tenancy/templates/tenancy/admin/store-roles.html |
Admin role editor template |
This RBAC system provides flexible, secure access control for store dashboards with clear separation between owners and team members.