Postgres触发器的副作用与行级安全性选择策略乱序发生

Car*_*rre 5 postgresql triggers row-level-security

语境

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

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

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

CREATE TABLE b (id TEXT);
Run Code Online (Sandbox Code Playgroud)

问题

考虑以下策略和触发器:

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();
Run Code Online (Sandbox Code Playgroud)

现在考虑以下语句:

INSERT INTO a VALUES ('fails') returning id;
Run Code Online (Sandbox Code Playgroud)

基于阅读命令类型表所应用策略以及对SQL的一般理解,我的期望是应该按顺序执行以下操作:

  1. ('fails')上演新行INSERT
  2. BEFORE触发器触发与NEW设置为新行
  3. 该行('fails')插入到b触发器过程中并从中返回,并保持不变
  4. INSERTWITH CHECK政策true进行评估,以true
  5. 系统会评估SELECTUSING政策select * from b where a.id = b.id由于步骤3,它应该返回true
  6. 通过所有策略后,该行将('fails')插入表中
  7. fails返回插入行的ID()

不幸的是(您可能已经猜到了),而不是发生上述步骤,我们看到了:

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

这个问题的目的是发现为什么没有发生预期的行为。

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

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)
Run Code Online (Sandbox Code Playgroud)

我尝试了什么?

  • 在触发器定义中进行 了BEFOREvs的实验AFTER
    • AFTER导致触发不执行在所有
  • 尝试定义适用于ALL命令的单个策略(使用相同的使用/带有检查表达式)
    • 导致相同的行为

附录

  • Postgres版本
    • PostgreSQL 10.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 6.4.0) 6.4.0, 64-bit
  • 如果尝试重现该问题,请确保您未使用SUPER权限运行,因为这将忽略行安全性

Car*_*rre 5

在与通用邮件列表中的其他 PostgreSQL 用户/开发人员反复交流后,确定此特定问题是由单个语句中的突变可见性引起的。 您可以在此处查看整个讨论。特别感谢 Dean Rasheed 解释了问题并提出了解决方案。为了 Stack Overflow 社区的利益,我在这里总结了他的回答。

综上所述,触发器插入EXISTS的行在行级安全性中的后续子句是不可见的SELECT由于整个语句在单个 PostgreSQL 快照中运行策略中。

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

对于用 SQL 或任何标准过程语言编写的函数,还有一个由易变性类别决定的第二个重要属性,即调用该函数的 SQL 命令所做的任何数据更改的可见性。VOLATILE 函数会看到这样的变化,而 STABLE 或 IMMUTABLE 函数不会。此行为是使用 MVCC 的快照行为实现的(请参阅第 13 章):STABLE 和 IMMUTABLE 函数使用在调用查询开始时建立的快照,而 VOLATILE 函数在它们执行的每个查询开始时获取新快照。

因此,此问题的一种解决方案是将 RLS 选择策略实现为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))
);
Run Code Online (Sandbox Code Playgroud)

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

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

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
Run Code Online (Sandbox Code Playgroud)

这种行为符合我的期望,所以我接受这是对问题的回答。不幸的是,该函数在查询执行期间会导致不可接受的优化栅栏,因此我不会在我的 RBAC 实现中使用它。我认为不可能有一个可优化的解决方案来解决我的问题,因为策略中的EXISTS表达式SELECT不能同时内联和 VOLATILE。