面向 SharePoint、Confluence 和 Google Drive 的 RAG:权限优先架构
企业级 RAG 真正的难题不是检索质量,而是权限:助手绝不能把文档呈现给无权查看它的用户。Embedding 会剥离 access control list,因此一条天真的"全部 embed、检索 top-k"流水线只按相似度匹配,而不按授权匹配,于是发生泄露。解决办法是架构层面的。请在检索层而非 prompt 中强制执行访问控制:把每个源文档的 ACL 作为带版本、可过滤的元数据附加到每一个 chunk 上,并在检索之前按提问用户的身份和组成员关系过滤向量搜索,这样模型只会看到经过授权的 chunk。传递最终用户的身份,而不是服务账户的身份;存储组 ID,而不是用户 ID;让 deny 覆盖 grant;并保持 ACL 新鲜,因为一项已被撤销但索引尚未跟上的权限就是一个正在发生的泄露。
这是我们 RAG 生产就绪清单的实现配套篇,那篇文章把按用户权限点名为最难的安全问题。本文讲的是你究竟该如何解决它。各厂商专有功能和 preview 功能更新很快;在动手构建之前,请重新核对所链接的文档。
正在你的文档库之上构建 RAG 助手吗?
预约免费咨询为什么权限是最难的部分
一句话概括失败模式:模型基于一份提问用户从未获准查看的文档,给出了一段流畅的回答,因为检索是按语义相似度而非按授权来匹配的。一旦文档被切成 chunk 并 embed 进共享索引,源系统的所有者、访问级别和敏感度标签就不会随向量一同传递,除非你刻意带上它们。向量存储根本没有"是谁在提问"这一概念。
Microsoft 365 Copilot 是教科书式的真实案例。它并不破坏权限,而是遵守权限。问题在于,它把那些本已过度共享但难以发现的内容变得极易被找到,于是无人察觉的潜在过度授权变成了一次提示词就能触发的查询。微软自己的补救措施很说明问题:它用的是把高风险内容移出助手可及范围的工具(restricted content discovery、data-access governance 报告),而不是请模型"乖一点"。
这正是关键所在:告诉模型不要泄露某份文档并不构成一种控制。OWASP 关于敏感信息泄露的指南明确指出,system prompt 中的限制可能不被遵守,并且可以被 prompt injection 绕过。Prompt injection 是头号 LLM 风险,而且可以间接到来,藏在一份被检索到的文档里。研究人员已演示出对生产 guardrail 近乎完全的规避,而 2026 年针对 Copilot 企业搜索的一条 prompt injection 链之所以奏效,恰恰是因为某项防泄露保护只作用于第一次请求。边界必须是架构,而不是 prompt。
原则:在检索层强制执行
向量存储一开始就绝不能返回未授权的文档,模型也绝不能处理它。做到这一点的方法是:在建索引时给每个 chunk 打上规范化、带版本的 ACL 元数据,并在检索时把权限过滤器纳入查询。在检索之后、应用层或 prompt 中过滤会以两种方式失败:它取回文档只为丢弃,白白浪费上下文窗口;而一旦应用逻辑有 bug 或被某次 injection 绕过,文档照样会流过去。后置过滤还会悄悄破坏召回率,因为把未授权的 chunk 从 top-k 集合中剔除后,若授权结果恰好落在 k 之外,模型就可能一无所获。请在搜索处做前置过滤,或者过取若干倍于 k 的结果再过滤。
各数据源的权限模型
每个数据源都有自己的 ACL 模型和自己的陷阱。你用一个能读取全部内容及其权限的高权限身份做摄取,把每个数据源都归一化为同一种形态,并解析为组 ID。
| 数据源 | 权限如何运作 | 需要处理的陷阱 |
|---|---|---|
| SharePoint / OneDrive (Microsoft Graph) | 权限从 site 级联到库再到条目;通过条目权限端点用 grantedToV2 读取 ACL | 共享单个文件会悄悄打断继承;"Limited Access"只是底层管道,并非读取授权;敏感度标签增加了第二道关卡(用户需要 EXTRACT 和 VIEW 权限,而不仅仅是 SharePoint 的 ACL) |
| Confluence | 有效查看权限是 space 权限与页面 restriction 的交集;查看 restriction 会继承到子页面 | Cloud restrictions API 不返回继承来的 restriction,所以你必须遍历每一个祖先并合并它们的读取 restriction,否则会泄露受限的子页面 |
| Google Drive | 六种角色,grantee 类型为 user/group/domain/anyone;通过带 permissionDetails 的权限列表读取 ACL 以获知继承关系 | Shared drives 严格扩张(子项不能拥有比其父项更少的访问权);父项的权限变更不会在子项留下任何 change-log 条目,因此你必须重新传播 |
一个常把团队绊倒的细节:对 Microsoft Graph 编程时,请针对 grantedToV2 和 grantedToIdentitiesV2 编码;较旧的 grantedTo 字段已被弃用。而且权限的可见性本身也是经过 access-trim 的,所以一次完整的 ACL 抓取需要一个具备正确 read-all scope 的 application identity,而不是委托的用户 token。
经得起考验的实现模式
- 在 chunk 上存储组 ID,而不是用户 ID。基数更低,且成员关系变更无需重建索引,只需在查询时解析组。请在 principal 一侧解析传递性的、嵌套的成员关系。
- 用最终用户的身份进行检索。用服务身份做摄取,但在查询时用最终用户的身份和 group claim 过滤,通过 OAuth on-behalf-of 传递。授权时使用不可变的 object ID 和 group object ID,绝不要用可变的显示名称。On-behalf-of 只对用户有效,对 service principal 无效,因此仅有服务账户的链路无法做按用户的裁剪。
- 在 ANN 查询处用集合成员测试做前置过滤。对用户允许的 principal 使用集合函数(即你的向量存储中相当于 search.in 或 $in 过滤器的功能);一长串相等条件的 OR 会慢上几秒。Deny 覆盖 grant。
- 处理 Entra 的 groups-overage。如果某用户属于约 200 个以上的组,identity token 会丢弃 groups claim 并发出一个 overage 指针;检索层必须检测到它并取回完整列表,否则组数量多的用户会被悄悄地过度过滤。
- pgvector 配合 Postgres row-level security 是一个干净的原语:一条 select policy 会给每一次查询(包括相似度搜索)追加一个权限谓词,因此在应用代码里无法被绕过。要留意延迟,因为该 policy 可能与近似索引相冲突;可用按租户的 partial index 或分区来缓解,并在连接池中为每个请求设置身份变量。
有一处真实的分歧值得点明。一派(包括 AWS)主张,仅靠查询时的元数据过滤还不够,你应当在检索时对照源系统重新核验授权,因为同步来的元数据会变陈旧。另一派(包括微软的参考模式)则把 ACL 同步进索引,并在查询时用用户的 token 强制执行,以追求吞吐量。这是一个真实的权衡:新鲜度对延迟。请按数据的敏感程度来抉择。
没人为之预留预算的新鲜度问题
当权限在摄取时被物化,源端的撤销不会自动传播,于是被撤权的用户在下一次同步之前仍可能在索引中看到某份文档。存在两个泄露窗口:源变更与索引更新之间的同步窗口,以及继承范围的盲区,某工具会刷新具有独立权限的条目的 ACL,却漏掉来自父范围的变更。SharePoint 对继承变更需要一次显式的权限重新同步;Google Drive 的 shared drive 父项变更不会在子项留下 change-log 条目。请使用源端的变更 feed(Google Drive 的 Changes API 在权限变更时也会触发,而不只是在编辑时),并按上文的分歧,要么用 webhook 更快地物化,要么做一次实时的最后一公里核验。
删除是另一半。被遗忘权意味着要在每一层删除:源对象、每一个派生 chunk 和每一个 embedding,外加缓存。请预先设计 chunk ID,使删除一份文档及其全部 chunk 成为一次廉价的操作。并且把 embedding 本身当作个人数据:embedding-inversion 研究已能仅凭向量重建出绝大多数的短源文本,所以一次留下 embedding 的删除其实什么都没真正遗忘。
GDPR 与数据驻留
权限优先的 RAG 能干净地映射到 GDPR。数据最小化原则反对把一切可触及的内容批量向量化;只检索并 embed 助手所需的内容。问责原则最好用一件具体的产物来落实:一份按查询记录的检索审计日志,记下哪些文档为哪个查询呈现给了哪个身份,这也让你事后能够调查疑似的陈旧 ACL 泄露。而数据存放在哪里仍然重要,所以请把推理路由到欧盟区域,并与模型提供商签订数据处理协议,这一点我们在 2026 年 AI 应用的欧盟数据驻留中有专门论述。
参考架构,以及反模式
端到端:用一个能读取全部内容和 ACL 的身份摄取每个数据源;逐条目提取有效 ACL(尊重被打断的继承、遍历 Confluence 的祖先、读取 Drive 的继承);切分、embed,并把规范化、带版本的 ACL 作为可过滤元数据附加到每一个 chunk 上;用最终用户身份加一个强制权限过滤器进行检索;仅从授权 chunk 生成;记录每一个决策;并基于源端变更 feed 运行一个新鲜度循环,对继承变更做显式重新同步,并级联删除到 chunk 和 embedding。
这些反模式,多数都是迟早会发生的真实事故:用一个没有权限过滤器的共享索引;在检索之后或在 prompt 中过滤;指望模型自我审查;用服务账户身份检索,从而毁掉按用户的裁剪;存储按用户的 ID 而非组 ID;把 SharePoint 的"Limited Access"当作读取授权;读取 Confluence restriction 时不遍历祖先;只镜像 SharePoint 的 ACL 却忽略标签加密;只在全量抓取时更新,导致撤权迟迟生效;以及删除了源文档却留下了 embedding。

