RAG 的访问控制是 RAG 在落地过程中一个很重要的环节,这篇来自 Pinecone 的文章介绍了基于 ReBAC 模型如何实现访问控制,以及当登陆不同用户时,相同问题给出不同的问答效果。enjoy~
访问控制在几乎所有面向用户的应用程序中都起着至关重要的作用,尤其是在涉及敏感信息的情况下。它在保护敏感信息方面发挥着重要作用,确保只有获得所需权限的人才能接触到这些信息。
在 RAG 应用程序中,很可能不是每个用户都能平等地访问所有索引文档。某些信息可能具有机密性,只有特定角色或指定人员才能访问--我们的 RAG 应用程序不能泄露任何敏感信息,这一点至关重要。这就凸显了强大而有效的访问控制机制的必要性。通过防止未经授权的访问,这些机制有助于保持数据的机密性。
在 Pinecone 中,您可以使用元数据将用户和角色与特定向量关联起来,但这并不符合关注点分离的核心架构原则:如果可以避免的话,系统不应过多地考虑 "外来 "因素。正如我们将在这篇文章中探讨的那样,RAG 用例非常适合查询后过滤流程,而且这种流程适合在数据库本身之外管理授权机制。
认证和授权
在具体实施 RAG 应用程序的访问控制之前,我们先来谈谈访问控制的组成部分,即身份验证和授权。
- 身份验证是验证用户身份的过程。这通常涉及用户提供某种形式的凭证,如用户名和密码。如果凭证与系统中存储的相符,用户就通过了身份验证,并被授予访问权限。
- 授权是确定特定身份可使用哪些资源的过程。有几种流行的授权范式:
- 访问控制列表(ACL)--对资源的访问权限由被授予权限的用户列表决定。这是一种易于理解和实施的简单模式,但随着用户和资源数量的增加,管理起来会变得很困难。
- 基于角色的访问控制(RBAC)--根据用户的角色授予访问权限。例如,"管理员 "角色可以访问所有内容,而 "访客 "角色的访问权限可能非常有限。这种模式比 ACL 更灵活,可扩展性更强,但管理起来仍然很复杂。
- 基于属性的访问控制(ABAC)--根据属性组合授予访问权限。这些属性可以与用户(如角色、部门、位置)、资源(如类型、敏感性、位置)、操作(如读、写、删除)和上下文(如时间、网络、设备)相关联。这种模式具有最大的灵活性和可扩展性,但管理起来也最为复杂。
- 基于关系的访问控制(ReBAC)--一种较新的模式,根据用户与资源之间的关系授予访问权限。例如,在社交网络应用程序中,用户可以访问与自己有某种 "关联 "的资源(如好友、关注者等)。这种模式可以处理复杂的访问控制场景,但实施和管理起来也很复杂。
RAG 应用程序的访问控制
既然我们已经对身份验证和授权有了很好的了解,那么让我们来看看如何在 RAG 应用程序中应用它们。粗略地说,我们可以把任何 RAG 系统分成两个阶段:摄取和查询。
- 在摄入阶段,我们必须将系统已知的身份与允许访问的资源关联起来。
- 在查询阶段,我们必须确定身份试图访问的文件中哪些是允许的,哪些不能包含在最终响应中。
为了演示 RAG 应用程序的基本授权流程,我们将应用 ReBAC 模型,原因如下:RBAC 和 ABAC 主要针对端点等应用资源的管理。例如,当我们构建一个应用程序时,我们希望确定哪个用户或角色可以执行映射到端点的特定操作。就 RAG 而言,授权的主要模式是在执行查询后过滤结果。这就要求我们在用户或角色与资源之间创建关系。
实际上,对于一组文档,我们将定义一组可能的类别。我们将为系统中的用户分配这些类别,并只允许他们访问属于其分配类别的文档。
我们将使用上一篇文章中介绍过的 RAG 应用程序 [1],并将其扩展为具有身份验证和授权功能(完整代码列表[2])。
为了方便身份验证和授权过程,我们将使用两种服务:
- Clerk [3] - 一个用户身份验证平台,开发人员可以用它来管理应用程序中的用户访问。它提供登录服务、双因素身份验证、会话管理和用户管理等功能,有助于确保应用程序的安全并保护用户数据。
- Aserto [4] 是一个权限即服务平台,开发人员可利用它将授权纳入自己的应用程序。它提供基于策略的权限、基于角色和属性的访问控制以及基于关系的访问控制等功能,使管理应用程序中谁有权访问什么变得更容易。
? Clerk
首先,我们注册一个 Clerk 账户并创建一组用户:
我们要在这里标记 "Admin "用户拥有一些额外权限,因此我们要在用户条目中添加一些私人元数据:
在本例中,管理员将是能够为特定用户分配类别的用户。在更现实的情况下,我们可能会希望将类别与角色或组关联起来,而不是与用户关联起来,这一点稍后再详述。
管理员用户登录应用程序后,将看到以下控件:
这样,他们就可以选择将哪些类别分配给每个用户。
接下来,我们需要将每个用户的身份与我们要索引的文档关联起来。我们将使用 Aserto 的目录服务来完成这项工作。
? Aserto
Aserto 目录存储授权决策所需的信息。它非常灵活,可轻松支持不同的访问控制策略,包括基于角色的访问控制(RBAC)、基于属性的访问控制(ABAC)和基于关系的访问控制(ReBAC)。
授权决定是对问题 "主体 S 是否被允许在资源 R 上执行操作 A?换句话说,授权决策决定主体(用户、组、服务等)是否拥有资源(文档、文件夹、项目等)上的给定权限。
我们可以把 Aserto 目录看作一个图,其中对象是节点,关系是边。在这种模式下,上一节中的授权问题可以重新表述为 "是否存在一条从节点 S 到节点 R 的路径,其中一条或多条边具有权限 P?
例如,资源对象类型可以定义 can_read、can_write 和 can_delete 权限,并通过所有者、编辑器和查看器关系授予这些权限。
此外,我们还可以定义权限:权限代表主体可以对资源执行的一种操作。与关系类似,每个权限都是作为对象类型定义的一部分在清单[5]中定义的。
与关系不同,权限不能明确分配。权限是通过使用权限操作符[6]组合关系和/或其他权限间接授予的。
Aserto 允许我们在清单文件中定义授权模型:
#yaml-language-server
schema=https://www.topaz.sh/schema/manifest.json
---
#model
model:
version:3
#objecttypedefinitions
types:
#userrepresentsauserthatcanbegrantedrole(s)
user:
relations:
manager:user
#grouprepresentsacollectionofusersand/or(nested)groups
group:
relations:
member:user|group#member
#identityrepresentsacollectionofidentitiesforusers
identity:
relations:
identifier:user
#resourcecreatorrepresentsausertypethatcancreatenewresources
resource-creator:
relations:
member:user|group#member
permissions:
can_create_resource:member
#resourcerepresentsaprotectedresource
resource:
relations:
owner:user
writer:user|group#member
reader:user|group#member
permissions:
can_read:reader|writer|owner
can_write:writer|owner
can_delete
wner
下面是该清单文件的可视化展示:
当使用 Aserto 创建一个新项目时,我们可以从这个初始模型开始,它可以在各种用例中发挥作用。自然,模型可以随着我们应用程序的变化和发展而发展。
Ingestion 摄入
与其他 RAG 导入流程一样,我们对每个文档进行迭代、嵌入并上载到 Pinecone 中。但在我们的案例中,我们还会将文档与其类别关联起来,并根据从应用程序中获取的映射,在系统中创建文档与用户之间的关系。
constindex=pinecone.Index(indexName)
constvectors=awaitPromise.all(documents.flat().map(embedDocument));
awaitPromise.all(Object.keys(usersDataAssignment).map(async(userId)=>{
constuserVectors=filterRecordsByUserAssignments(userId,vectors,usersDataAssignment)
constuserObject=awaitclerkClient.users.getUser(userId);
returnassignRelation(userObject,userVectors,'owner');
}));
//UpsertvectorsintothePineconeindex
awaitchunkedUpsert(index,vectors,'',10);
在这段代码中,我们遍历 userDataAssignment,它将系统中的每个用户与其允许查看的类别关联起来。我们会过滤我们创建的所有向量,并将这些向量与相应的用户关联起来。
让我们来看看在用户和文档之间创建关系的函数:
exportconstassignRelation=async(user:User,documents
ineconeRecord<CategorizedRecordMetadata>[],relationName:string)=>{
//Mapeachdocumenttoasetofoperationsforsettingupuser-documentrelations
constoperations=documents.map((document)=>{
//Constructadisplaynamefortheuser
constuserName=`${user.firstName}${user.lastName?'':''}${user.lastName??''}`
//Createauserobjectforthedirectoryservice
constuserObject={
id:user.id,
type:'user',
properties
bjectPropertiesAsStruct({
email:user.emailAddresses[0].emailAddress,
name:userName,
picture:user.imageUrl,
}),
displayName:userName
};
//Createadocumentobjectforthedirectoryservice
constdocumentObject={
id:document.id,
type:'resource',
properties:document.metadata?objectPropertiesAsStruct({
url:document.metadata.url,
category:document.metadata.category,
})
bjectPropertiesAsStruct({}),
displayName:document.metadata&&document.metadata.title?document.metadata.titleasstring:'',
};
//Definetherelationbetweentheuserandthedocument
constuserDocumentRelation={
subjectId:user.id,
subjectType:'user',
objectId:document.id,
objectType:'resource',
relation:relationName,
};
//Operationstosettheuseranddocumentobjectsinthedirectory
constobjectOperations:any[]=[
{
opCode:ImportOpCode.SET,
msg:{
case:ImportMsgCase.OBJECT,
value:userObject,
},
},
{
opCode:ImportOpCode.SET,
msg:{
case:ImportMsgCase.OBJECT,
value:documentObject,
},
}
];
//Operationtosettherelationbetweentheuserandthedocument
constrelationOperation:any={
opCode:ImportOpCode.SET,
msg:{
case:ImportMsgCase.RELATION,
value:userDocumentRelation,
},
};
//Combineobjectandrelationoperations
return[...objectOperations,relationOperation];
}).flat();
try{
//Createanasynciterablefromtheoperationsandimportthemtothedirectoryservice
constimportRequest=createAsyncIterable(operations);
constresp=awaitdirectoryClient.import(importRequest);
//Readandreturntheresultoftheimportoperation
constresult=await(readAsyncIterable(resp))
returnresult
}catch(error){
//Logandrethrowanyerrorsencounteredduringtheimport
console.error('Errorimportingrequest:',error);
throwerror;
}
}
应用
为了加强用户和他们可以访问的文档之间的关系,我们创建了一个函数,该函数将针对特定权限的目录执行 checkPermission 调用:
exportconstgetFilteredMatches=async(user:User|null,matches:ScoredPineconeRecord[],permission
ermission)=>{
//Checkifauserobjectisprovided
if(!user){
console.error('Nouserprovided.Returningemptyarray.')
return[];
}
//Performpermissionchecksforeachmatchconcurrently
constchecks=awaitPromise.all(matches.map(async(match)=>{
//Constructpermissionrequestobject
constpermissionRequest={
subjectId:user.id,//IDoftheuserrequestingaccess
subjectType:'user',//Typeofthesubjectrequestingaccess
objectId:match.id,//IDoftheobjectaccessisrequestedfor
objectType:'resource',//Typeoftheobjectaccessisrequestedfor
permission:'can_read',//Specificpermissionbeingchecked
}
//Checkpermissionfortheconstructedrequest
constresponse=awaitdirectoryClient.checkPermission(permissionRequest);
//Returntrueifpermissiongranted,falseotherwise
returnresponse?response.check:false
}));
//Filtermatcheswherepermissioncheckpassed
constfilteredMatches=matches.filter((match,index)=>checks[index]);
//Returnmatchesthatpassedthepermissioncheck
returnfilteredMatches
}
对每个主体、客体和权限三重进行权限检查只需几毫秒的时间,从而确保了应用程序的整体性能。
该函数在 getContext 函数中被调用,getContext 会检索相关文档,然后根据我们设置的权限筛选匹配文档。
exportconstgetContext=async({message,namespace,maxTokens=3000,minScore=0.95,getOnlyText=true,user}:
{message:string,namespace:string,maxTokens?:number,minScore?:number,getOnlyText?:boolean,user:User|null})
romise<ContextResponse>=>{
//Gettheembeddingsoftheinputmessage
constembedding=awaitgetEmbeddings(message);
//Retrievethematchesfortheembeddingsfromthespecifiednamespace
constmatches=awaitgetMatchesFromEmbeddings(embedding,10,namespace);
//Filteroutthematchesthathaveascorelowerthantheminimumscore
constqualifyingDocs=matches.filter(m=>m.score&&m.score>minScore);
letnoMatches=qualifyingDocs.length===0;
constfilteredMatches=awaitgetFilteredMatches(user,qualifyingDocs,Permission.READ);
letaccessNotice=false
if(filteredMatches.length<matches.length){
accessNotice=true
}
return{
documents:qualifyingDocs,
accessNotice,
noMatches
}
}
我们希望确保用户能意识到,万一系统中存在的某些文档对他们来说是不可用的。为此,我们会将找到的总匹配数与筛选出的匹配数进行比较。如果完全没有找到匹配,我们也会记录下来。
最后,我们只需将信息发送回客户端。我们将把访问信息作为上下文的一部分附加到发回的数据中:
const{documents,accessNotice,noMatches}=context;
data.append({context:[...documentsasPineconeRecord[]],
accessNotice,
noMatches});
现在我们准备测试授权机制。在我们的应用程序中,我们将使用与财务类别相关联的身份登录,因为它与财务部门有关:
我们将提出同样的问题,但与人力资源类别相关的用户不应该访问与财务相关的主题:
不出所料,我们得到的提示是,有些结果无法用于编写问题,而我们得到的内容实际上与我们的问题主题无关。
总结
在 RAG 应用程序领域,访问控制机制的重要性不容低估。管理敏感信息的任务非同小可,而 Aserto 和 Clerk 等服务可以让这一过程变得更加轻松。数据库的主流授权模式是在执行查询后过滤结果。在某些情况下,这可能并不可行,但在 RAG 使用案例中,这实际上是非常合适的:我们通常不会一次查询几十个结果,而使用 Aserto 等服务过滤这些结果在延迟方面是完全可行的。