# 统一数据范围执行器设计

> 适用范围：`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、交叉评查这类特例模块会越来越难治理

统一数据范围执行器是本轮权限架构改造的核心平台能力，优先级应为最高。
