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

@@ -176,14 +176,11 @@ class AuthManager:
"iat": datetime.now(UTC), # Issued at time (JWT standard claim)
}
# Include admin-specific information for admin users
if user.is_admin:
payload["is_super_admin"] = user.is_super_admin
# For platform admins, include their accessible platform IDs
if not user.is_super_admin:
accessible = user.get_accessible_platform_ids()
if accessible is not None:
payload["accessible_platforms"] = accessible
# For platform admins, include their accessible platform IDs
if user.is_admin and not user.is_super_admin:
accessible = user.get_accessible_platform_ids()
if accessible is not None:
payload["accessible_platforms"] = accessible
# Include platform context for platform admins
if platform_id is not None:
@@ -262,8 +259,6 @@ class AuthManager:
}
# Include admin-specific information if present
if "is_super_admin" in payload:
user_data["is_super_admin"] = payload["is_super_admin"]
if "accessible_platforms" in payload:
user_data["accessible_platforms"] = payload["accessible_platforms"]
@@ -334,9 +329,6 @@ class AuthManager:
raise UserNotActiveException()
# Attach admin-specific information to user object if present in token
# These become dynamic attributes on the user object for this request
if "is_super_admin" in user_data:
user.token_is_super_admin = user_data["is_super_admin"]
if "accessible_platforms" in user_data:
user.token_accessible_platforms = user_data["accessible_platforms"]
@@ -410,28 +402,24 @@ class AuthManager:
Raises:
AdminRequiredException: If user does not have admin role
"""
# Verify user has admin role
if current_user.role != "admin":
if not current_user.is_admin:
raise AdminRequiredException()
return current_user
def require_store(self, current_user: User) -> User:
"""
Require store role (store or admin).
Stores and admins can access store areas.
Require store user role (merchant_owner or store_member).
Args:
current_user: Current authenticated user
Returns:
User: The user if they have store or admin role
User: The user if they have a store-level role
Raises:
InsufficientPermissionsException: If user is not store or admin
InsufficientPermissionsException: If user is not a store user
"""
# Check if user has store or admin role (admins have full access)
if current_user.role not in ["store", "admin"]:
if not current_user.is_store_user:
raise InsufficientPermissionsException(
message="Store access required", required_permission="store"
)
@@ -491,9 +479,8 @@ class AuthManager:
email="admin@example.com",
username="admin",
hashed_password=hashed_password,
role="admin",
role="super_admin",
is_active=True,
is_super_admin=True,
)
# Save to database