feat: RBAC Phase 1 — consolidate user roles into 4-value enum
Some checks failed
Some checks failed
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user