Files
orion/docs/architecture/auth-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

19 KiB

Authentication & Role-Based Access Control (RBAC)

Complete guide to the authentication and authorization system powering the multi-tenant platform.

Overview

The platform uses a JWT-based authentication system combined with role-based access control (RBAC) to secure all interfaces:

  • Admin interface
  • Store dashboard
  • Shop storefront
  • REST API endpoints

Authentication System

Technology Stack

  • JWT (JSON Web Tokens): Stateless authentication
  • bcrypt: Secure password hashing
  • Jose: JWT encoding/decoding library
  • FastAPI Security: OAuth2 password bearer flow

Authentication Flow

sequenceDiagram
    participant Client
    participant API
    participant AuthManager
    participant Database

    Client->>API: POST /api/v1/auth/login<br/>{username, password}
    API->>AuthManager: authenticate_user()
    AuthManager->>Database: Query user by username/email
    Database-->>AuthManager: User record
    AuthManager->>AuthManager: verify_password()
    AuthManager-->>API: User object
    API->>AuthManager: create_access_token()
    AuthManager-->>API: JWT token
    API-->>Client: {access_token, token_type, expires_in}

    Note over Client: Store token

    Client->>API: GET /api/v1/resource<br/>Authorization: Bearer <token>
    API->>AuthManager: verify_token()
    AuthManager->>AuthManager: Decode JWT
    AuthManager->>Database: Query user by ID
    Database-->>AuthManager: User object
    AuthManager-->>API: Current user
    API->>API: Process request
    API-->>Client: Resource data

User Roles

The platform uses a 4-value role enum on the User model to distinguish user types:

User.role: "super_admin" | "platform_admin" | "merchant_owner" | "store_member"

Customer Role (Separate Model)

Access: Public shop and own account space

Capabilities:

  • Browse store shops
  • Place orders
  • Manage their own account and order history
  • View order status
  • Update profile information
  • Can register directly from shop frontend

Account Creation: Self-registration via shop frontend (email verification required)

Authentication: Standard JWT authentication (separate Customer model, not User)

Merchant Owner (role="merchant_owner")

Access: Full access to owned store dashboards

Characteristics:

  • Has ALL store permissions automatically (no role record needed)
  • Ownership determined by Merchant.owner_user_id
  • User.is_merchant_owner property returns True
  • User.is_store_user property returns True
  • User.is_owner_of(store_id) checks ownership

Capabilities:

  • Manage products and inventory
  • Process orders
  • View analytics and reports
  • Configure shop settings
  • Manage team members (invite, remove, update roles)
  • Access store-specific APIs

Account Creation: Created automatically when admin creates a store

Store Member (role="store_member")

Access: Store area based on assigned role permissions

Characteristics:

  • Permissions come from StoreUser.role_id -> Role.permissions
  • User.is_store_user property returns True
  • Must be invited by merchant owner via email

Capabilities: Limited based on assigned role (Manager, Staff, Support, Viewer, Marketing, or custom)

Account Creation: Invited by merchant owner via email

Permissions System: Team members can have granular permissions for different areas (up to 75 permissions)

Super Admin (role="super_admin")

Access: Full platform administration across all platforms

Characteristics:

  • User.is_super_admin property returns True (computed: role == "super_admin")
  • User.is_admin property returns True
  • Can access all platforms without restriction
  • Cannot access store portal (blocked by middleware)

Capabilities:

  • Manage all stores across all platforms
  • Create/manage store accounts
  • Access system settings
  • View all data across the platform
  • Manage users of all types
  • Access audit logs
  • Platform-wide analytics

Account Creation: Created by existing super admins on the backend

Platform Admin (role="platform_admin")

Access: Platform administration scoped to assigned platforms

Characteristics:

  • User.is_platform_admin property returns True (computed: role == "platform_admin")
  • User.is_admin property returns True
  • Scoped to specific platforms via AdminPlatform association
  • Cannot access store portal (blocked by middleware)

Capabilities:

  • Manage stores within assigned platforms
  • Access platform-scoped settings and analytics
  • View data within assigned platforms

Account Creation: Created by super admins

