feat: RBAC Phase 1 — consolidate user roles into 4-value enum
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Consolidate User.role (2-value: admin/store) + User.is_super_admin (boolean)
into a single 4-value UserRole enum: super_admin, platform_admin,
merchant_owner, store_member. Drop stale StoreUser.user_type column.
Fix role="user" bug in merchant creation.

Key changes:
- Expand UserRole enum from 2 to 4 values with computed properties
  (is_admin, is_super_admin, is_platform_admin, is_merchant_owner, is_store_user)
- Add Alembic migration (tenancy_003) for data migration + column drops
- Remove is_super_admin from JWT token payload
- Update all auth dependencies, services, routes, templates, JS, and tests
- Update all RBAC documentation

66 files changed, 1219 unit tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 22:44:29 +01:00
parent ef21d47533
commit 1dcb0e6c33
67 changed files with 874 additions and 616 deletions

View File

@@ -52,9 +52,13 @@ sequenceDiagram
## User Roles
The platform has three distinct user roles, each with specific permissions and access levels:
The platform uses a **4-value role enum** on the `User` model to distinguish user types:
### Customer Role
```
User.role: "super_admin" | "platform_admin" | "merchant_owner" | "store_member"
```
### Customer Role (Separate Model)
**Access**: Public shop and own account space
@@ -68,36 +72,56 @@ The platform has three distinct user roles, each with specific permissions and a
**Account Creation**: Self-registration via shop frontend (email verification required)
**Authentication**: Standard JWT authentication
**Authentication**: Standard JWT authentication (separate `Customer` model, not `User`)
### Store Role
### Merchant Owner (`role="merchant_owner"`)
**Access**: Store area based on permissions
**Access**: Full access to owned store dashboards
**Types**:
- **Store Owner**: Full access to store dashboard and settings
- **Store Team Members**: Access based on assigned permissions
**Characteristics**:
- Has **ALL store permissions** automatically (no role record needed)
- Ownership determined by `Merchant.owner_user_id`
- `User.is_merchant_owner` property returns `True`
- `User.is_store_user` property returns `True`
- `User.is_owner_of(store_id)` checks ownership
**Capabilities**:
- Manage products and inventory
- Process orders
- View analytics and reports
- Configure shop settings (owners only)
- Manage team members (owners only)
- Configure shop settings
- Manage team members (invite, remove, update roles)
- Access store-specific APIs
**Account Creation**:
- Owners: Created automatically when admin creates a store
- Team members: Invited by store owner via email
**Account Creation**: Created automatically when admin creates a store
**Permissions System**: Team members can have granular permissions for different areas
### Store Member (`role="store_member"`)
### Admin Role
**Access**: Store area based on assigned role permissions
**Access**: Full platform administration
**Characteristics**:
- Permissions come from `StoreUser.role_id -> Role.permissions`
- `User.is_store_user` property returns `True`
- Must be invited by merchant owner via email
**Capabilities**: Limited based on assigned role (Manager, Staff, Support, Viewer, Marketing, or custom)
**Account Creation**: Invited by merchant owner via email
**Permissions System**: Team members can have granular permissions for different areas (up to 75 permissions)
### Super Admin (`role="super_admin"`)
**Access**: Full platform administration across all platforms
**Characteristics**:
- `User.is_super_admin` property returns `True` (computed: `role == "super_admin"`)
- `User.is_admin` property returns `True`
- Can access all platforms without restriction
- Cannot access store portal (blocked by middleware)
**Capabilities**:
- Manage all stores
- Manage all stores across all platforms
- Create/manage store accounts
- Access system settings
- View all data across the platform
@@ -105,9 +129,24 @@ The platform has three distinct user roles, each with specific permissions and a
- Access audit logs
- Platform-wide analytics
**Account Creation**: Created by super admins on the backend
**Account Creation**: Created by existing super admins on the backend
**Super Privileges**: Admins can access all areas including store and customer sections
### Platform Admin (`role="platform_admin"`)
**Access**: Platform administration scoped to assigned platforms
**Characteristics**:
- `User.is_platform_admin` property returns `True` (computed: `role == "platform_admin"`)
- `User.is_admin` property returns `True`
- Scoped to specific platforms via `AdminPlatform` association
- Cannot access store portal (blocked by middleware)
**Capabilities**:
- Manage stores within assigned platforms
- Access platform-scoped settings and analytics
- View data within assigned platforms
**Account Creation**: Created by super admins
## Application Areas & Access Control
@@ -115,22 +154,23 @@ The platform has three distinct areas with different access requirements:
| Area | URL Pattern | Access | Purpose |
|------|-------------|--------|---------|
| **Admin** | `/admin/*` or `admin.platform.com` | Admin users only | Platform administration and store management |
| **Store** | `/store/*` | Store owners and team members | Store dashboard and shop management |
| **Admin** | `/admin/*` or `admin.platform.com` | Super admins and platform admins (`is_admin`) | Platform administration and store management |
| **Store** | `/store/*` | Merchant owners and store members (`is_store_user`) | Store dashboard and shop management |
| **Shop** | `/shop/*`, custom domains, subdomains | Customers and public | Public-facing eCommerce storefront |
| **API** | `/api/*` | All authenticated users (role-based) | REST API for all operations |
## Account Registration Flow
### Admin Accounts
### Admin Accounts (Super Admin & Platform Admin)
- ❌ Cannot register from frontend
- ✅ Created by super admins on the backend
- **Super Admins** (`role="super_admin"`): Created by existing super admins
-**Platform Admins** (`role="platform_admin"`): Created by super admins
- Used for: Platform administration
### Store Accounts
### Store Accounts (Merchant Owner & Store Member)
- ❌ Cannot register from frontend
-**Store Owners**: Automatically created when admin creates a new store
-**Team Members**: Invited by store owner via email invitation
-**Merchant Owners** (`role="merchant_owner"`): Automatically created when admin creates a new store
-**Store Members** (`role="store_member"`): Invited by merchant owner via email invitation
- Activation: Upon clicking email verification link
### Customer Accounts
@@ -201,7 +241,7 @@ def require_role(self, required_role: str) -> Callable
```
**Parameters**:
- `required_role` (str): The exact role name required (e.g., "admin", "store", "custom_role")
- `required_role` (str): The exact role name required (e.g., `"super_admin"`, `"platform_admin"`, `"merchant_owner"`, `"store_member"`)
**Returns**: A decorator function that:
1. Accepts a function as input
@@ -216,31 +256,31 @@ from models.database.user import User
router = APIRouter()
@router.get("/moderator-only")
@auth_manager.require_role("moderator")
async def moderator_endpoint(current_user: User):
"""Only users with role='moderator' can access this."""
return {"message": "Moderator access granted"}
@router.get("/owner-only")
@auth_manager.require_role("merchant_owner")
async def owner_endpoint(current_user: User):
"""Only users with role='merchant_owner' can access this."""
return {"message": "Merchant owner access granted"}
# Can also be used with custom roles
@router.get("/special-access")
@auth_manager.require_role("special_user")
async def special_endpoint(current_user: User):
return {"data": "special content"}
# Can also be used with other specific roles
@router.get("/super-admin-only")
@auth_manager.require_role("super_admin")
async def super_admin_endpoint(current_user: User):
return {"data": "super admin content"}
```
**Error Response**:
```json
{
"detail": "Required role 'moderator' not found. Current role: 'store'"
"detail": "Required role 'merchant_owner' not found. Current role: 'store_member'"
}
```
**Note**: For standard roles (admin, store, customer), prefer using the dedicated methods (`require_admin()`, `require_store()`, `require_customer()`) as they provide better error handling and custom exceptions.
**Note**: For standard access patterns, prefer using the dedicated methods (`require_admin()`, `require_store()`, `require_customer()`) or the computed properties (`is_admin`, `is_store_user`) as they provide better error handling and custom exceptions.
### create_default_admin_user()
Creates a default admin user if one doesn't already exist. This is typically used during initial application setup or database seeding.
Creates a default super admin user if one doesn't already exist. This is typically used during initial application setup or database seeding.
**Method Signature**:
```python
@@ -254,11 +294,11 @@ def create_default_admin_user(self, db: Session) -> User
**Behavior**:
1. Checks if a user with username "admin" already exists
2. If not found, creates a new admin user with:
2. If not found, creates a new super admin user with:
- Username: `admin`
- Email: `admin@example.com`
- Password: `admin123` (hashed with bcrypt)
- Role: `admin`
- Role: `super_admin`
- Status: Active
3. If found, returns the existing user without modification
@@ -295,7 +335,7 @@ def create_admin_from_env(db: Session):
username=admin_username,
email=admin_email,
hashed_password=auth_manager.hash_password(admin_password),
role="admin",
role="super_admin",
is_active=True
)
db.add(admin)
@@ -318,7 +358,7 @@ def create_admin_from_env(db: Session):
"sub": "123", // User ID (JWT standard claim)
"username": "testuser", // Username for display
"email": "user@example.com", // User email
"role": "store", // User role
"role": "merchant_owner", // User role (4-value enum)
"exp": 1700000000, // Expiration timestamp (JWT standard)
"iat": 1699999000 // Issued at timestamp (JWT standard)
}
@@ -342,23 +382,28 @@ JWT_EXPIRE_MINUTES=30
```mermaid
graph TD
A[Admin] --> B[Full Platform Access]
A --> C[Can Access All Areas]
A[Super Admin<br/>role=super_admin] --> B[Full Platform Access]
A --> C[All Platforms]
D[Store Owner] --> E[Store Dashboard]
AA[Platform Admin<br/>role=platform_admin] --> D2[Scoped Platform Access]
AA --> D3[Assigned Platforms Only]
D[Merchant Owner<br/>role=merchant_owner] --> E[Store Dashboard]
D --> F[Team Management]
D --> G[Shop Settings]
D --> H[All Store Data]
D --> H[All Store Permissions - 75]
I[Store Team Member] --> E
I --> J[Limited Based on Permissions]
I[Store Member<br/>role=store_member] --> E
I --> J[Role-Based Permissions]
K[Customer] --> L[Shop Access]
K[Customer<br/>separate model] --> L[Shop Access]
K --> M[Own Orders]
K --> N[Own Profile]
```
**Admin Override**: Admin users have implicit access to all areas, including store and customer sections. This allows admins to provide support and manage the platform effectively.
**Admin Override**: Admin users (`is_admin`: super admins and platform admins) have access to the admin portal. They cannot access the store portal directly -- these are separate security boundaries enforced by middleware.
**Note**: `is_super_admin` is no longer a database column. It is a computed property: `User.role == "super_admin"`. JWT tokens no longer include an `is_super_admin` claim; derive it from the `role` claim instead.
## Security Features
@@ -505,7 +550,7 @@ def test_password_hashing():
def test_create_token():
auth_manager = AuthManager()
user = create_test_user(role="store")
user = create_test_user(role="merchant_owner")
token_data = auth_manager.create_access_token(user)
@@ -600,11 +645,11 @@ async def dashboard(
db: Session = Depends(get_db)
):
"""Accessible by all authenticated users, but returns different data."""
if current_user.role == "admin":
# Admin sees everything
if current_user.is_admin:
# Admin (super_admin or platform_admin) sees platform data
data = get_admin_dashboard(db)
elif current_user.role == "store":
# Store sees their data only
elif current_user.is_store_user:
# Store user (merchant_owner or store_member) sees their store data
data = get_store_dashboard(db, current_user.id)
else:
# Customer sees their orders

View File

@@ -35,18 +35,20 @@ from app.modules.tenancy.models import User # noqa: API-007 violation
id: int # User ID
email: str # Email address
username: str # Username
role: str # "admin" or "store"
role: str # "super_admin", "platform_admin", "merchant_owner", or "store_member"
is_active: bool # Account status
```
### Admin-Specific Fields
```python
is_super_admin: bool # True for super admins
is_super_admin: bool # Computed: role == "super_admin" (not stored in DB or JWT)
accessible_platform_ids: list[int] | None # Platform IDs (None = all for super admin)
token_platform_id: int | None # Selected platform from JWT
token_platform_code: str | None # Selected platform code from JWT
```
**Note**: `is_super_admin` is no longer a database column or JWT claim. It is derived from `role == "super_admin"`. On the `User` model it is a computed property; on `UserContext` it is populated from the role field during `from_user()` construction.
### Store-Specific Fields
```python
token_store_id: int | None # Store ID from JWT
@@ -80,7 +82,9 @@ preferred_language: str | None
```
1. POST /api/v1/admin/auth/login
- Returns LoginResponse with user data and token
- Token includes: user_id, role, is_super_admin, accessible_platforms
- Token includes: user_id, role (e.g. "super_admin" or "platform_admin"),
accessible_platforms
- Note: is_super_admin is NOT in the JWT; derive from role == "super_admin"
2. GET /api/v1/admin/auth/accessible-platforms
- Returns list of platforms admin can access
@@ -93,29 +97,30 @@ preferred_language: str | None
4. Subsequent API calls
- Token decoded → UserContext populated
- current_user.token_platform_id available
- current_user.is_super_admin derived from role
```
### JWT Token → UserContext Mapping
When a JWT token is decoded, these fields are mapped:
| JWT Claim | UserContext Field |
|-----------|-------------------|
| `sub` | `id` |
| `username` | `username` |
| `email` | `email` |
| `role` | `role` |
| `is_super_admin` | `is_super_admin` |
| `accessible_platforms` | `accessible_platform_ids` |
| `platform_id` | `token_platform_id` |
| `platform_code` | `token_platform_code` |
| `store_id` | `token_store_id` |
| `store_code` | `token_store_code` |
| `store_role` | `token_store_role` |
| JWT Claim | UserContext Field | Notes |
|-----------|-------------------|-------|
| `sub` | `id` | |
| `username` | `username` | |
| `email` | `email` | |
| `role` | `role` | 4-value enum: `super_admin`, `platform_admin`, `merchant_owner`, `store_member` |
| *(derived from role)* | `is_super_admin` | Computed: `role == "super_admin"` (no longer a JWT claim) |
| `accessible_platforms` | `accessible_platform_ids` | |
| `platform_id` | `token_platform_id` | |
| `platform_code` | `token_platform_code` | |
| `store_id` | `token_store_id` | |
| `store_code` | `token_store_code` | |
| `store_role` | `token_store_role` | |
## Helper Methods
`UserContext` provides helper methods:
`UserContext` provides helper methods and computed properties:
```python
# Check platform access
@@ -127,10 +132,16 @@ platform_ids = current_user.get_accessible_platform_ids()
# Returns None for super admins (all platforms)
# Returns list[int] for platform admins
# Check role
if current_user.is_admin:
# Check role categories (computed from role field)
if current_user.is_admin: # role in ("super_admin", "platform_admin")
...
if current_user.is_store:
if current_user.is_super_admin: # role == "super_admin"
...
if current_user.is_platform_admin: # role == "platform_admin"
...
if current_user.is_merchant_owner: # role == "merchant_owner"
...
if current_user.is_store_user: # role in ("merchant_owner", "store_member")
...
# Full name