feat: add tenant-scoped rule and permission management

This commit is contained in:
wren
2026-05-21 22:03:08 +08:00
parent a2c2bf1969
commit 1f1bccf3b3
193 changed files with 64463 additions and 1771 deletions
@@ -0,0 +1,607 @@
# 角色硬编码与接口影响专项补充分析
> 适用范围:`leaudit-platform` 当前角色权限与地区隔离体系
> 文档定位:从主方案中拆出的专项补充稿,专门分析“硬编码角色如何改”和“哪些接口会被联动影响”。
---
## 1. 结论先行
当前项目里的角色硬编码不是零散问题,而是已经渗透到:
- 后端服务层能力派生
- 后端服务层动作放行
- 前端 UI 可见性和可编辑性
- 前端 guard、fallback、role mapping
因此,这类改造不能只改某一个 service,也不能只改权限表配置。
如果只改一部分,会出现 3 类典型问题:
1. 后端 permission 已放行,但服务层仍按角色名拒绝
2. 后端边界已收敛,但前端仍按旧角色逻辑展示按钮或入口
3. 菜单、页面、接口、详情、下载这些边界继续不一致
正确做法是:
1. 先识别所有硬编码角色位置
2. 再区分它们的职责类型
3. 最后按“能力抽象 -> 服务层替换 -> 前端去角色化”的顺序渐进改造
---
## 2. 当前硬编码角色的 4 种形态
## 2.1 服务层上下文派生型
这类代码并不直接判断某个 permission,而是把角色名先转成派生能力:
- `is_global`
- `can_manage`
- `is_super_admin`
- `is_area_admin`
- `bypass_area`
典型位置:
- `documentServiceImpl.py`
- `govdocServiceImpl.py`
- `usageStatsServiceImpl.py`
- `contractTemplateServiceImpl.py`
- `rbacAdminServiceImpl.py`
- `homeServiceImpl.py`
典型模式:
- `role_key IN ('super_admin', 'provincial_admin') => is_global`
- `role_key IN ('super_admin', 'provincial_admin', 'admin') => can_manage`
- `role_key = 'super_admin' => is_super_admin / bypass_area`
本质问题:
- 角色名被直接当成能力模型
- 一旦出现新领域管理员,所有上下文派生逻辑都要重复改
## 2.2 服务层动作白名单型
这类代码直接按角色名决定某个业务动作是否允许。
典型位置:
- `ragDatasetServiceImpl.py`
- `ragChatServiceImpl.py`
典型模式:
- `UserRole not in ("provincial_admin", "admin", "super_admin")`
- `user_role == "provincial_admin"`
- `UserRole == "admin"`
本质问题:
- 控制器层已经做 permission 校验
- 服务层又加了角色白名单
- 结果会出现“有权限但角色名不对仍被拒绝”
这是当前最需要优先清理的硬编码类型。
## 2.3 前端 UI 能力硬编码型
这类代码不是后端鉴权,但会直接影响用户感知边界。
典型位置:
- `components/dify-dataset-manager/index.tsx`
- `components/dify-dataset-manager/area-dataset-config.tsx`
- `hooks/use-area-dataset-config.ts`
典型模式:
- `provincial_admin` 可编辑全部
- `super_admin` 可编辑全部
- `admin` 仅可编辑本地区
本质问题:
- 前端自己在解释“谁能管理”
- 即使后端以后完成去角色化,前端仍可能展示旧权限状态
## 2.4 前端兼容层硬编码型
这类代码主要存在于路由、guard 和迁移兼容层。
典型位置:
- `legal-platform-frontend/lib/auth/user-routes.ts`
- `legal-platform-frontend/lib/api/legacy/auth/user-routes.ts`
- `legal-platform-frontend/lib/auth/guard.ts`
- `legal-platform-frontend/lib/auth/cross-checking-access.ts`
典型模式:
- `provincial_admin -> admin`
- `super_admin -> admin`
- `developer -> admin`
- `provincial_admin` 自动获得“省局 area”候选
本质问题:
- 前端把真实角色体系压缩成少数桶位
- 继续保留会让权限模型越来越难以统一
---
## 3. 为什么不能直接删掉所有角色硬编码
不是所有角色硬编码都应该同一天删除,因为它们承担职责不同。
## 3.1 可优先替换的
优先替换:
- 服务层动作白名单
- RAG 管理动作角色白名单
原因:
- 这些逻辑已经和 permission 决策重复
- 删除后收益最大,副作用相对可控
## 3.2 需要先抽象再替换的
需要先抽象:
- `is_global`
- `can_manage`
- `is_super_admin`
- `is_area_admin`
原因:
- 它们已经被多个模块用于拼接 SQL 过滤条件
- 直接删会导致大量数据边界逻辑断裂
正确做法:
- 先统一沉淀为能力派生层
- 再逐步把“角色名判断”替换为“能力决策”
## 3.3 可以保留为兼容层但必须收缩边界的
允许短期保留:
- route fallback
- role mapping
- 某些旧前端 guard
但必须满足:
1. 只存在于适配层
2. 不再扩散到新业务代码
3. 后续有明确移除计划
---
## 4. 推荐的替换目标
当前很多角色语义,其实应该改写成能力语义。
建议的替换关系如下:
- `super_admin` 全局绕过
替换为:`is_super_admin`
- `provincial_admin => 全省可见`
替换为:`effective_scope == ALL`
- `admin => 本地区管理`
替换为:`effective_scope == DEPT + domain manage permission`
- `common => 自己的数据`
替换为:`effective_scope == SELF`
- `provincial_admin/admin/super_admin` 共用白名单
替换为:显式 `create/update/delete/manage` permission
这一步的本质,是把“角色名”换成“能力决策”。
---
## 5. 建议新增的统一能力层
建议新增统一能力派生对象,例如:
- `is_super_admin`
- `has_global_scope`
- `has_area_scope`
- `can_manage_rbac`
- `can_manage_rag_dataset`
- `can_manage_contract_templates`
- `can_view_usage_stats`
- `can_bypass_home_area`
这些能力不应直接写死在角色名上,而应由以下信息共同决策:
- `roles`
- `permissions`
- `data_scope`
- 可选 `condition_filter`
建议新增两层统一能力:
1. `ScopeContextProvider`
2. `AdminCapabilityResolver`
前者负责“当前用户的通用数据边界”,后者负责“某个业务域是否具备管理能力”。
---
## 6. 当前高风险冲突区
## 6.1 RAG 是最典型的双轨冲突区
当前链路是:
1. 控制器层按 permission 校验
2. 服务层又按 `UserRole` 白名单校验
3. 最终管理能力同时受 permission 和角色名双重控制
风险:
- 未来新增 `rag_manager` 这类角色时,即使给了 `rag:dataset:manage/create/update/delete`,服务层仍会拒绝
优先级:
- 最高
## 6.2 RBAC 管理域存在“角色管理能力”和“权限管理能力”双重耦合
当前链路是:
1. `_assertManagePermission` 依赖 `can_manage`
2. `_assertPermission` 再校验具体权限点
风险:
- 如果未来某用户拥有 `rbac:*` 权限,但主角色不是 `admin/provincial_admin/super_admin`,仍可能被挡在第一层
优先级:
-
## 6.3 文档 / 公文 / 统计存在共性上下文派生复制
这些模块并没有直接角色白名单放行,但都重复实现了:
- `is_global`
- `can_manage`
- `is_super_admin`
风险:
- 改一个模块不改另一个,边界会漂移
优先级:
-
## 6.4 前端知识库管理仍在自行解释角色
风险:
- 后端已经 permission 化后,前端仍会显示不该显示的按钮
- 或相反,后端已允许,前端仍不展示入口
优先级:
-
---
## 7. 受影响接口分析
下面重点回答“其他接口会不会影响到”。
答案是:**会,而且影响面不小。**
## 7.1 文档模块
受影响接口包括:
- `POST /api/upload`
- `GET /api/documents/list`
- `GET /api/documents/status`
- `GET /api/documents/{DocumentId}`
- `GET /api/v3/review-points/{DocumentId}`
- `PATCH /api/v3/review-points/{ReviewPointResultId}/audit`
- `PATCH /api/v3/documents/{DocumentId}/confirm`
- `POST /api/documents/{DocumentId}/attachments`
- `PUT /api/documents/{DocumentId}`
- `DELETE /api/documents/{DocumentId}`
影响原因:
- 这些接口都依赖文档服务里的用户上下文派生和数据范围过滤
- 一旦 `is_global/can_manage` 的计算逻辑变化,全部都会联动
## 7.2 公文模块
受影响接口包括:
- `POST /api/govdoc/documents`
- `GET /api/govdoc/documents`
- `GET /api/govdoc/documents/{documentId}`
- `PATCH /api/govdoc/documents/{documentId}`
- `DELETE /api/govdoc/documents/{documentId}`
- `POST /api/govdoc/runs`
- `GET /api/govdoc/runs/{runId}`
- `GET /api/govdoc/runs/{runId}/result`
- `GET /api/govdoc/runs/{runId}/findings`
- `GET /api/govdoc/runs/{runId}/entities`
- `GET /api/govdoc/runs/{runId}/structure`
- `GET /api/govdoc/runs/{runId}/outline`
- `GET /api/govdoc/runs/{runId}/paragraphs`
- `GET /api/govdoc/runs/{runId}/report/html`
- `GET /api/govdoc/runs/{runId}/report/docx`
- `GET /api/govdoc/documents/{documentId}/original`
影响原因:
- 公文模块也使用了同构的上下文派生
- 尤其结果、报告、下载类接口最容易出现“资源详情边界未同步”的问题
## 7.3 统计模块
受影响接口包括:
- `GET /api/v3/usage-stats/overview`
- `GET /api/v3/usage-stats/trends`
- `GET /api/v3/usage-stats/by-users`
- `GET /api/v3/usage-stats/by-departments`
- `GET /api/v3/usage-stats/by-areas`
- `GET /api/v3/usage-stats/details`
影响原因:
- 当前统计接口对管理员可见性有明显上下文派生依赖
- 去角色化后,这些接口需要统一切到“统计域 permission + scope”模型
## 7.4 RAG 模块
受影响接口包括:
- `GET /api/v3/rag/apps`
- `GET /api/v3/rag/apps/default`
- `GET /api/v3/rag/datasets/my`
- `GET /api/v3/rag/datasets/admin`
- `POST /api/v3/rag/datasets/admin`
- `PUT /api/v3/rag/datasets/admin/{DatasetId}`
- `DELETE /api/v3/rag/datasets/admin/{DatasetId}`
- `GET /api/v3/rag/datasets/{DatasetId}`
- `PATCH /api/v3/rag/datasets/{DatasetId}`
- 各类 `/datasets/{DatasetId}/documents`
- 各类 `/datasets/{DatasetId}/segments`
- 各类检索测试接口
- `POST /api/v3/rag/chat/messages`
- 会话、消息反馈、会话重命名、删除等接口
影响原因:
- 控制器层和服务层当前存在双轨权限逻辑
- 这是最典型的“permission 改了,接口仍会被角色名卡住”的模块
## 7.5 RBAC 管理模块
受影响接口包括:
- `GET /api/v3/rbac/roles`
- `POST /api/v3/rbac/roles`
- `PUT /api/v3/rbac/roles/{RoleId}`
- `DELETE /api/v3/rbac/roles/{RoleId}`
- `GET /api/v3/rbac/users`
- `GET /api/admin/users/organizations/tree`
- `GET /api/v3/rbac/roles/{RoleId}/users`
- `POST /api/v3/rbac/users/{UserId}/roles`
- `DELETE /api/v3/rbac/users/{UserId}/roles/{RoleId}`
- `GET /api/v3/rbac/users/{UserId}/roles`
- `GET /api/v3/routes`
- `GET/PUT /api/rbac/roles/{RoleId}/routes`
- `GET/POST /api/v3/rbac/role-permissions`
- `POST /api/v3/rbac/roles/{RoleId}/access`
- `GET /api/v3/routes/{RouteId}/permissions`
影响原因:
- 当前同时依赖 `_assertManagePermission``_assertPermission`
- 第一层还是角色派生管理能力
## 7.6 首页入口模块
受影响接口:
- `GET /api/home/entry-modules`
影响原因:
- 当前首页入口存在 `super_admin` 的 area bypass 语义
- 这类入口可见性也属于权限边界的一部分
## 7.7 合同模板模块
受影响接口包括:
- `GET /api/v3/contract-templates/categories`
- `GET /api/v3/contract-templates`
- `POST /api/v3/contract-templates`
- `GET /api/v3/contract-templates/search`
- `GET /api/v3/contract-templates/{TemplateId}`
- `DELETE /api/v3/contract-templates/{TemplateId}`
影响原因:
- 当前业务语义明确写着“地区管理员才能上传”
- 如果只去角色化、不补合同模板域显式权限,这类能力会失焦
## 7.8 中影响模块
中影响但必须纳入联调范围的还有:
- 交叉评查模块
- 评查点模块
- 评查点分组模块
- 规则配置模块
原因:
- 它们虽然不一定都直接依赖角色名白名单
- 但仍依赖 permission、route、入口和关系访问逻辑
- 一旦整体权限能力模型调整,也必须回归验证
---
## 8. 前端联动影响
不能只看后端接口,前端也会同步受影响。
## 8.1 菜单与路由
受影响位置:
- `Sidebar.tsx`
- `user-routes.ts`
- `check-route-permission.ts`
- fallback route mapping
风险:
- 菜单可见性与真实接口权限继续分叉
- 某些角色被映射压扁后,真实权限无法完整反映
## 8.2 RAG 管理页面
受影响位置:
- `components/dify-dataset-manager/*`
- `hooks/use-area-dataset-config.ts`
风险:
- 后端能力已收敛,前端仍按旧角色名展示编辑入口
## 8.3 页面 guard
受影响位置:
- `lib/auth/guard.ts`
- `lib/auth/cross-checking-access.ts`
- `lib/auth/session-user.ts`
- `lib/auth/jwt.ts`
风险:
- `user_role` 被继续当成完整权限模型使用
---
## 9. 推荐改造顺序
建议按下面顺序推进,避免同时炸开所有联动面。
## 9.1 第一阶段
先做平台能力层:
- `PermissionDecisionService`
- `ScopeContextProvider`
- `AdminCapabilityResolver`
目标:
- 不先改业务接口行为,只先统一决策能力
## 9.2 第二阶段
优先改 RAG
- 清理服务层角色白名单
- 改为 permission + scope/policy 决策
原因:
- 这是最典型、收益也最大的双轨冲突区
## 9.3 第三阶段
接入共用上下文模块:
- 文档
- 公文
- 统计
原因:
- 它们共享大量上下文派生逻辑
- 最适合沉淀统一 `QueryScopeBuilder`
## 9.4 第四阶段
处理管理域:
- RBAC 管理
- 首页入口
- 合同模板
目标:
- 把“管理能力”从角色名迁移到领域 permission
## 9.5 第五阶段
清理前端角色解释层:
- role mapping
- fallback route
- guard
- UI 编辑能力判断
目标:
- 前端不再自行解释“谁是管理员”
---
## 10. 必做回归清单
建议单独维护一份“角色去硬编码回归清单”,至少覆盖:
1. 用户拥有 permission,但主角色不是 `admin/provincial_admin` 时,接口是否仍能正确访问
2. 用户获得某领域管理权限后,是否无需改代码即可生效
3. 菜单、按钮、接口、详情、下载、导出边界是否一致
4. 前端是否仍存在基于 `user_role` 的旧判断放大或缩小能力
5. RAG 管理接口是否已完全摆脱角色白名单
6. RBAC 管理接口是否已从“角色管理能力”切换到“权限管理能力”
7. 文档、公文、统计是否仍存在模块间边界不一致
---
## 11. 最终建议
这次专项分析的核心结论只有一句话:
**角色硬编码改造,本质上不是“替换几个 if”,而是把整套权限系统从“角色名驱动”升级为“能力决策驱动”。**
如果只做局部替换,问题会更隐蔽。
如果按下面顺序推进,风险最低:
1. 先抽象能力层
2. 先处理 RAG 双轨冲突
3. 再统一文档/公文/统计上下文派生
4. 再处理 RBAC 管理域和首页入口
5. 最后清理前端角色解释和 fallback
这样改完之后,项目才能真正从:
- “角色名决定能力”
走向:
- “权限点 + 数据范围 + 模块 policy 决定能力”
这才是后续权限平台可持续演进的正确方向。