---
title: "面向 SharePoint、Confluence、Drive 的权限优先 RAG"
canonical: https://wavect.io/zh/blog/rag-permissions-sharepoint-confluence-drive/
language: zh
description: "企业级 RAG 真正的难题是权限。如何在检索层强制执行访问控制：每个 chunk 带 ACL、按最终用户身份过滤、保持 ACL 新鲜，避免泄露。"
image: "https://wavect.io/img/blog/headers/header_rag-permissions-sharepoint-confluence-drive.png"
---

[**返回**](/zh/blog/overview/)

[![Kevin Riedl](/img/team/kevin.webp)](/zh/team/kevin-riedl/)

[Kevin Riedl](/zh/team/kevin-riedl/) https://linkedin.com/in/wsdt

14 min 阅读 · 11 Jun 2026

[**下一篇**](/zh/blog/rag-production-readiness-checklist-eu/)

# 面向 SharePoint、Confluence 和 Google Drive 的 RAG：权限优先架构

要点速览

企业级 RAG 真正的难题是权限：助手绝不能把文档呈现给无权查看它的用户。Embedding 会剥离 ACL，因此天真的“全部 embed、检索 top-k”流水线只按相似度匹配而非按授权匹配，于是泄露。请在检索层而非 prompt 中强制执行访问控制：把每个文档的 ACL 作为带版本、可过滤的元数据附加到每个 chunk 上，并在检索前按用户的身份和组过滤向量搜索，使模型只看到经过授权的 chunk。传递最终用户身份，而非服务账户；存储组 ID，而非用户 ID；deny 覆盖 grant；保持 ACL 新鲜，因为一项已被撤销但索引尚未跟上的权限就是一个正在发生的泄露。并把 embedding 本身当作个人数据对待。

企业级 [RAG](/zh/glossary/rag/) 真正的难题不是检索质量，而是权限：助手绝不能把文档呈现给无权查看它的用户。Embedding 会剥离 access control list，因此一条天真的"全部 embed、检索 top-k"流水线只按相似度匹配，而不按授权匹配，于是发生泄露。解决办法是架构层面的。请在**检索层**而非 prompt 中强制执行访问控制：把每个源文档的 ACL 作为带版本、可过滤的元数据附加到每一个 chunk 上，并在检索之前按提问用户的身份和组成员关系过滤向量搜索，这样模型只会看到经过授权的 chunk。传递最终用户的身份，而不是服务账户的身份；存储组 ID，而不是用户 ID；让 deny 覆盖 grant；并保持 ACL 新鲜，因为一项已被撤销但索引尚未跟上的权限就是一个正在发生的泄露。

这是我们 [RAG 生产就绪清单](/zh/blog/rag-production-readiness-checklist-eu/) 的实现配套篇，那篇文章把按用户权限点名为最难的安全问题。本文讲的是你究竟该如何解决它。各厂商专有功能和 preview 功能更新很快；在动手构建之前，请重新核对所链接的文档。

## 为什么权限是最难的部分

一句话概括失败模式：模型基于一份提问用户从未获准查看的文档，给出了一段流畅的回答，因为检索是按语义相似度而非按授权来匹配的。一旦文档被切成 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 应用的欧盟数据驻留](/zh/blog/eu-data-residency-ai-apps-2026/) 中有专门论述。

## 参考架构，以及反模式

端到端：用一个能读取全部内容和 ACL 的身份摄取每个数据源；逐条目提取有效 ACL（尊重被打断的继承、遍历 Confluence 的祖先、读取 Drive 的继承）；切分、embed，并把规范化、带版本的 ACL 作为可过滤元数据附加到每一个 chunk 上；用最终用户身份加一个强制权限过滤器进行检索；仅从授权 chunk 生成；记录每一个决策；并基于源端变更 feed 运行一个新鲜度循环，对继承变更做显式重新同步，并级联删除到 chunk 和 embedding。

这些反模式，多数都是迟早会发生的真实事故：用一个没有权限过滤器的共享索引；在检索之后或在 prompt 中过滤；指望模型自我审查；用服务账户身份检索，从而毁掉按用户的裁剪；存储按用户的 ID 而非组 ID；把 SharePoint 的"Limited Access"当作读取授权；读取 Confluence restriction 时不遍历祖先；只镜像 SharePoint 的 ACL 却忽略标签加密；只在全量抓取时更新，导致撤权迟迟生效；以及删除了源文档却留下了 embedding。

