Postgres 触发器副作用与行级安全性 select 策略无序发生

Postgres Trigger side-effect is occurring out of order with row-level security select policy

上下文

我正在使用行级安全性和触发器来实现纯 SQL RBAC 实现。这样做时,我在 INSERT 触发器和 SELECT 行级安全策略之间遇到了奇怪的行为。

为简单起见,此问题的其余部分将使用以下简化的 tables 来讨论该问题:

CREATE TABLE a (id TEXT);
ALTER TABLE a ENABLE ROW LEVEL SECURITY;
ALTER TABLE a FORCE ROW LEVEL SECURITY;

CREATE TABLE b (id TEXT);

问题

考虑以下策略和触发器:

CREATE POLICY aSelect ON a FOR SELECT
USING (EXISTS(
    select * from b where a.id = b.id
));

CREATE POLICY aInsert ON a FOR INSERT
WITH CHECK (true);

CREATE FUNCTION reproHandler() RETURNS TRIGGER AS $$
BEGIN
    RAISE NOTICE USING MESSAGE = 'inside trigger handler';
    INSERT INTO b (id) VALUES (NEW.id);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER reproTrigger BEFORE INSERT ON a
FOR EACH ROW EXECUTE PROCEDURE reproHandler();

现在考虑以下语句:

INSERT INTO a VALUES ('fails') returning id;

基于阅读 policies applied by command type table 和一般 SQL 理解,我的期望是以下事情应该按顺序发生:

  1. INSERT
  2. 准备了新行 ('fails')
  3. BEFORE 触发器触发,NEW 设置为新行
  4. ('fails') 被插入到 b 并且 return 从触发器过程中编辑
  5. INSERTWITH CHECK 政策 true 被评估为 true
  6. 已评估 SELECTUSING 政策 select * from b where a.id = b.id由于步骤 3
  7. ,这应该 return 正确
  8. 通过所有策略后,行 ('fails') 插入 table
  9. 插入行的id(fails)为returned

不幸的是(正如您可能已经猜到的那样),我们看到的不是上述步骤:

test=> INSERT INTO a VALUES ('fails') returning id;
NOTICE:  inside trigger handler
ERROR:  new row violates row-level security policy for table "a"

这个问题的目的是找出预期行为没有发生的原因。

请注意,以下语句按预期正确运行:

test=> INSERT INTO a VALUES ('works');
NOTICE:  inside trigger handler
INSERT 0 1
test=> select * from a; select * from b;
  id   
-------
 works
(1 row)

  id   
-------
 works
(1 row)

我尝试了什么?

附录

在与一般邮件列表中的其他 PostgreSQL users/developers 来回交流之后,确定此特定问题是由单个语句中的突变可见性引起的。 You can review the entire discussion here。特别感谢 Dean Rasheed 解释问题并提出解决方案。为了 Stack Overflow 社区的利益,我在这里总结了他的回答。

总而言之,由于整个语句 运行ning 在行级安全性 SELECT 策略中的后续 EXISTS 子句中,触发器插入的行不可见单个 PostgreSQL 快照。

解决此问题的一种方法是确保 EXISTS 子句是 运行 新快照。为此,EXISTS 子句可以使用标记为 VOLATILE 的 PostgreSQL 函数。此函数属性将使函数能够观察同一语句中所做的更改。有关详细信息,请参阅 the documentation。相关段落摘录在此供参考:

For functions written in SQL or in any of the standard procedural languages, there is a second important property determined by the volatility category, namely the visibility of any data changes that have been made by the SQL command that is calling the function. A VOLATILE function will see such changes, a STABLE or IMMUTABLE function will not. This behavior is implemented using the snapshotting behavior of MVCC (see Chapter 13): STABLE and IMMUTABLE functions use a snapshot established as of the start of the calling query, whereas VOLATILE functions obtain a fresh snapshot at the start of each query they execute.

因此,解决此问题的一种方法是将 RLS select 策略作为 VOLATILE 函数来实现。对政策的修改示例如下:

CREATE OR REPLACE FUNCTION rlsCheck(_id text) RETURNS TABLE (id text) AS $$
    select * from b where b.id = _id
$$ LANGUAGE sql VOLATILE;

CREATE POLICY reproPolicySelect ON a FOR SELECT
USING (
    EXISTS(select * from rlsCheck(a.id))
);

在此解决方案中,从 table a 投影的每一行将要求函数 rlsCheck returns 至少一行。此函数将 运行 与每个投影行的新快照。每次调用 rlsCheck 生成的新快照将允许它看到原始示例中 INSERT 触发器对 table b 的修改。

如果进行上述修改并运行测试,您将看到以下行为:

test=> select * from a;
id 
----
(0 rows)

test=> select * from b;
id 
----
(0 rows)

test=> insert into a values ('hi') returning id;
NOTICE:  inside trigger handler
id 
----
hi
(1 row)

INSERT 0 1

此行为符合我的预期,因此我接受此作为问题的答案。不幸的是,该函数会在查询执行期间导致 unacceptable 优化栅栏,因此我不会在我的 RBAC 实现中使用它。我不相信可以为我的问题找到可优化的解决方案,因为 SELECT 策略中的 EXISTS 表达式不能同时内联和 VOLATILE。