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:
@@ -2,13 +2,15 @@
|
||||
"""
|
||||
User model with authentication support.
|
||||
|
||||
ROLE CLARIFICATION:
|
||||
- User.role should ONLY contain platform-level roles:
|
||||
* "admin" - Platform administrator (full system access)
|
||||
* "store" - Any user who owns or is part of a store team
|
||||
ROLE SYSTEM (Phase 1 — Consolidated 4-value enum):
|
||||
- User.role contains one of 4 values:
|
||||
* "super_admin" — Platform super administrator (all platforms, all settings)
|
||||
* "platform_admin" — Platform admin scoped to assigned platforms
|
||||
* "merchant_owner" — Owns merchant(s) and all their stores
|
||||
* "store_member" — Team member on specific store(s)
|
||||
|
||||
- Store-specific roles (manager, staff, etc.) are stored in StoreUser.role
|
||||
- Customers are NOT in the User table - they use the Customer model
|
||||
- Store-specific granular permissions are in StoreUser.role_id -> Role.permissions
|
||||
- Customers are NOT in the User table — they use the Customer model
|
||||
"""
|
||||
|
||||
import enum
|
||||
@@ -23,12 +25,14 @@ from models.database.base import TimestampMixin
|
||||
class UserRole(str, enum.Enum):
|
||||
"""Platform-level user roles."""
|
||||
|
||||
ADMIN = "admin" # Platform administrator
|
||||
STORE = "store" # Store owner or team member
|
||||
SUPER_ADMIN = "super_admin" # Platform super administrator
|
||||
PLATFORM_ADMIN = "platform_admin" # Platform admin (scoped to assigned platforms)
|
||||
MERCHANT_OWNER = "merchant_owner" # Owns merchant(s) and all their stores
|
||||
STORE_MEMBER = "store_member" # Team member on specific store(s)
|
||||
|
||||
|
||||
class User(Base, TimestampMixin):
|
||||
"""Represents a platform user (admins and stores only)."""
|
||||
"""Represents a platform user (admins, merchant owners, and store team)."""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
@@ -39,18 +43,13 @@ class User(Base, TimestampMixin):
|
||||
last_name = Column(String)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
|
||||
# Platform-level role only (admin or store)
|
||||
role = Column(String, nullable=False, default=UserRole.STORE.value)
|
||||
# Consolidated role — one of: super_admin, platform_admin, merchant_owner, store_member
|
||||
role = Column(String, nullable=False, default=UserRole.STORE_MEMBER.value)
|
||||
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_email_verified = Column(Boolean, default=False, nullable=False)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
# Super admin flag (only meaningful when role='admin')
|
||||
# Super admins have access to ALL platforms and global settings
|
||||
# Platform admins (is_super_admin=False) are assigned to specific platforms
|
||||
is_super_admin = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Language preference (NULL = use context default: store dashboard_language or system default)
|
||||
# Supported: en, fr, de, lb
|
||||
preferred_language = Column(String(5), nullable=True)
|
||||
@@ -92,15 +91,34 @@ class User(Base, TimestampMixin):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
return self.username
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is a platform admin."""
|
||||
return self.role == UserRole.ADMIN.value
|
||||
# =========================================================================
|
||||
# Role-checking properties
|
||||
# =========================================================================
|
||||
|
||||
@property
|
||||
def is_store(self) -> bool:
|
||||
"""Check if user is a store (owner or team member)."""
|
||||
return self.role == UserRole.STORE.value
|
||||
def is_super_admin(self) -> bool:
|
||||
"""Check if user is a super admin."""
|
||||
return self.role == UserRole.SUPER_ADMIN.value
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is any kind of platform admin (super or scoped)."""
|
||||
return self.role in (UserRole.SUPER_ADMIN.value, UserRole.PLATFORM_ADMIN.value)
|
||||
|
||||
@property
|
||||
def is_platform_admin(self) -> bool:
|
||||
"""Check if user is a scoped platform admin (not super admin)."""
|
||||
return self.role == UserRole.PLATFORM_ADMIN.value
|
||||
|
||||
@property
|
||||
def is_merchant_owner(self) -> bool:
|
||||
"""Check if user is a merchant owner."""
|
||||
return self.role == UserRole.MERCHANT_OWNER.value
|
||||
|
||||
@property
|
||||
def is_store_user(self) -> bool:
|
||||
"""Check if user is a merchant owner or store member."""
|
||||
return self.role in (UserRole.MERCHANT_OWNER.value, UserRole.STORE_MEMBER.value)
|
||||
|
||||
def is_owner_of(self, store_id: int) -> bool:
|
||||
"""
|
||||
@@ -155,16 +173,6 @@ class User(Base, TimestampMixin):
|
||||
# Admin Platform Access Methods
|
||||
# =========================================================================
|
||||
|
||||
@property
|
||||
def is_super_admin_user(self) -> bool:
|
||||
"""Check if user is a super admin (can access all platforms)."""
|
||||
return self.role == UserRole.ADMIN.value and self.is_super_admin
|
||||
|
||||
@property
|
||||
def is_platform_admin(self) -> bool:
|
||||
"""Check if user is a platform admin (access to assigned platforms only)."""
|
||||
return self.role == UserRole.ADMIN.value and not self.is_super_admin
|
||||
|
||||
def can_access_platform(self, platform_id: int) -> bool:
|
||||
"""
|
||||
Check if admin can access a specific platform.
|
||||
|
||||
Reference in New Issue
Block a user