Complete the public -> platform naming migration across the codebase. This aligns with the naming convention where "platform" refers to the marketing/public-facing pages of the platform itself. Changes: - Update all imports from public to platform modules - Update template references from public/ to platform/ - Update route registrations to use platform prefix - Update documentation to reflect new naming - Update test files for platform API endpoints Files affected: - app/api/main.py - router imports - app/modules/*/routes/*/platform.py - route definitions - app/modules/*/templates/*/platform/ - template files - app/modules/routes.py - route discovery - docs/* - documentation updates - tests/integration/api/v1/platform/ - test files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1965 lines
56 KiB
Markdown
1965 lines
56 KiB
Markdown
# Role-Based Access Control (RBAC) Developer Guide
|
|
|
|
**Version:** 1.0
|
|
**Last Updated:** November 2025
|
|
**Audience:** Development Team
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Introduction](#introduction)
|
|
2. [RBAC Overview](#rbac-overview)
|
|
3. [System Architecture](#system-architecture)
|
|
4. [User Types & Contexts](#user-types-contexts)
|
|
5. [Database Schema](#database-schema)
|
|
6. [Permission System](#permission-system)
|
|
7. [Authentication Flow](#authentication-flow)
|
|
8. [Authorization Implementation](#authorization-implementation)
|
|
9. [Team Management](#team-management)
|
|
10. [Code Examples](#code-examples)
|
|
11. [Best Practices](#best-practices)
|
|
12. [Testing Guidelines](#testing-guidelines)
|
|
13. [Troubleshooting](#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:**
|
|
```python
|
|
# 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:**
|
|
```python
|
|
# 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:**
|
|
```python
|
|
# 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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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).
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
# 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
|
|
```python
|
|
"dashboard.view" # View dashboard
|
|
```
|
|
|
|
#### Products
|
|
```python
|
|
"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
|
|
```python
|
|
"stock.view" # View stock levels
|
|
"stock.edit" # Adjust stock quantities
|
|
"stock.transfer" # Transfer stock between locations
|
|
```
|
|
|
|
#### Orders
|
|
```python
|
|
"orders.view" # View orders
|
|
"orders.edit" # Edit order details
|
|
"orders.cancel" # Cancel orders
|
|
"orders.refund" # Process refunds
|
|
```
|
|
|
|
#### Customers
|
|
```python
|
|
"customers.view" # View customer list
|
|
"customers.edit" # Edit customer details
|
|
"customers.delete" # Delete customers
|
|
"customers.export" # Export customer data
|
|
```
|
|
|
|
#### Marketing
|
|
```python
|
|
"marketing.view" # View marketing campaigns
|
|
"marketing.create" # Create campaigns
|
|
"marketing.send" # Send marketing emails
|
|
```
|
|
|
|
#### Reports
|
|
```python
|
|
"reports.view" # View basic reports
|
|
"reports.financial" # View financial reports
|
|
"reports.export" # Export report data
|
|
```
|
|
|
|
#### Settings
|
|
```python
|
|
"settings.view" # View settings
|
|
"settings.edit" # Edit basic settings
|
|
"settings.theme" # Edit theme/branding
|
|
"settings.domains" # Manage custom domains
|
|
```
|
|
|
|
#### Team Management
|
|
```python
|
|
"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
|
|
```python
|
|
"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`:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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/platform/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:
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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`.
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```javascript
|
|
// 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**
|
|
|
|
```python
|
|
@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**
|
|
|
|
```python
|
|
# 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**
|
|
|
|
```python
|
|
from app.core.permissions import VendorPermissions
|
|
|
|
require_vendor_permission(VendorPermissions.PRODUCTS_CREATE.value)
|
|
```
|
|
|
|
**❌ DON'T: Use magic strings**
|
|
|
|
```python
|
|
# BAD - typos won't be caught
|
|
require_vendor_permission("products.creat") # Typo!
|
|
```
|
|
|
|
### 3. Owner Permission Bypass
|
|
|
|
**✅ DO: Let owners bypass permission checks automatically**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
# 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**
|
|
|
|
```python
|
|
# 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**
|
|
|
|
```python
|
|
# 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**
|
|
|
|
```javascript
|
|
// 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.
|
|
|
|
```python
|
|
# 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.
|
|
|
|
```python
|
|
# 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:**
|
|
1. Token expired (default 30 minutes)
|
|
2. Token malformed
|
|
3. Token signature invalid
|
|
|
|
**Solutions:**
|
|
```python
|
|
# 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:**
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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](../backend/rbac-quick-reference.md) for a condensed cheat sheet of common imports, route patterns, and permission constants.
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Authentication System](authentication.md) - JWT authentication implementation
|
|
- [Architecture Overview](../architecture/auth-rbac.md) - System-wide authentication and RBAC
|
|
- [Backend Development](../backend/overview.md) - Backend development guide
|
|
- [API Reference](../backend/middleware-reference.md) - Auto-generated API documentation
|
|
|
|
---
|
|
|
|
**Document Version:** 1.0
|
|
**Last Updated:** November 2025
|
|
**Maintained By:** Backend Team
|