![Kevin Riedl](/img/team/kevin.webp)

"模型不是你的访问控制，prompt 也不是你的安全边界。如果某个用户无权读取一份文档，向量存储就绝不能返回它。其余的一切，标签、新鲜度、审计日志，都是在服务于这一条规则。"

## 常见问题

### 如何在 RAG 中强制执行权限？

在建索引时把每个源文档的 ACL（允许的用户和组）作为带版本、可过滤的元数据附加到每一个 chunk 上，然后在检索之前按提问用户的身份和组成员关系过滤向量搜索。模型只会看到经过授权的 chunk。绝不要在 prompt 中或检索之后才强制执行。

### RAG 会遵守 SharePoint 权限吗？

默认不会，因为 embedding 会剥离 ACL。只有当你把 SharePoint 和 Graph 的权限（使用 grantedToV2、被打断的继承，以及敏感度标签的 EXTRACT 和 VIEW 权限）与内容一并摄取，并在查询时用已登录用户的身份做 security-trim 时，它才会遵守。Microsoft 365 Copilot 原生就这么做。

### RAG 中的 security trimming 是什么？

就是移除当前用户无权查看的结果。Early-binding 的 security trimming 把用户允许的 principal 集合作为强制过滤器应用在搜索处，于是未授权的文档从不会被返回；与之相对的后置过滤是先取回再丢弃，既更慢又容易泄露。

### 如何在 RAG 中实现按用户的权限？

用服务身份做摄取，但通过 OAuth on-behalf-of 用最终用户的身份检索，使用不可变的 object ID 和 group claim。在 chunk 上存储组 ID，在查询时把用户解析为组，对向量搜索做前置过滤，并让 deny 覆盖 grant。

### 我该在每个 chunk 上存用户 ID 还是组 ID？

组 ID。它们的基数更低，且成员关系变更无需重建索引，只需在查询时解析组。请在查询时于 principal 一侧解析传递性的、嵌套的组成员关系。

### 如何保持权限新鲜，以免在撤权后发生泄露？

使用源端的变更 feed（Google Drive 的 Changes API、SharePoint 的 delta 加 indexer、Confluence 的轮询）做增量 ACL 同步，并对继承的或父范围的变更做显式重新同步，那正是常见盲区。要么用 webhook 更快地物化，要么在查询时做一次实时的最后一公里授权核验。

### 我能不能直接告诉模型不要泄露受限文档？

不能。OWASP 指出 system prompt 中的限制可能不被遵守，并且可以被 prompt injection 绕过，研究人员也已展示出对生产 guardrail 近乎完全的规避。访问控制必须是架构层面的，落在检索层。

### pgvector 能强制执行按用户的访问吗？

能。Postgres 的 row-level security 会给每一次查询（包括相似度查询）追加一个权限谓词，因此在应用代码里无法被绕过。通过会话变量注入身份。要留意延迟，因为该 policy 可能与近似索引相冲突；可用按租户的 partial index 或分区来缓解。

### 在 GDPR 下，文档 embedding 算个人数据吗？

请当作个人数据对待。Embedding-inversion 攻击仅凭向量就能重建出绝大多数的短源文本，因此一个 embedding 可以识别出数据主体。被遗忘权因此要求连 embedding 也一并删除，而不仅仅是源文档。

### Confluence 的权限摄取有何不同？

有效查看权限是 space 权限与页面 restriction 的交集，且查看 restriction 会继承到子页面。Cloud restrictions API 不返回继承来的 restriction，所以你必须遍历祖先并合并它们的读取 restriction，否则会泄露受限的子页面。

## 最终思考

面向你自有文档库的企业级 RAG，成败系于一条规则：如果某个用户不能读取一份文档，检索器就绝不能返回它，模型也绝不能看到它。Embedding 会剥离 ACL，prompt 不可被信赖去把它补回来，而撤权之后的泄露窗口是真实存在的。

所以请构建权限优先的系统。把每个源的 ACL 作为带版本的元数据带到每一个 chunk 上，用最终用户的身份加一个强制过滤器进行检索，从源端的变更 feed 保持 ACL 新鲜，把 embedding 当作个人数据，并记录助手把什么展示给了谁。把这套架构做对，RAG 的其余部分就是容易的那部分了。

## 你可能也喜欢..

