Skip to content

检索时授权 ​

这是整个项目的核心主张:身份被编译成一个 SQL 谓词,关键词通道与向量通道都带着它执行。未授权的行不离开数据库,因此没有任何下游组件需要「记得过滤」。

授权属于检索环节,不是事后过滤

一、先说结论 ​

  • 事后过滤既泄漏又损召回:数据进了应用内存就会流向重排、prompt、日志与 trace;被过滤掉的又正是索引已选中的 top-k。
  • 授权谓词由纯函数编译:(Principal, PolicyVersion) → SqlPredicate,所有身份属性都是绑定参数,绝不做字符串拼接。
  • 授权与适用范围是两件事:租户、密级、部门、项目属于授权,计入安全门禁;region、有效期、文档状态属于适用范围,影响相关性而非访问权。
  • 存在性也要保护:无权访问与不存在,对外必须表现一致。
  • 决策表已全部生效:policy abac/1,租户、密级、部门、项目四条规则都编译进了查询。demo 语料已经带上访问标签,评测门禁用 30 条授权负例和人工标注的可见集合检验这四条规则;本文明确区分设计目标与已验证范围。

二、三条不变量 ​

不变量含义当前验证到哪一级
安全未授权的 chunk 不离开 SQL 边界,不进入应用内存、重排、prompt、日志、trace、缓存与响应完整决策表:评测门禁(30 条授权负例,逐 chunk 检查文档与版本,逐身份比较完整可见列表)、基于属性的测试对照独立参考实现、每条规则在三种策略上的集成测试、架构测试
召回加上授权谓词后,授权子集内的排序质量不应低于在该子集上做精确检索构造性成立:v0.1 不建 ANN 索引,所有已授权行都是候选
存在性「你无权查看」与「它不存在」从外部看完全一致部分:检索不返回过滤计数与被排除行的任何元数据;404 与统一拒答要等 M2–M3

把三条分开写有实际意义。安全与召回常被混为一谈:「在数据库里过滤」保证了安全,但并不自动保证召回——带 WHERE 的 HNSW 近似检索会在过滤条件很窄时返回远少于 top-k 的结果。v0.1 用精确检索把这个问题挡在门外,将来引入索引时必须专门测量。

三、决策表 ​

四条授权规则以及它们编译成的 SQL 谓词

一个 chunk 对某个身份可见,当且仅当下表每一行都成立(行之间是 AND),默认拒绝。

#属性身份侧文档侧规则身份缺失该属性时
1租户tenant_id(必需声明)chunk.tenant_id相等token 被拒绝
2密级clearanceclassificationrank(classification) <= rank(clearance),顺序 public < internal < confidential < restricted视为 public
3部门departmentallowed_departments数组为空,或部门在数组中只能看到无部门限制的文档
4项目projects[]required_projects数组为空,或交集非空(any-of)只能看到无项目限制的文档

四行均已实现。推迟到 v0.2 的还有 groups、角色、显式 deny 与 chunk 级标签;如果将来加入 chunk 级标签,它只能收紧文档权限,绝不能放宽。

四、编译后的形态 ​

当前生效的谓词(policy abac/1)。它的文本对所有身份都相同,只有绑定的值不同,所以任何 claim 的值都改变不了查询的形状:

sql
c.tenant_id = :auth_tenant_id
AND v.classification_rank <= :auth_clearance_rank
AND (cardinality(v.allowed_departments) = 0 OR CAST(:auth_department AS text) = ANY(v.allowed_departments))
AND (cardinality(v.required_projects)  = 0 OR v.required_projects && CAST(:auth_projects AS text[]))

几处「默认拒绝」的细节:

  • 缺失或无法识别的密级 claim 绑定为最低级——拼错的 claim 永远不会放宽权限;
  • 缺失的部门绑定为 NULL,与 NULL 比较永远不成立;缺失的项目绑定为空数组,和任何数组都没有交集;
  • 项目列表作为一个数组字面量绑定,每个元素都加引号,值里的逗号、花括号、引号都只是数据。

关键词通道、向量通道和 chunk 列表嵌入的是同一个谓词对象;融合与去重只对 SQL 已放行的行排序。

标签变更不依赖模型服务 ​

标签属于文档版本,随入库请求提交(classification、allowedDepartments、requiredProjects);不带标签的文档在租户内公开、不受限。只有内容、格式、标签三者都与当前版本相同,才算「未变更」。

只改标签时,系统新建一个版本行,把原版本的 chunk 整体接管过来,并在同一个事务里翻转指针——不切分、不算向量。这样收回权限不需要模型服务在线,并且对下一次查询生效。

