Files
orion/docs/backend/store-rbac.md
Samir Boulahtit f95db7c0b1
Some checks failed
CI / ruff (push) Successful in 9s
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 started running
feat(roles): add admin store roles page, permission i18n, and menu integration
- 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>
2026-02-26 23:31:27 +01:00

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

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

  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

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.