"模型不是你的访问控制,prompt 也不是你的安全边界。如果某个用户无权读取一份文档,向量存储就绝不能返回它。其余的一切,标签、新鲜度、审计日志,都是在服务于这一条规则。"
常见问题
如何在 RAG 中强制执行权限?
RAG 会遵守 SharePoint 权限吗?
RAG 中的 security trimming 是什么?
如何在 RAG 中实现按用户的权限?
我该在每个 chunk 上存用户 ID 还是组 ID?
如何保持权限新鲜,以免在撤权后发生泄露?
我能不能直接告诉模型不要泄露受限文档?
pgvector 能强制执行按用户的访问吗?
在 GDPR 下,文档 embedding 算个人数据吗?
Confluence 的权限摄取有何不同?
最终思考
面向你自有文档库的企业级 RAG,成败系于一条规则:如果某个用户不能读取一份文档,检索器就绝不能返回它,模型也绝不能看到它。Embedding 会剥离 ACL,prompt 不可被信赖去把它补回来,而撤权之后的泄露窗口是真实存在的。
所以请构建权限优先的系统。把每个源的 ACL 作为带版本的元数据带到每一个 chunk 上,用最终用户的身份加一个强制过滤器进行检索,从源端的变更 feed 保持 ACL 新鲜,把 embedding 当作个人数据,并记录助手把什么展示给了谁。把这套架构做对,RAG 的其余部分就是容易的那部分了。
想在你的 SharePoint、Confluence 或 Drive 之上构建权限优先的 RAG 吗?
预约免费咨询