Postgres触发器的副作用是由于行级安全性选择策略而发生的

时间:2018-09-29 06:22:23

标签: 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);

问题

请考虑以下策略和触发器:

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. ('fails')暂存了新行INSERT
  2. BEFORE设置为新行的情况下触发NEW触发器
  3. 将行('fails')插入到b中,并从触发过程中直接返回
  4. INSERT的{​​{1}}策略WITH CHECK评估为true
  5. 评估true的{​​{1}}策略SELECT由于步骤3,它应该返回true
  6. 已通过所有策略,在表中插入了行USING
  7. 返回插入行的ID(select * from b where a.id = b.id

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

('fails')

此问题的目的是发现为什么未发生预期的行为。

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

fails

我尝试了什么?

  • 在触发器定义中用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) 进行了实验
    • BEFORE导致触发器根本不执行
  • 对定义适用于AFTER命令的单个策略进行了实验(具有相同的using / with check表达式)
    • 导致相同的行为

附录

  • Postgres版本
    • AFTER
  • 如果您尝试重现该问题,请确保您未使用SUPER权限运行,因为这将忽略行安全性

1 个答案:

答案 0 :(得分:0)

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

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

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

  

对于用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))
);

在此解决方案中,从表a投影的每一行都将要求函数rlsCheck返回至少一行。该功能将为每个计划的行运行一个新的快照。在原始示例中,每次调用rlsCheck生成的新快照将使它能够查看由INSERT触发器对表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

此行为符合我的预期,因此我接受此作为问题的答案。不幸的是,该函数在查询执行期间导致了无法接受的优化范围,因此我将不在我的RBAC实现中使用它。我不认为有可能对我的问题有一个可优化的解决方案,因为EXISTS策略中的SELECT表达式不能同时内联和挥发。