[**面向欧盟的 RAG 生产就绪清单** 把一个 RAG 演示推进到员工或客户可以依赖、并能在 GDPR 与《AI 法案》下站得住脚的程度，究竟需要什么。](/zh/blog/rag-production-readiness-checklist-eu/) [**AI Enablement 与通用型 AI 咨询的对比** 一方递给你一份战略幻灯片。另一方在你的基础设施上交付一套可运行、由你的团队拥有并能自行运营的方案。](/zh/compare/ai-enablement-vs-generic-ai-consultancy/)

智能体工程

## 继续浏览此集群

编程智能体、MCP、上下文系统、评估与可靠自动化控制。

[从核心文章开始**AI 智能体的图工程：知识图谱什么时候值得做？**](/zh/blog/graph-engineering-ai-agents/)

- [智能体编辑为何需要语义身份：用 Rust 构建 SEMAPRAX](/zh/blog/semantic-identity-rust-agent-edits/)
- [OpenViking 2026 评测：文件系统式记忆适合生产吗？](/zh/blog/openviking-agent-memory-review/)
- [LLM-as-a-Verifier 详解：架构、成本与生产适用性](/zh/blog/llm-as-a-verifier/)
- [TrueForge 评测：开源 Agent Harness 是否达到生产要求？](/zh/blog/trueforge-agent-harness-review/)
- [智能体可读的网站：llms.txt、Markdown 镜像，以及会坏在哪里](/zh/blog/agent-readable-website-llms-txt-markdown-mirrors/)

只收重要内容

## 关注与你相关的内容

每当我们发布新文章，你会收到一封简短邮件。你可以关注整个博客，也可以只选感兴趣的主题。

[**返回**](/zh/blog/overview/)

[![Kevin Riedl](/img/team/kevin.webp)](/zh/team/kevin-riedl/)

[Kevin Riedl](/zh/team/kevin-riedl/) https://linkedin.com/in/wsdt

14 min 阅读 · 11 Jun 2026

[**下一篇**](/zh/blog/rag-production-readiness-checklist-eu/)

邮件订阅新文章 ×

×

通过邮件获取新文章

我们发布时给你一封简短邮件。免费，不做跟踪。

## Structured Data

