16 KiB
统一数据范围执行器设计
适用范围:
leaudit-platform当前RBAC + 单地区隔离 + 模块特例策略体系
文档定位:把“data_scope 已建模但未统一执行”的现状,收敛为一套后端可落地的统一执行器设计。
1. 设计结论
当前系统真正缺的不是权限表,也不是权限点,而是一个统一的“数据范围执行层”。
现状已经具备:
roles.data_scoperole_permissions.data_scoperole_permissions.grant_type = GRANT / DENY- 用户主地区
sso_users.area - 多个模块已存在真实可运行的数据边界实现
当前系统真正缺失的是:
- 平台层没有把“功能权限 + 数据范围 + 模块特例策略”合并成一次统一决策
PermissionServiceImpl只返回布尔值,不返回 scope 决策- 文档、公文、统计、合同模板、RBAC 管理域都在各自重复写
is_global/can_manage/area/created_by - RAG、交叉评查等特殊模块没有被纳入统一决策框架,只能继续靠模块内手工判断
因此,建议新增统一执行链:
PermissionDecisionServiceDataScopeResolverQueryScopeBuilderModulePolicyRegistryScopeAwarePermissionFacade
目标不是推翻当前业务 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.arearequest_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:不做数据范围控制ALLDEPTSELFRELATIONPUBLIC_MIXEDCUSTOM
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
细化如下:
- 控制器或 service 构造
ScopeContext PermissionDecisionService先确认该用户是否拥有功能权限- 若被
DENY命中,直接拒绝 - 若
GRANT命中,则计算有效范围 - 若模块存在特例策略,由
ModulePolicy二次修饰决策 - 由
QueryScopeBuilder将决策转换为统一 SQL 子句 - 业务 service 只负责把子句注入原查询,不再自行解释角色
5. 数据来源与优先级规则
5.1 数据来源
统一执行器的决策数据来自 4 层:
sso_users.arearoles.data_scoperole_permissions.data_scoperole_permissions.grant_type / condition_filter
5.2 功能准入优先级
功能准入保持现有语义,但要扩展为“带授权明细返回”:
- 精确
DENY - 通配符
DENY - 精确
GRANT - 通配符
GRANT - 无匹配则拒绝
这部分沿用 PermissionServiceImpl 现有规则,不改语义。
5.3 范围优先级
建议统一采用:
role_permissions.data_scoperoles.data_scope- 模块默认值
即:
- 权限级 scope 覆盖角色级 scope
- 角色级 scope 覆盖模块默认值
- 若都没有,读接口默认
SELF,配置接口默认NONE
5.4 多角色合并规则
建议采用:
- 先按
DENY决定是否整体拒绝 - 在所有有效
GRANT中取“最大可见范围” - 若存在模块策略,则再由模块策略收敛边界
范围大小建议定义为:
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_byowner_iduser_id
必须按模块字段映射规范执行,不能每个模块再自行猜测。
6.4 RELATION
适用于交叉评查等成员关系模型。
含义:
- 可访问与当前用户存在显式关系绑定的数据
例如:
task_member.user_id = current_user_idproposal.created_by = current_user_iddocument属于当前参与任务
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
使用统计模块
按口径不同分两类:
- 用户口径 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_idproposal.created_byvote.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_areaDEPT:固定d.region = user_areaSELF:固定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/DEPTby-users/by-departments/details:支持ALL/DEPT/SELFSELF时只允许看到自己的登录、上传、评查明细areaScope=document与areaScope=user由模块策略切换字段映射
8.5 RagPolicy
建议策略:
- 读取类接口:
PUBLIC_MIXED - 管理类接口:
ALL/DEPT/SELF+ 资源属地校验
拆分为两类:
rag:app:read/rag:chat:use/rag:dataset:read- 允许
本地区 + 省级 + 公共
- 允许
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
优先级:
creator_fieldowner_fielduser_field
生成:
creator_field = :current_user_id
PUBLIC_MIXED
(
COALESCE(area_field, '') IN :visible_areas
OR public_field = TRUE
)
RELATION
由模块策略返回完整 SQL 子句,不走通用拼接。
10. 权限决策缓存建议
当前 PermissionServiceImpl 已有 60 秒权限集合缓存。
建议统一执行器增加两层缓存:
- 用户授权明细缓存
- 用户能力快照缓存
建议缓存内容:
- 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.pyservices/impl/govdocServiceImpl.pyservices/impl/usageStatsServiceImpl.pyservices/impl/ragDatasetServiceImpl.pyservices/impl/ragChatServiceImpl.pyservices/impl/rbacAdminServiceImpl.pyservices/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. 验收标准
统一执行器设计落地后,应满足以下标准:
- 新增一个 permission 时,可以同时配置 data_scope,并被平台自动执行
- 文档、公文、统计、RAG 不再自行硬编码角色解释范围
DENY、多角色合并、地区限制有统一规则- 详情、删除、下载、导出接口与列表接口共享同一 scope 决策
- 前端不再需要猜测“省级管理员/市级管理员能看到哪些数据”
15. 最终建议
这次改造不应该继续扩写更多 is_global/can_manage 工具函数,而应直接建立统一执行器。
如果继续在各 service 内复制现有模式,后续问题会持续扩大:
- 数据范围无法统一验收
- 新角色接入成本持续上升
- 前后端边界会长期不一致
- RAG、交叉评查这类特例模块会越来越难治理
统一数据范围执行器是本轮权限架构改造的核心平台能力,优先级应为最高。