微信扫码
与创始人交个朋友
我要投稿
RAG 的访问控制是 RAG 在落地过程中一个很重要的环节,这篇来自 Pinecone 的文章介绍了基于 ReBAC 模型如何实现访问控制,以及当登陆不同用户时,相同问题给出不同的问答效果。enjoy~
访问控制在几乎所有面向用户的应用程序中都起着至关重要的作用,尤其是在涉及敏感信息的情况下。它在保护敏感信息方面发挥着重要作用,确保只有获得所需权限的人才能接触到这些信息。
在 RAG 应用程序中,很可能不是每个用户都能平等地访问所有索引文档。某些信息可能具有机密性,只有特定角色或指定人员才能访问--我们的 RAG 应用程序不能泄露任何敏感信息,这一点至关重要。这就凸显了强大而有效的访问控制机制的必要性。通过防止未经授权的访问,这些机制有助于保持数据的机密性。
在 Pinecone 中,您可以使用元数据将用户和角色与特定向量关联起来,但这并不符合关注点分离的核心架构原则:如果可以避免的话,系统不应过多地考虑 "外来 "因素。正如我们将在这篇文章中探讨的那样,RAG 用例非常适合查询后过滤流程,而且这种流程适合在数据库本身之外管理授权机制。
在具体实施 RAG 应用程序的访问控制之前,我们先来谈谈访问控制的组成部分,即身份验证和授权。
既然我们已经对身份验证和授权有了很好的了解,那么让我们来看看如何在 RAG 应用程序中应用它们。粗略地说,我们可以把任何 RAG 系统分成两个阶段:摄取和查询。
为了演示 RAG 应用程序的基本授权流程,我们将应用 ReBAC 模型,原因如下:RBAC 和 ABAC 主要针对端点等应用资源的管理。例如,当我们构建一个应用程序时,我们希望确定哪个用户或角色可以执行映射到端点的特定操作。就 RAG 而言,授权的主要模式是在执行查询后过滤结果。这就要求我们在用户或角色与资源之间创建关系。
实际上,对于一组文档,我们将定义一组可能的类别。我们将为系统中的用户分配这些类别,并只允许他们访问属于其分配类别的文档。
我们将使用上一篇文章中介绍过的 RAG 应用程序 [1],并将其扩展为具有身份验证和授权功能(完整代码列表[2])。
为了方便身份验证和授权过程,我们将使用两种服务:
首先,我们注册一个 Clerk 账户并创建一组用户:
我们要在这里标记 "Admin "用户拥有一些额外权限,因此我们要在用户条目中添加一些私人元数据:
在本例中,管理员将是能够为特定用户分配类别的用户。在更现实的情况下,我们可能会希望将类别与角色或组关联起来,而不是与用户关联起来,这一点稍后再详述。
管理员用户登录应用程序后,将看到以下控件:
这样,他们就可以选择将哪些类别分配给每个用户。
接下来,我们需要将每个用户的身份与我们要索引的文档关联起来。我们将使用 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
# object type definitions
types:
# user represents a user that can be granted role(s)
user:
relations:
manager: user
# group represents a collection of users and/or (nested) groups
group:
relations:
member: user | group#member
# identity represents a collection of identities for users
identity:
relations:
identifier: user
# resource creator represents a user type that can create new resources
resource-creator:
relations:
member: user | group#member
permissions:
can_create_resource: member
# resource represents a protected resource
resource:
relations:
owner: user
writer: user | group#member
reader: user | group#member
permissions:
can_read: reader | writer | owner
can_write: writer | owner
can_delete: owner
下面是该清单文件的可视化展示:
当使用 Aserto 创建一个新项目时,我们可以从这个初始模型开始,它可以在各种用例中发挥作用。自然,模型可以随着我们应用程序的变化和发展而发展。
与其他 RAG 导入流程一样,我们对每个文档进行迭代、嵌入并上载到 Pinecone 中。但在我们的案例中,我们还会将文档与其类别关联起来,并根据从应用程序中获取的映射,在系统中创建文档与用户之间的关系。
const index = pinecone.Index(indexName)
const vectors = await Promise.all(documents.flat().map(embedDocument));
await Promise.all(Object.keys(usersDataAssignment).map(async (userId) => {
const userVectors = filterRecordsByUserAssignments(userId, vectors, usersDataAssignment)
const userObject = await clerkClient.users.getUser(userId);
return assignRelation(userObject, userVectors, 'owner');
}));
// Upsert vectors into the Pinecone index
await chunkedUpsert(index, vectors, '', 10);
在这段代码中,我们遍历 userDataAssignment,它将系统中的每个用户与其允许查看的类别关联起来。我们会过滤我们创建的所有向量,并将这些向量与相应的用户关联起来。
让我们来看看在用户和文档之间创建关系的函数:
export const assignRelation = async (user: User, documents: PineconeRecord<CategorizedRecordMetadata>[], relationName: string) => {
// Map each document to a set of operations for setting up user-document relations
const operations = documents.map((document) => {
// Construct a display name for the user
const userName = `${user.firstName}${user.lastName ? ' ' : ''}${user.lastName ?? ''}`
// Create a user object for the directory service
const userObject = {
id: user.id,
type: 'user',
properties: objectPropertiesAsStruct({
email: user.emailAddresses[0].emailAddress,
name: userName,
picture: user.imageUrl,
}),
displayName: userName
};
// Create a document object for the directory service
const documentObject = {
id: document.id,
type: 'resource',
properties: document.metadata ? objectPropertiesAsStruct({
url: document.metadata.url,
category: document.metadata.category,
}) : objectPropertiesAsStruct({}),
displayName: document.metadata && document.metadata.title ? document.metadata.title as string : '',
};
// Define the relation between the user and the document
const userDocumentRelation = {
subjectId: user.id,
subjectType: 'user',
objectId: document.id,
objectType: 'resource',
relation: relationName,
};
// Operations to set the user and document objects in the directory
const objectOperations: any[] = [
{
opCode: ImportOpCode.SET,
msg: {
case: ImportMsgCase.OBJECT,
value: userObject,
},
},
{
opCode: ImportOpCode.SET,
msg: {
case: ImportMsgCase.OBJECT,
value: documentObject,
},
}
];
// Operation to set the relation between the user and the document
const relationOperation: any = {
opCode: ImportOpCode.SET,
msg: {
case: ImportMsgCase.RELATION,
value: userDocumentRelation,
},
};
// Combine object and relation operations
return [...objectOperations, relationOperation];
}).flat();
try {
// Create an async iterable from the operations and import them to the directory service
const importRequest = createAsyncIterable(operations);
const resp = await directoryClient.import(importRequest);
// Read and return the result of the import operation
const result = await (readAsyncIterable(resp))
return result
} catch (error) {
// Log and rethrow any errors encountered during the import
console.error('Error importing request: ', error);
throw error;
}
}
为了加强用户和他们可以访问的文档之间的关系,我们创建了一个函数,该函数将针对特定权限的目录执行 checkPermission 调用:
export const getFilteredMatches = async (user: User | null, matches: ScoredPineconeRecord[], permission: Permission) => {
// Check if a user object is provided
if (!user) {
console.error('No user provided. Returning empty array.')
return [];
}
// Perform permission checks for each match concurrently
const checks = await Promise.all(matches.map(async (match) => {
// Construct permission request object
const permissionRequest = {
subjectId: user.id, // ID of the user requesting access
subjectType: 'user', // Type of the subject requesting access
objectId: match.id, // ID of the object access is requested for
objectType: 'resource', // Type of the object access is requested for
permission: 'can_read', // Specific permission being checked
}
// Check permission for the constructed request
const response = await directoryClient.checkPermission(permissionRequest);
// Return true if permission granted, false otherwise
return response ? response.check : false
}));
// Filter matches where permission check passed
const filteredMatches = matches.filter((match, index) => checks[index]);
// Return matches that passed the permission check
return filteredMatches
}
对每个主体、客体和权限三重进行权限检查只需几毫秒的时间,从而确保了应用程序的整体性能。
该函数在 getContext 函数中被调用,getContext 会检索相关文档,然后根据我们设置的权限筛选匹配文档。
export const getContext = async ({ message, namespace, maxTokens = 3000, minScore = 0.95, getOnlyText = true, user }:
{ message: string, namespace: string, maxTokens?: number, minScore?: number, getOnlyText?: boolean, user: User | null }): Promise<ContextResponse> => {
// Get the embeddings of the input message
const embedding = await getEmbeddings(message);
// Retrieve the matches for the embeddings from the specified namespace
const matches = await getMatchesFromEmbeddings(embedding, 10, namespace);
// Filter out the matches that have a score lower than the minimum score
const qualifyingDocs = matches.filter(m => m.score && m.score > minScore);
let noMatches = qualifyingDocs.length === 0;
const filteredMatches = await getFilteredMatches(user, qualifyingDocs, Permission.READ);
let accessNotice = false
if (filteredMatches.length < matches.length) {
accessNotice = true
}
return {
documents: qualifyingDocs,
accessNotice,
noMatches
}
}
我们希望确保用户能意识到,万一系统中存在的某些文档对他们来说是不可用的。为此,我们会将找到的总匹配数与筛选出的匹配数进行比较。如果完全没有找到匹配,我们也会记录下来。
最后,我们只需将信息发送回客户端。我们将把访问信息作为上下文的一部分附加到发回的数据中:
const { documents, accessNotice, noMatches } = context;
data.append({ context: [...documents as PineconeRecord[]],
accessNotice,
noMatches });
现在我们准备测试授权机制。在我们的应用程序中,我们将使用与财务类别相关联的身份登录,因为它与财务部门有关:
我们将提出同样的问题,但与人力资源类别相关的用户不应该访问与财务相关的主题:
不出所料,我们得到的提示是,有些结果无法用于编写问题,而我们得到的内容实际上与我们的问题主题无关。
在 RAG 应用程序领域,访问控制机制的重要性不容低估。管理敏感信息的任务非同小可,而 Aserto 和 Clerk 等服务可以让这一过程变得更加轻松。数据库的主流授权模式是在执行查询后过滤结果。在某些情况下,这可能并不可行,但在 RAG 使用案例中,这实际上是非常合适的:我们通常不会一次查询几十个结果,而使用 Aserto 等服务过滤这些结果在延迟方面是完全可行的。
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-03-30
2024-04-26
2024-05-10
2024-04-12
2024-05-28
2024-04-25
2024-05-14
2024-07-18
2024-08-13
2024-04-26