应用工程

企业 RAG 的文档权限继承

企业把网盘、知识库和工单接入 RAG 后,最危险的问题往往不是模型答错,而是它答对了一个提问者本来无权查看的事实。原系统里一份文档可能只对财务组开放;进入统一向量库后,如果检索只按语义相似度排序,权限边界就被索引层抹平了。

文档权限继承的目标很明确:用户通过 RAG 能看到的内容,不得超过他在原始系统中能看到的内容。实现它需要把身份、ACL、检索、缓存和撤销串成一条闭环,不能只在最终答案前加一次应用层判断。

一、权限继承不是给文档加一个部门标签

真实 ACL 通常包含用户、群组、域、角色、分享链接、文档所有者和显式拒绝。用户还可能属于嵌套群组,文档夹继承父目录权限,又对某个子文件单独覆盖。把这些关系压缩成 department=finance,会丢失例外和拒绝规则。

正确做法是先定义授权语义。每个索引对象至少要保存来源系统、资源 ID、ACL 版本、允许主体、拒绝主体和可见范围;主体使用稳定内部 ID,不直接依赖可能变化的邮箱。文档切成多个 chunk 后,每个 chunk 都必须携带同一资源权限引用。

如果来源系统支持“通过链接可访问”,还要决定 RAG 是否继承这类能力。匿名链接往往不适合直接映射到企业聊天入口,默认关闭通常更安全。

二、先统一身份,再同步 ACL

RAG 应用中的用户身份必须能映射回来源系统主体。SSO 的 sub、企业目录 user_id、网盘账户 ID 可能不同,需要维护显式映射表。不能只用显示名,也不要默认同邮箱一定代表同一个安全主体。

群组成员关系建议在授权服务中解析,并保留版本或更新时间。每次查询都实时遍历所有来源系统会带来高延迟;完全依赖长时间缓存又会让离职、转组后的权限迟迟不撤销。常见折中是短期缓存主体集合,同时订阅目录变更事件主动失效。

身份映射失败时应拒绝访问,而不是退回“公共用户”。缺少来源账户的用户只能检索明确标记为组织公开的文档。

三、ACL 数据模型要支持继承与显式拒绝

可以将来源系统复杂权限规范化为可检索的安全描述:

{
  "resource_id": "drive:file:8f21",
  "acl_version": "sha256:...",
  "allow": ["user:u_17", "group:g_finance"],
  "deny": ["user:u_contractor"],
  "scope": "organization",
  "inherited_from": "drive:folder:42"
}

授权计算要明确优先级。例如显式 deny 是否覆盖群组 allow,子资源覆盖还是合并父目录规则,都应忠实复现来源系统语义。若连接器无法表达某类规则,宁可暂不索引该资源,也不要自行放宽。

索引中可以只保存 acl_id,把完整规则放在独立授权服务;也可以展开允许主体用于向量库过滤。前者更新集中,后者检索更快。大型系统常把两者组合:索引保存粗粒度候选过滤字段,授权服务对候选做最终精确判断。

四、权限过滤必须发生在检索前

“先检索 top 20,再删掉无权结果”看似简单,却有两个问题。第一,用户可见文档可能因无权文档占满 top 20 而永远进不了候选;第二,检索日志、重排器或调试界面已经接触了无权文本,泄露发生在生成答案之前。

正确顺序是先把用户主体集合转换为过滤条件,再执行向量或关键词检索。向量数据库需要支持 metadata filter,并确保过滤在候选生成阶段生效,而不是只在返回结果时处理。

def retrieve(query, principal):
    subjects = authz.resolve_subjects(principal)
    acl_filter = {
        "allow_subject": {"$in": subjects},
        "deny_subject": {"$nin": subjects},
        "acl_version": {"$exists": True},
    }
    return vector_store.search(query, filter=acl_filter, top_k=12)

