带权限的RAG
Fine-grained access control with Retrieval Augmented Generation.
由于 pgvector 是建立在 Postgres 之上的,你可以使用 行级安全(RLS) 在向量数据库上实现细粒度的访问控制。这意味着你可以限制在向量相似度搜索中,只有有权限的用户才能看到特定的文档。Supabase 还支持 外部数据封装器(FDW),这意味着如果你的用户数据不在 Supabase 中,你也可以用外部数据库或数据源来确定这些权限。
🌐 Since pgvector is built on top of Postgres, you can implement fine-grained access control on your vector database using Row Level Security (RLS). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports Foreign Data Wrappers (FDW) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase.
使用本指南来学习在执行增强生成检索(RAG)时如何限制文档访问。
🌐 Use this guide to learn how to restrict access to documents when performing retrieval augmented generation (RAG).
示例 #
🌐 Example
在典型的 RAG 设置中,你的文档会被拆分成小块子部分,然后在这些部分上进行相似性匹配:
🌐 In a typical RAG setup, your documents are chunked into small subsections and similarity is performed over those sections:
1-- Track documents/pages/files/etc2create table documents (3 id bigint primary key generated always as identity,4 name text not null,5 owner_id uuid not null references auth.users (id) default auth.uid(),6 created_at timestamp with time zone not null default now()7);89-- Store the content and embedding vector for each section in the document10-- with a reference to original document (one-to-many)11create table document_sections (12 id bigint primary key generated always as identity,13 document_id bigint not null references documents (id),14 content text not null,15 embedding extensions.vector (384)16);注意每个文档上 owner_id 的记录。创建一个 RLS 策略,根据用户是否拥有相关文档来限制他们访问 document_sections:
🌐 Notice the record of owner_id on each document. Create an RLS policy that restricts access to document_sections based on whether or not they own the linked document:
1-- Grant the privileges the roles need2GRANT SELECT ON public.document_sections TO authenticated;34-- enable row level security5alter table document_sections enable row level security;67-- setup RLS for select operations8create policy "Users can query their own document sections"9on document_sections for select to authenticated using (10 document_id in (11 select id12 from documents13 where (owner_id = (select auth.uid()))14 )15);在这个例子中,当通过你项目自动生成的 REST API 执行查询时,会使用内置的 auth.uid() 函数来确定当前用户。如果你是通过直接的 Postgres 连接访问 Supabase 数据库,请参见下方的 直接 Postgres 连接 了解如何实现相同的访问控制。
🌐 In this example, the current user is determined using the built-in auth.uid() function when the query is executed through your project's auto-generated REST API. If you are connecting to your Supabase database through a direct Postgres connection, see Direct Postgres Connection below for directions on how to achieve the same access control.
现在,每个在 document_sections 上执行的 select 查询都会根据当前用户是否有权限访问这些部分,自动过滤返回的结果。
🌐 Now every select query executed on document_sections will implicitly filter the returned sections based on whether or not the current user has access to them.
例如,执行:
🌐 For example, executing:
1select * from document_sections;作为已认证用户,将只返回他们拥有的行(由关联文档确定)。更重要的是,对这些部分的语义搜索(或任何其他额外的筛选)仍将继续遵守这些行级安全策略(RLS):
🌐 as an authenticated user will only return rows that they are the owner of (as determined by the linked document). More importantly, semantic search over these sections (or any additional filtering for that matter) will continue to respect these RLS policies:
1-- Perform inner product similarity based on a match_threshold2select *3from document_sections4where document_sections.embedding <#> embedding < -match_threshold5order by document_sections.embedding <#> embedding;上面的例子只配置了用户对 select 的访问权限。如果你愿意,你可以为插入、更新和删除操作创建更多的 RLS 策略,以便将相同的权限逻辑应用到这些操作上。有关 RLS 策略的更详细指南,请参见 行级安全。
🌐 The above example only configures select access to users. If you wanted, you could create more RLS policies for inserts, updates, and deletes in order to apply the same permission logic for those other operations. See Row Level Security for a more in-depth guide on RLS policies.
备用方案 #
🌐 Alternative scenarios
每个应用都有自己独特的需求,可能会和上面的例子不同。这里是我们常见的一些替代场景,以及它们在 Supabase 中的实现方式。
🌐 Every app has its own unique requirements and may differ from the above example. Here are some alternative scenarios we often see and how they are implemented in Supabase.
多人拥有的文件 #
🌐 Documents owned by multiple people
与其在 users 和 documents 之间使用一对多的关系,你可能需要一个多对多的关系,这样多个人就可以访问同一个文档。用一个联接表重新实现它:
🌐 Instead of a one-to-many relationship between users and documents, you may require a many-to-many relationship so that multiple people can access the same document. Reimplement this using a join table:
1create table document_owners (2 id bigint primary key generated always as identity,3 owner_id uuid not null references auth.users (id) default auth.uid(),4 document_id bigint not null references documents (id)5);那么你的 RLS 策略将会变成:
🌐 Then your RLS policy would change to:
1create policy "Users can query their own document sections"2on document_sections for select to authenticated using (3 document_id in (4 select document_id5 from document_owners6 where (owner_id = (select auth.uid()))7 )8);我们没有直接查询 documents 表,而是查询了连接表。
🌐 Instead of directly querying the documents table, we query the join table.
用户和文档数据存储在 Supabase 之外 #
🌐 User and document data live outside of Supabase
你可能有一个现有的系统,在单独的数据库中存储用户、文档及其权限。考虑一下这种情况:这些数据存在另一个 Postgres 数据库中。我们将使用外部数据封装器(FDW)从你的 Supabase 数据库连接到外部数据库:
🌐 You may have an existing system that stores users, documents, and their permissions in a separate database. Consider the scenario where this data exists in another Postgres database. We'll use a foreign data wrapper (FDW) to connect to the external DB from within your Supabase DB:
RLS 对延迟很敏感,所以在实现这种方法之前应该格外小心。使用 查询计划分析器 测量你的查询执行时间,以确保它们在预期范围内。对于企业应用,请联系 enterprise@supabase.io。
🌐 RLS is latency-sensitive, so extra caution should be taken before implementing this method. Use the query plan analyzer to measure execution times for your queries to ensure they are within expected ranges. For enterprise applications, contact enterprise@supabase.io.
对于 Postgres 以外的数据源,请参见 Foreign Data Wrappers 了解目前支持的外部数据源列表。如果你的数据存在于列表中未提供的源中,请联系 support,我们很乐意讨论你的使用案例。
🌐 For data sources other than Postgres, see Foreign Data Wrappers for a list of external sources supported today. If your data lives in a source not provided in the list, contact support and we'll be happy to discuss your use case.
假设你的外部数据库包含像这样的 users 和 documents 表:
🌐 Assume your external DB contains a users and documents table like this:
1create table public.users (2 id bigint primary key generated always as identity,3 email text not null,4 created_at timestamp with time zone not null default now()5);67create table public.documents (8 id bigint primary key generated always as identity,9 name text not null,10 owner_id bigint not null references public.users (id),11 created_at timestamp with time zone not null default now()12);在你的 Supabase 数据库中,创建与上述表关联的外部表:
🌐 In your Supabase DB, create foreign tables that link to the above tables:
1create schema external;2create extension postgres_fdw with schema extensions;34-- Setup the foreign server5create server foreign_server6 foreign data wrapper postgres_fdw7 options (host '<db-host>', port '<db-port>', dbname '<db-name>');89-- Map local 'authenticated' role to external 'postgres' user10create user mapping for authenticated11 server foreign_server12 options (user 'postgres', password '<user-password>');1314-- Import foreign 'users' and 'documents' tables into 'external' schema15import foreign schema public limit to (users, documents)16 from server foreign_server into external;这个例子把 Supabase 中的 authenticated 角色映射到外部数据库中的 postgres 用户。在生产环境中,最好在外部数据库上创建一个自定义用户,该用户只具有访问所需信息的最低权限。
🌐 This example maps the authenticated role in Supabase to the postgres user in the external DB. In production, it's best to create a custom user on the external DB that has the minimum permissions necessary to access the information you need.
在 Supabase 数据库中,我们使用内置的 authenticated 角色,当终端用户通过自动生成的 REST API 发起认证请求时会自动使用。如果你打算通过直接的 Postgres 连接而不是 REST API 连接 Supabase 数据库,你可以把它改成任何你喜欢的用户。更多信息请参见 直接 Postgres 连接。
🌐 On the Supabase DB, we use the built-in authenticated role which is automatically used when end users make authenticated requests over your auto-generated REST API. If you plan to connect to your Supabase DB over a direct Postgres connection instead of the REST API, you can change this to any user you like. See Direct Postgres Connection for more info.
我们会把 document_sections 及其嵌入存储在 Supabase 中,这样我们就可以通过 pgvector 对它们进行相似性搜索。
🌐 We'll store document_sections and their embeddings in Supabase so that we can perform similarity search over them via pgvector.
1create table document_sections (2 id bigint primary key generated always as identity,3 document_id bigint not null,4 content text not null,5 embedding extensions.vector (384)6);我们通过 document_id 保留对外部文档的引用,但不使用外键引用,因为外键只能添加到本地表中。记得使用和你的外部文档表相同的 ID 数据类型。
🌐 We maintain a reference to the foreign document via document_id, but without a foreign key reference since foreign keys can only be added to local tables. Be sure to use the same ID data type that you use on your external documents table.
既然我们在 Supabase 之外管理用户和身份验证,我们有两个选择:
🌐 Since we're managing users and authentication outside of Supabase, we have two options:
- 直接连接到 Supabase 数据库的 Postgres,并在每次请求时设置当前用户
- 从你的系统生成一个自定义 JWT,并用它来通过 REST API 进行认证
直接连接Postgres #
🌐 Direct Postgres connection
你可以直接使用项目页面上的连接信息连接到你的 Supabase Postgres 数据库。要用这种方法使用 RLS,我们会使用一个包含当前用户 ID 的自定义会话变量:
🌐 You can directly connect to your Supabase Postgres DB using the connection info on a project page. To use RLS with this method, we use a custom session variable that contains the current user's ID:
1-- enable row level security2alter table document_sections enable row level security;34-- setup RLS for select operations5create policy "Users can query their own document sections"6on document_sections for select to authenticated using (7 document_id in (8 select id9 from external.documents10 where owner_id = current_setting('app.current_user_id')::bigint11 )12);会话变量是通过 current_setting() 函数访问的。我们这里将变量命名为 app.current_user_id,但你可以修改成任何你喜欢的名字。我们还将它转换为 bigint 类型,因为那是 user.id 列的数据类型。根据你使用的 ID 类型,将其改成相应的数据类型即可。
🌐 The session variable is accessed through the current_setting() function. We name the variable app.current_user_id here, but you can modify this to any name you like. We also cast it to a bigint since that was the data type of the user.id column. Change this to whatever data type you use for your ID.
现在对于每个请求,我们在会话开始时设置用户的 ID:
🌐 Now for every request, we set the user's ID at the beginning of the session:
1set app.current_user_id = '<current-user-id>';那么之后的所有查询都会继承该用户的权限:
🌐 Then all subsequent queries will inherit the permission of that user:
1-- Only document sections owned by the user are returned2select *3from document_sections4where document_sections.embedding <#> embedding < -match_threshold5order by document_sections.embedding <#> embedding;你可能会想完全放弃 RLS,然后在 where 子句中按用户进行过滤。虽然这样也能工作,但我们建议把 RLS 作为一种通用的最佳实践,因为即使将来引入新的查询和应用逻辑,RLS 仍然会被应用。
🌐 You might be tempted to discard RLS completely and filter by user within the where clause. Though this will work, we recommend RLS as a general best practice since RLS is always applied even as new queries and application logic is introduced in the future.
使用自定义 JWT 的 REST API #
🌐 Custom JWT with REST API
如果你想使用自动生成的 REST API,通过外部认证提供商的 JWT 来查询你的 Supabase 数据库,你可以让你的认证提供商为 Supabase 签发一个自定义的 JWT。
🌐 If you would like to use the auto-generated REST API to query your Supabase database using JWTs from an external auth provider, you can get your auth provider to issue a custom JWT for Supabase.
查看 Clerk Supabase 文档 了解如何操作的示例。根据需要修改这些说明以适应你自己的认证提供商。
🌐 See the Clerk Supabase docs for an example of how this can be done. Modify the instructions to work with your own auth provider as needed.
现在我们可以使用第一个例子中的相同 RLS 策略了:
🌐 Now we can use the same RLS policy from our first example:
1-- enable row level security2alter table document_sections enable row level security;34-- setup RLS for select operations5create policy "Users can query their own document sections"6on document_sections for select to authenticated using (7 document_id in (8 select id9 from documents10 where (owner_id = (select auth.uid()))11 )12);在底层,auth.uid() 引用 current_setting('request.jwt.claim.sub'),它对应于 JWT 的 sub(主题)声明。这个设置会在每次请求 REST API 开始时自动设置。
🌐 Under the hood, auth.uid() references current_setting('request.jwt.claim.sub') which corresponds to the JWT's sub (subject) claim. This setting is automatically set at the beginning of each request to the REST API.
之后的所有查询都会继承该用户的权限:
🌐 All subsequent queries will inherit the permission of that user:
1-- Only document sections owned by the user are returned2select *3from document_sections4where document_sections.embedding <#> embedding < -match_threshold5order by document_sections.embedding <#> embedding;其他情景 #
🌐 Other scenarios
针对这个问题有无数种方法,取决于每个系统的复杂性。幸运的是,Postgres 自带了所有提供访问控制所需的基础功能,你可以根据自己的项目需求来使用。
🌐 There are endless approaches to this problem based on the complexities of each system. Luckily Postgres comes with all the primitives needed to provide access control in the way that works best for your project.
如果上面的示例不适合你的使用情况,或者你需要稍微调整它们以更好地适应你现有的系统,随时可以联系 支持,我们很乐意为你提供帮助。
🌐 If the examples above didn't fit your use case or you need to adjust them slightly to better fit your existing system, feel free to reach out to support and we'll be happy to assist you.