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:
@@ -0,0 +1,134 @@
|
||||
"""tenancy: RBAC cleanup — consolidate user roles and drop legacy columns
|
||||
|
||||
Migrates users.role from old 3-value scheme (admin/store/user) to new
|
||||
4-value scheme (super_admin/platform_admin/merchant_owner/store_member).
|
||||
Drops the now-redundant users.is_super_admin and store_users.user_type columns.
|
||||
|
||||
Revision ID: tenancy_003
|
||||
Revises: tenancy_002
|
||||
Create Date: 2026-02-19
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "tenancy_003"
|
||||
down_revision = "tenancy_002"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Data migration — remap role values using is_super_admin
|
||||
# and merchant ownership to determine the new role.
|
||||
#
|
||||
# Order matters: the 'admin' rows must be split before we touch
|
||||
# 'store' or 'user' rows, and 'merchant_owner' for role='user'
|
||||
# (bug fix) must come before the catch-all 'store_member' update.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# admin + is_super_admin=true -> super_admin
|
||||
op.execute(
|
||||
"UPDATE users SET role = 'super_admin' "
|
||||
"WHERE role = 'admin' AND is_super_admin = true"
|
||||
)
|
||||
|
||||
# admin + is_super_admin=false -> platform_admin
|
||||
op.execute(
|
||||
"UPDATE users SET role = 'platform_admin' "
|
||||
"WHERE role = 'admin' AND is_super_admin = false"
|
||||
)
|
||||
|
||||
# store users who own a merchant -> merchant_owner
|
||||
op.execute(
|
||||
"UPDATE users SET role = 'merchant_owner' "
|
||||
"WHERE role = 'store' "
|
||||
"AND id IN (SELECT owner_user_id FROM merchants)"
|
||||
)
|
||||
|
||||
# legacy 'user' role (bug — should have been 'store') -> merchant_owner
|
||||
op.execute(
|
||||
"UPDATE users SET role = 'merchant_owner' "
|
||||
"WHERE role = 'user'"
|
||||
)
|
||||
|
||||
# remaining store users (not merchant owners) -> store_member
|
||||
op.execute(
|
||||
"UPDATE users SET role = 'store_member' "
|
||||
"WHERE role = 'store' "
|
||||
"AND id NOT IN (SELECT owner_user_id FROM merchants)"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Drop legacy columns that are now redundant
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# users.is_super_admin is replaced by role = 'super_admin'
|
||||
op.drop_column("users", "is_super_admin")
|
||||
|
||||
# store_users.user_type is replaced by users.role (merchant_owner vs store_member)
|
||||
op.drop_column("store_users", "user_type")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Re-add dropped columns
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column(
|
||||
"is_super_admin",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default="false",
|
||||
),
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
"store_users",
|
||||
sa.Column(
|
||||
"user_type",
|
||||
sa.String(),
|
||||
nullable=False,
|
||||
server_default="member",
|
||||
),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Reverse data migration — map new roles back to old scheme
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# Mark super admins
|
||||
op.execute(
|
||||
"UPDATE users SET is_super_admin = true "
|
||||
"WHERE role = 'super_admin'"
|
||||
)
|
||||
|
||||
# super_admin + platform_admin -> admin
|
||||
op.execute(
|
||||
"UPDATE users SET role = 'admin' "
|
||||
"WHERE role IN ('super_admin', 'platform_admin')"
|
||||
)
|
||||
|
||||
# merchant_owner + store_member -> store
|
||||
op.execute(
|
||||
"UPDATE users SET role = 'store' "
|
||||
"WHERE role IN ('merchant_owner', 'store_member')"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Restore store_users.user_type based on merchant ownership
|
||||
#
|
||||
# If the user owns the merchant that owns the store, they are
|
||||
# an 'owner'; otherwise 'member'.
|
||||
# ------------------------------------------------------------------
|
||||
op.execute(
|
||||
"UPDATE store_users SET user_type = 'owner' "
|
||||
"WHERE user_id IN ("
|
||||
" SELECT m.owner_user_id FROM merchants m"
|
||||
" JOIN stores s ON s.merchant_id = m.id"
|
||||
" WHERE s.id = store_users.store_id"
|
||||
")"
|
||||
)
|
||||
Reference in New Issue
Block a user