6.1 KiB
6.1 KiB
后台菜单 & RBAC:Portal 拆分与套餐权限白名单设计
目标:在不引入任何兼容/兜底逻辑的前提下,将“管理端(Admin) / 租户端(Tenant)”的后台菜单与 RBAC 权限体系彻底隔离,同时支持“租户后台菜单按套餐固定”的能力控制。
1. 总体原则(强约束)
- 权限码全局唯一:权限
Code作为系统全局唯一标识,不再按租户重复维护。 - 菜单固定:后台菜单定义固定,不允许运行期在管理端新增/修改菜单结构(只允许代码/种子变更)。
- 租户菜单按套餐固定:租户套餐决定“可用功能集合(权限白名单)”,再叠加租户内 RBAC 进行二次裁剪。
- 无兜底:
- 不允许“缺字段时使用备用字段”的回退行为(例如菜单 RequiredPermissions 缺失就回退 MetaPermissions)。
- 不允许“租户为空就自动用默认租户”等默认化行为。
2. 数据模型(推荐落库形态)
2.1 Identity 库(takeout_identity_db)
2.1.1 permissions(全局权限定义)
- 定位:全局权限字典(权限码唯一)。
- 变化:
- 移除
TenantId维度(不再按租户存权限定义)。 - 新增
Portal(int,枚举:0=Admin,1=Tenant)。 Code建唯一索引(UNIQUE(Code))。
- 移除
2.1.2 menu_definitions(后台菜单定义)
- 定位:菜单树定义(仅菜单结构与元数据),按 Portal 分两套:Admin / Tenant。
- 新增字段:
Portal(int,枚举:0=Admin,1=Tenant)。
- 变化:
- 移除
TenantId(菜单不按租户维护)。 - 新增索引:
(Portal, ParentId, SortOrder)。
- 移除
- 可见性规则:
RequiredPermissions为空:菜单默认可见。RequiredPermissions非空:只要命中“权限集合”即可可见(Any 语义)。
2.1.3 roles / user_roles / role_permissions / identity_users
- 定位:RBAC 核心表;支持同表存 Admin 与 Tenant 两套体系。
- 新增字段:
Portal(int,0=Admin,1=Tenant)。
- 变化:
TenantId改为可空:Portal=Admin:TenantId IS NULL(平台侧账号/角色不绑定租户)。Portal=Tenant:TenantId IS NOT NULL(租户侧 RBAC 强制绑定租户)。
- 建议加
CHECK约束,彻底杜绝脏数据。 - 索引建议(使用部分索引/过滤索引避免 NULL 唯一性漏洞):
roles:UNIQUE(Code) WHERE Portal=0UNIQUE(TenantId, Code) WHERE Portal=1
identity_users:UNIQUE(Account) WHERE Portal=0UNIQUE(TenantId, Account) WHERE Portal=1UNIQUE(Email) WHERE Portal=0 AND Email IS NOT NULLUNIQUE(TenantId, Email) WHERE Portal=1 AND Email IS NOT NULLUNIQUE(Phone) WHERE Portal=0 AND Phone IS NOT NULLUNIQUE(TenantId, Phone) WHERE Portal=1 AND Phone IS NOT NULL
user_roles:UNIQUE(UserId, RoleId) WHERE Portal=0UNIQUE(TenantId, UserId, RoleId) WHERE Portal=1
role_permissions:UNIQUE(RoleId, PermissionId) WHERE Portal=0UNIQUE(TenantId, RoleId, PermissionId) WHERE Portal=1
2.2 App 库(takeout_app_db)
2.2.1 新增 tenant_package_permission_codes(套餐权限白名单)
- 定位:定义“某套餐允许哪些权限码”。
- 字段建议:
id(bigint,雪花)tenant_package_id(bigint,引用tenant_packages.id)permission_code(varchar,存权限码)- 审计字段(CreatedAt/UpdatedAt/DeletedAt/CreatedBy/UpdatedBy/DeletedBy)
- 唯一约束:
UNIQUE(tenant_package_id, permission_code)
说明:权限定义与套餐在不同数据库时,不建议做跨库外键;以
permission_code作为稳定契约即可(依赖permissions.code全局唯一)。
3. 迁移与回填策略(可直接落到 EF Migration)
3.1 Identity 库迁移(Portal & 去 TenantId)
- 加列:给
identity_users/roles/user_roles/role_permissions/menu_definitions增加Portal(先允许 NULL)。 - 改列:将
identity_users/roles/user_roles/role_permissions的TenantId改为可空。 - 回填 Portal(示例规则,避免人工配置):
roles:Code='PlatformAdmin'=>Portal=Admin,其余Portal=Tenant。identity_users:凡是绑定了PlatformAdmin角色的用户 =>Portal=Admin,其余Portal=Tenant。user_roles/role_permissions:按关联roles.Portal回填Portal。
- 回填 TenantId:
Portal=Admin的roles/user_roles/role_permissions/identity_users:将TenantId置空。
- 去租户列:
permissions:删除TenantId,新增Portal,重建UNIQUE(Code)。menu_definitions:删除TenantId,新增索引(Portal, ParentId, SortOrder)。
- 约束与索引:
Portal改为NOT NULL。- 增加 CHECK 约束(见 2.1.3)。
- 删除旧的
TenantId_*唯一索引,建立新的过滤索引。
3.2 App 库迁移(套餐白名单表)
- 新建
tenant_package_permission_codes。 - (可选)从旧的
FeaturePoliciesJson或配置中一次性回填白名单(没有就先留空,表示该套餐无额外能力)。
4. 读取链路(租户后台菜单)
- 获取租户当前订阅:
tenant_subscriptions->tenant_package_id。 - 获取套餐白名单:
tenant_package_permission_codes->permission_code[]。 - 获取用户 RBAC 权限:由租户侧
user_roles + role_permissions + permissions计算(或由 Token claim 携带)。 - 权限集合求交集:
Allowed = PackageWhitelist ∩ UserPermissions。 - 加载菜单树:
menu_definitions where Portal=Tenant,按Allowed过滤并构建树。
5. 回滚策略(建议)
- Identity:迁移前先做数据库快照或导出;回滚时以 migration down 还原结构,并按需要将
TenantId回填回原表(仅在确认数据未被新逻辑写入时执行)。 - App:删除新表即可(若无业务写入)。