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:
@@ -144,7 +144,7 @@ class TestCreateAccessToken:
|
||||
payload = jose_jwt.decode(
|
||||
result["access_token"], auth_manager.secret_key, algorithms=["HS256"]
|
||||
)
|
||||
assert payload["is_super_admin"] is True
|
||||
assert payload["role"] == "super_admin"
|
||||
assert "accessible_platforms" not in payload
|
||||
|
||||
def test_platform_admin_with_platforms(self, db, auth_manager, test_platform_admin):
|
||||
@@ -155,7 +155,7 @@ class TestCreateAccessToken:
|
||||
payload = jose_jwt.decode(
|
||||
result["access_token"], auth_manager.secret_key, algorithms=["HS256"]
|
||||
)
|
||||
assert payload["is_super_admin"] is False
|
||||
assert payload["role"] == "platform_admin"
|
||||
assert payload["accessible_platforms"] == [1, 2, 3]
|
||||
|
||||
def test_platform_admin_without_platforms(self, db, auth_manager, test_platform_admin):
|
||||
@@ -166,7 +166,7 @@ class TestCreateAccessToken:
|
||||
payload = jose_jwt.decode(
|
||||
result["access_token"], auth_manager.secret_key, algorithms=["HS256"]
|
||||
)
|
||||
assert payload["is_super_admin"] is False
|
||||
assert payload["role"] == "platform_admin"
|
||||
assert "accessible_platforms" not in payload
|
||||
|
||||
def test_store_context(self, auth_manager, test_user):
|
||||
@@ -223,7 +223,7 @@ class TestCreateAccessToken:
|
||||
payload = jose_jwt.decode(
|
||||
result["access_token"], auth_manager.secret_key, algorithms=["HS256"]
|
||||
)
|
||||
assert "is_super_admin" not in payload
|
||||
assert payload["role"] not in ("super_admin", "platform_admin")
|
||||
assert "accessible_platforms" not in payload
|
||||
|
||||
|
||||
@@ -271,10 +271,10 @@ class TestVerifyToken:
|
||||
|
||||
def test_valid_admin_token(self, auth_manager):
|
||||
token = self._encode(
|
||||
self._base_payload(is_super_admin=True), auth_manager.secret_key
|
||||
self._base_payload(role="super_admin"), auth_manager.secret_key
|
||||
)
|
||||
data = auth_manager.verify_token(token)
|
||||
assert data["is_super_admin"] is True
|
||||
assert data["role"] == "super_admin"
|
||||
|
||||
def test_valid_platform_token(self, auth_manager):
|
||||
token = self._encode(
|
||||
@@ -288,7 +288,7 @@ class TestVerifyToken:
|
||||
def test_valid_all_claims(self, auth_manager):
|
||||
token = self._encode(
|
||||
self._base_payload(
|
||||
is_super_admin=False,
|
||||
role="platform_admin",
|
||||
accessible_platforms=[1, 2],
|
||||
platform_id=1,
|
||||
platform_code="us",
|
||||
@@ -383,7 +383,7 @@ class TestGetCurrentUser:
|
||||
token_data = auth_manager.create_access_token(test_super_admin)
|
||||
creds = self._make_credentials(token_data["access_token"])
|
||||
user = auth_manager.get_current_user(db, creds)
|
||||
assert user.token_is_super_admin is True
|
||||
assert user.is_super_admin is True
|
||||
|
||||
def test_attaches_platform_attrs(self, db, auth_manager, test_admin):
|
||||
token_data = auth_manager.create_access_token(
|
||||
@@ -408,7 +408,7 @@ class TestGetCurrentUser:
|
||||
token_data = auth_manager.create_access_token(test_user)
|
||||
creds = self._make_credentials(token_data["access_token"])
|
||||
user = auth_manager.get_current_user(db, creds)
|
||||
assert not hasattr(user, "token_is_super_admin")
|
||||
assert user.is_super_admin is False
|
||||
assert not hasattr(user, "token_platform_id")
|
||||
assert not hasattr(user, "token_store_id")
|
||||
|
||||
@@ -468,38 +468,50 @@ class TestRequireRole:
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestRequireAdmin:
|
||||
"""Test require_admin method."""
|
||||
"""Test require_admin method (accepts super_admin and platform_admin)."""
|
||||
|
||||
def test_admin_accepted(self, auth_manager):
|
||||
def test_super_admin_accepted(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.role = "admin"
|
||||
user.is_admin = True
|
||||
result = auth_manager.require_admin(user)
|
||||
assert result is user
|
||||
|
||||
def test_platform_admin_accepted(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.is_admin = True
|
||||
result = auth_manager.require_admin(user)
|
||||
assert result is user
|
||||
|
||||
def test_non_admin_rejected(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.role = "user"
|
||||
user.is_admin = False
|
||||
with pytest.raises(AdminRequiredException):
|
||||
auth_manager.require_admin(user)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestRequireStore:
|
||||
"""Test require_store method (accepts store and admin roles)."""
|
||||
"""Test require_store method (accepts merchant_owner and store_member)."""
|
||||
|
||||
def test_store_accepted(self, auth_manager):
|
||||
def test_merchant_owner_accepted(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.role = "store"
|
||||
user.is_store_user = True
|
||||
assert auth_manager.require_store(user) is user
|
||||
|
||||
def test_admin_accepted(self, auth_manager):
|
||||
def test_store_member_accepted(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.role = "admin"
|
||||
user.is_store_user = True
|
||||
assert auth_manager.require_store(user) is user
|
||||
|
||||
def test_admin_rejected(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.is_store_user = False
|
||||
with pytest.raises(InsufficientPermissionsException):
|
||||
auth_manager.require_store(user)
|
||||
|
||||
def test_customer_rejected(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.role = "customer"
|
||||
user.is_store_user = False
|
||||
with pytest.raises(InsufficientPermissionsException):
|
||||
auth_manager.require_store(user)
|
||||
|
||||
@@ -520,7 +532,7 @@ class TestRequireCustomer:
|
||||
|
||||
def test_store_rejected(self, auth_manager):
|
||||
user = Mock(spec=User)
|
||||
user.role = "store"
|
||||
user.role = "merchant_owner"
|
||||
with pytest.raises(InsufficientPermissionsException):
|
||||
auth_manager.require_customer(user)
|
||||
|
||||
@@ -535,7 +547,7 @@ class TestCreateDefaultAdminUser:
|
||||
def test_creates_admin_when_none_exists(self, db, auth_manager):
|
||||
user = auth_manager.create_default_admin_user(db)
|
||||
assert user.username == "admin"
|
||||
assert user.role == "admin"
|
||||
assert user.role == "super_admin"
|
||||
assert user.is_super_admin is True
|
||||
assert user.is_active is True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user