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.roleshould ONLY contain:"admin"or"vendor"- Vendor-specific roles (manager, staff, etc.) belong in
VendorUser.role - Customers are separate in the
Customermodel (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_tokenfield in VendorUserinvitation_sent_atandinvitation_accepted_attimestamps- Email sending service
- Invitation acceptance endpoint
Flow:
- Owner invites team member via email
- System creates User account (inactive) and VendorUser (pending)
- Invitation email sent with unique token
- Team member clicks link, sets password, activates account
- VendorUser.is_active set to True
4. ✅ Define Permission Constants
Create: app/core/permissions.py with:
VendorPermissionsenum (all available permissions)PermissionGroupsclass (preset role permissions)PermissionCheckerutility 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 permissionrequire_vendor_owner()- Owner onlyrequire_any_vendor_permission(*permissions)- Any of listrequire_all_vendor_permissions(*permissions)- All of listget_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 invitationsaccept_invitation()- Activate accountsremove_team_member()- Deactivate membersupdate_member_role()- Change permissionsget_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 vendorInsufficientVendorPermissionsException- Missing permissionVendorOwnerOnlyException- Owner-only operationCannotRemoveVendorOwnerException- Prevent owner removalInvalidInvitationTokenException- Bad invitationMaxTeamMembersReachedException- 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
1. Cookie Path Isolation (Already Implemented ✓)
- Admin:
/adminpath - Vendor:
/vendorpath - Customer:
/shoppath - 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_verifiedto User model - Update User.role to only accept 'admin'/'vendor'
- Add
user_typeto 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
- Backup database
- Run Alembic migration (see migration guide)
- Update User roles (convert 'user' to 'vendor')
- Set vendor owners in vendor_users
- Create default roles for all vendors
- Assign roles to existing team members
- Verify migration with SQL queries
- Test system thoroughly
- 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
- Review all generated files in
/home/claude/ - Integrate changes into your codebase
- Create and run database migration
- Update existing routes with permission checks
- Implement team management UI
- Test thoroughly in development
- Deploy to staging for user acceptance testing
- Deploy to production with monitoring
Support
All implementation files have been created in /home/claude/:
user_model_improved.py- Updated User modelvendor_user_improved.py- Updated VendorUser modelpermissions.py- Permission systemvendor_exceptions.py- Vendor exceptionsdeps_permissions.py- Permission dependenciesvendor_team_service.py- Team management serviceteam_routes_example.py- Example routesrbac_migration_guide.md- Database migration guide
These are ready to be integrated into your project structure.