Move 39 documentation files from top-level docs/ into each module's docs/ folder, accessible via symlinks from docs/modules/. Create data-model.md files for 10 modules with full schema documentation. Replace originals with redirect stubs. Remove empty guide stubs. Modules migrated: tenancy, billing, loyalty, marketplace, orders, messaging, cms, catalog, inventory, hosting, prospecting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17 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 customerssettings.edit- Cannot change core settingssettings.domains- Cannot manage domainsteam.*- 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)
4. Support (6 permissions)
Use case: Customer support team
Has access to:
- Dashboard view
- Products (view only)
- Orders (view, edit)
- Customers (view, edit)
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)
6. Marketing (7 permissions)
Use case: Marketing team focused on campaigns
Has access to:
- Dashboard (view), Customers (view, export)
- Marketing (all), Reports (view)
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 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
)
)
):
# 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
)
)
):
# 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 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)
):
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. Deactivation
Owner deactivates member → StoreUser updated:
{
"is_active": False
}
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 |
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
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 |