Application Areas & Access Control

The platform has three distinct areas with different access requirements:

Area URL Pattern Access Purpose
Admin /admin/* or admin.platform.com Super admins and platform admins (is_admin) Platform administration and store management
Store /store/* Merchant owners and store members (is_store_user) Store dashboard and shop management
Shop /shop/*, custom domains, subdomains Customers and public Public-facing eCommerce storefront
API /api/* All authenticated users (role-based) REST API for all operations

Account Registration Flow

Admin Accounts (Super Admin & Platform Admin)

  • Cannot register from frontend
  • Super Admins (role="super_admin"): Created by existing super admins
  • Platform Admins (role="platform_admin"): Created by super admins
  • Used for: Platform administration

Store Accounts (Merchant Owner & Store Member)

  • Cannot register from frontend
  • Merchant Owners (role="merchant_owner"): Automatically created when admin creates a new store
  • Store Members (role="store_member"): Invited by merchant owner via email invitation
  • Activation: Upon clicking email verification link

Customer Accounts

  • Can register directly on store shop
  • Activation: Upon clicking registration email link
  • Used for: Shopping and order management

Role Enforcement Methods

The AuthManager class provides several methods for role-based access control:

require_admin()

Restricts access to admin users only.

Usage:

from fastapi import Depends
from models.database.user import User
from middleware.auth import auth_manager

@app.get("/admin/dashboard")
async def admin_dashboard(
    current_user: User = Depends(auth_manager.require_admin)
):
    return {"message": "Admin access"}

Raises: AdminRequiredException if user is not admin

require_store()

Allows access to store users and admins.

Usage:

@app.get("/store/products")
async def store_products(
    current_user: User = Depends(auth_manager.require_store)
):
    return {"products": [...]}

Raises: InsufficientPermissionsException if user is not store or admin

require_customer()

Allows access to customer users and admins.

Usage:

@app.get("/shop/orders")
async def customer_orders(
    current_user: User = Depends(auth_manager.require_customer)
):
    return {"orders": [...]}

Raises: InsufficientPermissionsException if user is not customer or admin

require_role()

Custom role enforcement for specific roles. This method returns a decorator factory that creates role-checking decorators for any role name.

Method Signature:

def require_role(self, required_role: str) -> Callable

Parameters:

  • required_role (str): The exact role name required (e.g., "super_admin", "platform_admin", "merchant_owner", "store_member")

Returns: A decorator function that:

  1. Accepts a function as input
  2. Returns a wrapper that validates the current user's role
  3. Raises HTTPException(403) if the role doesn't match

Usage Example:

from fastapi import Depends, APIRouter
from middleware.auth import auth_manager
from models.database.user import User

router = APIRouter()

@router.get("/owner-only")
@auth_manager.require_role("merchant_owner")
async def owner_endpoint(current_user: User):
    """Only users with role='merchant_owner' can access this."""
    return {"message": "Merchant owner access granted"}

# Can also be used with other specific roles
@router.get("/super-admin-only")
@auth_manager.require_role("super_admin")
async def super_admin_endpoint(current_user: User):
    return {"data": "super admin content"}

Error Response:

{
  "detail": "Required role 'merchant_owner' not found. Current role: 'store_member'"
}

Note: For standard access patterns, prefer using the dedicated methods (require_admin(), require_store(), require_customer()) or the computed properties (is_admin, is_store_user) as they provide better error handling and custom exceptions.

create_default_admin_user()

Creates a default super admin user if one doesn't already exist. This is typically used during initial application setup or database seeding.

Method Signature:

def create_default_admin_user(self, db: Session) -> User

Parameters:

  • db (Session): SQLAlchemy database session

Returns: User object (either the existing admin user or the newly created one)

Behavior:

  1. Checks if a user with username "admin" already exists
  2. If not found, creates a new super admin user with:
    • Username: admin
    • Email: admin@example.com
    • Password: admin123 (hashed with bcrypt)
    • Role: super_admin
    • Status: Active
  3. If found, returns the existing user without modification

Usage Example:

from app.core.database import SessionLocal
from middleware.auth import auth_manager

# During application startup or database initialization
db = SessionLocal()
try:
    admin_user = auth_manager.create_default_admin_user(db)
    print(f"Admin user ready: {admin_user.username}")
finally:
    db.close()

Security Warning: ⚠️ The default credentials (admin / admin123) should be changed immediately after first login in production environments. Consider using environment variables for initial admin credentials:

# Example: Custom admin creation with env variables
import os

def create_admin_from_env(db: Session):
    admin_username = os.getenv("ADMIN_USERNAME", "admin")
    admin_password = os.getenv("ADMIN_PASSWORD", "admin123")
    admin_email = os.getenv("ADMIN_EMAIL", "admin@example.com")

    # Check if admin exists
    admin = db.query(User).filter(User.username == admin_username).first()
    if not admin:
        admin = User(
            username=admin_username,
            email=admin_email,
            hashed_password=auth_manager.hash_password(admin_password),
            role="super_admin",
            is_active=True
        )
        db.add(admin)
        db.commit()
    return admin

Typical Use Cases:

  • Initial database setup scripts
  • Application bootstrap/initialization
  • Development environment setup
  • Testing fixtures

JWT Token Structure

Token Payload

{
  "sub": "123",                          // User ID (JWT standard claim)
  "username": "testuser",                // Username for display
  "email": "user@example.com",           // User email
  "role": "merchant_owner",             // User role (4-value enum)
  "exp": 1700000000,                     // Expiration timestamp (JWT standard)
  "iat": 1699999000                      // Issued at timestamp (JWT standard)
}

Token Configuration

Variable Description Default
JWT_SECRET_KEY Secret key for JWT signing Development key (change in production!)
JWT_EXPIRE_MINUTES Token expiration time in minutes 30

Environment Configuration:

# .env
JWT_SECRET_KEY=your-super-secret-key-change-in-production
JWT_EXPIRE_MINUTES=30

Permission Hierarchy

graph TD
    A[Super Admin<br/>role=super_admin] --> B[Full Platform Access]
    A --> C[All Platforms]

    AA[Platform Admin<br/>role=platform_admin] --> D2[Scoped Platform Access]
    AA --> D3[Assigned Platforms Only]

    D[Merchant Owner<br/>role=merchant_owner] --> E[Store Dashboard]
    D --> F[Team Management]
    D --> G[Shop Settings]
    D --> H[All Store Permissions - 75]

    I[Store Member<br/>role=store_member] --> E
    I --> J[Role-Based Permissions]

    K[Customer<br/>separate model] --> L[Shop Access]
    K --> M[Own Orders]
    K --> N[Own Profile]

Admin Override: Admin users (is_admin: super admins and platform admins) have access to the admin portal. They cannot access the store portal directly -- these are separate security boundaries enforced by middleware.

Note: is_super_admin is no longer a database column. It is a computed property: User.role == "super_admin". JWT tokens no longer include an is_super_admin claim; derive it from the role claim instead.

Security Features

Password Security

Hashing:

  • Algorithm: bcrypt
  • Automatic salt generation
  • Configurable work factor

Example:

from middleware.auth import auth_manager

# Hash password
hashed = auth_manager.hash_password("user_password")

# Verify password
is_valid = auth_manager.verify_password("user_password", hashed)

Token Security

Features:

  • Signed with secret key (prevents tampering)
  • Includes expiration time
  • Stateless (no server-side session storage)
  • Short-lived (30 minutes default)

Best Practices:

  • Use HTTPS in production
  • Store tokens securely on client
  • Implement token refresh mechanism
  • Clear tokens on logout

Protection Against Common Attacks

SQL Injection:

  • SQLAlchemy ORM with parameterized queries
  • Input validation with Pydantic

XSS (Cross-Site Scripting):

  • Jinja2 auto-escaping
  • Content Security Policy headers

CSRF (Cross-Site Request Forgery):

  • JWT tokens in Authorization header (not cookies)
  • SameSite cookie attribute for session cookies

Brute Force:

  • Rate limiting on auth endpoints
  • Account lockout after failed attempts (future)

Authentication Endpoints

Register User

POST /api/v1/auth/register
Content-Type: application/json

{
  "email": "user@example.com",
  "username": "testuser",
  "password": "securepassword123"
}

Response:

{
  "id": 123,
  "username": "testuser",
  "email": "user@example.com",
  "role": "customer",
  "is_active": true
}

Login

POST /api/v1/auth/login
Content-Type: application/json

{
  "username": "testuser",
  "password": "securepassword123"
}

Response:

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "token_type": "bearer",
  "expires_in": 1800
}

Using Authentication

Include the JWT token in the Authorization header:

GET /api/v1/resource
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

Error Handling

Authentication Errors

Error Status Code Description
InvalidCredentialsException 401 Username/password incorrect
InvalidTokenException 401 JWT token invalid or malformed
TokenExpiredException 401 JWT token has expired
UserNotActiveException 403 User account is inactive

Authorization Errors

Error Status Code Description
AdminRequiredException 403 Endpoint requires admin role
InsufficientPermissionsException 403 User lacks required permissions

Testing Authentication

Unit Tests

import pytest
from middleware.auth import AuthManager

def test_password_hashing():
    auth_manager = AuthManager()

    password = "test_password"
    hashed = auth_manager.hash_password(password)

    assert auth_manager.verify_password(password, hashed)
    assert not auth_manager.verify_password("wrong_password", hashed)

def test_create_token():
    auth_manager = AuthManager()
    user = create_test_user(role="merchant_owner")

    token_data = auth_manager.create_access_token(user)

    assert "access_token" in token_data
    assert "token_type" in token_data
    assert token_data["token_type"] == "bearer"

Integration Tests

def test_login_flow(client):
    # Register user
    response = client.post("/api/v1/auth/register", json={
        "username": "testuser",
        "email": "test@example.com",
        "password": "password123"
    })
    assert response.status_code == 200

    # Login
    response = client.post("/api/v1/auth/login", json={
        "username": "testuser",
        "password": "password123"
    })
    assert response.status_code == 200
    token = response.json()["access_token"]

    # Access protected endpoint
    response = client.get(
        "/api/v1/profile",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200

Best Practices

For Developers

  1. Always hash passwords: Never store plain text passwords
  2. Use dependency injection: Leverage FastAPI's Depends for auth
  3. Validate tokens: Always validate tokens on protected endpoints
  4. Check permissions: Verify user has required role/permissions
  5. Log auth events: Track login, logout, failed attempts

For Operations

  1. Strong secret keys: Use long, random JWT secret keys
  2. HTTPS only: Never send tokens over HTTP
  3. Token expiration: Keep token lifetimes short
  4. Rotate secrets: Periodically rotate JWT secret keys
  5. Monitor auth logs: Watch for suspicious activity

For Security

  1. Rate limiting: Limit auth endpoint requests
  2. Account lockout: Implement after N failed attempts
  3. Email verification: Require email confirmation
  4. Password policies: Enforce strong password requirements
  5. 2FA support: Consider adding two-factor authentication

Examples

Protecting an Endpoint

from fastapi import Depends, APIRouter
from sqlalchemy.orm import Session
from middleware.auth import auth_manager
from app.core.database import get_db
from models.database.user import User

router = APIRouter()

@router.get("/stores")
async def get_stores(
    current_user: User = Depends(auth_manager.require_admin),
    db: Session = Depends(get_db)
):
    """Only admins can list all stores."""
    stores = db.query(Store).all()
    return {"stores": stores}

Multi-Role Access

@router.get("/dashboard")
async def dashboard(
    current_user: User = Depends(auth_manager.get_current_user),
    db: Session = Depends(get_db)
):
    """Accessible by all authenticated users, but returns different data."""
    if current_user.is_admin:
        # Admin (super_admin or platform_admin) sees platform data
        data = get_admin_dashboard(db)
    elif current_user.is_store_user:
        # Store user (merchant_owner or store_member) sees their store data
        data = get_store_dashboard(db, current_user.id)
    else:
        # Customer sees their orders
        data = get_customer_dashboard(db, current_user.id)

    return data

Technical Reference

For detailed API documentation of authentication classes and methods: