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

16 KiB
Raw Blame History

权限字段映射与 SQL 改造规范

适用范围:leaudit-platform 后端权限改造中的查询语句、资源详情、导出下载、统计聚合、管理列表
文档定位:为统一数据范围执行器的接入提供字段映射标准、SQL 拼接规范、资源回溯规范和反模式约束。


1. 文档目标

这份文档解决的是一个非常具体的问题:

统一数据范围执行器设计出来以后,后端各模块到底怎么把它安全、稳定地接到现有 SQL 上。

当前项目最大风险不是“不会写 scope”,而是:

  1. 同一个模块里不同查询使用了不同字段口径
  2. 列表、详情、下载、导出没有复用同一主资源边界
  3. 各 service 自己拼 1 = 0regioncreated_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

建议结构:

@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.regionf.created_by

5.3 使用统计模块

这块必须拆成两个口径。

用户口径统计

主资源:sso_users uusage_login_events e

推荐映射:

类型 字段
area_field u.areae.area_snapshot
user_field u.ide.user_id

文档口径统计

主资源:文档上传/评查事件

推荐映射:

类型 字段
area_field d.region
creator_field f.created_by
user_field f.created_by

说明:

  • areaScope=userareaScope=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 时尽量统一
  • QueryScopeBuilderModulePolicy 才能更容易复用

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 原名

即:

COALESCE(d.region, '') = :scope_area

而不是:

COALESCE(d.region, '') = :scope_region

这样更利于统一执行器复用。


8. 标准 SQL 子句模板

8.1 ALL

无地区限制时:

1 = 1

带请求地区过滤时:

COALESCE({area_field}, '') = :requested_area

8.2 DEPT

COALESCE({area_field}, '') = :scope_area

并且在进入 SQL 前先做规则:

  • requested_area 非空且不等于 scope_area,直接拒绝或生成 1 = 0

8.3 SELF

{creator_field} = :scope_user_id

若请求还传了 requested_user_id

  • scope_user_id 不相等时直接拒绝

8.4 PUBLIC_MIXED

(
  COALESCE({area_field}, '') IN :visible_areas
  OR {public_field} = TRUE
)

推荐 visible_areas 至少包含:

  • 当前用户地区
  • 省级
  • ''

具体由 RagPolicy 决定。

8.5 RELATION

不建议硬编码为通用模板。

应由 CrossReviewPolicy 输出,例如:

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. 业务筛选条件作为附加条件继续追加

示意:

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 详情接口

推荐模式:

不要先查,再判断。

而要直接:

SELECT ...
FROM ...
WHERE id = :resource_id
  AND {scope_clause.sql}

这样天然避免“查到了但后面忘记拒绝”的风险。

10.3 删除/更新接口

推荐两种方式:

方式 A:先用带 scope 的 SELECT 锁定资源

SELECT id
FROM ...
WHERE id = :resource_id
  AND {scope_clause.sql}
FOR UPDATE

方式 B:直接带 scope 执行 UPDATE/DELETE

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,再聚合

推荐:

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 范围

bool_or(r.role_key IN ('super_admin', 'provincial_admin'))

问题:

  • 角色名直接耦合能力
  • 新角色无法扩展

12.2 反模式:详情先查后判

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. 推荐代码组织

建议新增或统一下面这类方法:

build_scope_clause_for_document(...)
build_scope_clause_for_user(...)
build_scope_clause_for_dataset(...)
resolve_primary_resource_scope(...)

更推荐的最终形态是:

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_fieldcreator_field 是哪个
  3. 能明确说清详情/下载/导出是否回溯主资源
  4. 不再依赖角色名派生范围
  5. 新增接口时可以直接挂到统一执行器,而不是复制旧判断

如果做不到这 5 条,即使逻辑暂时跑通,也不算完成“统一权限架构”的 SQL 层落地。