policy_version 随每个结果返回,后续还会写入查询执行记录与审计事件(M2.5),因此「这条结果是在哪个策略下返回的」是可追溯的。

五、授权与适用范围为什么要分开 ​

一个 US 的员工看不到 EU 的休假政策,通常不是因为「无权访问」,而是「不适用于他」。把两者混在一起会有三个后果:

  1. 安全指标被稀释——权限负例里混进大量适用性用例,「零泄漏」这个结论就不值钱了;
  2. 无法表达「2024 年的政策是什么」这类合理需求;
  3. 用例里的「不该返回的文档」含义含混,到底是越权还是不相关。

因此评测数据把二者拆成 unauthorized_documents(安全门禁)与 hard_negative_documents(质量指标),报告也分开统计。

适用范围现在已经实现,时间语义也有了定论(ADR-0005):

  • asOf 只按有效期过滤文档的当前版本,默认是请求发生的时间;版本在 valid_from <= asOf < valid_to 时适用,缺失的一端视为不限。它不会选出历史版本,授权也始终按当前版本的标签判断。
  • 想回答「2025 年的政策是什么」,做法是把被取代的制度作为一篇独立文档发布,并给它封闭的有效期——而不是去翻同一篇文档的历史。
  • region 默认取身份 token 里的值;没有标注 region 的文档适用于所有地区;没有 region 的身份不做地区过滤。
  • asOf 和 region 可以由请求指定,因为它们不属于授权:换一个地区或日期提问,改变的是哪些内容相关,而不是这个身份能读什么。
  • 范围条件由独立的类型编译、使用自己的参数,和授权谓词是 SQL 里两个分开的条件。评测用的 chunk 列表可以去掉范围条件(includeOutOfScope),但去不掉授权谓词。

为什么不直接支持「选取历史版本」:那需要定义多个版本并存时取哪个、按当时的标签还是现在的标签判断权限、旧版本保留多久,还要有评测用的参考答案。其中任何一条出错,内容就会在错误的标签下被返回——那是泄漏,不是相关性问题。

六、不泄漏存在性 ​

  • 看不到的文档返回 404,与不存在的文档一致;
  • 响应与 trace 里不出现「已过滤 N 条」;
  • 无权访问导致的空结果,与语料中确实没有答案,返回结构完全一致。

已知残余风险:时延侧信道。授权命中与未命中的耗时可能不同,v0.1 不做缓解,威胁模型会明确写出这一点。

七、怎么保证它不被绕过 ​

评测安全门禁如何拿系统和手写标注做比较

防线内容
架构测试扫描主代码,只有 AuthorizedChunkQuery 的源码可以出现 from/join chunk;authorization 包不依赖 web、retrieval 与 JDBC
基于属性的测试jqwik 随机生成身份与带标签的文档(包括会破坏朴素引号处理的值),列表、关键词、向量三条查询路径的结果必须与一份独立的参考实现完全相同;参考实现只存在于测试代码里。测试数据经 JDBC 数组写入,不走生产代码的数组字面量,引号 bug 无法自我抵消
集成测试真实 pgvector 上覆盖决策表每条规则在三种策略上的表现、多条规则同时成立、恶意 claim 值、模型服务不可用时的纯标签变更、无效与权限不足的 token
评测门禁返回文档与人工标注的可见集合比较,而不是与 policy compiler 自己比较;有一条越权结果就让 CI 失败
变异验证开发时故意改坏谓词(<= 改成 <、去掉部门规则、数组元素不加引号),基于属性的测试都在几次尝试内失败,确认测试确实有拦截能力

M2 接下来补上:审计事件、存在性保护与威胁模型。

八、常见误区 ​

  • 「在 SQL 里过滤就等于安全且高召回」:安全成立,召回不一定。近似向量索引先取近邻再按条件过滤,过滤越窄结果越少。
  • 「返回 403 比 404 更诚实」:对未授权内容返回 403 等于承认该文档存在,这本身就是泄漏。
  • 「权限可以放在重排或 prompt 阶段兜底」:那意味着未授权内容已经进入了这些组件,兜底的是事故而不是权限。

小结 ​

授权写在查询里,是为了让「谁能看到什么」只有一处实现、一处验证。从租户隔离扩到完整决策表时,改动只发生在 PolicyCompiler 内部和标签的存储上,检索代码一行没动——这正是把授权编译成一个谓词对象的好处。接着看检索通道如何在这个谓词之上取回候选。

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权