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

15 KiB

RBAC Implementation Recommendations - Summary

Executive Summary

Your current authentication and authorization system is well-structured. Here are my recommendations to enhance it for proper role-based access control (RBAC) in your multi-tenant e-commerce platform.

Key Recommendations

1. Clarify User.role Field

Current Issue: The User.role field is used for both platform-level and vendor-specific roles.

Solution:

  • User.role should ONLY contain: "admin" or "vendor"
  • Vendor-specific roles (manager, staff, etc.) belong in VendorUser.role
  • Customers are separate in the Customer model (already correct)

Benefits:

  • Clear separation of concerns
  • Easier to manage platform-level access
  • Prevents confusion between contexts

2. Add VendorUser.user_type Field

Why: Distinguish between vendor owners and team members at the database level.

Implementation:

class VendorUserType(str, enum.Enum):
    OWNER = "owner"
    TEAM_MEMBER = "member"

Benefits:

  • Owners have automatic full permissions
  • Team members use role-based permissions
  • Easy to query for owners vs. members
  • Prevents accidentally removing owners

3. Implement Invitation System

Components Needed:

  • invitation_token field in VendorUser
  • invitation_sent_at and invitation_accepted_at timestamps
  • Email sending service
  • Invitation acceptance endpoint

Flow:

  1. Owner invites team member via email
  2. System creates User account (inactive) and VendorUser (pending)
  3. Invitation email sent with unique token
  4. Team member clicks link, sets password, activates account
  5. VendorUser.is_active set to True

4. Define Permission Constants

Create: app/core/permissions.py with:

  • VendorPermissions enum (all available permissions)
  • PermissionGroups class (preset role permissions)
  • PermissionChecker utility class

Example Permissions:

PRODUCTS_VIEW = "products.view"
PRODUCTS_CREATE = "products.create"
PRODUCTS_DELETE = "products.delete"
ORDERS_VIEW = "orders.view"
TEAM_INVITE = "team.invite"  # Owner only
SETTINGS_EDIT = "settings.edit"  # Owner/Manager only

5. Add Permission Checking Dependencies

Create FastAPI dependencies:

  • require_vendor_permission(permission) - Single permission
  • require_vendor_owner() - Owner only
  • require_any_vendor_permission(*permissions) - Any of list
  • require_all_vendor_permissions(*permissions) - All of list
  • get_user_permissions() - Get user's permission list

Usage in Routes:

@router.post("/products")
def create_product(
    user: User = Depends(require_vendor_permission("products.create"))
):
    # User verified to have products.create permission
    ...

6. Create Vendor Team Service

Service Responsibilities:

  • invite_team_member() - Send invitations
  • accept_invitation() - Activate accounts
  • remove_team_member() - Deactivate members
  • update_member_role() - Change permissions
  • get_team_members() - List team

Why Service Layer:

  • Business logic separate from routes
  • Reusable across different contexts
  • Easier to test
  • Consistent error handling

7. Add Helper Methods to Models

User Model:

@property
def is_admin(self) -> bool
def is_owner_of(self, vendor_id: int) -> bool
def is_member_of(self, vendor_id: int) -> bool
def get_vendor_role(self, vendor_id: int) -> str
def has_vendor_permission(self, vendor_id: int, permission: str) -> bool

VendorUser Model:

@property
def is_owner(self) -> bool
def has_permission(self, permission: str) -> bool
def get_all_permissions(self) -> list

8. Enhanced Exception Handling

Add Vendor-Specific Exceptions:

  • VendorAccessDeniedException - No access to vendor
  • InsufficientVendorPermissionsException - Missing permission
  • VendorOwnerOnlyException - Owner-only operation
  • CannotRemoveVendorOwnerException - Prevent owner removal
  • InvalidInvitationTokenException - Bad invitation
  • MaxTeamMembersReachedException - Team size limit

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                    Platform Level                        │
│  User.role: "admin" or "vendor"                         │
│  - Admins: Full platform access                         │
│  - Vendors: Access to their vendor(s)                   │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    Vendor Level                          │
│  VendorUser.user_type: "owner" or "member"              │
│  - Owners: Full vendor access (all permissions)         │
│  - Members: Role-based access (limited permissions)     │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    Role Level                            │
│  Role.permissions: ["products.view", ...]               │
│  - Presets: Manager, Staff, Support, Viewer, Marketing  │
│  - Custom: Owner can define custom roles                │
└─────────────────────────────────────────────────────────┘