这段代码只表达结构。不同数据库对数组、否定条件和多租户索引的语义不同,必须用实际查询计划验证。若数据库无法安全表达 deny 或嵌套群组,应在独立租户索引、授权代理或二阶段精确校验之间选择,不能假设过滤器天然正确。

五、生成、引用和工具调用也要继承权限

权限过滤不能止于检索器。重排模型、摘要服务和生成模型收到的上下文必须已经授权;答案引用的文档要再次绑定用户与 ACL 版本;点击脚注时,下载端点仍需重新鉴权,不能因为链接来自 RAG 就免检。

Agent 还能调用“读取完整文件”“搜索工单”等工具。每个工具都必须使用终端用户身份或受约束的委托凭证,不应使用一个能读取全公司的服务账号代替用户。服务账号可用于索引构建,但在线读取路径必须有资源级授权。

引用真实性与权限是两条不同检查线:证据可能确实支持答案,却不该给该用户看。关于 claim 与证据映射的校验,可参考本次新增的 RAG 引用真实性自动校验。

六、缓存是最容易遗漏的越权通道

语义缓存若只用 query 做 key,管理员问过的答案可能直接返回给普通员工。检索缓存、生成缓存和会话摘要都要纳入权限设计。安全的 cache key 至少包含租户、授权集合摘要、数据版本和模型配置。

不要直接把完整群组列表写进 key;可以对排序后的主体集合与 ACL epoch 做哈希。权限变更时提升相关租户或资源的 epoch,使旧缓存自然失效。对于高度敏感内容,可关闭跨用户生成缓存,只缓存授权后的文档特征。

会话也会发生权限漂移。用户在会话中途被移出项目组,旧消息里可能仍包含敏感摘要。系统需要在每轮调用时重新确认身份状态,并对长会话设置重新授权或清理上下文的策略。

七、撤销速度决定权限系统是否真正有效

新增权限晚几分钟通常影响可用性,撤销权限晚几小时则是安全问题。连接器应优先处理 delete、move、permission_changed 和 user_disabled 事件;定期全量扫描只能作为兜底。

事件处理要幂等,并按资源版本防止乱序。收到旧 ACL 事件时不能覆盖新状态。删除文档不仅要删主记录,也要删除所有 chunk、关键词索引、摘要、缓存和离线导出。失败队列必须告警,不能把“稍后重试”当作永久状态。

可设定明确的撤销 SLO,例如从来源权限改变到 RAG 拒绝访问的最大时间,并用定期探针测量。文档内容与索引版本管理的细节可参考 RAG 文档版本与索引一致性。

八、用负向测试证明没有越权

权限测试的核心不是“财务用户能搜到财务文档”,而是“非财务用户在各种路径下都拿不到”。测试矩阵应覆盖直接用户授权、群组授权、嵌套群组、显式拒绝、移出群组、文档移动、匿名链接和跨租户同名文档。

每个案例同时检查检索结果、生成上下文、引用跳转、缓存命中和日志内容。还要构造提示注入,例如要求 Agent 忽略权限、猜测其他部门文档标题、调用完整文件工具。权限必须由服务端策略执行,Prompt 中的“不要泄露”只能作为补充。

线上审计记录谁在何时以什么主体集合检索了哪些资源、使用哪个 ACL 版本以及最终是否放行。审计日志本身可能包含敏感元数据,需要单独授权与保留策略。

九、落地检查清单

上线前确认五个门槛:身份映射失败时默认拒绝;所有 chunk 绑定资源与 ACL 版本;候选生成前做权限过滤;缓存键包含授权边界;权限撤销能在 SLO 内清除索引和缓存。任何一项缺失,都不适合接入敏感文档。

实施顺序可以从单一来源、单一租户开始,先支持最小 ACL 语义并做负向测试,再逐步接入嵌套群组和复杂分享规则。每增加一种来源,都要写清它与统一模型之间的语义差异。

十、相关阅读