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,709 @@
# 统一数据范围执行器设计
> 适用范围:`leaudit-platform` 当前 `RBAC + 单地区隔离 + 模块特例策略` 体系
> 文档定位:把“data_scope 已建模但未统一执行”的现状,收敛为一套后端可落地的统一执行器设计。
---
## 1. 设计结论
当前系统真正缺的不是权限表,也不是权限点,而是一个统一的“数据范围执行层”。
现状已经具备:
- `roles.data_scope`
- `role_permissions.data_scope`
- `role_permissions.grant_type = GRANT / DENY`
- 用户主地区 `sso_users.area`
- 多个模块已存在真实可运行的数据边界实现
当前系统真正缺失的是:
1. 平台层没有把“功能权限 + 数据范围 + 模块特例策略”合并成一次统一决策
2. `PermissionServiceImpl` 只返回布尔值,不返回 scope 决策
3. 文档、公文、统计、合同模板、RBAC 管理域都在各自重复写 `is_global/can_manage/area/created_by`
4. RAG、交叉评查等特殊模块没有被纳入统一决策框架,只能继续靠模块内手工判断
因此,建议新增统一执行链:
1. `PermissionDecisionService`
2. `DataScopeResolver`
3. `QueryScopeBuilder`
4. `ModulePolicyRegistry`
5. `ScopeAwarePermissionFacade`
目标不是推翻当前业务 SQL,而是先把“决策入口”和“范围解释逻辑”统一,再逐模块替换已有散落实现。
---
## 2. 统一执行器要解决什么问题
## 2.1 功能准入和数据范围必须拆开
一个接口是否可调用,与一个用户调用后能看到多少数据,是两层不同问题:
- 功能准入:是否拥有 `permission_key`
- 数据范围:即便拥有该权限,可见范围到底是 `ALL / DEPT / SELF / RELATION / PUBLIC_MIXED`
当前系统多数地方只做了第一层,第二层分散在 service 内。
## 2.2 多角色用户必须有统一合并规则
当前库允许一个用户绑定多个角色:
- 一个角色可能 `GRANT documents:list:read + SELF`
- 另一个角色可能 `GRANT documents:list:read + DEPT`
- 还有角色可能 `DENY documents:list:read`
没有统一合并规则,就会出现:
- 有人按最大范围放行
- 有人按主角色放行
- 有人忽略 `DENY`
## 2.3 模块差异必须被显式建模
不是所有模块都适合硬套 `ALL/DEPT/SELF`
- 文档、公文、统计:典型数据范围模型
- RAG`地区 + 公开(public)` 混合模型
- 交叉评查:任务成员关系模型
- 规则/配置域:更偏功能权限域
所以统一执行器不能只有一个“拼 where region = ?”函数,而要允许模块策略扩展。
---
## 3. 总体架构
建议新增如下能力层。
## 3.1 核心对象
### 3.1.1 `ScopeContext`
表示一次权限决策的输入上下文。
建议字段:
```python
@dataclass
class ScopeContext:
user_id: int
permission_key: str
module: str
action: str
user_area: str | None
request_area: str | None = None
target_user_id: int | None = None
resource_id: int | None = None
extra_filters: dict[str, Any] | None = None
```
说明:
- `user_area` 来自 `sso_users.area`
- `request_area` 是接口显式传入的筛选地区
- `target_user_id` 适用于按用户过滤的查询
- `resource_id` 适用于详情、删除、下载这类单资源接口
- `extra_filters` 给统计、RAG、交叉评查等模块挂业务参数
### 3.1.2 `PermissionGrant`
表示从数据库查出的单条授权记录。
```python
@dataclass
class PermissionGrant:
role_id: int
role_key: str
permission_key: str
grant_type: str
role_scope: str | None
permission_scope: str | None
condition_filter: dict[str, Any] | None
priority: int
is_system_role: bool
```
### 3.1.3 `PermissionDecision`
表示平台最终产出的权限决策。
```python
@dataclass
class PermissionDecision:
allowed: bool
deny_reason: str | None
effective_scope: str | None
scope_mode: str
allowed_areas: list[str]
allowed_user_ids: list[int]
allow_public: bool
conditions: dict[str, Any]
matched_roles: list[str]
matched_permissions: list[str]
```
建议 `scope_mode` 取值:
- `NONE`:不做数据范围控制
- `ALL`
- `DEPT`
- `SELF`
- `RELATION`
- `PUBLIC_MIXED`
- `CUSTOM`
### 3.1.4 `ScopeClause`
表示最终用于拼接 SQL 的范围子句。
```python
@dataclass
class ScopeClause:
sql: str
params: dict[str, Any]
scope_mode: str
description: str
```
---
## 4. 执行链路
建议所有需要“权限 + 数据边界”的 service 统一走下面链路:
```text
Controller
-> ScopeAwarePermissionFacade.require()
-> PermissionDecisionService.decide()
-> PermissionGrantRepository.load_user_grants()
-> DataScopeResolver.resolve()
-> ModulePolicyRegistry.get(module).decorate()
-> QueryScopeBuilder.build()
-> Service 执行业务 SQL / ORM
```
细化如下:
1. 控制器或 service 构造 `ScopeContext`
2. `PermissionDecisionService` 先确认该用户是否拥有功能权限
3. 若被 `DENY` 命中,直接拒绝
4.`GRANT` 命中,则计算有效范围
5. 若模块存在特例策略,由 `ModulePolicy` 二次修饰决策
6.`QueryScopeBuilder` 将决策转换为统一 SQL 子句
7. 业务 service 只负责把子句注入原查询,不再自行解释角色
---
## 5. 数据来源与优先级规则
## 5.1 数据来源
统一执行器的决策数据来自 4 层:
1. `sso_users.area`
2. `roles.data_scope`
3. `role_permissions.data_scope`
4. `role_permissions.grant_type / condition_filter`
## 5.2 功能准入优先级
功能准入保持现有语义,但要扩展为“带授权明细返回”:
1. 精确 `DENY`
2. 通配符 `DENY`
3. 精确 `GRANT`
4. 通配符 `GRANT`
5. 无匹配则拒绝
这部分沿用 `PermissionServiceImpl` 现有规则,不改语义。
## 5.3 范围优先级
建议统一采用:
1. `role_permissions.data_scope`
2. `roles.data_scope`
3. 模块默认值
即:
- 权限级 scope 覆盖角色级 scope
- 角色级 scope 覆盖模块默认值
- 若都没有,读接口默认 `SELF`,配置接口默认 `NONE`
## 5.4 多角色合并规则
建议采用:
1. 先按 `DENY` 决定是否整体拒绝
2. 在所有有效 `GRANT` 中取“最大可见范围”
3. 若存在模块策略,则再由模块策略收敛边界
范围大小建议定义为:
```text
ALL > DEPT > SELF
GROUP 不再扩展,仅兼容映射
RELATION / PUBLIC_MIXED / CUSTOM 由模块策略生成
```
注意:
- `DENY` 是功能拒绝,不只是 scope 收缩
- 不能把一个 `DENY documents:list:read` 理解成“降到 SELF”
- 一旦该权限被 `DENY` 命中,应直接 `allowed = False`
## 5.5 `GROUP` 的处理
库里仍保留 `GROUP`,但当前系统没有成熟“组织组”模型。
建议:
- 现阶段统一映射为 `DEPT`
- 文档和代码中明确标注“仅兼容,不新增使用”
- 后续如果真要启用组织组,再单独扩展
---
## 6. 范围语义规范
## 6.1 `ALL`
含义:
- 可见该模块全部数据
- 可按任意地区或任意用户进一步筛选
适用:
- 省级管理员
- 超级管理员
- 某些总部配置权限
## 6.2 `DEPT`
本项目中 `DEPT` 语义应明确定义为:
- 同主地区 `area`
不是传统部门树。
含义:
- 只能看与自身 `user_area` 相同地区的数据
- 如接口显式传入 `request_area`,则必须等于 `user_area`
## 6.3 `SELF`
含义:
- 仅可看与自己直接归属的数据
优先字段通常是:
- `created_by`
- `owner_id`
- `user_id`
必须按模块字段映射规范执行,不能每个模块再自行猜测。
## 6.4 `RELATION`
适用于交叉评查等成员关系模型。
含义:
- 可访问与当前用户存在显式关系绑定的数据
例如:
- `task_member.user_id = current_user_id`
- `proposal.created_by = current_user_id`
- `document` 属于当前参与任务
## 6.5 `PUBLIC_MIXED`
适用于 RAG。
含义:
- 本地区资源可见
- 公共资源可见
- 省级公共资源可见
本质条件通常是:
```sql
resource.area IN (:user_area, '省级', '')
OR resource.is_public = TRUE
```
---
## 7. 通用字段映射规范
统一执行器要避免“模块自己猜字段名”,必须先定义字段映射表。
## 7.1 推荐映射对象
```python
@dataclass
class ScopeFieldMapping:
area_field: str | None = None
creator_field: str | None = None
owner_field: str | None = None
user_field: str | None = None
```
## 7.2 模块字段规范
### 文档模块
- area 字段:`d.region`
- self 字段:`f.created_by` 或主文档归属用户
说明:
- 当前文档真实归属更多落在文件记录 `created_by`
- 统一接入时要固定“列表/详情/附件/结果”的归属判断口径
### 公文模块
- area 字段:`d.region`
- self 字段:`f.created_by`
### 使用统计模块
按口径不同分两类:
- 用户口径 area`u.area`
- 文档口径 area`d.region`
- self 字段:`f.created_by``u.id`
### 合同模板模块
- area 字段:`t.region`
- self 字段:`t.created_by`
### RBAC 用户管理
- area 字段:`u.area`
- self 字段:通常不用
### RAG 数据集/应用
- area 字段:`dataset.area` / `app.area`
- self 字段:按创建者字段补充
- public 字段:`is_public`
### 交叉评查
- 不优先走 `area_field`
- 主入口走关系字段:
- `task_member.user_id`
- `proposal.created_by`
- `vote.user_id`
---
## 8. 模块策略设计
统一执行器不能只靠 `ALL/DEPT/SELF`,必须允许模块策略。
## 8.1 `ModulePolicy` 接口
```python
class ModulePolicy(Protocol):
def decorate(self, ctx: ScopeContext, decision: PermissionDecision) -> PermissionDecision: ...
def build_clause(self, ctx: ScopeContext, decision: PermissionDecision) -> ScopeClause | None: ...
```
说明:
- `decorate()` 负责把通用 scope 转成模块实际策略
- `build_clause()` 负责拼接模块专属条件
## 8.2 DocumentPolicy
适用模块:
- 文档列表
- 文档详情
- 文档状态
- 评查点聚合
- 附件追加
- 文档确认
建议策略:
- `ALL`:不加地区限制,但允许 `request_area`
- `DEPT`:固定 `d.region = user_area`
- `SELF`:固定 `f.created_by = current_user_id`
- 单资源接口必须先验证资源是否在 scope 内,再做更新/删除
## 8.3 GovdocPolicy
建议策略:
- 与文档模块同一套 area/self 语义
- `run/result/report/download` 等派生接口必须继承 document scope
- 不能只校验 `run_id` 是否存在,必须回溯到所属 `document_id`
## 8.4 UsageStatsPolicy
建议策略:
- `overview/trends/by-areas`:支持 `ALL/DEPT`
- `by-users/by-departments/details`:支持 `ALL/DEPT/SELF`
- `SELF` 时只允许看到自己的登录、上传、评查明细
- `areaScope=document``areaScope=user` 由模块策略切换字段映射
## 8.5 RagPolicy
建议策略:
- 读取类接口:`PUBLIC_MIXED`
- 管理类接口:`ALL/DEPT/SELF` + 资源属地校验
拆分为两类:
1. `rag:app:read` / `rag:chat:use` / `rag:dataset:read`
- 允许 `本地区 + 省级 + 公共`
2. `rag:dataset:manage/create/update/delete`
- 不再按 `UserRole` 白名单
- 统一按 `permission + scope` 决定
## 8.6 CrossReviewPolicy
建议策略:
- 主模型使用 `RELATION`
- `task:read` 不是 area 过滤,而是成员关系过滤
- `document:complete` 不是角色白名单,而是“拥有权限 + 是任务参与者/负责方”
- `proposal:read/export` 必须通过文档所属任务关系回溯校验
结论:
- 交叉评查接入统一执行器
- 但不强制落到普通 `ALL/DEPT/SELF`
- 它是统一框架下的特例策略,不是例外代码
## 8.7 RbacAdminPolicy
建议策略:
- 角色、用户、组织树、路由、授权查询等接口优先按 `u.area`
- 全局管理员看全部
- 地区管理员仅管理本地区用户
- 角色对象本身通常不按地区分表,但“角色绑定到哪些用户”仍受用户 area 边界约束
---
## 9. QueryScopeBuilder 设计
## 9.1 目标
`PermissionDecision` 转成业务可注入的条件,而不是让业务自己重新解释。
## 9.2 建议接口
```python
class QueryScopeBuilder:
def build_by_mapping(
self,
decision: PermissionDecision,
mapping: ScopeFieldMapping,
current_user_id: int,
user_area: str | None,
request_area: str | None = None,
) -> ScopeClause: ...
```
## 9.3 通用构造规则
### `ALL`
```sql
1 = 1
```
若带 `request_area`
```sql
COALESCE(area_field, '') = :request_area
```
### `DEPT`
```sql
COALESCE(area_field, '') = :scope_area
```
并且:
-`request_area` 非空且不等于 `user_area`,直接拒绝
### `SELF`
优先级:
1. `creator_field`
2. `owner_field`
3. `user_field`
生成:
```sql
creator_field = :current_user_id
```
### `PUBLIC_MIXED`
```sql
(
COALESCE(area_field, '') IN :visible_areas
OR public_field = TRUE
)
```
### `RELATION`
由模块策略返回完整 SQL 子句,不走通用拼接。
---
## 10. 权限决策缓存建议
当前 `PermissionServiceImpl` 已有 60 秒权限集合缓存。
建议统一执行器增加两层缓存:
1. 用户授权明细缓存
2. 用户能力快照缓存
建议缓存内容:
- grant / deny 结果
- 每个 `permission_key` 对应的有效 scope
- 用户 `area`
- 用户角色列表
注意:
- 缓存里只存决策原料,不缓存最终 SQL
- 角色变更、权限变更、用户地区变更后,应主动失效
---
## 11. 建议代码落点
建议新增目录:
```text
fastapi_modules/fastapi_leaudit/services/permission_scope/
decision_models.py
permissionDecisionService.py
dataScopeResolver.py
queryScopeBuilder.py
modulePolicies/
base.py
documentPolicy.py
govdocPolicy.py
usageStatsPolicy.py
ragPolicy.py
crossReviewPolicy.py
rbacAdminPolicy.py
```
建议改造现有:
- `services/impl/permissionServiceImpl.py`
- 保留布尔接口
- 新增返回授权明细的方法
- `services/permissionService.py`
- 增加 `GetPermissionDecision`
- `services/impl/documentServiceImpl.py`
- `services/impl/govdocServiceImpl.py`
- `services/impl/usageStatsServiceImpl.py`
- `services/impl/ragDatasetServiceImpl.py`
- `services/impl/ragChatServiceImpl.py`
- `services/impl/rbacAdminServiceImpl.py`
- `services/impl/contractTemplateServiceImpl.py`
---
## 12. 推荐接入顺序
## 12.1 第一批
- RAG
- 文档
- 公文
- 使用统计
原因:
- 这几块最明显存在范围逻辑分散或双轨冲突
- 风险最高,收益也最大
## 12.2 第二批
- RBAC 管理域
- 合同模板
- 首页入口模块
## 12.3 第三批
- 交叉评查接入统一决策框架
- 前端联动改为只认权限与服务端返回能力
---
## 13. 与现有代码的映射关系
当前散落逻辑,建议按下述替换:
- `documentServiceImpl._getCurrentUserContext`
- 替换为 `PermissionDecisionService + ScopeContext`
- `documentServiceImpl._buildDocumentScopeFilters`
- 替换为 `QueryScopeBuilder + DocumentPolicy`
- `govdocServiceImpl` 内 area/created_by 过滤
- 替换为 `GovdocPolicy`
- `usageStatsServiceImpl._build_user_scope_condition`
- 替换为 `UsageStatsPolicy`
- `contractTemplateServiceImpl``is_global/can_manage`
- 替换为 `ScopeDecision`
- `rbacAdminServiceImpl``bool_or(role_key IN (...))`
- 替换为统一管理域能力决策
- `ragDatasetServiceImpl` / `ragChatServiceImpl``UserRole` 白名单
- 替换为 `RagPolicy`
---
## 14. 验收标准
统一执行器设计落地后,应满足以下标准:
1. 新增一个 permission 时,可以同时配置 data_scope,并被平台自动执行
2. 文档、公文、统计、RAG 不再自行硬编码角色解释范围
3. `DENY`、多角色合并、地区限制有统一规则
4. 详情、删除、下载、导出接口与列表接口共享同一 scope 决策
5. 前端不再需要猜测“省级管理员/市级管理员能看到哪些数据”
---
## 15. 最终建议
这次改造不应该继续扩写更多 `is_global/can_manage` 工具函数,而应直接建立统一执行器。
如果继续在各 service 内复制现有模式,后续问题会持续扩大:
- 数据范围无法统一验收
- 新角色接入成本持续上升
- 前后端边界会长期不一致
- RAG、交叉评查这类特例模块会越来越难治理
统一数据范围执行器是本轮权限架构改造的核心平台能力,优先级应为最高。