Files
leaudit-platform-backend/docs/权限与地区隔离/权限字段映射与SQL改造规范.md
T

831 lines
16 KiB
Markdown
Raw 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.
# 权限字段映射与 SQL 改造规范
> 适用范围:`leaudit-platform` 后端权限改造中的查询语句、资源详情、导出下载、统计聚合、管理列表
> 文档定位:为统一数据范围执行器的接入提供字段映射标准、SQL 拼接规范、资源回溯规范和反模式约束。
---
## 1. 文档目标
这份文档解决的是一个非常具体的问题:
统一数据范围执行器设计出来以后,后端各模块到底怎么把它安全、稳定地接到现有 SQL 上。
当前项目最大风险不是“不会写 scope”,而是:
1. 同一个模块里不同查询使用了不同字段口径
2. 列表、详情、下载、导出没有复用同一主资源边界
3. 各 service 自己拼 `1 = 0``region``created_by`,可维护性很差
4. 多表 join 时,究竟该按 `u.area` 还是 `d.region`,目前没有统一规范
所以这份文档的目标是:
1. 定义统一字段映射标准
2. 定义统一 alias 规范
3. 定义统一 scope 子句拼接方式
4. 定义详情、下载、导出等派生资源的“主资源回溯”规则
5. 定义改造时哪些 SQL 能保留,哪些必须重构
---
## 2. 当前代码里已经暴露出的典型问题
结合现有实现,已经确认有以下模式:
- `documentServiceImpl` / `govdocServiceImpl`
- 通过 `d.region` + `f.created_by` 控制范围
- `usageStatsServiceImpl`
- 有时按 `u.area`
- 有时按 `d.region`
- 有时还要按 `e.area_snapshot`
- `contractTemplateServiceImpl`
- 既有 `t.region`
- 又有 `t.created_by`
- 还混入“省级模板可见”
- `rbacAdminServiceImpl`
- 用户列表、组织树本质上按 `u.area`
这说明项目不是“没有规则”,而是规则散了。
---
## 3. 核心原则
## 3.1 先确定主资源,再确定 scope 字段
SQL 改造第一步不是拼条件,而是先回答:
- 这个接口的主资源是谁?
例如:
- 文档列表:主资源是 `document`
- 文档状态:主资源仍是 `document`
- 公文报告下载:主资源不是 `report_artifact`,而是 `govdoc document`
- RAG 文档分段列表:主资源不是 `segment`,而是 `dataset`
- 交叉评查提案导出:主资源不是 `proposal`,而是“用户参与的任务文档”
只有先确定主资源,scope 才不会跑偏。
## 3.2 范围条件必须挂在主资源口径上
同一个接口里如果出现多表:
- `document d`
- `document_file f`
- `sso_users u`
必须先规定:
- area 看哪个表
- self 看哪个表
不能在同一个模块里一会儿 `u.area`,一会儿 `d.region`,又没有解释。
## 3.3 列表、详情、删除、下载、导出必须继承同一套 scope
这条必须强制执行。
不允许出现:
- 列表按文档范围过滤
- 详情直接按 `id`
- 下载直接按附件 `id`
- 导出直接按 run `id`
凡是派生资源,都必须回溯主资源。
---
## 4. 统一字段映射标准
## 4.1 标准字段分类
建议统一按 5 类字段管理:
1. `area_field`
2. `creator_field`
3. `owner_field`
4. `user_field`
5. `public_field`
建议结构:
```python
@dataclass
class ScopeFieldMapping:
area_field: str | None = None
creator_field: str | None = None
owner_field: str | None = None
user_field: str | None = None
public_field: str | None = None
```
## 4.2 字段语义定义
### `area_field`
表示资源的地区归属字段。
例如:
- `d.region`
- `t.region`
- `dataset.area`
- `app.area`
- `u.area`
### `creator_field`
表示“创建该资源的用户”。
例如:
- `f.created_by`
- `t.created_by`
- `proposal.created_by`
### `owner_field`
表示“资源所有者”,和创建者不一定相同。
如果当前项目没有成熟 owner 模型,可以先不启用。
### `user_field`
用于用户本身或用户快照类数据。
例如:
- `u.id`
- `e.user_id`
### `public_field`
表示是否公共可见。
例如:
- `dataset.is_public`
- `app.is_public`
---
## 5. 各模块字段映射表
## 5.1 文档模块
主资源:`leaudit_documents d`
推荐映射:
| 类型 | 字段 |
| --- | --- |
| `area_field` | `d.region` |
| `creator_field` | `f.created_by` |
| `owner_field` | 暂无 |
| `user_field` | `f.created_by` |
说明:
- 当前代码事实口径就是 `d.region + f.created_by`
- 文档的“自己数据”不建议改成 `d.created_by`,因为现有实现明显依赖文件记录
## 5.2 公文模块
主资源:`govdoc document / leaudit_documents d`
推荐映射:
| 类型 | 字段 |
| --- | --- |
| `area_field` | `d.region` |
| `creator_field` | `f.created_by` |
| `user_field` | `f.created_by` |
说明:
- 公文 run、报告、原文都必须回溯到 `d.region``f.created_by`
## 5.3 使用统计模块
这块必须拆成两个口径。
### 用户口径统计
主资源:`sso_users u``usage_login_events e`
推荐映射:
| 类型 | 字段 |
| --- | --- |
| `area_field` | `u.area``e.area_snapshot` |
| `user_field` | `u.id``e.user_id` |
### 文档口径统计
主资源:文档上传/评查事件
推荐映射:
| 类型 | 字段 |
| --- | --- |
| `area_field` | `d.region` |
| `creator_field` | `f.created_by` |
| `user_field` | `f.created_by` |
说明:
- `areaScope=user``areaScope=document` 本质上是两套字段映射切换
- 不能在一个 builder 里靠字符串替换硬凑
## 5.4 合同模板模块
主资源:`contract_template t`
推荐映射:
| 类型 | 字段 |
| --- | --- |
| `area_field` | `t.region` |
| `creator_field` | `t.created_by` |
| `public_field` | 暂无;如后续有省级公共模板逻辑,应单独建模 |
说明:
- 当前模块有“省级模板 + 本地区模板”可见语义
- 这不是 `is_public`
- 应在 `ContractTemplatePolicy` 中定义成“系统公共范围”,不要混同布尔公开字段
## 5.5 RBAC 用户管理
主资源:`sso_users u`
推荐映射:
| 类型 | 字段 |
| --- | --- |
| `area_field` | `u.area` |
| `user_field` | `u.id` |
说明:
- 角色对象本身不按 area
- 但“查看哪个用户、给谁分配角色”按 `u.area`
## 5.6 RAG 模块
### 数据集
主资源:`dataset`
| 类型 | 字段 |
| --- | --- |
| `area_field` | `dataset.area` |
| `creator_field` | `dataset.created_by` |
| `public_field` | `dataset.is_public` |
### 聊天应用
主资源:`app`
| 类型 | 字段 |
| --- | --- |
| `area_field` | `app.area` |
| `public_field` | `dataset.is_public` 或显式 `app.is_public` |
说明:
- 如果 app 的公开性来自关联 dataset,就要在 policy 层明确写清,不要分散在查询里隐式推断
## 5.7 交叉评查
主资源:任务关系,而不是单表字段。
推荐映射:
| 类型 | 字段 |
| --- | --- |
| `user_field` | `tm.user_id` |
| `creator_field` | `proposal.created_by` |
说明:
- 交叉评查以 `RELATION` 模型处理
- 这块不强制要求 `area_field`
---
## 6. Alias 统一规范
统一执行器要稳定接入,SQL alias 必须统一。
建议标准:
| 资源 | 推荐 alias |
| --- | --- |
| 文档主表 | `d` |
| 文档文件表 | `f` |
| 用户表 | `u` |
| 审查运行表 | `r` |
| 统计登录事件表 | `e` |
| 合同模板表 | `t` |
| RAG 数据集 | `dataset` |
| RAG 应用 | `app` |
| 交叉评查任务 | `task` |
| 交叉评查成员 | `tm` |
| 交叉评查提案 | `proposal` |
目的不是强迫重命名所有 SQL,而是:
- 新增或重构 SQL 时尽量统一
- `QueryScopeBuilder``ModulePolicy` 才能更容易复用
---
## 7. 参数命名规范
当前代码里已经有:
- `requested_region`
- `scope_region`
- `scope_user_id`
- `requested_user_id`
建议固化为统一标准:
| 参数 | 含义 |
| --- | --- |
| `requested_area` | 请求方传入的地区过滤 |
| `scope_area` | 当前 scope 决策后的有效地区 |
| `requested_user_id` | 请求方显式筛选的用户 |
| `scope_user_id` | scope 决策后的当前用户 ID |
| `resource_id` | 主资源 ID |
| `visible_areas` | `PUBLIC_MIXED` 或公共范围列表 |
不建议再混用:
- `query_area`
- `user_area`
- `region`
- `scope_region`
除非当前模块确实已经固定使用 `region` 作为数据库字段名,而不是参数名。
建议规则:
- 参数命名一律使用 `area`
- 数据库字段保留 `region/area` 原名
即:
```sql
COALESCE(d.region, '') = :scope_area
```
而不是:
```sql
COALESCE(d.region, '') = :scope_region
```
这样更利于统一执行器复用。
---
## 8. 标准 SQL 子句模板
## 8.1 `ALL`
无地区限制时:
```sql
1 = 1
```
带请求地区过滤时:
```sql
COALESCE({area_field}, '') = :requested_area
```
## 8.2 `DEPT`
```sql
COALESCE({area_field}, '') = :scope_area
```
并且在进入 SQL 前先做规则:
-`requested_area` 非空且不等于 `scope_area`,直接拒绝或生成 `1 = 0`
## 8.3 `SELF`
```sql
{creator_field} = :scope_user_id
```
若请求还传了 `requested_user_id`
-`scope_user_id` 不相等时直接拒绝
## 8.4 `PUBLIC_MIXED`
```sql
(
COALESCE({area_field}, '') IN :visible_areas
OR {public_field} = TRUE
)
```
推荐 `visible_areas` 至少包含:
- 当前用户地区
- `省级`
- `''`
具体由 `RagPolicy` 决定。
## 8.5 `RELATION`
不建议硬编码为通用模板。
应由 `CrossReviewPolicy` 输出,例如:
```sql
EXISTS (
SELECT 1
FROM leaudit_cross_review_task_member tm
WHERE tm.task_id = task.id
AND tm.user_id = :scope_user_id
AND tm.deleted_at IS NULL
)
```
---
## 9. 主资源回溯规范
这是本轮 SQL 改造里最重要的一条。
## 9.1 什么叫主资源回溯
当接口操作的不是主资源表,而是主资源衍生物时,必须回溯到主资源做 scope 判断。
## 9.2 必须回溯的场景
### 文档状态
虽然接口直接拿文档 ID 列表查状态,但本质仍是文档资源。
必须回溯:
- `document`
- `file`
### 公文 run / report / original
不能只按:
- `run_id`
- `artifact_id`
- `document_id`
直接查。
必须回溯到:
- `govdoc document d`
- `original file f`
### RAG dataset document / segment
不能只按:
- `document_id`
- `segment_id`
直接做可见性判断。
必须回溯到:
- `dataset`
### 交叉评查 proposal export
不能只按 `DocumentId` 导出。
必须回溯到:
- 当前用户是否属于该任务关系链
---
## 10. 推荐接入方式
## 10.1 列表接口
推荐模式:
1. 构造基础查询
2. 获取 `PermissionDecision`
3.`QueryScopeBuilder` 生成 `scope_clause`
4.`scope_clause.sql` 注入 `WHERE`
5. 业务筛选条件作为附加条件继续追加
示意:
```python
decision = await permissionScopeFacade.require(...)
scope_clause = queryScopeBuilder.build_by_mapping(...)
where_clauses = [
"d.deleted_at IS NULL",
scope_clause.sql,
]
params = {
**scope_clause.params,
}
```
## 10.2 详情接口
推荐模式:
不要先查,再判断。
而要直接:
```sql
SELECT ...
FROM ...
WHERE id = :resource_id
AND {scope_clause.sql}
```
这样天然避免“查到了但后面忘记拒绝”的风险。
## 10.3 删除/更新接口
推荐两种方式:
### 方式 A:先用带 scope 的 SELECT 锁定资源
```sql
SELECT id
FROM ...
WHERE id = :resource_id
AND {scope_clause.sql}
FOR UPDATE
```
### 方式 B:直接带 scope 执行 UPDATE/DELETE
```sql
UPDATE ...
SET ...
WHERE id = :resource_id
AND {scope_clause.sql}
```
建议优先用方式 A,更利于错误提示和审计。
## 10.4 下载/导出接口
推荐模式:
1. 先回溯主资源并带 scope 查出合法记录
2. 再根据合法记录生成下载地址或导出文件
不允许:
1. 先按附件 ID 或 artifact ID 取到 OSS URL
2. 再事后补判断
---
## 11. 统计聚合改造规范
统计类 SQL 最容易出错,因为不是简单查明细。
## 11.1 先收 scope,再聚合
推荐:
```sql
WITH scoped_docs AS (
SELECT d.id, d.region, f.created_by
FROM ...
WHERE ...
AND {scope_clause.sql}
)
SELECT ...
FROM scoped_docs
GROUP BY ...
```
不推荐:
先全量聚合,再在外层做地区过滤。
原因:
- 易错
- 性能差
- `SELF` 范围很容易被漏掉
## 11.2 用户口径和文档口径必须拆分
不要写一个函数同时隐式支持:
- `u.area`
- `d.region`
- `e.area_snapshot`
建议明确:
- `build_user_area_scope_clause()`
- `build_document_area_scope_clause()`
- `build_login_snapshot_scope_clause()`
或者在 `UsageStatsPolicy` 内按口径分发。
---
## 12. 反模式清单
以下写法在本轮改造中应视为禁用或逐步清理对象。
## 12.1 反模式:角色名直接拼 SQL 范围
```sql
bool_or(r.role_key IN ('super_admin', 'provincial_admin'))
```
问题:
- 角色名直接耦合能力
- 新角色无法扩展
## 12.2 反模式:详情先查后判
```python
row = await session.execute(...)
if row and current_user["can_manage"]:
...
```
问题:
- 非法资源已经被读出来
- 派生接口最容易漏判
## 12.3 反模式:用 `1 = 0` 到处散落表达拒绝
`1 = 0` 可以作为最终 SQL 结果,但不应成为业务逻辑表达方式。
更推荐:
- 在决策层直接拒绝
- 或由 builder 明确返回 `denied clause`
## 12.4 反模式:同一模块 area 字段口径漂移
例如:
- 文档列表按 `u.area`
- 文档详情按 `d.region`
这种写法必须统一。
## 12.5 反模式:下载 URL 先取后判
尤其是:
- 原文下载
- 报告下载
- 导出文件
必须先判 scope,再产出 URL。
---
## 13. 索引建议
统一执行器接入后,范围过滤会更集中,需要补齐索引。
建议优先确认以下索引:
### 文档/公文
- `documents(region)`
- `document_files(created_by)`
- `document_files(document_id, created_by)`
### 用户
- `sso_users(area)`
- `sso_users(status, deleted_at, area)`
### 合同模板
- `contract_templates(region)`
- `contract_templates(created_by)`
### RAG
- `rag_datasets(area, is_public)`
- `rag_apps(area)`
### 交叉评查
- `task_member(task_id, user_id)`
- `proposal(document_id, created_by)`
说明:
- 索引不是这轮文档的主体,但如果没有,会直接影响统一执行器上线后的查询性能
---
## 14. 推荐代码组织
建议新增或统一下面这类方法:
```python
build_scope_clause_for_document(...)
build_scope_clause_for_user(...)
build_scope_clause_for_dataset(...)
resolve_primary_resource_scope(...)
```
更推荐的最终形态是:
```python
decision = await permissionDecisionService.decide(ctx)
scope_clause = modulePolicy.build_clause(ctx, decision)
```
这样业务 service 只负责:
1. 自身业务查询
2. 接入 scope 子句
3. 做结果组装
而不是自己解释权限模型。
---
## 15. 分模块改造建议
## 15.1 文档与公文
优先动作:
1. 抽出统一 `DocumentScopeMapping`
2. 统一 `d.region + f.created_by`
3. 所有详情/状态/run/report/download 统一回溯主文档
## 15.2 使用统计
优先动作:
1. 拆分用户口径和文档口径
2. 不再通过字符串替换实现 `area_snapshot`
3.`SELF` 范围明确化
## 15.3 合同模板
优先动作:
1. 把“省级模板可见”收敛为 policy
2. 列表/搜索/详情共享同一范围规则
## 15.4 RBAC 用户管理
优先动作:
1. 用户列表、组织树、角色分配统一按 `u.area`
2. 角色元数据接口不强加 area scope
## 15.5 RAG
优先动作:
1. `PUBLIC_MIXED` 统一化
2. dataset 子资源全部回溯 dataset
---
## 16. 最终要求
后续所有权限改造相关 SQL,应满足以下要求:
1. 能明确说清主资源是谁
2. 能明确说清 `area_field``creator_field` 是哪个
3. 能明确说清详情/下载/导出是否回溯主资源
4. 不再依赖角色名派生范围
5. 新增接口时可以直接挂到统一执行器,而不是复制旧判断
如果做不到这 5 条,即使逻辑暂时跑通,也不算完成“统一权限架构”的 SQL 层落地。