# Store RBAC System - Complete Guide ## Overview The store dashboard implements a **Role-Based Access Control (RBAC)** system that distinguishes between **Owners** and **Team Members**, with granular permissions for team members. --- ## User Types ### 1. Merchant Owner **Who:** The user who created/owns the store account. **Characteristics:** - Has **ALL permissions** automatically (no role needed) - Cannot be removed or have permissions restricted - Can invite team members - Can create and manage roles - Identified by `User.role = "merchant_owner"` and `Merchant.owner_user_id` - `User.is_merchant_owner` property returns `True` - `User.is_store_user` property returns `True` - Ownership checked via `User.is_owner_of(store_id)` **Database:** ```python # User record for merchant owner { "id": 5, "role": "merchant_owner", # Role on User model } # StoreUser record for owner { "store_id": 1, "user_id": 5, "role_id": None, # No role needed (owner has all perms) "is_active": True } # Merchant record links ownership { "owner_user_id": 5 # Links to User.id } ``` **Note:** The `user_type` column was removed from StoreUser. Ownership is determined by `User.role == "merchant_owner"` and `Merchant.owner_user_id`, not by a field on StoreUser. **Permissions:** - ✅ **All 75 permissions** (complete access) - See full list below --- ### 2. Store Members (Team Members) **Who:** Users invited by the merchant owner to help manage the store. **Characteristics:** - Have **limited permissions** based on assigned role - Must be invited via email - Invitation must be accepted before activation - Can be assigned one of the pre-defined roles or custom role - Identified by `User.role = "store_member"` - `User.is_store_user` property returns `True` - Permissions come from `StoreUser.role_id -> Role.permissions` **Database:** ```python # User record for store member { "id": 7, "role": "store_member", # Role on User model } # StoreUser record for team member { "store_id": 1, "user_id": 7, "role_id": 3, # Role required for permission lookup "is_active": True, "invitation_token": None, # Accepted "invitation_accepted_at": "2024-11-15 10:30:00" } # Role record { "id": 3, "store_id": 1, "name": "Manager", "permissions": [ "dashboard.view", "products.view", "products.create", "products.edit", "orders.view", ... ] } ``` **Permissions:** - 🔒 **Limited** based on assigned role - Can have between 0 and 75 permissions - Common roles: Manager, Staff, Support, Viewer, Marketing --- ## Permission System ### All Available Permissions (75 total) ```python class StorePermissions(str, Enum): # Dashboard (1) DASHBOARD_VIEW = "dashboard.view" # Products (6) PRODUCTS_VIEW = "products.view" PRODUCTS_CREATE = "products.create" PRODUCTS_EDIT = "products.edit" PRODUCTS_DELETE = "products.delete" PRODUCTS_IMPORT = "products.import" PRODUCTS_EXPORT = "products.export" # Stock/Inventory (3) STOCK_VIEW = "stock.view" STOCK_EDIT = "stock.edit" STOCK_TRANSFER = "stock.transfer" # Orders (4) ORDERS_VIEW = "orders.view" ORDERS_EDIT = "orders.edit" ORDERS_CANCEL = "orders.cancel" ORDERS_REFUND = "orders.refund" # Customers (4) CUSTOMERS_VIEW = "customers.view" CUSTOMERS_EDIT = "customers.edit" CUSTOMERS_DELETE = "customers.delete" CUSTOMERS_EXPORT = "customers.export" # Marketing (3) MARKETING_VIEW = "marketing.view" MARKETING_CREATE = "marketing.create" MARKETING_SEND = "marketing.send" # Reports (3) REPORTS_VIEW = "reports.view" REPORTS_FINANCIAL = "reports.financial" REPORTS_EXPORT = "reports.export" # Settings (4) SETTINGS_VIEW = "settings.view" SETTINGS_EDIT = "settings.edit" SETTINGS_THEME = "settings.theme" SETTINGS_DOMAINS = "settings.domains" # Team Management (4) TEAM_VIEW = "team.view" TEAM_INVITE = "team.invite" TEAM_EDIT = "team.edit" TEAM_REMOVE = "team.remove" # Marketplace Imports (3) IMPORTS_VIEW = "imports.view" IMPORTS_CREATE = "imports.create" IMPORTS_CANCEL = "imports.cancel" ``` --- ## Pre-Defined Roles ### 1. Owner (All 75 permissions) **Use case:** Store owner (automatically assigned) - ✅ Full access to everything - ✅ Cannot be restricted - ✅ No role record needed (permissions checked differently) --- ### 2. Manager (43 permissions) **Use case:** Senior staff who manage most operations **Has access to:** - ✅ Dashboard, Products (all), Stock (all) - ✅ Orders (all), Customers (view, edit, export) - ✅ Marketing (all), Reports (all including financial) - ✅ Settings (view, theme) - ✅ Imports (all) **Does NOT have:** - ❌ `customers.delete` - Cannot delete customers - ❌ `settings.edit` - Cannot change core settings - ❌ `settings.domains` - Cannot manage domains - ❌ `team.*` - Cannot manage team members --- ### 3. Staff (10 permissions) **Use case:** Daily operations staff **Has access to:** - ✅ Dashboard view - ✅ Products (view, create, edit) - ✅ Stock (view, edit) - ✅ Orders (view, edit) - ✅ Customers (view, edit) **Does NOT have:** - ❌ Delete anything - ❌ Import/export - ❌ Marketing - ❌ Financial reports - ❌ Settings - ❌ Team management --- ### 4. Support (6 permissions) **Use case:** Customer support team **Has access to:** - ✅ Dashboard view - ✅ Products (view only) - ✅ Orders (view, edit) - ✅ Customers (view, edit) **Does NOT have:** - ❌ Create/delete products - ❌ Stock management - ❌ Marketing - ❌ Reports - ❌ Settings - ❌ Team management --- ### 5. Viewer (6 permissions) **Use case:** Read-only access for reporting/audit **Has access to:** - ✅ Dashboard (view) - ✅ Products (view) - ✅ Stock (view) - ✅ Orders (view) - ✅ Customers (view) - ✅ Reports (view) **Does NOT have:** - ❌ Edit anything - ❌ Create/delete anything - ❌ Marketing - ❌ Financial reports - ❌ Settings - ❌ Team management --- ### 6. Marketing (7 permissions) **Use case:** Marketing team focused on campaigns **Has access to:** - ✅ Dashboard (view) - ✅ Customers (view, export) - ✅ Marketing (all) - ✅ Reports (view) **Does NOT have:** - ❌ Products management - ❌ Orders management - ❌ Stock management - ❌ Financial reports - ❌ Settings - ❌ Team management --- ## Permission Checking Logic ### How Permissions Are Checked ```python # In User model (models/database/user.py) def has_store_permission(self, store_id: int, permission: str) -> bool: """Check if user has a specific permission in a store.""" # Step 1: Check if user is owner if self.is_owner_of(store_id): return True # ✅ Owners have ALL permissions # Step 2: Check team member permissions for vm in self.store_memberships: if vm.store_id == store_id and vm.is_active: if vm.role and permission in vm.role.permissions: return True # ✅ Permission found in role # No permission found return False ``` ### Permission Checking Flow ``` Request → Middleware → Extract store from URL ↓ Check user authentication ↓ Check if user is owner ├── YES → ✅ Allow (all permissions) └── NO ↓ Check if user is team member ├── NO → ❌ Deny └── YES ↓ Check if membership is active ├── NO → ❌ Deny └── YES ↓ Check if role has required permission ├── NO → ❌ Deny (403 Forbidden) └── YES → ✅ Allow ``` --- ## Using Permissions in Code ### 1. Require Specific Permission **When to use:** Endpoint needs one specific permission ```python from fastapi import APIRouter, Depends from app.api.deps import require_store_permission from app.core.permissions import StorePermissions from models.database.user import User router = APIRouter() @router.post("/products") def create_product( product_data: ProductCreate, user: User = Depends( require_store_permission(StorePermissions.PRODUCTS_CREATE.value) ) ): """ Create a product. Required permission: products.create ✅ Owner: Always allowed ✅ Manager: Allowed (has products.create) ✅ Staff: Allowed (has products.create) ❌ Support: Denied (no products.create) ❌ Viewer: Denied (no products.create) ❌ Marketing: Denied (no products.create) """ # Create product... pass ``` --- ### 2. Require ANY Permission **When to use:** Endpoint can be accessed with any of several permissions ```python @router.get("/dashboard") def view_dashboard( user: User = Depends( require_any_store_permission( StorePermissions.DASHBOARD_VIEW.value, StorePermissions.REPORTS_VIEW.value ) ) ): """ View dashboard. Required: dashboard.view OR reports.view ✅ Owner: Always allowed ✅ Manager: Allowed (has both) ✅ Staff: Allowed (has dashboard.view) ✅ Support: Allowed (has dashboard.view) ✅ Viewer: Allowed (has both) ✅ Marketing: Allowed (has both) """ # Show dashboard... pass ``` --- ### 3. Require ALL Permissions **When to use:** Endpoint needs multiple permissions ```python @router.post("/products/bulk-delete") def bulk_delete_products( user: User = Depends( require_all_store_permissions( StorePermissions.PRODUCTS_VIEW.value, StorePermissions.PRODUCTS_DELETE.value ) ) ): """ Bulk delete products. Required: products.view AND products.delete ✅ Owner: Always allowed ✅ Manager: Allowed (has both) ❌ Staff: Denied (no products.delete) ❌ Support: Denied (no products.delete) ❌ Viewer: Denied (no products.delete) ❌ Marketing: Denied (no products.delete) """ # Delete products... pass ``` --- ### 4. Require Owner Only **When to use:** Endpoint is owner-only (team management, critical settings) ```python from app.api.deps import require_store_owner @router.post("/team/invite") def invite_team_member( email: str, role_id: int, user: User = Depends(require_store_owner) ): """ Invite a team member. Required: Must be store owner ✅ Owner: Allowed ❌ Manager: Denied (not owner) ❌ All team members: Denied (not owner) """ # Invite team member... pass ``` --- ### 5. Get User Permissions **When to use:** Need to check permissions in business logic ```python from app.api.deps import get_user_permissions @router.get("/my-permissions") def list_my_permissions( permissions: list = Depends(get_user_permissions) ): """ Get all permissions for current user. Returns: - Owner: All 75 permissions - Team Member: Permissions from their role """ return {"permissions": permissions} ``` --- ## Database Schema ### StoreUser Table ```sql CREATE TABLE store_users ( id SERIAL PRIMARY KEY, store_id INTEGER NOT NULL REFERENCES stores(id), user_id INTEGER NOT NULL REFERENCES users(id), role_id INTEGER REFERENCES roles(id), -- NULL for merchant owners invited_by INTEGER REFERENCES users(id), invitation_token VARCHAR, invitation_sent_at TIMESTAMP, invitation_accepted_at TIMESTAMP, is_active BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- Note: user_type column removed. Ownership determined by -- User.role = 'merchant_owner' and Merchant.owner_user_id. ``` ### Role Table ```sql CREATE TABLE roles ( id SERIAL PRIMARY KEY, store_id INTEGER NOT NULL REFERENCES stores(id), name VARCHAR(100) NOT NULL, permissions JSON DEFAULT '[]', -- Array of permission strings created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ``` --- ## Team Member Lifecycle ### 1. Invitation ``` Merchant owner invites user: → User created with role="store_member" → StoreUser created: { "role_id": 3, -- Assigned role "is_active": False, "invitation_token": "abc123...", "invitation_sent_at": "2024-11-29 10:00:00", "invitation_accepted_at": null } ``` ### 2. Acceptance ``` User accepts invitation → StoreUser updated: { "is_active": True, "invitation_token": null, "invitation_accepted_at": "2024-11-29 10:30:00" } ``` ### 3. Active Member ``` Member can now access store dashboard with role permissions ``` ### 4. Deactivation ``` Owner deactivates member → StoreUser updated: { "is_active": False } ``` --- ## Common Use Cases ### Use Case 1: Dashboard Access **Q:** Can all users access the dashboard? **A:** Yes, if they have `dashboard.view` permission. - ✅ Owner: Always - ✅ Manager, Staff, Support, Viewer, Marketing: All have it - ❌ Custom role without `dashboard.view`: No --- ### Use Case 2: Product Management **Q:** Who can create products? **A:** Users with `products.create` permission. - ✅ Owner: Always - ✅ Manager: Yes (has permission) - ✅ Staff: Yes (has permission) - ❌ Support, Viewer, Marketing: No --- ### Use Case 3: Financial Reports **Q:** Who can view financial reports? **A:** Users with `reports.financial` permission. - ✅ Owner: Always - ✅ Manager: Yes (has permission) - ❌ Staff, Support, Viewer, Marketing: No --- ### Use Case 4: Team Management **Q:** Who can invite team members? **A:** Only the store owner. - ✅ Owner: Yes (owner-only operation) - ❌ All team members (including Manager): No --- ### Use Case 5: Settings Changes **Q:** Who can change store settings? **A:** Users with `settings.edit` permission. - ✅ Owner: Always - ❌ Manager: No (doesn't have permission) - ❌ All other roles: No --- ## Error Responses ### Missing Permission ```http HTTP 403 Forbidden { "error_code": "INSUFFICIENT_STORE_PERMISSIONS", "message": "You don't have permission to perform this action", "details": { "required_permission": "products.delete", "store_code": "orion" } } ``` ### Not Owner ```http HTTP 403 Forbidden { "error_code": "STORE_OWNER_ONLY", "message": "This operation requires store owner privileges", "details": { "operation": "team management", "store_code": "orion" } } ``` ### Inactive Membership ```http HTTP 403 Forbidden { "error_code": "INACTIVE_STORE_MEMBERSHIP", "message": "Your store membership is inactive" } ``` --- ## Summary ### Merchant Owner vs Store Member | Feature | Merchant Owner (`role="merchant_owner"`) | Store Member (`role="store_member"`) | |---------|-------|-------------| | **Permissions** | All 75 (automatic) | Based on role (0-75) | | **Role Required** | No | Yes | | **Can Be Removed** | No | Yes | | **Team Management** | Yes | No | | **Critical Settings** | Yes | No (usually) | | **Invitation Required** | No (creates store) | Yes | | **Ownership Determined By** | `Merchant.owner_user_id` | N/A | ### Permission Hierarchy ``` Merchant Owner (75 permissions) └─ Manager (43 permissions) └─ Staff (10 permissions) └─ Support (6 permissions) └─ Viewer (6 permissions, read-only) Marketing (7 permissions, specialized) ``` ### Best Practices 1. **Use Constants:** Always use `StorePermissions.PERMISSION_NAME.value` 2. **Least Privilege:** Give team members minimum permissions needed 3. **Owner Only:** Keep sensitive operations owner-only 4. **Custom Roles:** Create custom roles for specific needs 5. **Regular Audit:** Review team member permissions regularly --- ## Custom Role Management ### Overview Store owners can create, edit, and delete custom roles with granular permission selection. Preset roles (manager, staff, support, viewer, marketing) cannot be deleted but can be edited. ### Store Role CRUD API All endpoints require **store owner** authentication. | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/v1/store/team/roles` | List all roles (creates defaults if none exist) | | `POST` | `/api/v1/store/team/roles` | Create a custom role | | `PUT` | `/api/v1/store/team/roles/{id}` | Update a role's name/permissions | | `DELETE` | `/api/v1/store/team/roles/{id}` | Delete a custom role | **Validation rules:** - Cannot create/rename a role to a preset name (manager, staff, etc.) - Cannot delete preset roles - Cannot delete a role with assigned team members - All permission IDs are validated against the module-discovered permission catalog ### Permission Catalog API Returns all available permissions grouped by category, with human-readable labels and descriptions. ``` GET /api/v1/store/team/permissions/catalog ``` **Required Permission:** `team.view` **Response:** ```json { "categories": [ { "id": "team", "label": "tenancy.permissions.category.team", "permissions": [ { "id": "team.view", "label": "tenancy.permissions.team_view", "description": "tenancy.permissions.team_view_desc", "is_owner_only": false } ] } ] } ``` Permissions are discovered from all module `definition.py` files via `PermissionDiscoveryService.get_permissions_by_category()`. ### Role Editor UI The role editor page at `/store/{store_code}/team/roles`: - Lists all roles (preset + custom) with permission counts - Modal for creating/editing roles with a permission matrix - Permissions displayed with labels, IDs, and hover descriptions - Category-level "Select All / Deselect All" toggles - Owner-only permissions marked with an "Owner" badge **Alpine.js component:** `storeRoles()` in `app/modules/tenancy/static/store/js/roles.js` **Menu location:** Account section > Roles (requires `team.view` permission) --- ## Admin Store Roles Management ### Overview Super admins and platform admins can manage roles for any store via the admin panel. Platform admins are scoped to stores within their assigned platforms. ### Admin Role CRUD API All endpoints require **admin authentication** (`get_current_admin_api`). | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/v1/admin/store-roles?store_id=X` | List roles for a store | | `GET` | `/api/v1/admin/store-roles/permissions/catalog` | Permission catalog | | `POST` | `/api/v1/admin/store-roles?store_id=X` | Create a role | | `PUT` | `/api/v1/admin/store-roles/{id}?store_id=X` | Update a role | | `DELETE` | `/api/v1/admin/store-roles/{id}?store_id=X` | Delete a role | ### Platform Admin Scoping Platform admins can only access stores that belong to one of their assigned platforms: ```python # In StoreTeamService.validate_admin_store_access(): # 1. Super admin (accessible_platform_ids is None) → access all stores # 2. Platform admin → store must exist in StorePlatform where # platform_id is in the admin's accessible_platform_ids ``` The scoping is enforced at the service layer via `validate_admin_store_access()`, called by every admin endpoint before performing operations. ### Admin UI Page at `/admin/store-roles`: - Tom Select store search/selector (shared `initStoreSelector()` component) - Platform admins see only stores in their assigned platforms - Same role CRUD and permission matrix as the store-side UI - Located in the "User Management" admin menu section **Alpine.js component:** `adminStoreRoles()` in `app/modules/tenancy/static/admin/js/store-roles.js` ### Audit Trail All role operations are logged via `AuditAggregatorService`: | Action | Description | |--------|-------------| | `role.create` | Custom role created | | `role.update` | Role name or permissions modified | | `role.delete` | Custom role deleted | | `member.role_change` | Team member assigned a different role | | `member.invite` | Team member invited | | `member.remove` | Team member removed | --- ## Key Files | File | Purpose | |------|---------| | `app/modules/tenancy/services/store_team_service.py` | Role CRUD, platform scoping, audit trail | | `app/modules/tenancy/services/permission_discovery_service.py` | Permission catalog, role presets | | `app/modules/tenancy/routes/api/store_team.py` | Store team & role API endpoints | | `app/modules/tenancy/routes/api/admin_store_roles.py` | Admin store role API endpoints | | `app/modules/tenancy/schemas/team.py` | Request/response schemas | | `app/modules/tenancy/static/store/js/roles.js` | Store role editor Alpine.js component | | `app/modules/tenancy/static/admin/js/store-roles.js` | Admin role editor Alpine.js component | | `app/modules/tenancy/templates/tenancy/store/roles.html` | Store role editor template | | `app/modules/tenancy/templates/tenancy/admin/store-roles.html` | Admin role editor template | --- This RBAC system provides flexible, secure access control for store dashboards with clear separation between owners and team members.