Permission Hierarchy

Owner (VendorUser.user_type = "owner")
  └─> ALL permissions automatically
  
Team Member (VendorUser.user_type = "member")
  └─> Role.permissions (from VendorUser.role_id)
      ├─> Manager: Most permissions except critical settings/team
      ├─> Staff: Products, orders, customers (read/write)
      ├─> Support: Orders, customers (support focus)
      ├─> Viewer: Read-only access
      └─> Custom: Owner-defined permission sets

Security Best Practices

  • Admin: /admin path
  • Vendor: /vendor path
  • Customer: /shop path
  • Prevents cross-context cookie leakage

2. Role Checking at Route Level

# Good - Check at route definition
@router.post("/products")
def create_product(
    user: User = Depends(require_vendor_permission("products.create"))
):
    ...

# Bad - Check inside handler
@router.post("/products")
def create_product(user: User):
    if not has_permission(...):  # Don't do this
        raise Exception()

3. Block Admin from Vendor Routes

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

4. Owner Cannot Be Removed

if vendor_user.is_owner:
    raise CannotRemoveVendorOwnerException(vendor.vendor_code)

5. Inactive Users Have No Permissions

if not user.is_active or not vendor_user.is_active:
    return False  # No permissions

Frontend Integration

1. Get User Permissions on Load

// On vendor dashboard load
const response = await fetch('/api/v1/vendor/team/me/permissions');
const { permissions } = await response.json();

// Store in state
localStorage.setItem('permissions', JSON.stringify(permissions));

2. Show/Hide UI Elements

// Check permission before rendering
function canViewProducts() {
    const permissions = JSON.parse(localStorage.getItem('permissions'));
    return permissions.includes('products.view');
}

// In template
{canViewProducts() && <ProductsList />}

3. Disable Actions Without Permission

<button 
    disabled={!permissions.includes('products.delete')}
    onClick={handleDelete}
>
    Delete Product
</button>

Database Schema Summary

users
├── id (PK)
├── email (unique)
├── username (unique)
├── role ('admin' | 'vendor')  ← Platform role only
├── is_active
└── is_email_verified  ← New field

vendors
├── id (PK)
├── vendor_code (unique)
├── owner_user_id (FK → users.id)
└── ...

vendor_users (junction table with role info)
├── id (PK)
├── vendor_id (FK → vendors.id)
├── user_id (FK → users.id)
├── user_type ('owner' | 'member')  ← New field
├── role_id (FK → roles.id, nullable)  ← NULL for owners
├── invitation_token  ← New field
├── invitation_sent_at  ← New field
├── invitation_accepted_at  ← New field
└── is_active

roles (vendor-specific)
├── id (PK)
├── vendor_id (FK → vendors.id)
├── name (e.g., 'Manager', 'Staff')
└── permissions (JSON: ["products.view", ...])

customers (separate, vendor-scoped)
├── id (PK)
├── vendor_id (FK → vendors.id)
├── email (unique within vendor)
└── ...  (separate auth from User)

Implementation Checklist

Phase 1: Database & Models

  • Add is_email_verified to User model
  • Update User.role to only accept 'admin'/'vendor'
  • Add user_type to VendorUser model
  • Add invitation fields to VendorUser
  • Make VendorUser.role_id nullable
  • Create permissions.py with constants
  • Add helper methods to User model
  • Add helper methods to VendorUser model
  • Run database migration

Phase 2: Services & Logic

  • Create vendor_team_service.py
  • Implement invite_team_member()
  • Implement accept_invitation()
  • Implement remove_team_member()
  • Implement update_member_role()
  • Add vendor-specific exceptions
  • Set up email sending for invitations

Phase 3: API & Dependencies

  • Add permission checking dependencies to deps.py
  • Update vendor auth endpoint to block admins
  • Create team management routes
  • Add permission checks to existing routes
  • Create invitation acceptance endpoint (public)
  • Add /me/permissions endpoint

