为什么行级安全性RLS不使用索引?

问题描述

我已经向患者和治疗师提出了申请。它们都在同一个users表中。患者应该能够看到他们的治疗师,而治疗师应该能够看到他们的病人。

我已经建立了一个具有成对的用户ID的实例化视图(user_access_pairs),如果两个用户在该视图中有一行,则意味着嘿应该可以相互访问。

database> \d user_access_pairs
+----------+---------+-------------+
| Column   | Type    | Modifiers   |
|----------+---------+-------------|
| id1      | integer |             |
| id2      | integer |             |
+----------+---------+-------------+
Indexes:
    "index_user_access_pairs" UNIQUE,btree (id1,id2)

这是users表的定义,它有一堆更多的列,这些列与该问题无关。

database> \d users
+-----------------------------+-----------------------------+-----------------------------------------------------+
| Column                      | Type                        | Modifiers                                           |
|-----------------------------+-----------------------------+-----------------------------------------------------|
| id                          | integer                     |  not null default nextval('users_id_seq'::regclass) |
| first_name                  | character varying(255)      |                                                     |
| last_name                   | character varying(255)      |                                                     |
+-----------------------------+-----------------------------+-----------------------------------------------------+
Indexes:
    "users_pkey" PRIMARY KEY,btree (id)

我创建了一个RLS策略,该策略限制了使用jwt令牌的用户可以读取哪些users

create policy select_users_policy
  on public.users
  for select using (
    (current_setting('jwt.claims.user_id'::text,true)::integer,id) in (
      select id1,id2 from user_access_pairs
    )
  );

这似乎在逻辑上起作用,但是我的性能变糟。尽管有索引,查询计划者仍对user_access_pairs进行顺序扫描。

database> set jwt.claims.user_id to '2222';
database> explain analyze verbose
    select first_name,last_name
    from users
+------------------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN                                                                                                                         |
|------------------------------------------------------------------------------------------------------------------------------------|
| Seq Scan on public.users  (cost=231.84..547.19 rows=2386 width=14) (actual time=5.481..6.418 rows=2 loops=1)                       |
|   Output: users.first_name,users.last_name                                                                                        |
|   Filter: (hashed SubPlan 1)                                                                                                       |
|   Rows Removed by Filter: 4769                                                                                                     |
|   SubPlan 1                                                                                                                        |
|     ->  Seq Scan on public.user_access_pairs  (cost=0.00..197.67 rows=13667 width=8) (actual time=0.005..1.107 rows=13667 loops=1) |
|           Output: user_access_pairs.id1,user_access_pairs.id2                                                                     |
| Planning Time: 0.072 ms                                                                                                            |
| Execution Time: 6.521 ms                                                                                                           |
+------------------------------------------------------------------------------------------------------------------------------------+

但是,如果我切换到绕过RLS的超级用户角色并手动应用相同的过滤器,则性能会好得多。不是同一回事吗?

