行级别安全性

当您需要更精细的授权规则时,PostgreSQL的行级安全(RLS)是很好的选择。

策略是PostgreSQL的规则引擎。它们非常强大和灵活,允许你编写复杂的SQL规则,以满足你独特的业务需求。

策略

每个数据库表可以添加一个或多个策略(policy),每当表被访问(增,删,改、查等操作)时,都会执行(校验)设置的策略,就好比为每个sql语句添加了一个where的查询条件。以下是一个策略的例子:

1create policy "Individuals can view their own todos."
2    on todos for select
3    using ( auth.uid() = user_id );

上面的策略在执行sql查询时被转化成以下语句(每当用户查询todos这张表时,只会查询包含自己ID的记录):

1select *
2from todos
3where auth.uid() = todos.user_id; -- Policy is implicitly added.

辅助函数

Supabase为您提供了一些简单的功能,您可以在策略中使用这些功能。

auth.uid()#

返回当前前端请求的用户id值。

auth.jwt()#

返回当前前端请求的用户jwt值。

示例

下面是一些示例,向您展示PostgreSQL的RLS的强大功能。

允许读取访问

1-- 1. Create table
2create table profiles (
3  id uuid references auth.users,
4  avatar_url text
5);
6
7-- 2. Enable RLS
8alter table profiles
9  enable row level security;
10
11-- 3. Create Policy
12create policy "Public profiles are viewable by everyone."
13  on profiles for select using (
14    true
15  );
  1. 在pulic模式(Postgre默认模式)中创建名为profiles的表。
  2. 为该表开启行级别安全。
  3. 创建一个策略,允许所有用户查询(select)该表的数据。

限制更新

1-- 1. Create table
2create table profiles (
3  id uuid references auth.users,
4  avatar_url text
5);
6
7-- 2. Enable RLS
8alter table profiles
9  enable row level security;
10
11-- 3. Create Policy
12create policy "Users can update their own profiles."
13  on profiles for update using (
14    auth.uid() = id
15  );
  1. 在pulic模式(Postgre默认模式)中创建名为profiles的表。
  2. 为该表开启行级别安全。
  3. 创建一个策略,仅允许当前登录用户更新(update)自己的数据。

仅限匿名或已认证的有权访问

您可以添加Postgres角色

1create policy "Public profiles are viewable by everyone."
2on profiles for select
3to authenticated, anon
4using (
5  true
6);

连接相关的策略

策略甚至可以包括表联接。此示例显示了如何查询外部表以构建更高级的规则。

1create table teams (
2  id serial primary key,
3  name text
4);
5
6-- 2. Create many to many join
7create table members (
8  team_id bigint references teams,
9  user_id uuid references auth.users
10);
11
12-- 3. Enable RLS
13alter table teams
14  enable row level security;
15
16-- 4. Create Policy
17create policy "Team members can update team details if they belong to the team."
18  on teams
19  for update using (
20    auth.uid() in (
21      select user_id from members
22      where team_id = id
23    )
24  );

注意:如果还为members启用了RLS,则用户还必须具有对members_1的读取(_select)权限。否则,连接的查询将不会产生任何结果。

具有安全定义功能的策略

策略还可以使用安全定义功能。这在您希望限制对链接表的访问的多对多关系中非常有用。在上面的 teamsmembers示例之后,本示例显示了如何将安全定义功能与策略结合使用,以控制对 member表的访问。

1-- 1. Follow example for 'Policies with joins' above
2
3-- 2.  Enable RLS
4alter table members
5  enable row level security
6
7-- 3.  Create security definer function
8create or replace function get_teams_for_authenticated_user()
9returns setof bigint
10language sql
11security definer
12set search_path = public
13stable
14as $$
15    select team_id
16    from members
17    where user_id = auth.uid()
18$$;
19
20-- 4. Create Policy
21create policy "Team members can update team members if they belong to the team."
22  on members
23  for all using (
24    team_id in (
25      select get_teams_for_authenticated_user()
26    )
27  );

验证电子邮件域

Postgres有一个函数right(string,n),它返回字符串中最右边的n个字符。 您可以使用它来匹配员工的电子邮件域。

1-- 1. Create table
2create table leaderboard (
3  id uuid references auth.users,
4  high_score bigint
5);
6
7-- 2. Enable RLS
8alter table leaderboard
9  enable row level security;
10
11-- 3. Create Policy
12create policy "Only Blizzard staff can update leaderboard"
13  on leaderboard
14  for update using (
15    right(auth.jwt() ->> 'email', 13) = '@blizzard.com'
16  );

记录的有效期

策略也可以用于实现TTL或实时功能,您可以在Instagram故事或Snapchat中看到。 在下面的示例中,只有在过去24小时内创建了stories表的行时,这些行才可用。

策略可以用于实现资源有效期限制TTL(time to live)功能,在微信中也有类似的功能,例如朋友圈仅三天可见

