ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

企业 AI 应用的权限治理:别让 RAG 成为越权入口

企业 AI 应用的权限治理:别让 RAG 成为越权入口 核心命题权限过滤必须在检索之前不在展示之后。一句话带走过滤放在哪一层决定了它是安全边界还是心理安慰权限过滤必须在检索之前让无权数据根本不进入链路。企业知识库问答最容易被忽略的风险不是模型答错而是模型回答了当前用户不应该看到的问题。普通的 Web 应用越权用户最多看到不该看的页面RAG 应用越权模型会把不该看的内容「复述」出来而且用户可能根本不知道这是越权来的——他以为这是模型「自己知道的」其实是从别人的文档里检索出来的。更危险的是很多团队把权限过滤放在最后一步展示层觉得「反正用户看不到就行」。但实际上如果系统先从全量文档中召回再把结果交给重排模型、Prompt、日志或缓存最后才在展示层过滤未经授权的内容已经可能进入系统链路。未授权的内容已经进入了重排模型、Prompt、日志、缓存任何一处泄露都是安全事故。即使用户界面没有显示也不能把这种设计当作安全边界。两个典型事故•某企业知识库问答销售 A 提问时模型引用了销售 B 团队的客户跟进记录。原因是检索时没按团队过滤只在前端展示时判断结果日志里已经记录了未授权内容•某法务知识库实习生提问「合同模板」模型返回了仅合伙人可见的保密合同条款。原因是向量库没有权限标签检索返回了全量结果前端过滤没拦住结论权限治理必须从数据进入检索系统开始设计贯穿查询、召回、生成、引用、日志和缓存全链路。展示层过滤只是最后一道防线不是安全边界。本章讲清楚四件事权限模型要表达哪些维度、数据结构怎么设计、正确的安全链路是什么、怎么用代码实现权限前置过滤。一、权限模型需要表达哪些维度不能简化成「一个标签」一个企业文档至少需要表达以下授权维度少一个都可能出问题维度含义示例租户Tenant数据属于哪个组织/客户tenant_id: 「tenant-001」团队Team数据属于哪个团队team_id: 「team-sales-east」角色Role用户的角色决定可见范围role: 「sales_rep」 / 「sales_manager」客户范围Customer Scope销售能看哪些客户的数据customer_ids: [「cust-001」, 「cust-002」]文档状态Status文档是否生效effective_status: 「active」 / 「draft」 / 「expired」密级Classification数据的保密等级public / internal / confidential为什么不能简化成「文档有一个标签用户也有一个标签」标签可以帮助检索但授权判断应基于结构化策略。比如「团队」这个维度销售 A 属于「华东销售团队」他能看本团队的客户跟进但不能看「华北销售团队」的。如果只用一个标签「销售」那所有销售都能看到所有销售的数据越权了。多维度组合判断权限判断通常是「与」的关系——租户要匹配、团队要匹配、客户范围要包含、文档状态要是 active、密级要在用户可访问范围内。任何一个不满足就不能访问。这种「与」关系意味着权限模型不能简化成一个布尔值或一个标签它必须是一组结构化条件的联合判断。角色Role到底参不参与判断上表把role列为六个维度之一但在实际判断里它很少单独出现——它更像一个派生维度负责把「这个人是什么角色」翻译成「他因此拥有哪些具体权限」。映射关系通常有两条role派生出的 team 范围派生出的 clearance_levelsales_rep仅本人所在团队internalsales_manager本团队 下级团队internallegal_partner法务部confidentialintern仅本人所在团队public所以can_read()里不直接比较role而是比较由 role 派生出的accessible_teams和clearance_level。这样做的意义是角色与权限的映射集中在一处维护组织架构调整时改映射表即可不用改动判断逻辑。反过来如果把「经理能看到下级数据」这类规则硬写进判断函数每来一个新角色就要改一次代码。accessible_teams从哪来、什么时候失效这个数组不应该由人工在文档上手动配置而应由组织架构系统定期同步生成——用户转岗、离职、跨部门协作时权限要自动跟着变。三条纪律•同步而非手工手工维护的权限清单永远滞后于现实转岗的人还留着原团队权限是典型事故来源。•离职即回收离职账号要立刻从所有accessible_teams中移除并触发一次缓存失效——否则离职人员的会话缓存里可能还留着有权结果。•变更要留痕每次权限变更记录 who / when / why配合后面的审计日志形成完整链路。权限的「为什么这个人当时能看」必须在事后可回答。维度之间的优先级在实际工程中不同维度的校验顺序也有讲究。租户隔离是最外层的安全边界——如果租户不匹配后续所有判断都不需要执行直接拒绝。这是因为多租户架构下租户间的数据隔离是法律合规的硬性要求优先级高于一切业务逻辑。密级判断通常放在最后因为它依赖于前面所有维度都通过后才有意义——一个文档即使密级再高如果租户不对、团队不对也没有必要去比较密级。二、推荐的数据结构文档和用户上下文都要结构化文档结构带上权限字段document { doc_id: crm-followup-001, tenant_id: tenant-001, # 租户隔离 team_id: team-sales-east, # 团队隔离 owner_id: user-zhang, # 文档所有者 accessible_teams: [team-sales-east, team-management], # 可访问团队列表 customer_id: cust-001, # 关联客户 classification: internal, # 密级public/internal/confidential effective_status: active, # 生效状态 version: 2024.03, updated_at: 2024-03-15T10:30:00Z, content: 客户A本周跟进进展..., tags: [销售, 跟进记录] }用户上下文从身份系统获取不可信前端传入user_context { user_id: user-li, tenant_id: tenant-001, # 必须和文档的 tenant_id 匹配 team_id: team-sales-east, # 必须在文档的 accessible_teams 里 role: sales_manager, # 角色决定可访问密级 accessible_customer_ids: [cust-001, cust-002], # 能看的客户范围 clearance_level: internal, # 可访问的最高密级 auth_token: jwt-xxx # 用于服务端二次校验 }权限判断需要同时验证租户、团队、客户范围、文档状态、密级。只要关键条件不满足就不能把文档送入模型上下文。注意用户上下文必须从服务端身份系统获取不能信任前端传的 user_id 和 team_id否则用户可以伪造。为什么文档和用户上下文都要结构化有些团队尝试把权限信息塞进 tags 字段比如tags: [tenant:001, team:sales-east, classification:internal]然后在检索时用字符串匹配来过滤。这种做法有三个致命问题1字符串匹配无法表达「列表包含」关系比如 accessible_teams 是一个数组2无法做数值比较比如密级等级的 判断3tags 字段通常不会被向量数据库的索引引擎优化过滤性能差。正确的做法是把权限字段作为独立的 metadata 字段利用向量数据库的原生 metadata filter 能力在检索阶段就完成权限过滤。三、权限前置过滤正确的安全主链路正确的安全主链路应是用户提问 → 服务端获取用户上下文身份系统→ 检索时注入权限条件 → 只返回授权文档 → 重排 → Prompt → 生成 → 引用展示关键点权限过滤必须在检索之前或检索之中不能在检索之后。✗ 错误链路后置过滤用户提问 → 全量检索 → 拿到 Top-K → 在应用层过滤无权文档 → 重排 → Prompt → 生成问题无权文档已经进入了检索结果可能被日志记录、被缓存、被重排模型处理。即使最后过滤掉了泄露风险已经存在。✓ 正确链路前置过滤用户提问 → 服务端获取用户上下文 → 把 tenant_id、team_id、customer_ids 等作为检索条件 → 向量库只返回满足条件的文档 → 重排 → Prompt → 生成好处无权文档根本不会被检索出来不会进入后续任何环节。两种链路的本质区别后置过滤的安全边界是「用户看不到」前置过滤的安全边界是「数据不存在于链路中」。前者依赖多层防御不出现任何疏漏后者从源头消除了风险。用一个类比后置过滤像是在图书馆里先把所有书都搬到桌上再一本本检查借阅证是否允许前置过滤是在书架上就把不允许的书锁起来读者根本拿不到。工程折中有些基础设施如某些向量数据库暂时不支持检索前置过滤只能从候选集中过滤。这属于工程折中而不是理想安全模型。此时必须做到未授权文档不会进入 Prompt、重排模型、用户可见日志、缓存和调试输出通过越权测试验证用无权账号测试确认返回结果中没有无权内容记录过滤日志审计哪些文档被过滤掉了在候选集阶段过滤后如果剩余文档不足 Top-K不要二次全量检索补充——否则又回到了后置过滤的问题向量数据库的原生过滤能力对比主流向量数据库对 metadata filter 的支持程度不同。Milvus 和 Qdrant 支持在 ANN 检索阶段注入 filter 条件实现真正的前置过滤Pinecone 支持 metadata filter 但有一些限制Chroma 和 FAISS 的 filter 能力相对基础。选型时权限过滤能力应该是核心评估维度之一而不是等开发完了再补。四、Python 教学实现权限前置过滤的最小示例下面的示例使用结构化授权策略并采用「所有必要条件都满足」的判断方式。它是教学实现生产环境还应接入统一身份系统、策略引擎和检索服务的原生过滤能力。from dataclasses import dataclass, field from typing import List, Optional dataclass class Document: doc_id: str tenant_id: str team_id: str accessible_teams: List[str] customer_id: Optional[str] classification: str # public / internal / confidential effective_status: str # active / draft / expired dataclass class UserContext: user_id: str tenant_id: str team_id: str role: str accessible_customer_ids: List[str] clearance_level: str # public / internal / confidential # 密级等级数字越大越机密 CLASSIFICATION_LEVEL {public: 0, internal: 1, confidential: 2} def can_read(doc: Document, user: UserContext) - bool: 判断用户是否有权读取文档。 采用所有必要条件都满足的策略任何一条不满足就拒绝。 # 1. 租户必须匹配多租户隔离的第一道防线 if doc.tenant_id ! user.tenant_id: return False # 2. 文档必须是生效状态草稿和过期的不能看 if doc.effective_status ! active: return False # 3. 用户团队必须在文档的可访问团队列表中 if user.team_id not in doc.accessible_teams: return False # 4. 如果文档关联了客户用户必须在该客户的可访问范围内 if doc.customer_id and doc.customer_id not in user.accessible_customer_ids: return False # 5. 用户密级必须 文档密级不能看比自己权限高的文档 if CLASSIFICATION_LEVEL[user.clearance_level] CLASSIFICATION_LEVEL[doc.classification]: return False return True # 测试用例 doc_sales Document( doc_idcrm-001, tenant_idtenant-001, team_idteam-sales-east, accessible_teams[team-sales-east, team-management], customer_idcust-001, classificationinternal, effective_statusactive, ) user_sales_manager UserContext( user_iduser-li, tenant_idtenant-001, team_idteam-sales-east, rolesales_manager, accessible_customer_ids[cust-001, cust-002], clearance_levelinternal, ) user_intern UserContext( user_iduser-wang, tenant_idtenant-001, team_idteam-intern, roleintern, accessible_customer_ids[], clearance_levelpublic, ) print(销售经理能否读取销售跟进记录:, can_read(doc_sales, user_sales_manager)) # True print(实习生能否读取销售跟进记录:, can_read(doc_sales, user_intern)) # False团队不匹配 密级不够代码中的几个判断具有明确含义租户匹配多租户隔离的第一道防线A 租户永远看不到 B 租户的数据生效状态草稿和过期文档不能进入检索结果避免回答过期内容团队匹配团队粒度的权限控制不在可访问团队列表里的团队不能看客户范围销售只能看自己负责的客户数据不能看其他销售的客户密级判断用户的可访问密级必须大于等于文档密级实习生不能看机密文档生产环境需要补充的统一身份认证SSO、策略引擎如 OPA、检索引擎原生过滤如向量库的 metadata filter、缓存隔离、导出接口鉴权、越权回归测试。从教学到生产的距离上面的can_read函数是一个同步的、内存中的判断逻辑。在生产环境中它需要演变为1从 SSO 获取用户身份和权限属性而不是硬编码 UserContext2使用策略引擎如 Open Policy Agent来管理权限规则而不是写在代码里——因为权限规则会频繁变化硬编码意味着每次改规则都要发版3把权限条件翻译为向量数据库的 metadata filter 查询在检索阶段就完成过滤而不是检索后再逐条判断4加入缓存层时缓存的 key 必须包含用户权限维度避免用户 A 的缓存结果被用户 B 命中。五、越权测试怎么证明权限真的挡住了前面的链路设计和can_read()只是「声称」安全。要证明它真的安全必须用无权账号去主动尝试越权并确认系统挡住了。这件事不能靠代码评审替代——评审看的是逻辑越权测试看的是实际部署出来的那条链路包括你没想到的缓存、日志、导出接口。测试用例怎么造核心思路是「用 A 的身份去要 B 的数据」每个权限维度至少一条用例。编号测试场景构造方式期望结果T1跨租户访问用 tenant-002 的账号检索 tenant-001 的文档检索结果为空且无任何内容进入响应体T2跨团队访问用华东团队账号检索华北团队的跟进记录返回空不返回「部分命中」的中间态T3跨客户范围用销售 A 账号检索销售 B 负责的客户数据返回空T4密级不足用clearance_levelpublic的实习生账号检索 confidential 文档返回空T5非生效状态检索draft和expired文档返回空T6缓存穿透先让有权用户查询并写缓存再用无权用户用相同 query 查询无权用户不命中前者的缓存T7身份伪造篡改请求里的 user_id / team_id服务端以 JWT 为准篡改无效T8日志泄漏用无权账号发起查询检查服务端日志日志中不出现无权文档的正文内容什么算通过不是「页面上看不到」而是后端响应体、日志、缓存、调试输出四处都找不到无权内容。T1 到 T8 必须全部通过才算权限链路成立任何一条挂在「页面上看不到但响应体里有」都按不通过处理——那是和后置过滤等价的状态。什么时候跑三类节点必须跑一遍——1首期上线前2权限模型或检索链路发生任何变更后3定期回归比如每月一次。前两类是变更驱动第三类是防止「没人改但基础设施升级把 filter 能力悄悄降级了」。审计与告警越权测试是主动验证审计日志是被动留痕两者互补。记录项内容用途查询留痕谁、什么时候、问了什么、返回了哪些 doc_id事后回答「这个人当时看到了什么」过滤留痕本次查询过滤掉了哪些文档、命中了哪条规则排查「为什么该看到的没看到」越权尝试告警同一账号短时间内大量命中过滤规则识别被盗号或主动试探行为权限变更日志who / when / why见第一节解释某次访问当时为什么被允许注意过滤日志本身不能成为新的泄露点。记录「过滤掉了 doc-123」是安全的记录「doc-123 的正文是……」就是把敏感内容抄进了日志。日志只记标识和规则不记正文。把本章翻译成 AI FDE hub 工程契约企业 AI 应用的权限治理 这件事最终要落到一份能被四方签收的契约上。下面这份 YAML 就是它的字段模板# AI FDE hub · 权限治理 · 工程契约示例 chapter: 004 framework: AI FDE hub 三段式交付入场 / 搭建 / 离场 tags: [AI, 权限治理, RAG, 多租户] scope: role: AI FDE 工程师 boundary: 对接客户业务负责人、合规、研发、测试四方共同验收 permission_model: dimensions: [tenant_id, team_id, role, customer_scope, effective_status, classification] filter_position: 检索前置注入检索条件 fallback: 候选集过滤 越权测试验证 required_checks: - 租户匹配 - 文档生效状态为 active - 用户团队在可访问团队列表中 - 用户可访问客户范围包含文档关联客户 - 用户密级 文档密级和相邻章节的关系本章与前后相邻的第 3 章《AI 项目数据接入从数据源盘点到可用知识》、第 5 章《AI FDE 如何设计企业知识库检索链路》同处一个主题段共用同一套「产物 退出条件 决策门」语言——第 3 章把权限字段写进文档结构并前置到检索本章把这些字段变成不可绕过的判断第 5 章再在此基础上保证检索结果的相关性。Toy Project vs. 生产级系统 · 8 项关键差异这张表聚焦权限治理在生产环境最容易塌陷的地方。它的特殊之处在于权限的差距不会以「功能不好用」的形式暴露而是以「事故已经发生」的形式暴露——所以每一项都要在首期就补上。维度Toy Project生产级系统差距代价租户隔离单一租户演示数据无隔离概念多租户架构 tenant_id 硬隔离客户间数据交叉一次就是合同违约权限粒度硬编码「管理员/普通用户」六维度组合判断租户团队角色客户状态密级只分两级同公司内互相越权过滤位置前端展示层过滤检索前置注入 metadata filter无权内容进了日志和缓存删不干净身份来源前端传入 user_id服务端 SSO JWT 二次校验抓包即可伪造身份权限形同虚设密级管理无密级概念分级策略 用户 clearance 比对实习生能拿到合伙人级保密条款缓存隔离全局缓存所有用户共享缓存 key 含权限维度用户间不串甲的答案命中乙的缓存无声越权越权测试无定期用无权账号跑回归测试权限链路改了没人发现漏洞长期潜伏审计日志无日志过滤日志 越权尝试告警出事说不清谁看了什么无法定责客户现场最常见的三种反模式这三种做法在权限治理上线前后反复出现共同点是都把权限当成了「可以事后加的一层」而不是「必须一开始就成立的前提」。提前识别比事后补救成本低一个数量级——权限事故的补救成本往往是把已泄露的数据当作已泄露来处理。反模式典型表现正确做法不推荐原因先上线再补权限首期不做权限隔离「后面加个过滤就行」结果日志和缓存已经泄露了敏感数据权限模型在数据入库时就设计好首期就做到检索前置过滤泄露已经发生补过滤只能防住下一次把权限塞进 tags用tags: [tenant:001, team:sales]做字符串匹配过滤无法表达列表包含和数值比较权限字段作为独立 metadata 字段利用向量数据库原生 filter检索阶段过滤不了只能退回后置裁剪信任前端传入的身份前端传 user_id 和 team_id后端直接使用用户可以通过抓包伪造用户上下文从服务端 SSO 获取前端只传 auth_token改一个请求参数就能看到别人的数据附录权限治理速查一句话记法权限过滤必须在检索之前不在展示之后——过滤放在哪一层决定了它是安全边界还是心理安慰。本章机制表六个授权维度与判断要点。维度判断要点不满足的后果租户Tenanttenant_id 必须匹配最先判断客户间数据交叉一次就是合同违约团队Teamuser.team_id 必须在 doc.accessible_teams 中同公司内互相越权角色Role作为派生维度映射到 accessible_teams 与 clearance_level组织调整时判断逻辑四处散落客户范围Customer Scopedoc.customer_id 必须在 user.accessible_customer_ids 中销售看到他人客户数据文档状态Statuseffective_status 必须为 active回答草稿或过期内容密级Classificationuser.clearance_level 必须 doc.classification实习生拿到合伙人级保密条款边界表前置过滤与后置过滤差在哪。边界安全边界其实是漏洞所在前置过滤数据不存在于链路中依赖向量库原生 metadata filter 能力后置过滤用户看不到不是安全边界无权内容已进日志、缓存、Prompt现场症状速查现场症状根因判断先做什么不该做什么日志里出现无权文档正文过滤放在展示层日志已记录全量结果把权限条件前移到检索阶段只在前端加一层过滤用户 A 命中用户 B 的缓存缓存 key 未含权限维度让缓存 key 带上用户权限维度继续用全局共享缓存抓包即可伪造身份后端信任前端传入的 user_id / team_id用户上下文从服务端 SSO 获取直接采用请求里的身份字段实习生返回保密合同条款向量库无权限标签检索返回全量给文档补 metadata 权限字段靠前端裁剪补漏权限改了没人发现漏洞缺越权回归测试用无权账号定期跑 T1–T8用代码评审替代越权测试转岗后仍能看原团队数据权限手工维护未随组织架构同步由组织架构系统定期同步权限继续人工在文档上配置一个判断顺序先比 tenant_id → 再看 effective_status → 再比 team_id 与 accessible_teams → 再比 customer_id 与 accessible_customer_ids → 最后比 clearance_level 与 classification任一条不满足即拒绝。一句收尾越权泄露一旦发生就没有「修复」这一步只有「善后」——所以权限只能按前置条件排期不能按优先级临时补。和相邻章节的关系本章与第 3 章《AI 项目数据接入从数据源盘点到可用知识》、第 5 章《AI FDE 如何设计企业知识库检索链路》同处一个主题段第 3 章把权限字段写进文档结构并前置到检索本章把这些字段变成不可绕过的判断第 5 章再在此基础上保证检索结果的相关性。思考题为什么「在前端展示层过滤无权内容」不能算安全边界请说出至少 3 个理由。参考答案1未授权内容已经进入了检索结果可能被日志记录日志如果被未授权人员查看就是泄露2未授权内容可能进入缓存缓存被其他用户命中时可能返回无权内容3未授权内容可能进入 Prompt模型可能在回答中「复述」出来用户看到的就是越权内容4重排模型可能基于未授权内容进行排序影响最终结果5前端过滤是客户端逻辑用户可以通过抓包、改请求等方式绕过。正确的做法是在检索前置过滤让无权内容根本不进入系统链路。换个说法权限治理和前几章的工程问题有一处根本不同——数据接错了可以重灌模型答偏了可以调 prompt但越权泄露一旦发生就没有「修复」这一步只有「善后」。所以它不能按优先级排期只能按前置条件处理在设计检索链路的第一天权限就必须是其中一段而不是上线前临时补的一层。AI FDE hub本文由 AI FDE hub 出品关注「AI PDE 陪跑计划」获取 AI 现场交付方法论
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进