Files
orion/docs/__REVAMPING/RBAC/RBAC_DEVELOPER_GUIDE.md
2025-11-15 20:59:22 +01:00

71 KiB

Role-Based Access Control (RBAC) Developer Guide

Version: 1.0
Last Updated: November 2025
Audience: Development Team


Table of Contents

  1. Introduction
  2. RBAC Overview
  3. System Architecture
  4. User Types & Contexts
  5. Database Schema
  6. Permission System
  7. Authentication Flow
  8. Authorization Implementation
  9. Team Management
  10. Code Examples
  11. Best Practices
  12. Testing Guidelines
  13. Troubleshooting

Introduction

This guide documents the Role-Based Access Control (RBAC) system implemented in our multi-tenant e-commerce platform. The system provides granular access control across three distinct contexts: Platform Administration, Vendor Management, and Customer Shopping.

Purpose

The RBAC system ensures that:

  • Users can only access resources they're authorized to see
  • Permissions are granular and context-specific
  • Multi-tenancy is enforced at the database and application level
  • Team collaboration is secure and auditable

Key Principles

  1. Context Isolation - Admin, vendor, and customer contexts are completely isolated
  2. Least Privilege - Users have only the permissions they need
  3. Owner Authority - Vendor owners have complete control over their vendor
  4. Team Flexibility - Vendor teams can be structured with various role types
  5. Security First - Cookie path isolation and role enforcement prevent unauthorized access

RBAC Overview

Three-Tier Permission Model

┌─────────────────────────────────────────────────────────┐
│                   PLATFORM LEVEL                         │
│                   User.role                              │
│                                                           │
│  ┌──────────┐                  ┌──────────┐            │
│  │  Admin   │                  │  Vendor  │            │
│  │ (admin)  │                  │ (vendor) │            │
│  └──────────┘                  └──────────┘            │
└─────────────────────────────────────────────────────────┘
                                        │
                                        ▼
┌─────────────────────────────────────────────────────────┐
│                   VENDOR LEVEL                           │
│                VendorUser.user_type                      │
│                                                           │
│  ┌──────────┐                  ┌──────────────┐        │
│  │  Owner   │                  │ Team Member  │        │
│  │ (owner)  │                  │  (member)    │        │
│  └──────────┘                  └──────────────┘        │
└─────────────────────────────────────────────────────────┘
                                        │
                                        ▼
┌─────────────────────────────────────────────────────────┐
│                 PERMISSION LEVEL                         │
│                  Role.permissions                        │
│                                                           │
│  Manager, Staff, Support, Viewer, Marketing, Custom     │
└─────────────────────────────────────────────────────────┘

Context Separation

The application operates in three isolated contexts:

Context Routes Authentication User Type
Admin /admin/* admin_token cookie Platform Admins
Vendor /vendor/* vendor_token cookie Vendor Owners & Teams
Shop /shop/account/* customer_token cookie Customers

Important: These contexts are security boundaries. Admin users cannot access vendor routes, vendor users cannot access admin routes, and customers are entirely separate.


System Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Request                               │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│                  Middleware Layer                            │
│                                                              │
│  • VendorContextMiddleware                                  │
│  • VendorDetectionMiddleware                                │
│  • AuthenticationMiddleware                                 │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│              FastAPI Route Handler                           │
│                                                              │
│  Dependencies:                                               │
│  • get_current_admin_from_cookie_or_header()                │
│  • get_current_vendor_from_cookie_or_header()               │
│  • require_vendor_permission("permission.name")             │
│  • require_vendor_owner()                                   │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│                    Service Layer                             │
│                                                              │
│  • vendor_team_service                                      │
│  • auth_service                                             │
│  • customer_service                                         │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│                   Database Layer                             │
│                                                              │
│  • User, VendorUser, Role, Customer                         │
└─────────────────────────────────────────────────────────────┘

Component Responsibilities

Authentication Layer

  • Validates JWT tokens
  • Verifies cookie paths match routes
  • Manages token lifecycle (creation, refresh, expiry)
  • Handles dual storage (cookies + headers)

Authorization Layer

  • Checks user roles and permissions
  • Enforces vendor ownership rules
  • Validates team member access
  • Blocks cross-context access

Service Layer

  • Implements business logic
  • Manages team invitations
  • Handles role assignments
  • Provides reusable authorization checks

User Types & Contexts

Platform Admins

Characteristics:

  • User.role = "admin"
  • Full access to /admin/* routes
  • Manage all vendors and users
  • Cannot access vendor or customer portals

Use Cases:

  • Platform configuration
  • Vendor approval/verification
  • User management
  • System monitoring

Authentication:

# Login endpoint
POST /api/v1/admin/auth/login

# Cookie set
admin_token (path=/admin, httponly=true)

# Access routes
GET /admin/dashboard
GET /admin/vendors
POST /admin/users/{user_id}/suspend

Vendor Owners

Characteristics:

  • User.role = "vendor"
  • VendorUser.user_type = "owner"
  • Automatic full permissions within their vendor
  • Can invite and manage team members
  • Cannot be removed from their vendor

Use Cases:

  • Complete vendor management
  • Team administration
  • Financial oversight
  • Settings configuration

Special Privileges:

# Automatic permissions
def has_permission(self, permission: str) -> bool:
    if self.is_owner:
        return True  # Owners bypass permission checks

Vendor Team Members

Characteristics:

  • User.role = "vendor"
  • VendorUser.user_type = "member"
  • Permissions defined by Role.permissions
  • Invited by vendor owner via email
  • Can be assigned different roles (Manager, Staff, etc.)

Use Cases:

  • Day-to-day operations based on role
  • Collaborative vendor management
  • Specialized functions (marketing, support)

Role Examples:

# Manager - Nearly full access
permissions = [
    "products.view", "products.create", "products.edit",
    "orders.view", "orders.edit", "orders.cancel",
    "customers.view", "customers.edit",
    "reports.view", "reports.financial"
]

# Staff - Operational access
permissions = [
    "products.view", "products.create", "products.edit",
    "orders.view", "orders.edit",
    "customers.view"
]

# Support - Customer service focus
permissions = [
    "orders.view", "orders.edit",
    "customers.view", "customers.edit"
]

Customers

Characteristics:

  • Separate Customer model (not in User table)
  • Vendor-scoped authentication
  • Can self-register on vendor shops
  • Access only their own account + shop catalog

Use Cases:

  • Browse vendor products
  • Place orders
  • Manage account information
  • View order history

Important: Customers are NOT in the User table. They use a separate authentication system and cannot access admin or vendor portals.


Database Schema

Entity Relationship Diagram

┌──────────────────┐
│      users       │
│                  │
│  id (PK)         │◄──────┐
│  email           │       │
│  username        │       │
│  role            │       │  owner_user_id
│  ('admin' |      │       │
│   'vendor')      │       │
│  is_active       │       │
│  is_email_       │       │
│  verified        │       │
└──────────────────┘       │
        │                  │
        │                  │
        │                  │
        ▼                  │
┌──────────────────┐       │
│  vendor_users    │       │
│                  │       │
│  id (PK)         │       │
│  vendor_id (FK) ─┼───┐   │
│  user_id (FK) ───┼─┐ │   │
│  role_id (FK)    │ │ │   │
│  user_type       │ │ │   │
│  ('owner' |      │ │ │   │
│   'member')      │ │ │   │
│  invitation_     │ │ │   │
│  token           │ │ │   │
│  invitation_     │ │ │   │
│  sent_at         │ │ │   │
│  invitation_     │ │ │   │
│  accepted_at     │ │ │   │
│  invited_by (FK) │ │ │   │
│  is_active       │ │ │   │
└──────────────────┘ │ │   │
        │            │ │   │
        │ role_id    │ │   │
        │            │ │   │
        ▼            │ │   │
┌──────────────────┐ │ │   │
│      roles       │ │ │   │
│                  │ │ │   │
│  id (PK)         │ │ │   │
│  vendor_id (FK) ─┼─┘ │   │
│  name            │   │   │
│  permissions     │   │   │
│  (JSONB)         │   │   │
└──────────────────┘   │   │
                       │   │
                       ▼   │
┌──────────────────┐       │
│     vendors      │       │
│                  │       │
│  id (PK)         │       │
│  vendor_code     │       │
│  subdomain       │       │
│  name            │       │
│  owner_user_id ──┼───────┘
│  is_active       │
│  is_verified     │
└──────────────────┘
        │
        │
        ▼
┌──────────────────┐
│    customers     │
│ (SEPARATE AUTH)  │
│                  │
│  id (PK)         │
│  vendor_id (FK)  │
│  email           │
│  hashed_password │
│  customer_number │
│  is_active       │
└──────────────────┘

Key Tables

users

Primary platform user table for admins and vendors.

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    username = Column(String, unique=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    role = Column(String, nullable=False)  # 'admin' or 'vendor'
    is_active = Column(Boolean, default=True)
    is_email_verified = Column(Boolean, default=False)

Important Fields:

  • role: Only contains "admin" or "vendor" (platform-level role)
  • is_email_verified: Required for team member invitations

vendors

Vendor entities representing businesses on the platform.

class Vendor(Base):
    __tablename__ = "vendors"
    
    id = Column(Integer, primary_key=True)
    vendor_code = Column(String, unique=True, nullable=False)
    subdomain = Column(String, unique=True, nullable=False)
    name = Column(String, nullable=False)
    owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    is_active = Column(Boolean, default=True)
    is_verified = Column(Boolean, default=False)

Important Fields:

  • owner_user_id: The user who owns this vendor (full permissions)
  • vendor_code: Used in URLs for vendor context
  • subdomain: For subdomain-based routing

vendor_users

Junction table linking users to vendors with role information.

class VendorUser(Base):
    __tablename__ = "vendor_users"
    
    id = Column(Integer, primary_key=True)
    vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    user_type = Column(String, nullable=False)  # 'owner' or 'member'
    role_id = Column(Integer, ForeignKey("roles.id"), nullable=True)
    invited_by = Column(Integer, ForeignKey("users.id"))
    invitation_token = Column(String, nullable=True)
    invitation_sent_at = Column(DateTime, nullable=True)
    invitation_accepted_at = Column(DateTime, nullable=True)
    is_active = Column(Boolean, default=False)  # Activated on acceptance

Important Fields:

  • user_type: Distinguishes owners ("owner") from team members ("member")
  • role_id: NULL for owners (they have all permissions), set for team members
  • invitation_*: Fields for tracking invitation workflow
  • is_active: FALSE until invitation accepted (for team members)

roles

Vendor-specific role definitions with permissions.

class Role(Base):
    __tablename__ = "roles"
    
    id = Column(Integer, primary_key=True)
    vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
    name = Column(String, nullable=False)
    permissions = Column(JSONB, default=[])  # PostgreSQL JSONB

Important Fields:

  • vendor_id: Roles are vendor-scoped, not platform-wide
  • name: Role name (e.g., "Manager", "Staff", "Support")
  • permissions: Array of permission strings (e.g., ["products.view", "products.create"])

customers

Separate customer authentication system (vendor-scoped).

class Customer(Base):
    __tablename__ = "customers"
    
    id = Column(Integer, primary_key=True)
    vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
    email = Column(String, nullable=False)  # Unique within vendor
    hashed_password = Column(String, nullable=False)
    customer_number = Column(String, nullable=False)
    is_active = Column(Boolean, default=True)

Important Note: Customers are NOT in the users table. They have a completely separate authentication system and are scoped to individual vendors.


Permission System

Permission Structure

Permissions follow a hierarchical naming convention: resource.action

# Format
"{resource}.{action}"

# Examples
"products.view"      # View products
"products.create"    # Create new products
"products.edit"      # Edit existing products
"products.delete"    # Delete products
"orders.cancel"      # Cancel orders
"team.invite"        # Invite team members (owner only)
"settings.edit"      # Edit vendor settings
"reports.financial"  # View financial reports

Available Permissions

Dashboard

"dashboard.view"     # View dashboard

Products

"products.view"      # View product list
"products.create"    # Create new products
"products.edit"      # Edit products
"products.delete"    # Delete products
"products.import"    # Import products from CSV/marketplace
"products.export"    # Export products

Stock/Inventory

"stock.view"         # View stock levels
"stock.edit"         # Adjust stock quantities
"stock.transfer"     # Transfer stock between locations

Orders

"orders.view"        # View orders
"orders.edit"        # Edit order details
"orders.cancel"      # Cancel orders
"orders.refund"      # Process refunds

Customers

"customers.view"     # View customer list
"customers.edit"     # Edit customer details
"customers.delete"   # Delete customers
"customers.export"   # Export customer data

Marketing

"marketing.view"     # View marketing campaigns
"marketing.create"   # Create campaigns
"marketing.send"     # Send marketing emails

Reports

"reports.view"       # View basic reports
"reports.financial"  # View financial reports
"reports.export"     # Export report data

Settings

"settings.view"      # View settings
"settings.edit"      # Edit basic settings
"settings.theme"     # Edit theme/branding
"settings.domains"   # Manage custom domains

Team Management

"team.view"          # View team members
"team.invite"        # Invite new members (owner only)
"team.edit"          # Edit member roles (owner only)
"team.remove"        # Remove members (owner only)

Marketplace Imports

"imports.view"       # View import jobs
"imports.create"     # Create import jobs
"imports.cancel"     # Cancel import jobs

Permission Constants

All permissions are defined in app/core/permissions.py:

from enum import Enum

class VendorPermissions(str, Enum):
    """All available vendor permissions."""
    
    # Dashboard
    DASHBOARD_VIEW = "dashboard.view"
    
    # Products
    PRODUCTS_VIEW = "products.view"
    PRODUCTS_CREATE = "products.create"
    PRODUCTS_EDIT = "products.edit"
    PRODUCTS_DELETE = "products.delete"
    PRODUCTS_IMPORT = "products.import"
    PRODUCTS_EXPORT = "products.export"
    
    # ... (see permissions.py for complete list)

Role Presets

Pre-configured role templates for common team structures:

class PermissionGroups:
    """Pre-defined permission sets for common roles."""
    
    # Owner - All permissions (automatic)
    OWNER = set(p.value for p in VendorPermissions)
    
    # Manager - Most permissions except team management
    MANAGER = {
        "dashboard.view",
        "products.view", "products.create", "products.edit", "products.delete",
        "stock.view", "stock.edit", "stock.transfer",
        "orders.view", "orders.edit", "orders.cancel", "orders.refund",
        "customers.view", "customers.edit", "customers.export",
        "marketing.view", "marketing.create", "marketing.send",
        "reports.view", "reports.financial", "reports.export",
        "settings.view", "settings.theme",
        "imports.view", "imports.create"
    }
    
    # Staff - Day-to-day operations
    STAFF = {
        "dashboard.view",
        "products.view", "products.create", "products.edit",
        "stock.view", "stock.edit",
        "orders.view", "orders.edit",
        "customers.view"
    }
    
    # Support - Customer service focused
    SUPPORT = {
        "dashboard.view",
        "products.view",
        "orders.view", "orders.edit",
        "customers.view", "customers.edit"
    }
    
    # Viewer - Read-only access
    VIEWER = {
        "dashboard.view",
        "products.view",
        "stock.view",
        "orders.view",
        "customers.view",
        "reports.view"
    }
    
    # Marketing - Marketing and customer communication
    MARKETING = {
        "dashboard.view",
        "customers.view", "customers.export",
        "marketing.view", "marketing.create", "marketing.send",
        "reports.view"
    }

Custom Roles

Owners can create custom roles with specific permission sets:

# Creating a custom role
custom_permissions = [
    "products.view",
    "products.create",
    "orders.view",
    "customers.view"
]

role = Role(
    vendor_id=vendor.id,
    name="Product Manager",
    permissions=custom_permissions
)

Authentication Flow

Admin Authentication

┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ POST /api/v1/admin/auth/login
       │ { username, password }
       ▼
┌─────────────────────────────┐
│  Admin Auth Endpoint        │
│                             │
│  1. Validate credentials    │
│  2. Check role == "admin"   │
│  3. Generate JWT            │
└──────┬──────────────────────┘
       │
       │ Set-Cookie: admin_token=<JWT>
       │             Path=/admin
       │             HttpOnly=true
       │             Secure=true (prod)
       │             SameSite=Lax
       │
       │ Response: { access_token, user }
       ▼
┌─────────────┐
│   Client    │
│             │
│ 🍪 admin_token (path=/admin)      │
│ 💾 localStorage.token              │
└─────────────┘

Vendor Authentication

┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ POST /api/v1/vendor/auth/login
       │ { username, password }
       ▼
┌─────────────────────────────┐
│  Vendor Auth Endpoint       │
│                             │
│  1. Validate credentials    │
│  2. Block if admin          │
│  3. Find vendor membership  │
│  4. Get role (owner/member) │
│  5. Generate JWT            │
└──────┬──────────────────────┘
       │
       │ Set-Cookie: vendor_token=<JWT>
       │             Path=/vendor
       │             HttpOnly=true
       │             Secure=true (prod)
       │             SameSite=Lax
       │
       │ Response: { access_token, user, vendor, role }
       ▼
┌─────────────┐
│   Client    │
│             │
│ 🍪 vendor_token (path=/vendor)    │
│ 💾 localStorage.token              │
└─────────────┘

Customer Authentication

┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ POST /api/v1/public/vendors/{id}/customers/login
       │ { username, password }
       ▼
┌─────────────────────────────┐
│  Customer Auth Endpoint     │
│                             │
│  1. Validate vendor         │
│  2. Validate credentials    │
│  3. Generate JWT            │
└──────┬──────────────────────┘
       │
       │ Set-Cookie: customer_token=<JWT>
       │             Path=/shop
       │             HttpOnly=true
       │             Secure=true (prod)
       │             SameSite=Lax
       │
       │ Response: { access_token, user }
       ▼
┌─────────────┐
│   Client    │
│             │
│ 🍪 customer_token (path=/shop)    │
│ 💾 localStorage.token              │
└─────────────┘

Critical Security Feature:

Cookies are restricted by path to prevent cross-context authentication:

# Admin cookie
response.set_cookie(
    key="admin_token",
    value=jwt_token,
    path="/admin",  # Only sent to /admin/* routes
    httponly=True,
    secure=True,
    samesite="lax"
)

# Vendor cookie
response.set_cookie(
    key="vendor_token",
    value=jwt_token,
    path="/vendor",  # Only sent to /vendor/* routes
    httponly=True,
    secure=True,
    samesite="lax"
)

# Customer cookie
response.set_cookie(
    key="customer_token",
    value=jwt_token,
    path="/shop",  # Only sent to /shop/* routes
    httponly=True,
    secure=True,
    samesite="lax"
)

Why This Matters:

  • Admin cookies are never sent to vendor routes
  • Vendor cookies are never sent to admin routes
  • Customer cookies are never sent to admin/vendor routes
  • Prevents accidental cross-context authorization

Dual Token Storage

The system uses dual token storage for flexibility:

  1. HTTP-Only Cookie - For page navigation (automatic)
  2. localStorage - For API calls (manual headers)
// Login stores both
const response = await fetch('/api/v1/vendor/auth/login', {
    method: 'POST',
    body: JSON.stringify({ username, password })
});

const data = await response.json();
// Cookie set automatically by server
// Store token for API calls
localStorage.setItem('token', data.access_token);

// Page navigation - cookie sent automatically
window.location.href = '/vendor/ACME/dashboard';

// API call - use stored token
fetch('/api/v1/vendor/ACME/products', {
    headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`
    }
});

Authorization Implementation

FastAPI Dependencies

The system uses FastAPI dependencies for consistent authorization checks.

Location

All authorization dependencies are in app/api/deps.py.

Basic Authentication Dependencies

from fastapi import Depends, Request
from sqlalchemy.orm import Session
from app.core.database import get_db
from models.database.user import User

# Admin authentication (cookie OR header)
def get_current_admin_from_cookie_or_header(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current admin user from cookie OR Authorization header.
    
    Checks:
    1. admin_token cookie (path=/admin)
    2. Authorization: Bearer <token> header
    3. Validates role == "admin"
    
    Use for: Admin HTML pages
    """
    # Implementation checks cookie first, then header
    # Returns User object if authenticated as admin
    # Raises AdminRequiredException if not admin

# Vendor authentication (cookie OR header)
def get_current_vendor_from_cookie_or_header(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current vendor user from cookie OR Authorization header.
    
    Checks:
    1. vendor_token cookie (path=/vendor)
    2. Authorization: Bearer <token> header
    3. Blocks admin users
    4. Validates vendor membership
    
    Use for: Vendor HTML pages
    """
    # Implementation checks cookie first, then header
    # Returns User object if authenticated as vendor
    # Raises InsufficientPermissionsException if admin

# API-only authentication (header required)
def get_current_admin_api(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current admin from Authorization header only.
    
    Use for: Admin API endpoints
    """

def get_current_vendor_api(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current vendor from Authorization header only.
    
    Use for: Vendor API endpoints
    """

Permission-Based Dependencies

from app.core.permissions import VendorPermissions

def require_vendor_permission(permission: str):
    """
    Dependency factory for requiring specific permission.
    
    Usage:
        @router.post("/products")
        def create_product(
            user: User = Depends(require_vendor_permission("products.create"))
        ):
            # User verified to have products.create permission
            ...
    """
    def permission_checker(
        request: Request,
        db: Session = Depends(get_db),
        current_user: User = Depends(get_current_vendor_from_cookie_or_header)
    ) -> User:
        vendor = request.state.vendor  # Set by middleware
        
        if not current_user.has_vendor_permission(vendor.id, permission):
            raise InsufficientVendorPermissionsException(
                required_permission=permission,
                vendor_code=vendor.vendor_code
            )
        
        return current_user
    
    return permission_checker

def require_vendor_owner(
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_vendor_from_cookie_or_header)
) -> User:
    """
    Require vendor owner role.
    
    Usage:
        @router.post("/team/invite")
        def invite_member(
            user: User = Depends(require_vendor_owner)
        ):
            # User verified to be vendor owner
            ...
    """
    vendor = request.state.vendor
    
    if not current_user.is_owner_of(vendor.id):
        raise VendorOwnerOnlyException(
            operation="team management",
            vendor_code=vendor.vendor_code
        )
    
    return current_user

def require_any_vendor_permission(*permissions: str):
    """
    Require ANY of the specified permissions.
    
    Usage:
        @router.get("/dashboard")
        def dashboard(
            user: User = Depends(require_any_vendor_permission(
                "dashboard.view",
                "reports.view"
            ))
        ):
            # User has at least one permission
            ...
    """

def require_all_vendor_permissions(*permissions: str):
    """
    Require ALL of the specified permissions.
    
    Usage:
        @router.post("/products/bulk-delete")
        def bulk_delete(
            user: User = Depends(require_all_vendor_permissions(
                "products.view",
                "products.delete"
            ))
        ):
            # User has all permissions
            ...
    """

def get_user_permissions(
    request: Request,
    current_user: User = Depends(get_current_vendor_from_cookie_or_header)
) -> list:
    """
    Get all permissions for current user.
    
    Returns list of permission strings.
    
    Usage:
        @router.get("/me/permissions")
        def my_permissions(
            permissions: list = Depends(get_user_permissions)
        ):
            return {"permissions": permissions}
    """

Model Helper Methods

User Model

# In models/database/user.py

class User(Base):
    # ... fields ...
    
    @property
    def is_admin(self) -> bool:
        """Check if user is platform admin."""
        return self.role == "admin"
    
    @property
    def is_vendor(self) -> bool:
        """Check if user is vendor."""
        return self.role == "vendor"
    
    def is_owner_of(self, vendor_id: int) -> bool:
        """Check if user owns a specific vendor."""
        return any(v.id == vendor_id for v in self.owned_vendors)
    
    def is_member_of(self, vendor_id: int) -> bool:
        """Check if user is member of vendor (owner or team)."""
        if self.is_owner_of(vendor_id):
            return True
        return any(
            vm.vendor_id == vendor_id and vm.is_active
            for vm in self.vendor_memberships
        )
    
    def get_vendor_role(self, vendor_id: int) -> str:
        """Get role name within specific vendor."""
        if self.is_owner_of(vendor_id):
            return "owner"
        
        for vm in self.vendor_memberships:
            if vm.vendor_id == vendor_id and vm.is_active:
                return vm.role.name if vm.role else "member"
        
        return None
    
    def has_vendor_permission(self, vendor_id: int, permission: str) -> bool:
        """Check if user has specific permission in vendor."""
        # Owners have all permissions
        if self.is_owner_of(vendor_id):
            return True
        
        # Check team member permissions
        for vm in self.vendor_memberships:
            if vm.vendor_id == vendor_id and vm.is_active:
                if vm.role and permission in vm.role.permissions:
                    return True
        
        return False

VendorUser Model

# In models/database/vendor.py

class VendorUser(Base):
    # ... fields ...
    
    @property
    def is_owner(self) -> bool:
        """Check if this is an owner membership."""
        return self.user_type == "owner"
    
    @property
    def is_team_member(self) -> bool:
        """Check if this is a team member (not owner)."""
        return self.user_type == "member"
    
    @property
    def is_invitation_pending(self) -> bool:
        """Check if invitation is pending acceptance."""
        return (
            self.invitation_token is not None and
            self.invitation_accepted_at is None
        )
    
    def has_permission(self, permission: str) -> bool:
        """Check if this membership has specific permission."""
        # Owners have all permissions
        if self.is_owner:
            return True
        
        # Inactive users have no permissions
        if not self.is_active:
            return False
        
        # Check role permissions
        if self.role and self.role.permissions:
            return permission in self.role.permissions
        
        return False
    
    def get_all_permissions(self) -> list:
        """Get all permissions for this membership."""
        if self.is_owner:
            from app.core.permissions import VendorPermissions
            return [p.value for p in VendorPermissions]
        
        if self.role and self.role.permissions:
            return self.role.permissions
        
        return []

Team Management

Invitation Flow

The system uses email-based invitations for team member onboarding.

Complete Flow Diagram

┌──────────────────────────────────────────────────────────────┐
│                    INVITATION WORKFLOW                        │
└──────────────────────────────────────────────────────────────┘

1. Owner initiates invitation
   └─> POST /api/v1/vendor/{code}/team/invite
       Body: { email, role }

2. System creates/updates records
   ├─> User record (if doesn't exist)
   │   - email: from invitation
   │   - username: auto-generated
   │   - role: "vendor"
   │   - is_active: FALSE
   │   - is_email_verified: FALSE
   │
   └─> VendorUser record
       - vendor_id: current vendor
       - user_id: from User
       - user_type: "member"
       - role_id: from role selection
       - invitation_token: secure random string
       - invitation_sent_at: now()
       - invited_by: current user
       - is_active: FALSE

3. Email sent to invitee
   └─> Contains: invitation link with token
       Link: /vendor/invitation/accept?token={invitation_token}

4. Invitee clicks link
   └─> GET /vendor/invitation/accept?token={token}
       Displays form: password, first_name, last_name

5. Invitee submits form
   └─> POST /api/v1/vendor/team/accept-invitation
       Body: { invitation_token, password, first_name, last_name }

6. System activates account
   ├─> User updates:
   │   - hashed_password: from form
   │   - first_name, last_name: from form
   │   - is_active: TRUE
   │   - is_email_verified: TRUE
   │
   └─> VendorUser updates:
       - is_active: TRUE
       - invitation_accepted_at: now()
       - invitation_token: NULL (cleared)

7. Member can now login
   └─> POST /api/v1/vendor/auth/login
       Redirect to vendor dashboard

Service Layer Implementation

Team management is handled by VendorTeamService in app/services/vendor_team_service.py.

Key Methods

class VendorTeamService:
    
    def invite_team_member(
        self,
        db: Session,
        vendor: Vendor,
        inviter: User,
        email: str,
        role_name: str,
        custom_permissions: Optional[List[str]] = None
    ) -> Dict[str, Any]:
        """
        Invite a new team member.
        
        Steps:
        1. Check team size limits
        2. Find or create User account
        3. Create or update VendorUser with invitation
        4. Generate secure invitation token
        5. Send invitation email
        
        Returns:
            {
                "invitation_token": str,
                "email": str,
                "role": str,
                "existing_user": bool
            }
        """
    
    def accept_invitation(
        self,
        db: Session,
        invitation_token: str,
        password: str,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        Accept team invitation and activate account.
        
        Steps:
        1. Validate invitation token
        2. Check token not expired (7 days)
        3. Update User (password, name, active status)
        4. Update VendorUser (active, accepted timestamp)
        5. Clear invitation token
        
        Returns:
            {
                "user": User,
                "vendor": Vendor,
                "role": str
            }
        """
    
    def remove_team_member(
        self,
        db: Session,
        vendor: Vendor,
        user_id: int
    ) -> bool:
        """
        Remove team member (soft delete).
        
        Cannot remove owner.
        Sets VendorUser.is_active = False
        """
    
    def update_member_role(
        self,
        db: Session,
        vendor: Vendor,
        user_id: int,
        new_role_name: str,
        custom_permissions: Optional[List[str]] = None
    ) -> VendorUser:
        """
        Update team member's role.
        
        Cannot change owner's role.
        Creates new role if custom permissions provided.
        """
    
    def get_team_members(
        self,
        db: Session,
        vendor: Vendor,
        include_inactive: bool = False
    ) -> List[Dict[str, Any]]:
        """
        Get all team members for a vendor.
        
        Returns list of member info including:
        - Basic user info
        - Role and permissions
        - Invitation status
        - Active status
        """

API Routes

Complete team management routes in app/api/v1/vendor/team.py.

router = APIRouter(prefix="/team")

# List team members
@router.get("/members")
def list_team_members(
    request: Request,
    user: User = Depends(require_vendor_permission("team.view"))
):
    """List all team members."""

# Invite team member (owner only)
@router.post("/invite")
def invite_team_member(
    invitation: InviteTeamMemberRequest,
    user: User = Depends(require_vendor_owner)
):
    """Send team invitation email."""

# Accept invitation (public, no auth)
@router.post("/accept-invitation")
def accept_invitation(
    acceptance: AcceptInvitationRequest
):
    """Accept invitation and activate account."""

# Remove team member (owner only)
@router.delete("/members/{user_id}")
def remove_team_member(
    user_id: int,
    user: User = Depends(require_vendor_owner)
):
    """Remove team member from vendor."""

# Update member role (owner only)
@router.put("/members/{user_id}/role")
def update_member_role(
    user_id: int,
    role_update: UpdateMemberRoleRequest,
    user: User = Depends(require_vendor_owner)
):
    """Change team member's role."""

# Get current user's permissions
@router.get("/me/permissions")
def get_my_permissions(
    permissions: list = Depends(get_user_permissions)
):
    """Get current user's permission list."""

Security Considerations

Owner Protection

Owners cannot be removed or have their role changed:

# In remove_team_member
if vendor_user.is_owner:
    raise CannotRemoveVendorOwnerException(vendor.vendor_code)

# In update_member_role
if vendor_user.is_owner:
    raise CannotRemoveVendorOwnerException(vendor.vendor_code)

Invitation Token Security

  • Tokens are 32-byte cryptographically secure random strings
  • Single-use (cleared after acceptance)
  • Expire after 7 days
  • Unique per invitation
def _generate_invitation_token(self) -> str:
    """Generate secure invitation token."""
    import secrets
    return secrets.token_urlsafe(32)

Admin Blocking

Admins are blocked from vendor routes:

# In vendor auth endpoint
if user.role == "admin":
    raise InvalidCredentialsException(
        "Admins cannot access vendor portal"
    )

# In vendor dependencies
if current_user.role == "admin":
    raise InsufficientPermissionsException(
        "Vendor access only"
    )

Code Examples

Example 1: Protected Route with Permission Check

from fastapi import APIRouter, Depends
from app.api.deps import require_vendor_permission
from app.core.permissions import VendorPermissions
from models.database.user import User

router = APIRouter()

@router.post("/products")
def create_product(
    product_data: ProductCreate,
    user: User = Depends(require_vendor_permission(
        VendorPermissions.PRODUCTS_CREATE.value
    ))
):
    """
    Create a new product.
    
    Requires: products.create permission
    
    The dependency automatically:
    1. Authenticates the user
    2. Gets vendor from request.state
    3. Checks user has products.create permission
    4. Returns User if authorized
    5. Raises InsufficientVendorPermissionsException if not
    """
    vendor = request.state.vendor
    
    # User is authenticated and authorized
    # Proceed with business logic
    product = product_service.create(
        db=db,
        vendor_id=vendor.id,
        user_id=user.id,
        data=product_data
    )
    
    return {"product": product}

Example 2: Owner-Only Route

from app.api.deps import require_vendor_owner

@router.delete("/team/members/{member_id}")
def remove_team_member(
    member_id: int,
    user: User = Depends(require_vendor_owner)
):
    """
    Remove a team member.
    
    Requires: Vendor owner role
    
    The dependency automatically:
    1. Authenticates the user
    2. Checks user is owner of current vendor
    3. Returns User if owner
    4. Raises VendorOwnerOnlyException if not owner
    """
    vendor = request.state.vendor
    
    # User is verified owner
    vendor_team_service.remove_team_member(
        db=db,
        vendor=vendor,
        user_id=member_id
    )
    
    return {"message": "Member removed"}

Example 3: Multi-Permission Route

from app.api.deps import require_all_vendor_permissions

@router.post("/products/bulk-import")
def bulk_import_products(
    file: UploadFile,
    user: User = Depends(require_all_vendor_permissions(
        VendorPermissions.PRODUCTS_VIEW.value,
        VendorPermissions.PRODUCTS_CREATE.value,
        VendorPermissions.PRODUCTS_IMPORT.value
    ))
):
    """
    Bulk import products from CSV.
    
    Requires ALL of:
    - products.view
    - products.create
    - products.import
    
    The dependency checks user has ALL specified permissions.
    """
    vendor = request.state.vendor
    
    # User has all required permissions
    result = import_service.process_csv(
        db=db,
        vendor_id=vendor.id,
        file=file
    )
    
    return {"imported": result.success_count}

Example 4: Service Layer Permission Check

# In service layer
class ProductService:
    
    def create_product(
        self,
        db: Session,
        vendor: Vendor,
        user: User,
        data: ProductCreate
    ) -> Product:
        """
        Create a product.
        
        Note: Permission checking should be done at route level,
        not in service layer. Services assume authorization
        has already been verified.
        """
        product = Product(
            vendor_id=vendor.id,
            created_by=user.id,
            **data.dict()
        )
        
        db.add(product)
        db.commit()
        db.refresh(product)
        
        return product

Important: Permission checks belong in route dependencies, not service layers. Services assume the caller is authorized.

Example 5: Manual Permission Check

Sometimes you need to check permissions programmatically:

@router.get("/dashboard")
def dashboard(
    request: Request,
    user: User = Depends(get_current_vendor_from_cookie_or_header)
):
    """
    Dashboard with conditional content based on permissions.
    """
    vendor = request.state.vendor
    
    # Get user's permissions
    permissions = []
    if user.is_owner_of(vendor.id):
        # Owners get all permissions
        from app.core.permissions import VendorPermissions
        permissions = [p.value for p in VendorPermissions]
    else:
        # Get from role
        for vm in user.vendor_memberships:
            if vm.vendor_id == vendor.id and vm.is_active:
                permissions = vm.get_all_permissions()
                break
    
    # Conditional data based on permissions
    data = {
        "basic_stats": get_basic_stats(vendor),
    }
    
    if "reports.financial" in permissions:
        data["financial_stats"] = get_financial_stats(vendor)
    
    if "team.view" in permissions:
        data["team_stats"] = get_team_stats(vendor)
    
    return data

Example 6: Frontend Permission Checking

// On login, fetch user's permissions
async function login(username, password) {
    const response = await fetch('/api/v1/vendor/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });
    
    const data = await response.json();
    
    // Store token
    localStorage.setItem('token', data.access_token);
    
    // Fetch permissions
    const permResponse = await fetch('/api/v1/vendor/team/me/permissions', {
        headers: {
            'Authorization': `Bearer ${data.access_token}`
        }
    });
    
    const { permissions } = await permResponse.json();
    
    // Store permissions
    localStorage.setItem('permissions', JSON.stringify(permissions));
    
    // Navigate
    window.location.href = '/vendor/dashboard';
}

// Check permission before showing UI element
function canCreateProducts() {
    const permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
    return permissions.includes('products.create');
}

// In React/Alpine.js component
{canCreateProducts() && (
    <button onClick={handleCreateProduct}>
        Create Product
    </button>
)}

// Disable button if no permission
<button 
    disabled={!canCreateProducts()}
    onClick={handleCreateProduct}
>
    Create Product
</button>

Best Practices

1. Route-Level Authorization

DO: Check permissions at route level using dependencies

@router.post("/products")
def create_product(
    data: ProductCreate,
    user: User = Depends(require_vendor_permission("products.create"))
):
    # Permission already verified
    return product_service.create(data)

DON'T: Check permissions in service layer

# BAD
def create_product(db: Session, user: User, data: ProductCreate):
    if not user.has_permission("products.create"):
        raise Exception("No permission")
    # ...

2. Use Type-Safe Permission Constants

DO: Use VendorPermissions enum

from app.core.permissions import VendorPermissions

require_vendor_permission(VendorPermissions.PRODUCTS_CREATE.value)

DON'T: Use magic strings

# BAD - typos won't be caught
require_vendor_permission("products.creat")  # Typo!

3. Owner Permission Bypass

DO: Let owners bypass permission checks automatically

def has_permission(self, permission: str) -> bool:
    if self.is_owner:
        return True  # Owners have all permissions
    # Check role permissions...

DON'T: Explicitly check owner in every route

# BAD - redundant
if not user.is_owner and not user.has_permission("products.create"):
    raise Exception()

4. Service Layer Design

DO: Keep services authorization-agnostic

# Service assumes caller is authorized
def create_product(db: Session, vendor_id: int, data: dict) -> Product:
    product = Product(vendor_id=vendor_id, **data)
    db.add(product)
    db.commit()
    return product

DON'T: Mix authorization into services

# BAD
def create_product(db: Session, user: User, data: dict):
    if not user.is_active:
        raise Exception()
    # ...

5. Frontend Permission Checks

DO: Hide/disable UI elements without permission

// Hide button if no permission
{hasPermission('products.delete') && (
    <DeleteButton />
)}

// Disable button if no permission
<button disabled={!hasPermission('products.edit')}>
    Edit
</button>

DON'T: Rely only on frontend checks

Backend MUST always verify permissions. Frontend checks are for UX only.

6. Error Handling

DO: Use specific exception types

from app.exceptions import (
    InsufficientVendorPermissionsException,
    VendorOwnerOnlyException,
    VendorAccessDeniedException
)

if not user.has_permission(permission):
    raise InsufficientVendorPermissionsException(
        required_permission=permission,
        vendor_code=vendor.vendor_code
    )

DON'T: Use generic exceptions

# BAD
raise Exception("No permission")

7. Invitation Token Security

DO: Use cryptographically secure tokens

import secrets

def generate_token():
    return secrets.token_urlsafe(32)

DON'T: Use weak token generation

# BAD
import random
token = str(random.randint(1000000, 9999999))

8. Context Detection

DO: Use middleware for vendor context

# Middleware sets request.state.vendor
vendor = request.state.vendor

DON'T: Extract vendor from URL in every route

# BAD
@router.get("/vendor/{vendor_code}/products")
def list_products(vendor_code: str):
    vendor = db.query(Vendor).filter_by(vendor_code=vendor_code).first()
    # ...

9. Admin Access Restrictions

DO: Block admins from vendor routes

if current_user.role == "admin":
    raise InsufficientPermissionsException(
        "Admins cannot access vendor portal"
    )

This prevents admins from accidentally accessing vendor areas.

10. Testing Permissions

DO: Test all permission combinations

def test_create_product_with_permission():
    """User with products.create can create products."""
    # Setup user with permission
    # Make request
    # Assert success

def test_create_product_without_permission():
    """User without products.create cannot create products."""
    # Setup user without permission
    # Make request
    # Assert 403 Forbidden

def test_owner_can_always_create_product():
    """Owners can create products regardless of role."""
    # Setup owner (no specific role)
    # Make request
    # Assert success

Testing Guidelines

Unit Tests

Test permission logic in isolation.

# tests/unit/test_permissions.py

def test_owner_has_all_permissions():
    """Owners have all permissions automatically."""
    user = create_user()
    vendor = create_vendor(owner=user)
    vendor_user = create_vendor_user(
        user=user,
        vendor=vendor,
        user_type="owner"
    )
    
    assert vendor_user.has_permission("products.create")
    assert vendor_user.has_permission("orders.delete")
    assert vendor_user.has_permission("team.invite")
    # All permissions should return True

def test_team_member_respects_role():
    """Team members have only their role's permissions."""
    user = create_user()
    vendor = create_vendor()
    role = create_role(
        vendor=vendor,
        name="Staff",
        permissions=["products.view", "products.create"]
    )
    vendor_user = create_vendor_user(
        user=user,
        vendor=vendor,
        user_type="member",
        role=role
    )
    
    assert vendor_user.has_permission("products.view")
    assert vendor_user.has_permission("products.create")
    assert not vendor_user.has_permission("products.delete")
    assert not vendor_user.has_permission("team.invite")

def test_inactive_user_has_no_permissions():
    """Inactive users have no permissions."""
    user = create_user()
    vendor = create_vendor()
    role = create_role(
        vendor=vendor,
        permissions=["products.view"]
    )
    vendor_user = create_vendor_user(
        user=user,
        vendor=vendor,
        role=role,
        is_active=False  # Inactive
    )
    
    assert not vendor_user.has_permission("products.view")

Integration Tests

Test full request/response cycles with authentication.

# tests/integration/test_product_routes.py

def test_create_product_with_permission(client, auth_headers):
    """Authenticated user with permission can create product."""
    # Setup: Create user with products.create permission
    user = create_vendor_team_member(
        permissions=["products.create"]
    )
    token = create_auth_token(user)
    
    # Request
    response = client.post(
        "/api/v1/vendor/ACME/products",
        json={"name": "Test Product", "price": 9.99},
        headers={"Authorization": f"Bearer {token}"}
    )
    
    # Assert
    assert response.status_code == 201
    assert response.json()["name"] == "Test Product"

def test_create_product_without_permission(client):
    """User without permission cannot create product."""
    # Setup: Create user WITHOUT products.create
    user = create_vendor_team_member(
        permissions=["products.view"]  # Can view but not create
    )
    token = create_auth_token(user)
    
    # Request
    response = client.post(
        "/api/v1/vendor/ACME/products",
        json={"name": "Test Product"},
        headers={"Authorization": f"Bearer {token}"}
    )
    
    # Assert
    assert response.status_code == 403
    assert "INSUFFICIENT_VENDOR_PERMISSIONS" in response.json()["error_code"]

def test_owner_bypasses_permission_check(client):
    """Vendor owner can create products without explicit permission."""
    # Setup: Create owner (no specific role)
    user, vendor = create_vendor_with_owner()
    token = create_auth_token(user)
    
    # Request
    response = client.post(
        f"/api/v1/vendor/{vendor.vendor_code}/products",
        json={"name": "Test Product"},
        headers={"Authorization": f"Bearer {token}"}
    )
    
    # Assert - Owner can create even without explicit permission
    assert response.status_code == 201

def test_admin_blocked_from_vendor_route(client):
    """Admins cannot access vendor routes."""
    # Setup: Create admin user
    admin = create_admin_user()
    token = create_auth_token(admin)
    
    # Request
    response = client.get(
        "/api/v1/vendor/ACME/products",
        headers={"Authorization": f"Bearer {token}"}
    )
    
    # Assert
    assert response.status_code == 403
    assert "INSUFFICIENT_PERMISSIONS" in response.json()["error_code"]

System Tests

Test complete workflows end-to-end.

# tests/system/test_team_invitation_workflow.py

def test_complete_invitation_workflow(client, db):
    """Test full team invitation and acceptance flow."""
    
    # 1. Owner logs in
    owner_token = login_as_owner(client, "owner@acme.com", "password")
    
    # 2. Owner invites team member
    invite_response = client.post(
        "/api/v1/vendor/ACME/team/invite",
        json={
            "email": "newmember@example.com",
            "role": "Staff"
        },
        headers={"Authorization": f"Bearer {owner_token}"}
    )
    assert invite_response.status_code == 200
    
    # 3. Get invitation token from database (simulating email)
    vendor_user = db.query(VendorUser).filter_by(
        email="newmember@example.com"
    ).first()
    invitation_token = vendor_user.invitation_token
    assert invitation_token is not None
    assert vendor_user.is_active == False
    
    # 4. New member accepts invitation
    accept_response = client.post(
        "/api/v1/vendor/team/accept-invitation",
        json={
            "invitation_token": invitation_token,
            "password": "newpassword123",
            "first_name": "New",
            "last_name": "Member"
        }
    )
    assert accept_response.status_code == 200
    
    # 5. Verify account is activated
    db.refresh(vendor_user)
    assert vendor_user.is_active == True
    assert vendor_user.invitation_token is None
    assert vendor_user.invitation_accepted_at is not None
    
    # 6. New member can log in
    login_response = client.post(
        "/api/v1/vendor/auth/login",
        json={
            "username": "newmember@example.com",
            "password": "newpassword123"
        }
    )
    assert login_response.status_code == 200
    assert "access_token" in login_response.json()
    
    # 7. New member has correct permissions
    member_token = login_response.json()["access_token"]
    perms_response = client.get(
        "/api/v1/vendor/ACME/team/me/permissions",
        headers={"Authorization": f"Bearer {member_token}"}
    )
    assert "products.view" in perms_response.json()["permissions"]
    assert "products.create" in perms_response.json()["permissions"]
    assert "team.invite" not in perms_response.json()["permissions"]

Performance Tests

Test permission checking doesn't cause performance issues.

# tests/performance/test_permission_checks.py

def test_permission_check_performance(client, db):
    """Permission checks should be fast."""
    # Setup: Create vendor with many team members
    vendor = create_vendor()
    for i in range(100):
        create_vendor_team_member(vendor)
    
    # Create test user
    user = create_vendor_team_member(
        vendor=vendor,
        permissions=["products.view"]
    )
    token = create_auth_token(user)
    
    # Test: Make many authenticated requests
    import time
    start = time.time()
    
    for _ in range(100):
        response = client.get(
            f"/api/v1/vendor/{vendor.vendor_code}/products",
            headers={"Authorization": f"Bearer {token}"}
        )
        assert response.status_code == 200
    
    elapsed = time.time() - start
    avg_per_request = elapsed / 100
    
    # Permission check should be <10ms per request
    assert avg_per_request < 0.01

Test Coverage Requirements

  • Unit Tests: 100% coverage of permission logic
  • Integration Tests: All permission combinations for each route
  • System Tests: Complete workflows (invitation, role changes, etc.)
  • Performance Tests: Permission checks under load

Troubleshooting

Common Issues

Issue: "INVALID_TOKEN" error

Symptoms:

  • API calls return 401 Unauthorized
  • Error message: "Invalid token"

Causes:

  1. Token expired (default 30 minutes)
  2. Token malformed
  3. Token signature invalid

Solutions:

# Check token expiry
import jwt
token = "eyJ0eXAi..."
decoded = jwt.decode(token, verify=False)
print(decoded['exp'])  # Unix timestamp

# Compare with current time
import time
if decoded['exp'] < time.time():
    print("Token expired - user needs to re-login")

# Verify token manually
from middleware.auth import AuthManager
auth = AuthManager()
try:
    user_data = auth.verify_token(token)
    print("Token valid")
except Exception as e:
    print(f"Token invalid: {e}")

Issue: User can't access route despite having permission

Symptoms:

  • Route returns 403 Forbidden
  • User believes they have required permission

Debug Steps:

# 1. Check user's actual permissions
user = db.query(User).get(user_id)
vendor = db.query(Vendor).get(vendor_id)

print(f"Is owner? {user.is_owner_of(vendor.id)}")

if not user.is_owner_of(vendor.id):
    # Get team membership
    vendor_user = db.query(VendorUser).filter_by(
        user_id=user.id,
        vendor_id=vendor.id
    ).first()
    
    print(f"Has membership? {vendor_user is not None}")
    print(f"Is active? {vendor_user.is_active if vendor_user else 'N/A'}")
    print(f"Role: {vendor_user.role.name if vendor_user and vendor_user.role else 'N/A'}")
    print(f"Permissions: {vendor_user.role.permissions if vendor_user and vendor_user.role else []}")

# 2. Check specific permission
permission = "products.create"
has_perm = user.has_vendor_permission(vendor.id, permission)
print(f"Has {permission}? {has_perm}")

Issue: Admin can't access vendor routes

Symptoms:

  • Admin user gets 403 on vendor routes

This is intentional! Admins are blocked from vendor routes for security.

Solutions:

  1. Create separate vendor account for vendor management
  2. Have admin create vendor, then use vendor owner account
# Admin workflow
# 1. Admin creates vendor (from admin portal)
# 2. System creates vendor owner user automatically
# 3. Admin logs out of admin portal
# 4. Vendor owner logs into vendor portal

Symptoms:

  • "Invalid invitation token" error
  • Invitation expired error

Debug Steps:

# Check invitation in database
token = "abc123..."
vendor_user = db.query(VendorUser).filter_by(
    invitation_token=token
).first()

if not vendor_user:
    print("Token not found - may have been used already")
elif vendor_user.invitation_accepted_at:
    print("Invitation already accepted")
else:
    # Check expiry
    from datetime import datetime, timedelta
    sent_at = vendor_user.invitation_sent_at
    expiry = sent_at + timedelta(days=7)
    
    if datetime.utcnow() > expiry:
        print(f"Invitation expired on {expiry}")
    else:
        print("Invitation is valid")

Symptoms:

  • Page navigation returns 401
  • API calls with header work fine

Causes:

  1. Cookie path doesn't match route
  2. Cookie expired
  3. Browser blocking cookies

Debug Steps:

// Check cookies in browser console
document.cookie.split(';').forEach(c => console.log(c.trim()));

// Verify correct cookie exists
// admin_token for /admin/* routes
// vendor_token for /vendor/* routes
// customer_token for /shop/* routes

// Check cookie path
// In DevTools → Application → Cookies
// Path should match route prefix

Issue: Permission changes not taking effect

Symptoms:

  • Updated role permissions
  • User still has old permissions

Causes:

  1. JWT token contains old permissions (cached)
  2. User needs to re-login

Solution:

# Permissions are NOT stored in JWT token
# They're fetched from database on each request
# So permission changes take effect immediately

# If issue persists, check:
# 1. Database was actually updated
role = db.query(Role).get(role_id)
print(f"Current permissions: {role.permissions}")

# 2. Cache invalidation if using caching
# Clear any permission caches

# 3. User might need to refresh page
# Frontend may have cached permission list

Debugging Tools

Check User's Complete Access

def debug_user_access(user_id: int, vendor_id: int):
    """Print complete access information for debugging."""
    user = db.query(User).get(user_id)
    vendor = db.query(Vendor).get(vendor_id)
    
    print(f"\n=== User Access Debug ===")
    print(f"User: {user.username} ({user.email})")
    print(f"User Role: {user.role}")
    print(f"User Active: {user.is_active}")
    print(f"\nVendor: {vendor.name} ({vendor.vendor_code})")
    print(f"Vendor Active: {vendor.is_active}")
    
    # Check ownership
    is_owner = user.is_owner_of(vendor.id)
    print(f"\nIs Owner: {is_owner}")
    
    if is_owner:
        print("✓ Has ALL permissions (owner)")
        return
    
    # Check membership
    vendor_user = db.query(VendorUser).filter_by(
        user_id=user.id,
        vendor_id=vendor.id
    ).first()
    
    if not vendor_user:
        print("✗ No vendor membership found")
        return
    
    print(f"\nMembership Status: {vendor_user.user_type}")
    print(f"Active: {vendor_user.is_active}")
    print(f"Invited By: User #{vendor_user.invited_by}")
    print(f"Invitation Accepted: {vendor_user.invitation_accepted_at}")
    
    if vendor_user.role:
        print(f"\nRole: {vendor_user.role.name}")
        print(f"Permissions ({len(vendor_user.role.permissions)}):")
        for perm in vendor_user.role.permissions:
            print(f"  - {perm}")
    else:
        print("\n✗ No role assigned")

Test Permission Check

def test_permission(user_id: int, vendor_id: int, permission: str):
    """Test if user has specific permission."""
    user = db.query(User).get(user_id)
    vendor = db.query(Vendor).get(vendor_id)
    
    has_perm = user.has_vendor_permission(vendor.id, permission)
    
    print(f"\n=== Permission Test ===")
    print(f"User: {user.username}")
    print(f"Vendor: {vendor.vendor_code}")
    print(f"Permission: {permission}")
    print(f"Result: {'✓ GRANTED' if has_perm else '✗ DENIED'}")
    
    # Show reason
    if user.is_owner_of(vendor.id):
        print("Reason: User is owner (has all permissions)")
    elif has_perm:
        vendor_user = db.query(VendorUser).filter_by(
            user_id=user.id,
            vendor_id=vendor.id
        ).first()
        print(f"Reason: Role '{vendor_user.role.name}' includes this permission")
    else:
        print("Reason: User does not have this permission")

Validate JWT Token

def validate_token(token: str):
    """Validate and decode JWT token."""
    from middleware.auth import AuthManager
    auth = AuthManager()
    
    print(f"\n=== Token Validation ===")
    
    try:
        # Decode without verification first
        import jwt
        decoded = jwt.decode(token, verify=False)
        
        print(f"User ID: {decoded.get('sub')}")
        print(f"Username: {decoded.get('username')}")
        print(f"Email: {decoded.get('email')}")
        print(f"Role: {decoded.get('role')}")
        print(f"Issued At: {datetime.fromtimestamp(decoded.get('iat'))}")
        print(f"Expires At: {datetime.fromtimestamp(decoded.get('exp'))}")
        
        # Now verify
        user_data = auth.verify_token(token)
        print("\n✓ Token is valid")
        
    except jwt.ExpiredSignatureError:
        print("\n✗ Token has expired")
    except jwt.InvalidTokenError as e:
        print(f"\n✗ Token is invalid: {e}")

Logging Best Practices

Add comprehensive logging for troubleshooting:

import logging

logger = logging.getLogger(__name__)

# In authentication
logger.info(f"User login attempt: {username}")
logger.info(f"User {username} logged in successfully")
logger.warning(f"Failed login attempt for: {username}")

# In authorization
logger.debug(f"Checking permission {permission} for user {user.id}")
logger.warning(f"Permission denied: {user.id} lacks {permission}")

# In team management
logger.info(f"Team member invited: {email} to {vendor.vendor_code}")
logger.info(f"Invitation accepted: {user.id} joined {vendor.vendor_code}")
logger.warning(f"Failed to remove owner {user.id} from {vendor.vendor_code}")

Conclusion

This RBAC system provides comprehensive, secure access control for the multi-tenant e-commerce platform. Key features:

  • Three-tier permission hierarchy (Platform → Vendor → Permission)
  • Context isolation (Admin, Vendor, Customer)
  • Flexible role system with presets and custom roles
  • Secure invitation workflow for team management
  • Owner authority with automatic full permissions
  • Cookie path isolation for security
  • Comprehensive testing support

Quick Reference

# Authentication
from app.api.deps import (
    get_current_admin_from_cookie_or_header,
    get_current_vendor_from_cookie_or_header,
    get_current_customer_from_cookie_or_header,
    get_current_admin_api,
    get_current_vendor_api,
    get_current_customer_api
)

# Authorization
from app.api.deps import (
    require_vendor_permission,
    require_vendor_owner,
    require_any_vendor_permission,
    require_all_vendor_permissions,
    get_user_permissions
)

# Permissions
from app.core.permissions import (
    VendorPermissions,
    PermissionGroups,
    PermissionChecker
)

# Services
from app.services.vendor_team_service import vendor_team_service

# Exceptions
from app.exceptions import (
    InsufficientVendorPermissionsException,
    VendorOwnerOnlyException,
    VendorAccessDeniedException,
    InvalidInvitationTokenException,
    CannotRemoveVendorOwnerException
)

Support

For questions or issues:

  1. Check this guide first
  2. Review code examples in relevant files
  3. Check test files for usage patterns
  4. Contact the backend team

Document Version: 1.0
Last Updated: November 2025
Maintained By: Backend Team