Phase 4: Testing

  • Test owner has all permissions
  • Test team members respect role permissions
  • Test invitation flow end-to-end
  • Test cannot remove owner
  • Test admin blocked from vendor routes
  • Test permission checking in all routes
  • Test inactive users have no access

Phase 5: Frontend

  • Load user permissions on login
  • Show/hide UI based on permissions
  • Implement invitation acceptance page
  • Add team management UI (owner only)
  • Add role selector when inviting
  • Show permission lists for roles

Migration Steps

  1. Backup database
  2. Run Alembic migration (see migration guide)
  3. Update User roles (convert 'user' to 'vendor')
  4. Set vendor owners in vendor_users
  5. Create default roles for all vendors
  6. Assign roles to existing team members
  7. Verify migration with SQL queries
  8. Test system thoroughly
  9. Deploy to production

Common Pitfalls to Avoid

Don't: Mix Platform and Vendor Roles

# Bad
user.role = "manager"  # Vendor role in User table
# Good
user.role = "vendor"  # Platform role
vendor_user.role.name = "manager"  # Vendor role

Don't: Check Permissions in Business Logic

# Bad
def create_product(user, product_data):
    if not has_permission(user, "products.create"):
        raise Exception()
    # ... create product
# Good
@router.post("/products")
def create_product(
    product_data: ProductCreate,
    user: User = Depends(require_vendor_permission("products.create"))
):
    # Permission already checked by dependency
    return service.create_product(user, product_data)

Don't: Allow Owner Removal

# Bad
def remove_team_member(vendor, user_id):
    vendor_user = get_vendor_user(vendor, user_id)
    vendor_user.is_active = False
# Good
def remove_team_member(vendor, user_id):
    vendor_user = get_vendor_user(vendor, user_id)
    if vendor_user.is_owner:
        raise CannotRemoveVendorOwnerException(vendor.vendor_code)
    vendor_user.is_active = False

Don't: Forget to Check is_active

# Bad
def has_permission(user, vendor_id, permission):
    return permission in user.get_vendor_role(vendor_id).permissions
# Good
def has_permission(user, vendor_id, permission):
    if not user.is_active:
        return False
    
    vendor_user = get_vendor_user(user, vendor_id)
    if not vendor_user or not vendor_user.is_active:
        return False
    
    return vendor_user.has_permission(permission)

Questions & Answers

Q: Can a user be a team member of multiple vendors? A: Yes! A user can have multiple VendorUser entries, one per vendor. Each has independent role/permissions.

Q: Can a vendor have multiple owners? A: No. Each vendor has one owner (Vendor.owner_user_id). The owner can delegate management via Manager role, but there's only one true owner.

Q: What happens to team members when owner changes? A: Team members remain. Update Vendor.owner_user_id and both old/new owner's VendorUser.user_type.

Q: Can admins access vendor dashboards? A: No. Admins are blocked from vendor routes. This is intentional security.

Q: How do customers authenticate? A: Customers use the Customer model with separate authentication. They're vendor-scoped and can't access vendor/admin areas.

Q: Can permissions be customized per team member? A: Yes! When inviting, pass custom_permissions array to override role presets.

Q: How do invitation links work? A: Invitation token is a secure random string stored in VendorUser. Link format: /vendor/invitation/accept?token=<token>. Token is single-use and expires in 7 days.

Next Steps

  1. Review all generated files in /home/claude/
  2. Integrate changes into your codebase
  3. Create and run database migration
  4. Update existing routes with permission checks
  5. Implement team management UI
  6. Test thoroughly in development
  7. Deploy to staging for user acceptance testing
  8. Deploy to production with monitoring

Support

All implementation files have been created in /home/claude/:

  • user_model_improved.py - Updated User model
  • vendor_user_improved.py - Updated VendorUser model
  • permissions.py - Permission system
  • vendor_exceptions.py - Vendor exceptions
  • deps_permissions.py - Permission dependencies
  • vendor_team_service.py - Team management service
  • team_routes_example.py - Example routes
  • rbac_migration_guide.md - Database migration guide

These are ready to be integrated into your project structure.