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:
@@ -155,23 +155,36 @@ StorePermissions.IMPORTS_CANCEL
|
||||
|
||||
---
|
||||
|
||||
## User Role Properties
|
||||
|
||||
```python
|
||||
# Check if super admin (computed: role == "super_admin")
|
||||
user.is_super_admin # bool
|
||||
|
||||
# Check if any admin (computed: role in ("super_admin", "platform_admin"))
|
||||
user.is_admin # bool
|
||||
|
||||
# Check if platform admin (computed: role == "platform_admin")
|
||||
user.is_platform_admin # bool
|
||||
|
||||
# Check if merchant owner (computed: role == "merchant_owner")
|
||||
user.is_merchant_owner # bool
|
||||
|
||||
# Check if store-level user (computed: role in ("merchant_owner", "store_member"))
|
||||
user.is_store_user # bool
|
||||
```
|
||||
|
||||
## User Helper Methods
|
||||
|
||||
```python
|
||||
# Check if admin
|
||||
user.is_admin # bool
|
||||
|
||||
# Check if store
|
||||
user.is_store # bool
|
||||
|
||||
# Check store ownership
|
||||
# Check store ownership (via Merchant.owner_user_id)
|
||||
user.is_owner_of(store_id) # bool
|
||||
|
||||
# Check store membership
|
||||
user.is_member_of(store_id) # bool
|
||||
|
||||
# Get role in store
|
||||
user.get_store_role(store_id) # str: "owner" | "member" | None
|
||||
user.get_store_role(store_id) # str: "owner" | role name | None
|
||||
|
||||
# Check specific permission
|
||||
user.has_store_permission(store_id, "products.create") # bool
|
||||
@@ -182,7 +195,7 @@ user.has_store_permission(store_id, "products.create") # bool
|
||||
## StoreUser Helper Methods
|
||||
|
||||
```python
|
||||
# Check if owner
|
||||
# Check if owner (derived from User.role and Merchant.owner_user_id)
|
||||
store_user.is_owner # bool
|
||||
|
||||
# Check if team member
|
||||
@@ -331,7 +344,8 @@ async function getPermissions() {
|
||||
### Unit Test
|
||||
```python
|
||||
def test_owner_has_all_permissions():
|
||||
store_user = create_store_user(user_type="owner")
|
||||
user = create_user(role="merchant_owner")
|
||||
store_user = create_store_user(user=user, store=store)
|
||||
assert store_user.has_permission("products.create")
|
||||
assert store_user.has_permission("team.invite")
|
||||
```
|
||||
@@ -423,8 +437,9 @@ token = "eyJ0eXAi..."
|
||||
decoded = jwt.decode(token, verify=False)
|
||||
print(f"User ID: {decoded['sub']}")
|
||||
print(f"Username: {decoded['username']}")
|
||||
print(f"Role: {decoded['role']}")
|
||||
print(f"Role: {decoded['role']}") # super_admin, platform_admin, merchant_owner, or store_member
|
||||
print(f"Expires: {decoded['exp']}")
|
||||
# Note: is_super_admin is no longer in JWT tokens; derive from role == "super_admin"
|
||||
```
|
||||
|
||||
### Check Cookie
|
||||
|
||||
@@ -8,56 +8,76 @@ The store dashboard implements a **Role-Based Access Control (RBAC)** system tha
|
||||
|
||||
## User Types
|
||||
|
||||
### 1. Store Owner
|
||||
### 1. Merchant Owner
|
||||
|
||||
**Who:** The user who created the store account.
|
||||
**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 `StoreUser.user_type = "owner"`
|
||||
- Linked via `Store.owner_user_id → User.id`
|
||||
- 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,
|
||||
"user_type": "owner", # ✓ Owner
|
||||
"role_id": None, # No role needed
|
||||
"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. Team Members
|
||||
### 2. Store Members (Team Members)
|
||||
|
||||
**Who:** Users invited by the store owner to help manage the store.
|
||||
**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 `StoreUser.user_type = "member"`
|
||||
- Permissions come from `StoreUser.role_id → Role.permissions`
|
||||
- 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,
|
||||
"user_type": "member", # ✓ Team member
|
||||
"role_id": 3, # ✓ Role required
|
||||
"role_id": 3, # Role required for permission lookup
|
||||
"is_active": True,
|
||||
"invitation_token": None, # Accepted
|
||||
"invitation_accepted_at": "2024-11-15 10:30:00"
|
||||
@@ -463,8 +483,7 @@ CREATE TABLE store_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
store_id INTEGER NOT NULL REFERENCES stores(id),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
user_type VARCHAR NOT NULL, -- 'owner' or 'member'
|
||||
role_id INTEGER REFERENCES roles(id), -- NULL for owners
|
||||
role_id INTEGER REFERENCES roles(id), -- NULL for merchant owners
|
||||
invited_by INTEGER REFERENCES users(id),
|
||||
invitation_token VARCHAR,
|
||||
invitation_sent_at TIMESTAMP,
|
||||
@@ -473,6 +492,8 @@ CREATE TABLE store_users (
|
||||
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
|
||||
@@ -495,9 +516,11 @@ CREATE TABLE roles (
|
||||
### 1. Invitation
|
||||
|
||||
```
|
||||
Owner invites user → StoreUser created:
|
||||
Merchant owner invites user:
|
||||
→ User created with role="store_member"
|
||||
→ StoreUser created:
|
||||
{
|
||||
"user_type": "member",
|
||||
"role_id": 3, -- Assigned role
|
||||
"is_active": False,
|
||||
"invitation_token": "abc123...",
|
||||
"invitation_sent_at": "2024-11-29 10:00:00",
|
||||
@@ -642,21 +665,22 @@ HTTP 403 Forbidden
|
||||
|
||||
## Summary
|
||||
|
||||
### Owner vs Team Member
|
||||
### Merchant Owner vs Store Member
|
||||
|
||||
| Feature | Owner | Team 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) |
|
||||
| **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
|
||||
|
||||
```
|
||||
Owner (75 permissions)
|
||||
Merchant Owner (75 permissions)
|
||||
└─ Manager (43 permissions)
|
||||
└─ Staff (10 permissions)
|
||||
└─ Support (6 permissions)
|
||||
|
||||
Reference in New Issue
Block a user