Files
orion/app/modules/tenancy/docs/rbac.md
Samir Boulahtit f141cc4e6a docs: migrate module documentation to single source of truth
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>
2026-03-08 23:38:37 +01:00

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" 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:

# 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:

# 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)

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

  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

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