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 理解,我的期望是以下事情应该按顺序发生:
- 为
INSERT
准备了新行 ('fails')
BEFORE
触发器触发,NEW
设置为新行
- 行
('fails')
被插入到 b
并且 return 从触发器过程中编辑
INSERT
的 WITH CHECK
政策 true
被评估为 true
- 已评估
SELECT
的 USING
政策 select * from b where a.id = b.id
。 由于步骤 3 ,这应该 return 正确
- 通过所有策略后,行
('fails')
插入 table
- 插入行的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)
我尝试了什么?
- 在触发器定义中使用
BEFORE
与 AFTER
进行了实验
AFTER
导致触发器根本不执行
- 尝试定义适用于
ALL
命令的单个策略(使用相同的 using/with 检查表达式)
- 结果相同
附录
- Postgres 版本
PostgreSQL 10.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 6.4.0) 6.4.0, 64-bit
- 如果您尝试重现问题,请确保您没有 运行 SUPER 权限,因为那样会忽略行安全性
在与一般邮件列表中的其他 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。
上下文
我正在使用行级安全性和触发器来实现纯 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 理解,我的期望是以下事情应该按顺序发生:
- 为
INSERT
准备了新行 BEFORE
触发器触发,NEW
设置为新行- 行
('fails')
被插入到b
并且 return 从触发器过程中编辑 INSERT
的WITH CHECK
政策true
被评估为true
- 已评估
SELECT
的USING
政策select * from b where a.id = b.id
。 由于步骤 3 ,这应该 return 正确
- 通过所有策略后,行
('fails')
插入 table - 插入行的id(
fails
)为returned
('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
进行了实验AFTER
导致触发器根本不执行
- 尝试定义适用于
ALL
命令的单个策略(使用相同的 using/with 检查表达式)- 结果相同
附录
- Postgres 版本
PostgreSQL 10.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 6.4.0) 6.4.0, 64-bit
- 如果您尝试重现问题,请确保您没有 运行 SUPER 权限,因为那样会忽略行安全性
在与一般邮件列表中的其他 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。