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

@@ -75,9 +75,8 @@ class TestAdminPlatformModel:
email="temp_admin@example.com",
username="temp_admin",
hashed_password=auth_manager.hash_password("temppass"),
role="admin",
role="platform_admin",
is_active=True,
is_super_admin=False,
)
db.add(temp_admin)
db.flush()
@@ -168,13 +167,13 @@ class TestAdminPlatformModel:
class TestUserAdminMethods:
"""Test User model admin-related methods."""
def test_is_super_admin_user_true(self, db, test_super_admin):
"""Test is_super_admin_user property for super admin."""
assert test_super_admin.is_super_admin_user is True
def test_is_super_admin_true(self, db, test_super_admin):
"""Test is_super_admin property for super admin."""
assert test_super_admin.is_super_admin is True
def test_is_super_admin_user_false_for_platform_admin(self, db, test_platform_admin):
"""Test is_super_admin_user property for platform admin."""
assert test_platform_admin.is_super_admin_user is False
def test_is_super_admin_false_for_platform_admin(self, db, test_platform_admin):
"""Test is_super_admin property for platform admin."""
assert test_platform_admin.is_super_admin is False
def test_is_platform_admin_true(self, db, test_platform_admin):
"""Test is_platform_admin property for platform admin."""

View File

@@ -256,9 +256,8 @@ class TestAdminPlatformServiceQueries:
email="another_padmin@example.com",
username="another_padmin",
hashed_password=auth_manager.hash_password("pass"), # noqa: SEC001
role="admin",
role="platform_admin",
is_active=True,
is_super_admin=False,
)
db.add(another_admin)
db.flush()
@@ -343,9 +342,8 @@ class TestAdminPlatformServiceSuperAdmin:
email="another_super@example.com",
username="another_super",
hashed_password=auth_manager.hash_password("pass"), # noqa: SEC001
role="admin",
role="super_admin",
is_active=True,
is_super_admin=True,
)
db.add(another_super)
db.commit()
@@ -427,8 +425,8 @@ class TestAdminPlatformServiceCreatePlatformAdmin:
assert user is not None
assert user.email == "new_padmin@example.com"
assert user.username == "new_padmin"
assert user.role == "admin"
assert user.is_super_admin is False
assert user.role == "platform_admin"
assert user.is_super_admin is False # platform_admin, not super_admin
assert user.first_name == "New"
assert user.last_name == "Admin"
assert len(assignments) == 2

View File

@@ -23,7 +23,7 @@ class TestEmailVerificationToken:
email=f"evk_{uid}@test.com",
username=f"evk_{uid}",
hashed_password="hashed_password", # noqa: SEC001
role="store",
role="merchant_owner",
is_active=True,
is_email_verified=False,
)

View File

@@ -11,7 +11,7 @@ from app.modules.tenancy.exceptions import (
InvalidInvitationTokenException,
UserNotFoundException,
)
from app.modules.tenancy.models import Role, Store, StoreUser, StoreUserType, User
from app.modules.tenancy.models import Role, Store, StoreUser, User
from app.modules.tenancy.services.store_team_service import store_team_service
# =============================================================================
@@ -43,7 +43,6 @@ def store_owner(db, team_store, test_user):
store_user = StoreUser(
store_id=team_store.id,
user_id=test_user.id,
user_type=StoreUserType.OWNER.value,
is_active=True,
)
db.add(store_user)
@@ -67,7 +66,6 @@ def team_member(db, team_store, other_user, auth_manager):
store_user = StoreUser(
store_id=team_store.id,
user_id=other_user.id,
user_type=StoreUserType.TEAM_MEMBER.value,
role_id=role.id,
is_active=True,
invitation_accepted_at=datetime.utcnow(),
@@ -88,7 +86,7 @@ def pending_invitation(db, team_store, test_user, auth_manager):
email=f"pending_{unique_id}@example.com",
username=f"pending_{unique_id}",
hashed_password=auth_manager.hash_password("temppass"), # noqa: SEC001
role="store",
role="store_member",
is_active=False,
)
db.add(new_user)
@@ -107,7 +105,6 @@ def pending_invitation(db, team_store, test_user, auth_manager):
store_user = StoreUser(
store_id=team_store.id,
user_id=new_user.id,
user_type=StoreUserType.TEAM_MEMBER.value,
role_id=role.id,
invited_by=test_user.id,
invitation_token=f"pending_token_{unique_id}",
@@ -130,7 +127,7 @@ def expired_invitation(db, team_store, test_user, auth_manager):
email=f"expired_{unique_id}@example.com",
username=f"expired_{unique_id}",
hashed_password=auth_manager.hash_password("temppass"), # noqa: SEC001
role="store",
role="store_member",
is_active=False,
)
db.add(new_user)
@@ -149,7 +146,6 @@ def expired_invitation(db, team_store, test_user, auth_manager):
store_user = StoreUser(
store_id=team_store.id,
user_id=new_user.id,
user_type=StoreUserType.TEAM_MEMBER.value,
role_id=role.id,
invited_by=test_user.id,
invitation_token=f"expired_token_{unique_id}",

View File

@@ -32,7 +32,7 @@ class TestUserAuthServicePasswordReset:
email=f"uas_{uid}@test.com",
username=f"uas_{uid}",
hashed_password=auth.hash_password("oldpass123"), # noqa: SEC001
role="store",
role="merchant_owner",
is_active=True,
is_email_verified=True,
)
@@ -52,7 +52,7 @@ class TestUserAuthServicePasswordReset:
email=f"uasi_{uid}@test.com",
username=f"uasi_{uid}",
hashed_password=auth.hash_password("inactive123"), # noqa: SEC001
role="store",
role="merchant_owner",
is_active=False,
is_email_verified=True,
)
@@ -118,7 +118,7 @@ class TestUserAuthServiceEmailVerification:
email=f"uasev_{uid}@test.com",
username=f"uasev_{uid}",
hashed_password="hashed_password", # noqa: SEC001
role="store",
role="merchant_owner",
is_active=True,
is_email_verified=False,
)
@@ -135,7 +135,7 @@ class TestUserAuthServiceEmailVerification:
email=f"uasv_{uid}@test.com",
username=f"uasv_{uid}",
hashed_password="hashed_password", # noqa: SEC001
role="store",
role="merchant_owner",
is_active=True,
is_email_verified=True,
)

View File

@@ -18,7 +18,7 @@ class TestUserModel:
email="db_test@example.com",
username="dbtest",
hashed_password="hashed_password_123", # noqa: SEC001
role="user",
role="store_member",
is_active=True,
)
@@ -29,7 +29,7 @@ class TestUserModel:
assert user.id is not None
assert user.email == "db_test@example.com"
assert user.username == "dbtest"
assert user.role == "user"
assert user.role == "store_member"
assert user.is_active is True
assert user.created_at is not None
assert user.updated_at is not None
@@ -86,7 +86,7 @@ class TestUserModel:
db.refresh(user)
assert user.is_active is True # Default
assert user.role == "store" # Default (UserRole.STORE)
assert user.role == "store_member" # Default (UserRole.STORE_MEMBER)
def test_user_optional_fields(self, db):
"""Test User model with optional fields."""

View File

@@ -23,7 +23,7 @@ class TestUserPasswordResetToken:
email=f"prt_{uid}@test.com",
username=f"prt_{uid}",
hashed_password="hashed_password", # noqa: SEC001
role="store",
role="merchant_owner",
is_active=True,
is_email_verified=True,
)