```json
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@id": "https://wavect.io/#organization",
      "@type": [
        "Organization",
        "ProfessionalService",
        "LocalBusiness"
      ],
      "employee": [
        {
          "@id": "https://wavect.io/team/kevin-riedl/#person",
          "@type": "Person",
          "jobTitle": "Managing Director",
          "name": "Kevin Riedl",
          "url": "https://wavect.io/team/kevin-riedl/",
          "worksFor": {
            "@id": "https://wavect.io/#organization",
            "@type": [
              "Organization",
              "ProfessionalService",
              "LocalBusiness"
            ]
          }
        },
        {
          "@id": "https://wavect.io/team/christof-jori/#person",
          "@type": "Person",
          "jobTitle": "Managing Director",
          "name": "Christof Jori",
          "url": "https://wavect.io/team/christof-jori/",
          "worksFor": {
            "@id": "https://wavect.io/#organization",
            "@type": [
              "Organization",
              "ProfessionalService",
              "LocalBusiness"
            ]
          }
        }
      ],
      "founder": [
        {
          "@id": "https://wavect.io/team/kevin-riedl/#person",
          "@type": "Person",
          "jobTitle": "Managing Director",
          "name": "Kevin Riedl",
          "url": "https://wavect.io/team/kevin-riedl/",
          "worksFor": {
            "@id": "https://wavect.io/#organization",
            "@type": [
              "Organization",
              "ProfessionalService",
              "LocalBusiness"
            ]
          }
        },
        {
          "@id": "https://wavect.io/team/christof-jori/#person",
          "@type": "Person",
          "jobTitle": "Managing Director",
          "name": "Christof Jori",
          "url": "https://wavect.io/team/christof-jori/",
          "worksFor": {
            "@id": "https://wavect.io/#organization",
            "@type": [
              "Organization",
              "ProfessionalService",
              "LocalBusiness"
            ]
          }
        }
      ],
      "legalRepresentative": [
        {
          "@id": "https://wavect.io/team/kevin-riedl/#person",
          "@type": "Person",
          "jobTitle": "Managing Director",
          "name": "Kevin Riedl",
          "url": "https://wavect.io/team/kevin-riedl/",
          "worksFor": {
            "@id": "https://wavect.io/#organization",
            "@type": [
              "Organization",
              "ProfessionalService",
              "LocalBusiness"
            ]
          }
        },
        {
          "@id": "https://wavect.io/team/christof-jori/#person",
          "@type": "Person",
          "jobTitle": "Managing Director",
          "name": "Christof Jori",
          "url": "https://wavect.io/team/christof-jori/",
          "worksFor": {
            "@id": "https://wavect.io/#organization",
            "@type": [
              "Organization",
              "ProfessionalService",
              "LocalBusiness"
            ]
          }
        }
      ],
      "name": "Wavect GmbH",
      "subjectOf": {
        "@id": "https://wavect.io/verified-claims.json#dataset",
        "@type": "Dataset",
        "creator": {
          "@id": "https://wavect.io/#organization",
          "@type": [
            "Organization",
            "ProfessionalService",
            "LocalBusiness"
          ]
        },
        "description": "A machine-readable registry of quantitative and qualitative claims published by Wavect, with review dates, localized page appearances and public third-party citations where available.",
        "inLanguage": "en",
        "isAccessibleForFree": true,
        "license": "https://creativecommons.org/licenses/by/4.0/",
        "name": "Wavect verified publication claims",
        "url": "https://wavect.io/verified-claims.json"
      },
      "url": "https://wavect.io/"
    },
    {
      "@id": "https://wavect.io/team/kevin-riedl/#person",
      "@type": "Person",
      "jobTitle": "Managing Director",
      "name": "Kevin Riedl",
      "sameAs": [
        "https://www.wikidata.org/wiki/Q139796365",
        "https://www.linkedin.com/in/wsdt",
        "https://github.com/wsdt"
      ],
      "url": "https://wavect.io/team/kevin-riedl/",
      "worksFor": {
        "@id": "https://wavect.io/#organization",
        "@type": [
          "Organization",
          "ProfessionalService",
          "LocalBusiness"
        ]
      }
    },
    {
      "@id": "https://wavect.io/team/christof-jori/#person",
      "@type": "Person",
      "jobTitle": "Managing Director",
      "name": "Christof Jori",
      "sameAs": [
        "https://www.wikidata.org/wiki/Q139796367",
        "https://www.linkedin.com/in/jocr77/",
        "https://github.com/jo-chris"
      ],
      "url": "https://wavect.io/team/christof-jori/",
      "worksFor": {
        "@id": "https://wavect.io/#organization",
        "@type": [
          "Organization",
          "ProfessionalService",
          "LocalBusiness"
        ]
      }
    },
    {
      "@id": "https://wavect.io/#website",
      "@type": "WebSite",
      "inLanguage": [
        "en",
        "de",
        "es",
        "zh"
      ],
      "name": "Wavect",
      "potentialAction": {
        "@type": "SearchAction",
        "query-input": "required name=search_term_string",
        "target": {
          "@type": "EntryPoint",
          "urlTemplate": "https://wavect.io/search/?q={search_term_string}"
        }
      },
      "publisher": {
        "@id": "https://wavect.io/#organization",
        "@type": [
          "Organization",
          "ProfessionalService",
          "LocalBusiness"
        ]
      },
      "url": "https://wavect.io/"
    },
    {
      "@id": "https://wavect.io/zh/blog/rag-permissions-sharepoint-confluence-drive/#webpage",
      "@type": "WebPage",
      "dateModified": "2026-07-07",
      "inLanguage": "zh",
      "isPartOf": {
        "@id": "https://wavect.io/#website",
        "@type": "WebSite"
      },
      "lastReviewed": "2026-07-07",
      "url": "https://wavect.io/zh/blog/rag-permissions-sharepoint-confluence-drive/"
    }
  ]
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "abstract": "企业级 RAG 真正的难题是权限：助手绝不能把文档呈现给无权查看它的用户。Embedding 会剥离 ACL，因此天真的“全部 embed、检索 top-k”流水线只按相似度匹配而非按授权匹配，于是泄露。请在检索层而非 prompt 中强制执行访问控制：把每个文档的 ACL 作为带版本、可过滤的元数据附加到每个 chunk 上，并在检索前按用户的身份和组过滤向量搜索，使模型只看到经过授权的 chunk。传递最终用户身份，而非服务账户；存储组 ID，而非用户 ID；deny 覆盖 grant；保持 ACL 新鲜，因为一项已被撤销但索引尚未跟上的权限就是一个正在发生的泄露。并把 embedding 本身当作个人数据对待。",
  "articleBody": " 博客概览/AI 与智能体/智能体工程 面向 SharePoint、Confluence 和 Google Drive 的 RAG：权限优先架构 要点速览 企业级 RAG 真正的难题是权限：助手绝不能把文档呈现给无权查看它的用户。Embedding 会剥离 ACL，因此天真的“全部 embed、检索 top-k”流水线只按相似度匹配而非按授权匹配，于是泄露。请在检索层而非 prompt 中强制执行访问控制：把每个文档的 ACL 作为带版本、可过滤的元数据附加到每个 chunk 上，并在检索前按用户的身份和组过滤向量搜索，使模型只看到经过授权的 chunk。传递最终用户身份，而非服务账户；存储组 ID，而非用户 ID；deny 覆盖 grant；保持 ACL 新鲜，因为一项已被撤销但索引尚未跟上的权限就是一个正在发生的泄露。并把 embedding 本身当作个人数据对待。 企业级 RAG 真正的难题不是检索质量，而是权限：助手绝不能把文档呈现给无权查看它的用户。Embedding 会剥离 access control list，因此一条天真的\"全部 embed、检索 top-k\"流水线只按相似度匹配，而不按授权匹配，于是发生泄露。解决办法是架构层面的。请在检索层而非 prompt 中强制执行访问控制：把每个源文档的 ACL 作为带版本、可过滤的元数据附加到每一个 chunk 上，并在检索之前按提问用户的身份和组成员关系过滤向量搜索，这样模型只会看到经过授权的 chunk。传递最终用户的身份，而不是服务账户的身份；存储组 ID，而不是用户 ID；让 deny 覆盖 grant；并保持 ACL 新鲜，因为一项已被撤销但索引尚未跟上的权限就是一个正在发生的泄露。 这是我们 RAG 生产就绪清单的实现配套篇，那篇文章把按用户权限点名为最难的安全问题。本文讲的是你究竟该如何解决它。各厂商专有功能和 preview 功能更新很快；在动手构建之前，请重新核对所链接的文档。 为什么权限是最难的部分 一句话概括失败模式：模型基于一份提问用户从未获准查看的文档，给出了一段流畅的回答，因为检索是按语义相似度而非按授权来匹配的。一旦文档被切成 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 中强制执行权限？ 在建索引时把每个源文档的 ACL（允许的用户和组）作为带版本、可过滤的元数据附加到每一个 chunk 上，然后在检索之前按提问用户的身份和组成员关系过滤向量搜索。模型只会看到经过授权的 chunk。绝不要在 prompt 中或检索之后才强制执行。 RAG 会遵守 SharePoint 权限吗？ 默认不会，因为",
  "articleSection": "Engineering",
  "author": {
    "@id": "https://wavect.io/team/kevin-riedl/#person",
    "@type": "Person",
    "name": "Kevin Riedl",
    "sameAs": [
      "https://www.wikidata.org/wiki/Q139796365",
      "https://www.linkedin.com/in/wsdt",
      "https://github.com/wsdt"
    ],
    "url": "https://wavect.io/team/kevin-riedl/"
  },
  "dateModified": "2026-07-07",
  "datePublished": "2026-06-11",
  "description": "企业级 RAG 真正的难题是权限：助手绝不能把文档呈现给无权查看它的用户。Embedding 会剥离 ACL，因此天真的“全部 embed、检索 top-k”流水线只按相似度匹配而非按授权匹配，于是泄露。请在检索层而非 prompt 中强制执行访问控制：把每个文档的 ACL 作为带版本、可过滤的元数据附加到每个 chunk 上，并在检索前按用户的身份和组过滤向量搜索，使模型只看到经过授权的 chunk。传递最终用户身份，而非服务账户；存储组 ID，而非用户 ID；deny 覆盖 grant；保持 ACL 新鲜，因为一项已被撤销但索引尚未跟上的权限就是一个正在发生的泄露。并把 embedding 本身当作个人数据对待。",
  "headline": "面向 SharePoint、Confluence、Drive 的权限优先 RAG",
  "image": "https://wavect.io/img/blog/headers/header_rag-permissions-sharepoint-confluence-drive.svg",
  "inLanguage": "zh",
  "mainEntityOfPage": {
    "@id": "https://wavect.io/zh/blog/rag-permissions-sharepoint-confluence-drive/",
    "@type": "WebPage"
  },
  "publisher": {
    "@id": "https://wavect.io/#organization",
    "@type": [
      "Organization",
      "ProfessionalService",
      "LocalBusiness"
    ]
  },
  "url": "https://wavect.io/zh/blog/rag-permissions-sharepoint-confluence-drive/",
  "wordCount": 575
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "item": "https://wavect.io/zh/",
      "name": "首页",
      "position": 1
    },
    {
      "@type": "ListItem",
      "item": "https://wavect.io/zh/blog/overview/",
      "name": "博客概览",
      "position": 2
    },
    {
      "@type": "ListItem",
      "item": "https://wavect.io/zh/blog/topics/ai-agents/",
      "name": "AI 与智能体",
      "position": 3
    },
    {
      "@type": "ListItem",
      "item": "https://wavect.io/zh/blog/clusters/agent-engineering/",
      "name": "智能体工程",
      "position": 4
    },
    {
      "@type": "ListItem",
      "item": "https://wavect.io/zh/blog/rag-permissions-sharepoint-confluence-drive/",
      "name": "面向 SharePoint、Confluence、Drive 的权限优先 RAG | ",
      "position": 5
    }
  ]
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "在建索引时把每个源文档的 ACL（允许的用户和组）作为带版本、可过滤的元数据附加到每一个 chunk 上，然后在检索之前按提问用户的身份和组成员关系过滤向量搜索。模型只会看到经过授权的 chunk。绝不要在 prompt 中或检索之后才强制执行。"
      },
      "name": "如何在 RAG 中强制执行权限？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "默认不会，因为 embedding 会剥离 ACL。只有当你把 SharePoint 和 Graph 的权限（使用 grantedToV2、被打断的继承，以及敏感度标签的 EXTRACT 和 VIEW 权限）与内容一并摄取，并在查询时用已登录用户的身份做 security-trim 时，它才会遵守。Microsoft 365 Copilot 原生就这么做。"
      },
      "name": "RAG 会遵守 SharePoint 权限吗？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "就是移除当前用户无权查看的结果。Early-binding 的 security trimming 把用户允许的 principal 集合作为强制过滤器应用在搜索处，于是未授权的文档从不会被返回；与之相对的后置过滤是先取回再丢弃，既更慢又容易泄露。"
      },
      "name": "RAG 中的 security trimming 是什么？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "用服务身份做摄取，但通过 OAuth on-behalf-of 用最终用户的身份检索，使用不可变的 object ID 和 group claim。在 chunk 上存储组 ID，在查询时把用户解析为组，对向量搜索做前置过滤，并让 deny 覆盖 grant。"
      },
      "name": "如何在 RAG 中实现按用户的权限？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "组 ID。它们的基数更低，且成员关系变更无需重建索引，只需在查询时解析组。请在查询时于 principal 一侧解析传递性的、嵌套的组成员关系。"
      },
      "name": "我该在每个 chunk 上存用户 ID 还是组 ID？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "使用源端的变更 feed（Google Drive 的 Changes API、SharePoint 的 delta 加 indexer、Confluence 的轮询）做增量 ACL 同步，并对继承的或父范围的变更做显式重新同步，那正是常见盲区。要么用 webhook 更快地物化，要么在查询时做一次实时的最后一公里授权核验。"
      },
      "name": "如何保持权限新鲜，以免在撤权后发生泄露？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "不能。OWASP 指出 system prompt 中的限制可能不被遵守，并且可以被 prompt injection 绕过，研究人员也已展示出对生产 guardrail 近乎完全的规避。访问控制必须是架构层面的，落在检索层。"
      },
      "name": "我能不能直接告诉模型不要泄露受限文档？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "能。Postgres 的 row-level security 会给每一次查询（包括相似度查询）追加一个权限谓词，因此在应用代码里无法被绕过。通过会话变量注入身份。要留意延迟，因为该 policy 可能与近似索引相冲突；可用按租户的 partial index 或分区来缓解。"
      },
      "name": "pgvector 能强制执行按用户的访问吗？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "请当作个人数据对待。Embedding-inversion 攻击仅凭向量就能重建出绝大多数的短源文本，因此一个 embedding 可以识别出数据主体。被遗忘权因此要求连 embedding 也一并删除，而不仅仅是源文档。"
      },
      "name": "在 GDPR 下，文档 embedding 算个人数据吗？"
    },
    {
      "@type": "Question",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "有效查看权限是 space 权限与页面 restriction 的交集，且查看 restriction 会继承到子页面。Cloud restrictions API 不返回继承来的 restriction，所以你必须遍历祖先并合并它们的读取 restriction，否则会泄露受限的子页面。"
      },
      "name": "Confluence 的权限摄取有何不同？"
    }
  ]
}
```