以下实例实现了此功能,表stories中只可以查询到过去24小时内的记录,超过24小时的记录将查看不到。

1-- 1. Create table
2create table if not exists stories (
3  id uuid not null primary key DEFAULT uuid_generate_v4(),
4  created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
5  content text not null
6);
7
8-- 2. Enable RLS
9alter table stories
10  enable row level security;
11
12-- 3. Create Policy
13create policy "Stories are live for a day"
14  on stories
15  for select using (
16    created_at > (current_timestamp - interval '1 day')
17  );

高级策略

Supabase 的高级策略功能允许您使用 SQL 的全部功能来创建复杂的规则。 在上述例子中,我们使用了两个表,即帖子表和评论表(postscomments)。并且我们创建了一个评论策略,这个策略依赖于帖子策略。这意味着在执行评论策略之前,系统会检查帖子策略是否允许进行评论操作。这种依赖关系可以帮助您构建更复杂、更灵活的数据访问控制规则。

1create table posts (
2  id            serial    primary key,
3  creator_id    uuid      not null     references auth.users(id),
4  title         text      not null,
5  body          text      not null,
6  publish_date  date      not null     default now(),
7  audience      uuid[]    null -- many to many table omitted for brevity
8);
9
10create table comments (
11  id            serial    primary key,
12  post_id       int       not null     references posts(id)  on delete cascade,
13  user_id       uuid      not null     references auth.users(id),
14  body          text      not null,
15  comment_date  date      not null     default now()
16);
17
18create policy "Creator can see their own posts"
19on posts
20for select
21using (
22  auth.uid() = posts.creator_id
23);
24
25create policy "Logged in users can see the posts if they belong to the post 'audience'."
26on posts
27for select
28using (
29  auth.uid() = any (posts.audience)
30);
31
32create policy "Users can see all comments for posts they have access to."
33on comments
34for select
35using (
36  exists (
37    select 1 from posts
38    where posts.id = comments.post_id
39  )
40);

提示

为数据库表启用Realtime功能#

实时服务器根据行级别安全(RLS)策略向授权用户广播数据库更改。 建议您对添加到发布中的表启用行级别安全性并设置行安全策略。 但是,您可以选择禁用表上的RLS,并将更改广播到所有连接的客户端。

1/**
2 * REALTIME SUBSCRIPTIONS
3 * Realtime enables listening to any table in your public schema.
4 */
5
6begin;
7  -- remove the realtime publication
8  drop publication if exists supabase_realtime;
9
10  -- re-create the publication but don't enable it for any tables
11  create publication supabase_realtime;
12commit;
13
14-- add a table to the publication
15alter publication supabase_realtime add table products;
16
17-- add other tables to the publication
18alter publication supabase_realtime add table posts;

你并不一定要使用策略

您也可以将授权规则放在中间件中,类似于使用任何其他后端<->中间件<->前端架构,在此架构中创建安全规则的方式。

策略是一种工具。在“serverless/Jamstack”设置的情况下,它们特别有效,因为您根本不必部署任何中间件。

然而,如果您想为应用程序使用另一种授权方法,这也可以。MemFireCloud只是“普通的Postgres”,所以如果您的应用程序 与Postgres一起工作,那么它也可以与MemFireCloud一起工作。

如果你打算使用这种方法,请确保为你的表启用RLS(行级安全性)。然后使用service_role密钥(对于我们的客户端库)或postgres角色 - 这两者都可以绕过RLS。 使用这种方法,你无需创建任何策略,仅启用RLS就足够了:"

1create table profiles (
2  id serial primary key,
3  email text
4);
5
6alter table profiles enable row level security;

切勿在客户端上使用服务密钥

MemFireCloud提供特殊的服务密钥(service_key),它可以绕过所有RLS。 而这个服务密钥不应在浏览器中使用或向客户公开,但它们对于管理任务非常有用。

caution

如果设置了用户令牌使用服务密钥初始化客户端,不会覆盖行级安全(RLS)。如果用户使用客户端登录,MemFireCloud将遵循该用户的行级安全策略。

测试相关的策略

MemFireCloud提供了一些辅助SQL过程,允许您在数据库中直接测试数据访问策略,而无需通过前端界面并模拟不同用户登录来进行测试。这些辅助过程可以帮助您更方便地验证和调试数据库的策略设置。

