Renamed all documentation files to follow kebab-case naming standard: - UPPERCASE files → lowercase (e.g., RBAC.md → rbac.md) - snake_case files → kebab-case (e.g., icons_guide.md → icons-guide.md) - SCREAMING_SNAKE_CASE → kebab-case (e.g., DATABASE_SETUP_GUIDE.md → database-setup-guide.md) Files renamed (15 total): API Documentation: - api/RBAC.md → api/rbac.md Architecture: - architecture/API_CONSOLIDATION_PROPOSAL.md → api-consolidation-proposal.md - architecture/API_MIGRATION_STATUS.md → api-migration-status.md Development: - development/AUTH_DEPENDENCIES_GUIDE.md → auth-dependencies-guide.md - development/CUSTOMER_AUTHENTICATION_IMPLEMENTATION.md → customer-authentication-implementation.md - development/CUSTOMER_AUTH_SUMMARY.md → customer-auth-summary.md - development/icons_guide.md → icons-guide.md Database Seeder: - database-seeder/DATABASE_INIT_GUIDE.md → database-init-guide.md - database-seeder/DATABASE_QUICK_REFERENCE_GUIDE.md → database-quick-reference-guide.md - database-seeder/DATABASE_SEEDER_DOCUMENTATION.md → database-seeder-documentation.md - database-seeder/MAKEFILE_DATABASE_SEEDER.md → makefile-database-seeder.md Error Rendering: - error-rendering/ERROR_RENDERING_DEVELOPER_DOCUMENTATION.md → error-rendering-developer-documentation.md - error-rendering/HTML_ERROR_RENDERING_FLOW_DIAGRAM.md → html-error-rendering-flow-diagram.md Getting Started: - getting-started/DATABASE_QUICK_REFERENCE.md → database-quick-reference.md - getting-started/DATABASE_SETUP_GUIDE.md → database-setup-guide.md Updates: - Updated all references in mkdocs.yml - Updated all cross-references in markdown files - Verified mkdocs builds without warnings or errors Standard: Use kebab-case (lowercase-with-hyphens) for all markdown files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
56 KiB
Role-Based Access Control (RBAC) Developer Guide
Version: 1.0 Last Updated: November 2025 Audience: Development Team
Table of Contents
- Introduction
- RBAC Overview
- System Architecture
- User Types & Contexts
- Database Schema
- Permission System
- Authentication Flow
- Authorization Implementation
- Team Management
- Code Examples
- Best Practices
- Testing Guidelines
- 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
- Context Isolation - Admin, vendor, and customer contexts are completely isolated
- Least Privilege - Users have only the permissions they need
- Owner Authority - Vendor owners have complete control over their vendor
- Team Flexibility - Vendor teams can be structured with various role types
- 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
Customermodel (not inUsertable) - 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 contextsubdomain: 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 membersinvitation_*: Fields for tracking invitation workflowis_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-widename: 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 │
└─────────────┘
Cookie Path Isolation
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:
- HTTP-Only Cookie - For page navigation (automatic)
- 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("/vendor/{code}/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: 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.
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"]
Troubleshooting
Common Issues
Issue: "INVALID_TOKEN" error
Symptoms:
- API calls return 401 Unauthorized
- Error message: "Invalid token"
Causes:
- Token expired (default 30 minutes)
- Token malformed
- 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:
- Create separate vendor account for vendor management
- 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
Quick Reference
See RBAC Quick Reference for a condensed cheat sheet of common imports, route patterns, and permission constants.
Related Documentation
- Authentication System - JWT authentication implementation
- Architecture Overview - System-wide authentication and RBAC
- Backend Development - Backend development guide
- API Reference - Auto-generated API documentation
Document Version: 1.0 Last Updated: November 2025 Maintained By: Backend Team