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:
@@ -8,8 +8,6 @@ other models such as User (owner), Product, Customer, and Order.
|
||||
Note: MarketplaceImportJob relationships are owned by the marketplace module.
|
||||
"""
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
@@ -420,19 +418,14 @@ class Store(Base, TimestampMixin):
|
||||
}
|
||||
|
||||
|
||||
class StoreUserType(str, enum.Enum):
|
||||
"""Types of store users."""
|
||||
|
||||
OWNER = "owner" # Store owner (full access to store area)
|
||||
TEAM_MEMBER = "member" # Team member (role-based access to store area)
|
||||
|
||||
|
||||
class StoreUser(Base, TimestampMixin):
|
||||
"""
|
||||
Represents a user's membership in a store.
|
||||
Represents a user's team membership in a store.
|
||||
|
||||
- Owner: Created automatically when store is created
|
||||
- Team Member: Invited by owner via email
|
||||
Ownership is determined via User.is_owner_of(store_id) which checks
|
||||
Merchant.owner_user_id, NOT a field on this table.
|
||||
|
||||
This table is for team members only (invited by owner).
|
||||
"""
|
||||
|
||||
__tablename__ = "store_users"
|
||||
@@ -446,10 +439,7 @@ class StoreUser(Base, TimestampMixin):
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
"""Foreign key linking to the associated User."""
|
||||
|
||||
# Distinguish between owner and team member
|
||||
user_type = Column(String, nullable=False, default=StoreUserType.TEAM_MEMBER.value)
|
||||
|
||||
# Role for team members (NULL for owners - they have all permissions)
|
||||
# Role for team members (determines granular permissions)
|
||||
role_id = Column(Integer, ForeignKey("roles.id"), nullable=True)
|
||||
"""Foreign key linking to the associated Role."""
|
||||
|
||||
@@ -480,22 +470,14 @@ class StoreUser(Base, TimestampMixin):
|
||||
"""Relationship to the Role model, representing the role held by the store user."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the StoreUser instance.
|
||||
|
||||
Returns:
|
||||
str: A string that includes the store_id, the user_id and the user_type of the StoreUser instance.
|
||||
"""
|
||||
return f"<StoreUser(store_id={self.store_id}, user_id={self.user_id}, type={self.user_type})>"
|
||||
"""Return a string representation of the StoreUser instance."""
|
||||
role_name = self.role.name if self.role else "no-role"
|
||||
return f"<StoreUser(store_id={self.store_id}, user_id={self.user_id}, role={role_name})>"
|
||||
|
||||
@property
|
||||
def is_owner(self) -> bool:
|
||||
"""Check if this is an owner membership."""
|
||||
return self.user_type == StoreUserType.OWNER.value
|
||||
|
||||
@property
|
||||
def is_team_member(self) -> bool:
|
||||
"""Check if this is a team member (not owner)."""
|
||||
return self.user_type == StoreUserType.TEAM_MEMBER.value
|
||||
"""Check if this user is the owner of the store (via merchant ownership)."""
|
||||
return self.user.is_owner_of(self.store_id)
|
||||
|
||||
@property
|
||||
def is_invitation_pending(self) -> bool:
|
||||
@@ -506,7 +488,7 @@ class StoreUser(Base, TimestampMixin):
|
||||
"""
|
||||
Check if user has a specific permission.
|
||||
|
||||
Owners always have all permissions.
|
||||
Owners (merchant owners) always have all permissions.
|
||||
Team members check their role's permissions.
|
||||
"""
|
||||
# Owners have all permissions
|
||||
@@ -526,7 +508,6 @@ class StoreUser(Base, TimestampMixin):
|
||||
def get_all_permissions(self) -> list:
|
||||
"""Get all permissions this user has."""
|
||||
if self.is_owner:
|
||||
# Return all possible permissions from discovery service
|
||||
from app.modules.tenancy.services.permission_discovery_service import (
|
||||
permission_discovery_service,
|
||||
)
|
||||
@@ -571,4 +552,4 @@ class Role(Base, TimestampMixin):
|
||||
return f"<Role(id={self.id}, name='{self.name}', store_id={self.store_id})>"
|
||||
|
||||
|
||||
__all__ = ["Store", "StoreUser", "StoreUserType", "Role"]
|
||||
__all__ = ["Store", "StoreUser", "Role"]
|
||||
|
||||
Reference in New Issue
Block a user