database> set jwt.claims.user_id to '2222';
database> explain analyze verbose
   select first_name,last_name
   from users
   where (current_setting('jwt.claims.user_id'::text,id) in (
     select id1,id2 from user_access_pairs
   )
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| QUERY PLAN
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Nested Loop  (cost=4.59..27.86 rows=2 width=14) (actual time=0.041..0.057 rows=2 loops=1)
|   Output: users.first_name,users.last_name
|   Inner Unique: true
|   ->  Bitmap Heap Scan on public.user_access_pairs  (cost=4.31..11.26 rows=2 width=4) (actual time=0.029..0.036 rows=2 loops=1)
|         Output: user_access_pairs.id1,user_access_pairs.id2
|         Filter: ((current_setting('jwt.claims.user_id'::text,true))::integer = user_access_pairs.id1)
|         Heap Blocks: exact=2
|         ->  Bitmap Index Scan on index_user_access_pairs  (cost=0.00..4.31 rows=2 width=0) (actual time=0.018..0.018 rows=2 loops=1)
|               Index Cond: (user_access_pairs.id1 = (current_setting('jwt.claims.user_id'::text,true))::integer)
|   ->  Index Scan using users_pkey on public.users  (cost=0.28..8.30 rows=1 width=18) (actual time=0.008..0.008 rows=1 loops=2)
|         Output: users.id,users.email,users.encrypted_password,users.first_name,users.last_name,users.roles_mask,users.reset_password_token,users.reset_password_sent_at,users.remember_created_at,users.sign_in_count,users.current_sign_in_at,users.last_sign_in_at,|         Index Cond: (users.id = user_access_pairs.id2)
| Planning Time: 0.526 ms
| Execution Time: 0.116 ms
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

为什么在查询时RLS不使用索引?

PS 我正在使用PostgreSQL 12.4版

database> select version()
+-------------------------------------------------------------------------------------------------------------------------------+
| version                                                                                                                       |
|-------------------------------------------------------------------------------------------------------------------------------|
| PostgreSQL 12.4 (Ubuntu 12.4-0ubuntu0.20.04.1) on x86_64-pc-linux-gnu,compiled by gcc (Ubuntu 9.3.0-10ubuntu2) 9.3.0,64-bit |
+-------------------------------------------------------------------------------------------------------------------------------+

编辑

感谢您的回应Laurenz。它大大提高了性能。 但是我仍然要进行一些序列扫描。

这是劳伦兹(Laurenz)建议的更新政策。

create policy select_users_policy
  on public.users
  for select using (
    exists (
      select 1
      from user_access_pairs
      where
        id1 = current_setting('jwt.claims.user_id'::text,true)::integer
        and id2 = users.id
    )
  );

即使策略中的users查询正在使用索引,使用RLS查询此表仍然可以对exists表进行seq扫描。

database> set jwt.claims.user_id to '2222';
database> explain analyze verbose
  select first_name,last_name
  from users
+-------------------------------------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN                                                                                                                                            |
|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| Seq Scan on public.users  (cost=0.00..40048.81 rows=2394 width=14) (actual time=0.637..1.216 rows=2 loops=1)                                          |
|   Output: users.first_name,users.last_name                                                                                                           |
|   Filter: (alternatives: SubPlan 1 or hashed SubPlan 2)                                                                                               |
|   Rows Removed by Filter: 4785                                                                                                                        |
|   SubPlan 1                                                                                                                                           |
|     ->  Index Only Scan using index_user_access_pairs on public.user_access_pairs  (cost=0.29..8.31 rows=1 width=0) (never executed)                  |
|           Index Cond: ((user_access_pairs.id1 = (current_setting('jwt.claims.user_id'::text,true))::integer) AND (user_access_pairs.id2 = users.id)) |
|           Heap Fetches: 0                                                                                                                             |
|   SubPlan 2                                                                                                                                           |
|     ->  Bitmap Heap Scan on public.user_access_pairs user_access_pairs_1  (cost=4.31..11.26 rows=2 width=4) (actual time=0.075..0.083 rows=2 loops=1) |
|           Output: user_access_pairs_1.id2                                                                                                             |
|           Recheck Cond: (user_access_pairs_1.id1 = (current_setting('jwt.claims.user_id'::text,true))::integer)                                      |
|           Heap Blocks: exact=2                                                                                                                        |
|           ->  Bitmap Index Scan on index_user_access_pairs_on_id1  (cost=0.00..4.31 rows=2 width=0) (actual time=0.064..0.064 rows=2 loops=1)         |
|                 Index Cond: (user_access_pairs_1.id1 = (current_setting('jwt.claims.user_id'::text,true))::integer)                                  |
| Planning Time: 0.572 ms                                                                                                                               |
| Execution Time: 1.295 ms                                                                                                                              |
+-------------------------------------------------------------------------------------------------------------------------------------------------------+

以下是“手动”执行的相同查询,但没有RLS进行比较。这次没有seq扫描,并且性能明显更好(尤其是在更大的数据集上运行时)

database> set jwt.claims.user_id to '2222';
database> explain analyze verbose
    select first_name,last_name
    from users
    where exists (
       select 1
       from user_access_pairs
       where
         id1 = current_setting('jwt.claims.user_id'::text,true)::integer
         and id2 = users.id
     )

+---------------------------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN                                                                                                                                  |
|---------------------------------------------------------------------------------------------------------------------------------------------|
| Nested Loop  (cost=4.59..27.86 rows=2 width=14) (actual time=0.020..0.033 rows=2 loops=1)                                                   |
|   Output: users.first_name,users.last_name                                                                                                 |
|   Inner Unique: true                                                                                                                        |
|   ->  Bitmap Heap Scan on public.user_access_pairs  (cost=4.31..11.26 rows=2 width=4) (actual time=0.013..0.016 rows=2 loops=1)             |
|         Output: user_access_pairs.id1,user_access_pairs.id2                                                                                |
|         Recheck Cond: (user_access_pairs.id1 = (current_setting('jwt.claims.user_id'::text,true))::integer)                                |
|         Heap Blocks: exact=2                                                                                                                |
|         ->  Bitmap Index Scan on index_user_access_pairs_on_id1  (cost=0.00..4.31 rows=2 width=0) (actual time=0.010..0.010 rows=2 loops=1) |
|               Index Cond: (user_access_pairs.id1 = (current_setting('jwt.claims.user_id'::text,true))::integer)                            |
|   ->  Index Scan using users_pkey on public.users  (cost=0.28..8.30 rows=1 width=18) (actual time=0.006..0.006 rows=1 loops=2)              |
|         Output: users.id,users.roles_mask                        |
|         Index Cond: (users.id = user_access_pairs.id2)                                                                                      |
| Planning Time: 0.464 ms                                                                                                                     |
| Execution Time: 0.075 ms                                                                                                                    |
+---------------------------------------------------------------------------------------------------------------------------------------------+

我猜想查询计划者会将这两个查询视为相同。为什么它们有所不同?如何避免seq扫描?

解决方法

在没有RLS策略的情况下,您没有看到与看似等效查询相同的计划的原因是之前考虑了子查询上拉。这是一个规划师怪癖。

总而言之,不幸的是,RLS策略与子查询相结合并不是在性能方面彼此互为好友。

仅供参考,比较以下两个查询可以看到类似的表现形式:

SELECT ... FROM my_table WHERE                     EXISTS(SELECT ...);
SELECT ... FROM my_table WHERE CASE WHEN true THEN EXISTS(SELECT ...) END;

此处,尽管两个查询都是等效的,但是第二个查询会为子查询生成一个(散列的)子计划,因为不必要的CASE WHEN true的折叠是在子查询上拉之后完成的。 / p>

免责声明:我在IRC #postgresql上从RhodiumToad获得了这些信息,但是用我自己的话来解释/简化了它。

,

我不能全力以赴,但我认为您应该以更明智的政策制定更好的计划:

CREATE POLICY select_users_policy ON public.users
  FOR SELECT
  USING (
     EXISTS (SELECT 1 FROM user_access_pairs
             WHERE id1 = current_setting('jwt.claims.user_id'::text,true)
               AND id2 = users.id)
  );

我想提及的是,基于用户可以随时更改的占位符变量将行级安全性置于可疑状态。

,

this comment的作者(通过反复试验)提出了将子查询强制转换为ARRAY的解决方案。完全不确定它是否适用于您的情况,而只是表明,非常出乎意料的技巧显然会吓到优化器执行其工作。

所以您可以尝试:

create policy select_users_policy
on public.users
for select using (
  users.id = any (
    array(
        select id1
        from user_access_pairs
        where 
            id1 = current_setting('jwt.claims.user_id'::text,true)::integer
            and id2 = users.id
        )
    )
);

很尴尬,但是谁知道...

,

问题中未提及,但我假设从public.users的读取是从另一个面向API的架构(我们称为api)触发的。

subZero Slack上的一个人共享:

我遇到了同样的问题,并根据我的api视图定义了RLS,从而解决了seq扫描问题。但是,在更改这些视图时要维护有点麻烦,因为对于迁移,我必须首先删除RLS策略,更改视图,然后重新创建策略。 ...当RLS中涉及子查询时,我使用api视图。

因此,他们使用完全相同的规则,但是引用了api.fooapi.bar views instead of public.foo and public.bar`表。

您可以尝试以下方法:

create policy select_users_policy
  on public.users
  for select using (
    exists (
      select 1
      from api.user_access_pairs
      where
        id1 = current_setting('jwt.claims.user_id'::text,true)::integer
        and id2 = api.users.id
    )
  );

因此,这假设您在users模式中镜像了api的{​​{1}}视图中,并且也将public.users移动到了user_access_pairs(或创建了一个查看它的引用。)

我不清楚这是否有效,因为查询首先是从api架构中的视图/函数触发的,因此在该架构中引用视图对于查询优化器而言在某种程度上不会造成混淆,或者这仅仅是使优化程序启动的一个技巧,而不管查询是如何产生的。 (在我看来,后者似乎更有可能,但谁知道。)

,

subZero Slack上的另一个用户共享了一个解决方案,该解决方案基于将当前用户权限的查询包装在一个函数中。就您而言,类似:

create policy select_users_policy
  on public.users
  for select using (
    id IN (
      select * from current_user_read_users()
   )
  );

您将创建一个current_user_read_users()函数,该函数根据j user_id从jwt中查找user_access_pairs并返回当前用户可能读取的用户集。

此函数与user_access_pairs视图具有相同的所有者,或者用SECURITY DEFINER声明该函数(以便绕过RLS)可能并不重要。可能重要的部分仅仅是将子查询拉入一个函数(以某种方式帮助优化器),但 据报道,其他情况有助于解决其他性能问题。

最后,您可能想尝试将其放入api视图中,就像我报告的the other solution一样。

一个警告:

在权限表本身上有一个循环依赖问题,所以我不得不做一个特殊情况策略。不过,那没有任何性能问题,所以很好。

(请注意,在这种情况下,权限保存在中,可以由管理员用户编辑,而不是像您本例那样生成。)

,

一种解决方案(基于this post,还有其他一些好的建议和基准)是根本不使用RLS,而是将过滤器构建到视图中。

create view api.allowed_users
with (security_barrier)
as
  select id,first_name,last_name,favorite_color
  from public.users
  join user_access_pairs uap
    on uap.id1 = current_setting('jwt.claims.user_id'::text,true)::integer

您已经在user_access_pairs视图中表示了访问策略,因此可以说RLS规则并没有真正添加任何内容。

({security_barrier是为了防止潜在的信息泄漏,但是会带来性能损失,因此请检查您的情况是否必要。)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...