Files
TakeoutSaaS.Docs/Document/17_后台菜单RBAC_Portal拆分与套餐白名单设计.md

119 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后台菜单 & RBACPortal 拆分与套餐权限白名单设计
> 目标:在**不引入任何兼容/兜底逻辑**的前提下,将“管理端(Admin) / 租户端(Tenant)”的后台菜单与 RBAC 权限体系彻底隔离,同时支持“租户后台菜单按套餐固定”的能力控制。
## 1. 总体原则(强约束)
1. **权限码全局唯一**:权限 `Code` 作为系统全局唯一标识,不再按租户重复维护。
2. **菜单固定**:后台菜单定义固定,不允许运行期在管理端新增/修改菜单结构(只允许代码/种子变更)。
3. **租户菜单按套餐固定**:租户套餐决定“可用功能集合(权限白名单)”,再叠加租户内 RBAC 进行二次裁剪。
4. **无兜底**
- 不允许“缺字段时使用备用字段”的回退行为(例如菜单 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=0`
- `UNIQUE(TenantId, Code) WHERE Portal=1`
- `identity_users`
- `UNIQUE(Account) WHERE Portal=0`
- `UNIQUE(TenantId, Account) WHERE Portal=1`
- `UNIQUE(Email) WHERE Portal=0 AND Email IS NOT NULL`
- `UNIQUE(TenantId, Email) WHERE Portal=1 AND Email IS NOT NULL`
- `UNIQUE(Phone) WHERE Portal=0 AND Phone IS NOT NULL`
- `UNIQUE(TenantId, Phone) WHERE Portal=1 AND Phone IS NOT NULL`
- `user_roles`
- `UNIQUE(UserId, RoleId) WHERE Portal=0`
- `UNIQUE(TenantId, UserId, RoleId) WHERE Portal=1`
- `role_permissions`
- `UNIQUE(RoleId, PermissionId) WHERE Portal=0`
- `UNIQUE(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
1. **加列**:给 `identity_users/roles/user_roles/role_permissions/menu_definitions` 增加 `Portal`(先允许 NULL
2. **改列**:将 `identity_users/roles/user_roles/role_permissions``TenantId` 改为可空。
3. **回填 Portal**(示例规则,避免人工配置):
- `roles``Code='PlatformAdmin'` => `Portal=Admin`,其余 `Portal=Tenant`
- `identity_users`:凡是绑定了 `PlatformAdmin` 角色的用户 => `Portal=Admin`,其余 `Portal=Tenant`
- `user_roles/role_permissions`:按关联 `roles.Portal` 回填 `Portal`
4. **回填 TenantId**
- `Portal=Admin``roles/user_roles/role_permissions/identity_users`:将 `TenantId` 置空。
5. **去租户列**
- `permissions`:删除 `TenantId`,新增 `Portal`,重建 `UNIQUE(Code)`
- `menu_definitions`:删除 `TenantId`,新增索引 `(Portal, ParentId, SortOrder)`
6. **约束与索引**
- `Portal` 改为 `NOT NULL`
- 增加 CHECK 约束(见 2.1.3)。
- 删除旧的 `TenantId_*` 唯一索引,建立新的过滤索引。
### 3.2 App 库迁移(套餐白名单表)
1. 新建 `tenant_package_permission_codes`
2. (可选)从旧的 `FeaturePoliciesJson` 或配置中一次性回填白名单(没有就先留空,表示该套餐无额外能力)。
---
## 4. 读取链路(租户后台菜单)
1. **获取租户当前订阅**`tenant_subscriptions` -> `tenant_package_id`
2. **获取套餐白名单**`tenant_package_permission_codes` -> `permission_code[]`
3. **获取用户 RBAC 权限**:由租户侧 `user_roles + role_permissions + permissions` 计算(或由 Token claim 携带)。
4. **权限集合求交集**`Allowed = PackageWhitelist ∩ UserPermissions`
5. **加载菜单树**`menu_definitions where Portal=Tenant`,按 `Allowed` 过滤并构建树。
---
## 5. 回滚策略(建议)
- Identity迁移前先做数据库快照或导出回滚时以 migration down 还原结构,并按需要将 `TenantId` 回填回原表(仅在确认数据未被新逻辑写入时执行)。
- App删除新表即可若无业务写入