1grant anon, authenticated to postgres;
2
3create or replace procedure auth.login_as_user (user_email text)
4    language plpgsql
5    as $$
6declare
7    auth_user auth.users;
8begin
9    select
10        * into auth_user
11    from
12        auth.users
13    where
14        email = user_email;
15    execute format('set request.jwt.claim.sub=%L', (auth_user).id::text);
16    execute format('set request.jwt.claim.role=%I', (auth_user).role);
17    execute format('set request.jwt.claim.email=%L', (auth_user).email);
18    execute format('set request.jwt.claims=%L', json_strip_nulls(json_build_object('app_metadata', (auth_user).raw_app_meta_data))::text);
19
20    raise notice '%', format( 'set role %I; -- logging in as %L (%L)', (auth_user).role, (auth_user).id, (auth_user).email);
21    execute format('set role %I', (auth_user).role);
22end;
23$$;
24
25create or replace procedure auth.login_as_anon ()
26    language plpgsql
27    as $$
28begin
29    set request.jwt.claim.sub='';
30    set request.jwt.claim.role='';
31    set request.jwt.claim.email='';
32    set request.jwt.claims='';
33    set role anon;
34end;
35$$;
36
37create or replace procedure auth.logout ()
38    language plpgsql
39    as $$
40begin
41    set request.jwt.claim.sub='';
42    set request.jwt.claim.role='';
43    set request.jwt.claim.email='';
44    set request.jwt.claims='';
45    set role postgres;
46end;
47$$;

要切换到指定用户(通过电子邮件),使用 call auth.login_as_user('my@email.com');。您也可以切换到匿名角色,使用 call auth.login_as_anon();。 完成后,使用 call auth.logout(); 将自己切换回 postgres 角色。

这些过程也可以用于编写针对策略的 pgTAP 单元测试。

查看psql使用此功能的交互示例。

该示例表明,在教程示例中,public.profiles 表确实可以由postgres角色和行的所有者进行更新,但无法通过匿名连接进行更新。

1
2postgres=> select id, email from auth.users;
3                  id                  |       email
4--------------------------------------+-------------------
5 d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | user1@example.com
6 15d6811a-16ee-4fa2-9b18-b63085688be4 | user2@example.com
7 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | user3@example.com
8(3 rows)
9
10postgres=> table public.profiles;
11                  id                  | updated_at | username | full_name | avatar_url | website
12--------------------------------------+------------+----------+-----------+------------+---------
13 d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 |            | user1    | User 1    |            |
14 15d6811a-16ee-4fa2-9b18-b63085688be4 |            | user2    | User 2    |            |
15 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 |            | user3    | User 3    |            |
16(3 rows)
17
18postgres=> call auth.login_as_anon();
19CALL
20postgres=> update public.profiles set updated_at=now();
21UPDATE 0 -- anon users cannot update any profile but see all of them
22postgres=> table public.profiles;
23                  id                  | updated_at | username | full_name | avatar_url | website
24--------------------------------------+------------+----------+-----------+------------+---------
25 d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 |            | user1    | User 1    |            |
26 15d6811a-16ee-4fa2-9b18-b63085688be4 |            | user2    | User 2    |            |
27 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 |            | user3    | User 3    |            |
28(3 rows)
29
30postgres=> call auth.logout();
31CALL
32postgres=> call auth.login_as_user('user1@example.com');
33NOTICE:  set role authenticated; -- logging in as 'd4f0aa86-e6f6-41d1-bd32-391f077cf1b9' ('user1@example.com')
34CALL
35postgres=> update public.profiles set updated_at=now();
36UPDATE 1 -- authenticated users can update their own profile and see all of them
37postgres=> table public.profiles;
38                  id                  |          updated_at           | username | full_name | avatar_url | website
39--------------------------------------+-------------------------------+----------+-----------+------------+---------
40 15d6811a-16ee-4fa2-9b18-b63085688be4 |                               | user1    | User 1    |            |
41 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 |                               | user2    | User 2    |            |
42 d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:39:16.204612+00 | user3    | User 3    |            |
43(3 rows)
44
45postgres=> call auth.logout();
46CALL
47postgres=> update public.profiles set updated_at=now();
48UPDATE 3 -- the 'postgres' role can update and see all profiles
49postgres=> table public.profiles;
50                  id                  |          updated_at           | username | full_name | avatar_url | website
51--------------------------------------+-------------------------------+----------+-----------+------------+---------
52 15d6811a-16ee-4fa2-9b18-b63085688be4 | 2023-02-18 21:40:08.216324+00 | user1    | User 1    |            |
53 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | 2023-02-18 21:40:08.216324+00 | user2    | User 2    |            |
54 d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:40:08.216324+00 | user3    | User 3    |            |
55(3 rows)

已经弃用的功能

我们已经弃用了一些功能,以确保RLS策略具有更好的性能和可扩展性。

auth.role()

caution

auth.role() 函数已被弃用,推荐使用 Postgres 中原生支持的 TO 字段。

1-- DEPRECATED
2create policy "Public profiles are viewable by everyone."
3on profiles for select using (
4  auth.role() = 'authenticated' or auth.role() = 'anon'
5);
6
7-- RECOMMENDED
8create policy "Public profiles are viewable by everyone."
9on profiles for select
10to authenticated, anon
11using (
12  true
13);

auth.email()

caution

auth.email() 函数已被弃用,转而使用 auth.jwt() ->> 'email'

返回提出请求的用户的电子邮件。