Files

16 KiB
Raw Permalink Blame History

统一数据范围执行器设计

适用范围: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

表示一次权限决策的输入上下文。

建议字段:

@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

表示从数据库查出的单条授权记录。

@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

表示平台最终产出的权限决策。

@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 的范围子句。

@dataclass
class ScopeClause:
    sql: str
    params: dict[str, Any]
    scope_mode: str
    description: str

4. 执行链路

建议所有需要“权限 + 数据边界”的 service 统一走下面链路:

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. 若存在模块策略,则再由模块策略收敛边界

范围大小建议定义为:

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。

含义:

  • 本地区资源可见
  • 公共资源可见
  • 省级公共资源可见

本质条件通常是:

resource.area IN (:user_area, '省级', '')
OR resource.is_public = TRUE

7. 通用字段映射规范

统一执行器要避免“模块自己猜字段名”,必须先定义字段映射表。

7.1 推荐映射对象

@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

使用统计模块

按口径不同分两类:

  • 用户口径 areau.area
  • 文档口径 aread.region
  • self 字段:f.created_byu.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 接口

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=documentareaScope=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 建议接口

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

1 = 1

若带 request_area

COALESCE(area_field, '') = :request_area

DEPT

COALESCE(area_field, '') = :scope_area

并且:

  • request_area 非空且不等于 user_area,直接拒绝

SELF

优先级:

  1. creator_field
  2. owner_field
  3. user_field

生成:

creator_field = :current_user_id

PUBLIC_MIXED

(
  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. 建议代码落点

建议新增目录:

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
  • contractTemplateServiceImplis_global/can_manage
    • 替换为 ScopeDecision
  • rbacAdminServiceImplbool_or(role_key IN (...))
    • 替换为统一管理域能力决策
  • ragDatasetServiceImpl / ragChatServiceImplUserRole 白名单
    • 替换为 RagPolicy

14. 验收标准

统一执行器设计落地后,应满足以下标准:

  1. 新增一个 permission 时,可以同时配置 data_scope,并被平台自动执行
  2. 文档、公文、统计、RAG 不再自行硬编码角色解释范围
  3. DENY、多角色合并、地区限制有统一规则
  4. 详情、删除、下载、导出接口与列表接口共享同一 scope 决策
  5. 前端不再需要猜测“省级管理员/市级管理员能看到哪些数据”

15. 最终建议

这次改造不应该继续扩写更多 is_global/can_manage 工具函数,而应直接建立统一执行器。

如果继续在各 service 内复制现有模式,后续问题会持续扩大:

  • 数据范围无法统一验收
  • 新角色接入成本持续上升
  • 前后端边界会长期不一致
  • RAG、交叉评查这类特例模块会越来越难治理

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