Docs and tests for RLS policies applied by command type
While looking at the INSERT ... ON CONFLICT DO SELECT patch, I noticed
that the "Policies Applied by Command Type" table on the CREATE POLICY
page doesn't fully or accurately describe all the policies that are
actually checked in all cases:
* INSERT ON CONFLICT checks the new row from the INSERT against SELECT
policy expressions, regardless of what ON CONFLICT action is
performed.
* If an ON CONFLICT DO UPDATE is executed, the new row from the
auxiliary UPDATE command is also checked against SELECT policy
expressions.
* MERGE always checks all candidate source and target rows against
SELECT policy expressions, even if no action is performed.
* MERGE ... THEN INSERT checks the new row against SELECT policy
expressions, if there is a RETURNING clause.
* MERGE ... THEN UPDATE always checks the new and existing rows
against SELECT policy expressions, even if there is no RETURNING
clause.
* MERGE ... THEN DELETE isn't mentioned at all. It always checks the
existing row against SELECT policy expressions.
I think having MERGE use the same row in the doc table as other
commands makes it harder to read, and it would be better to just list
each of the MERGE cases separately, even if that does involve some
repetition.
In addition, a paragraph above the table for INSERT policies says:
"""
Note that INSERT with ON CONFLICT DO UPDATE checks INSERT policies'
WITH CHECK expressions only for rows appended to the relation by the
INSERT path.
"""
Maybe that was once true, but it isn't true now, in any supported PG
version. The WITH CHECK expressions from INSERT policies are always
checked, regardless of which path it ends up taking.
I think it would be good to have regression tests specifically
covering all these cases. Yes, there are a lot of existing RLS
regression tests, but they tend to cover more complex scenarios, and
focus on whether the result of the command was what was expected,
rather than precisely which policies were checked in the process.
Thus, it's not obvious whether they provide complete coverage.
So patch 0001, attached, adds a new set of regression tests, near the
start of rowsecurity.sql, which specifically tests which policies are
applied for each command variant.
Patch 0002 updates the doc table to try to be clearer and more
accurate, and consistent with the test results from 0001, and fixes
the paragraph mentioned above.
Regards,
Dean
Attachments:
v1-0001-New-RLS-tests-to-test-policies-applied-by-command.patchtext/x-patch; charset=US-ASCII; name=v1-0001-New-RLS-tests-to-test-policies-applied-by-command.patchDownload
From c2c49cd10f001a5ee7a2d52083b2fcd3232fc53e Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 27 Mar 2025 14:08:09 +0000
Subject: [PATCH v1 1/2] New RLS tests to test policies applied by command
type.
The existing RLS tests focus on the outcome of various testing
scenarios, rather than the exact policies applied. These new tests
list out the policies applied for each command type, including the
different paths through INSERT ... ON CONFLICT and MERGE.
---
src/test/regress/expected/rowsecurity.out | 226 ++++++++++++++++++++++
src/test/regress/sql/rowsecurity.sql | 111 +++++++++++
2 files changed, 337 insertions(+)
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 87929191d06..ce80cbde938 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -31,6 +31,232 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
COST 0.0000001 LANGUAGE plpgsql
AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Test policies applied by command type
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
+ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
+INSERT INTO rls_test_src VALUES (1, 'src a');
+CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
+ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
+CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
+ $$ BEGIN new.c = upper(new.b); RETURN new; END; $$
+ LANGUAGE plpgsql;
+CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
+ FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION ins_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'INSERT CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE POLICY sel_pol ON rls_test_src FOR SELECT
+ USING (sel_using_fn('rls_test_src', rls_test_src));
+CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
+ USING (upd_using_fn('rls_test_src', rls_test_src))
+ WITH CHECK (upd_check_fn('rls_test_src', rls_test_src));
+CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
+ USING (sel_using_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
+ WITH CHECK (ins_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
+ USING (upd_using_fn('rls_test_tgt', rls_test_tgt))
+ WITH CHECK (upd_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
+ USING (del_using_fn('rls_test_tgt', rls_test_tgt));
+GRANT SELECT, UPDATE ON rls_test_src TO public;
+GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_test_src;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR UPDATE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR NO KEY UPDATE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR SHARE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR KEY SHARE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+TRUNCATE rls_test_tgt;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+ a | b | c
+---+-------+-------
+ 1 | tgt a | TGT A
+(1 row)
+
+UPDATE rls_test_tgt SET b = 'tgt b';
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 1 | tgt d | TGT D
+(1 row)
+
+BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+DELETE FROM rls_test_tgt RETURNING *;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 1 | tgt d | TGT D
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
+NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A")
+INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
+NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt c","TGT C")
+NOTICE: UPDATE USING on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(2,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt d","TGT D")
+INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A")
+ a | b | c
+---+-------+-------
+ 3 | tgt a | TGT A
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt c","TGT C")
+NOTICE: UPDATE USING on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(3,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 3 | tgt d | TGT D
+(1 row)
+
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN DO NOTHING;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a');
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a')
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt a | TGT A
+(1 row)
+
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt b';
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt c'
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt c | TGT C
+(1 row)
+
+BEGIN;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt c | TGT C
+(1 row)
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE rls_test_src, rls_test_tgt;
+DROP FUNCTION rls_test_tgt_set_c;
+DROP FUNCTION sel_using_fn, ins_check_fn, upd_using_fn, upd_check_fn, del_using_fn;
-- BASIC Row-Level Security Scenario
SET SESSION AUTHORIZATION regress_rls_alice;
CREATE TABLE uaccount (
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index f61dbbf9581..4bec2672a4f 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -41,6 +41,117 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Test policies applied by command type
+SET SESSION AUTHORIZATION regress_rls_alice;
+
+CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
+ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
+INSERT INTO rls_test_src VALUES (1, 'src a');
+
+CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
+ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
+
+CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
+ $$ BEGIN new.c = upper(new.b); RETURN new; END; $$
+ LANGUAGE plpgsql;
+CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
+ FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+
+CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION ins_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'INSERT CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+
+CREATE POLICY sel_pol ON rls_test_src FOR SELECT
+ USING (sel_using_fn('rls_test_src', rls_test_src));
+CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
+ USING (upd_using_fn('rls_test_src', rls_test_src))
+ WITH CHECK (upd_check_fn('rls_test_src', rls_test_src));
+
+CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
+ USING (sel_using_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
+ WITH CHECK (ins_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
+ USING (upd_using_fn('rls_test_tgt', rls_test_tgt))
+ WITH CHECK (upd_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
+ USING (del_using_fn('rls_test_tgt', rls_test_tgt));
+
+GRANT SELECT, UPDATE ON rls_test_src TO public;
+GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+
+SELECT * FROM rls_test_src;
+SELECT * FROM rls_test_src FOR UPDATE;
+SELECT * FROM rls_test_src FOR NO KEY UPDATE;
+SELECT * FROM rls_test_src FOR SHARE;
+SELECT * FROM rls_test_src FOR KEY SHARE;
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
+TRUNCATE rls_test_tgt;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+
+UPDATE rls_test_tgt SET b = 'tgt b';
+UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
+UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+
+BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
+BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
+DELETE FROM rls_test_tgt RETURNING *;
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
+INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
+INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *;
+INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
+ROLLBACK;
+
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN DO NOTHING;
+
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a');
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a')
+ RETURNING *;
+
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt b';
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt c'
+ RETURNING *;
+
+BEGIN;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE;
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE
+ RETURNING *;
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE rls_test_src, rls_test_tgt;
+DROP FUNCTION rls_test_tgt_set_c;
+DROP FUNCTION sel_using_fn, ins_check_fn, upd_using_fn, upd_check_fn, del_using_fn;
+
-- BASIC Row-Level Security Scenario
SET SESSION AUTHORIZATION regress_rls_alice;
--
2.43.0
v1-0002-doc-Improve-the-Policies-Applied-by-Command-Type-.patchtext/x-patch; charset=US-ASCII; name=v1-0002-doc-Improve-the-Policies-Applied-by-Command-Type-.patchDownload
From 441f55244b19b7ffdadf77dc0c9bcd311ec56d00 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 27 Mar 2025 14:19:15 +0000
Subject: [PATCH v1 2/2] doc: Improve the "Policies Applied by Command Type"
table.
This table was missing MERGE ... THEN DELETE and some of the policies
applied during INSERT ... ON CONFLICT and MERGE. Fix that, and try to
improve readability by listing the various MERGE cases separately,
rather than together with INSERT/UPDATE/DELETE.
In addition, correct the paragraph above the table to note that an
INSERT ... ON CONFLICT DO NOTHING/UPDATE checks all new rows against
the INSERT policy expressions, regardless of whether the rows end up
being inserted.
---
doc/src/sgml/ref/create_policy.sgml | 70 +++++++++++++++++++++++------
1 file changed, 57 insertions(+), 13 deletions(-)
diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
index e76c342d3da..2196fbb1174 100644
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -293,9 +293,9 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</para>
<para>
Note that <literal>INSERT</literal> with <literal>ON CONFLICT DO
- UPDATE</literal> checks <literal>INSERT</literal> policies'
- <literal>WITH CHECK</literal> expressions only for rows appended
- to the relation by the <literal>INSERT</literal> path.
+ NOTHING/UPDATE</literal> checks <literal>INSERT</literal> policies'
+ <literal>WITH CHECK</literal> expressions for all rows, regardless
+ of whether they end up being inserted.
</para>
</listitem>
</varlistentry>
@@ -440,15 +440,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>INSERT</command> / <command>MERGE ... THEN INSERT</command></entry>
- <entry>—</entry>
- <entry>New row</entry>
- <entry>—</entry>
- <entry>—</entry>
- <entry>—</entry>
- </row>
- <row>
- <entry><command>INSERT ... RETURNING</command></entry>
+ <entry><command>INSERT</command></entry>
<entry>
New row <footnote id="rls-select-priv">
<para>
@@ -464,7 +456,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>UPDATE</command> / <command>MERGE ... THEN UPDATE</command></entry>
+ <entry><command>UPDATE</command></entry>
<entry>
Existing & new rows <footnoteref linkend="rls-select-priv"/>
</entry>
@@ -483,14 +475,66 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
<entry>Existing row</entry>
</row>
+ <row>
+ <entry><command>INSERT ... ON CONFLICT</command></entry>
+ <entry>New row</entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
<row>
<entry><command>ON CONFLICT DO UPDATE</command></entry>
+ <entry>
+ Existing & new rows <footnote id="rls-on-conflict-update-priv">
+ <para>
+ New row of the auxiliary <command>UPDATE</command> command, which
+ might be different from the new row of the original
+ <command>INSERT</command> command.
+ </para>
+ </footnote>
+ </entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ <entry>
+ New row <footnoteref linkend="rls-on-conflict-update-priv"/>
+ </entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE</command></entry>
+ <entry>All candidate source & target rows</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN INSERT</command></entry>
+ <entry>
+ New row <footnoteref linkend="rls-select-priv"/>
+ </entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN UPDATE</command></entry>
<entry>Existing & new rows</entry>
<entry>—</entry>
<entry>Existing row</entry>
<entry>New row</entry>
<entry>—</entry>
</row>
+ <row>
+ <entry><command>MERGE ... THEN DELETE</command></entry>
+ <entry>Existing row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ </row>
</tbody>
</tgroup>
</table>
--
2.43.0
I’ve had a look at this.
• The doc updates make it much clearer how things work.
• For the new test, I’ve verified that they pass. I also think that having them is very good, considering the complexity of the RLS system. I found the added test quite hectic to follow, but this could just be a me problem (not very used to reading the postgres test code). I’ve attached a patch with AI-generated comments, which helped me understand the test, if that is of any help. If you could add some yourself, that would of course be even better.
Regardless of if those comments are included or not, which I leave to you, I think this is good to commit.
/Viktor
Show quoted text
On 20 Oct 2025 at 15:29 +0200, Dean Rasheed <dean.a.rasheed@gmail.com>, wrote:
While looking at the INSERT ... ON CONFLICT DO SELECT patch, I noticed
that the "Policies Applied by Command Type" table on the CREATE POLICY
page doesn't fully or accurately describe all the policies that are
actually checked in all cases:* INSERT ON CONFLICT checks the new row from the INSERT against SELECT
policy expressions, regardless of what ON CONFLICT action is
performed.* If an ON CONFLICT DO UPDATE is executed, the new row from the
auxiliary UPDATE command is also checked against SELECT policy
expressions.* MERGE always checks all candidate source and target rows against
SELECT policy expressions, even if no action is performed.* MERGE ... THEN INSERT checks the new row against SELECT policy
expressions, if there is a RETURNING clause.* MERGE ... THEN UPDATE always checks the new and existing rows
against SELECT policy expressions, even if there is no RETURNING
clause.* MERGE ... THEN DELETE isn't mentioned at all. It always checks the
existing row against SELECT policy expressions.I think having MERGE use the same row in the doc table as other
commands makes it harder to read, and it would be better to just list
each of the MERGE cases separately, even if that does involve some
repetition.In addition, a paragraph above the table for INSERT policies says:
"""
Note that INSERT with ON CONFLICT DO UPDATE checks INSERT policies'
WITH CHECK expressions only for rows appended to the relation by the
INSERT path.
"""Maybe that was once true, but it isn't true now, in any supported PG
version. The WITH CHECK expressions from INSERT policies are always
checked, regardless of which path it ends up taking.I think it would be good to have regression tests specifically
covering all these cases. Yes, there are a lot of existing RLS
regression tests, but they tend to cover more complex scenarios, and
focus on whether the result of the command was what was expected,
rather than precisely which policies were checked in the process.
Thus, it's not obvious whether they provide complete coverage.So patch 0001, attached, adds a new set of regression tests, near the
start of rowsecurity.sql, which specifically tests which policies are
applied for each command variant.Patch 0002 updates the doc table to try to be clearer and more
accurate, and consistent with the test results from 0001, and fixes
the paragraph mentioned above.Regards,
Dean
Attachments:
v1-0003-Adding-comments-to-new-RLS-tests.patchapplication/octet-streamDownload
From babf1cc1a3486adfbd78fc6f63f3b4559e9bc793 Mon Sep 17 00:00:00 2001
From: Viktor Holmberg <v@viktorh.net>
Date: Mon, 20 Oct 2025 15:52:24 +0200
Subject: [PATCH 3/3] Adding comments to new RLS tests
---
src/test/regress/expected/rowsecurity.out | 22 ++++++++++++++++++++++
src/test/regress/sql/rowsecurity.sql | 22 ++++++++++++++++++++++
2 files changed, 44 insertions(+)
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index a32f8f07e43..f945115ece8 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -33,9 +33,12 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
-- Test policies applied by command type
SET SESSION AUTHORIZATION regress_rls_alice;
+-- Setup: Create a source table (for MERGE operations)
CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
INSERT INTO rls_test_src VALUES (1, 'src a');
+-- Setup: Create a target table with a trigger that sets column c = UPPER(b)
+-- This trigger ensures that policy functions see the "final" row state
CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
@@ -43,6 +46,8 @@ CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
LANGUAGE plpgsql;
CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+-- Setup: Create policy functions that emit NOTICE messages
+-- These let us verify which policies are applied and when
CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
@@ -58,6 +63,7 @@ CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
+-- Setup: Create policies on both tables
CREATE POLICY sel_pol ON rls_test_src FOR SELECT
USING (sel_using_fn('rls_test_src', rls_test_src));
CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
@@ -75,6 +81,8 @@ CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
GRANT SELECT, UPDATE ON rls_test_src TO public;
GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
SET SESSION AUTHORIZATION regress_rls_bob;
+-- Test: SELECT with different locking clauses
+-- Verify that FOR UPDATE/SHARE/etc apply UPDATE USING policies
SELECT * FROM rls_test_src;
NOTICE: SELECT USING on rls_test_src.(1,"src a")
a | b
@@ -114,6 +122,8 @@ NOTICE: SELECT USING on rls_test_src.(1,"src a")
1 | src a
(1 row)
+-- Test: Basic INSERT
+-- Verify INSERT CHECK is applied; with RETURNING also applies SELECT USING
INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
TRUNCATE rls_test_tgt;
@@ -125,6 +135,10 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
1 | tgt a | TGT A
(1 row)
+-- Test: Basic UPDATE
+-- Verify UPDATE USING (on old row) and UPDATE CHECK (on new row) are applied
+-- With WHERE clause, also applies SELECT USING when reading rows to update
+-- With RETURNING, applies SELECT USING on result
UPDATE rls_test_tgt SET b = 'tgt b';
NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
@@ -143,6 +157,8 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
1 | tgt d | TGT D
(1 row)
+-- Test: Basic DELETE
+-- Verify DELETE USING is applied; with WHERE or RETURNING also applies SELECT USING
BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
@@ -156,12 +172,16 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
1 | tgt d | TGT D
(1 row)
+-- Test: INSERT ON CONFLICT DO NOTHING
+-- Verify INSERT CHECK is applied for all rows (even those that conflict)
INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+-- Test: INSERT ON CONFLICT DO UPDATE
+-- Verify INSERT CHECK on all rows, then UPDATE policies on conflicting rows
BEGIN;
INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt a","TGT A")
@@ -194,6 +214,8 @@ NOTICE: SELECT USING on rls_test_tgt.(3,"tgt d","TGT D")
(1 row)
ROLLBACK;
+-- Test: MERGE operations
+-- Verify policies for MERGE variants match the documented table
MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
WHEN NOT MATCHED THEN DO NOTHING;
NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 36046426048..d893bbf71e3 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -44,10 +44,13 @@ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
-- Test policies applied by command type
SET SESSION AUTHORIZATION regress_rls_alice;
+-- Setup: Create a source table (for MERGE operations)
CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
INSERT INTO rls_test_src VALUES (1, 'src a');
+-- Setup: Create a target table with a trigger that sets column c = UPPER(b)
+-- This trigger ensures that policy functions see the "final" row state
CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
@@ -57,6 +60,8 @@ CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+-- Setup: Create policy functions that emit NOTICE messages
+-- These let us verify which policies are applied and when
CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
@@ -73,6 +78,7 @@ CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
+-- Setup: Create policies on both tables
CREATE POLICY sel_pol ON rls_test_src FOR SELECT
USING (sel_using_fn('rls_test_src', rls_test_src));
CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
@@ -94,27 +100,41 @@ GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
SET SESSION AUTHORIZATION regress_rls_bob;
+-- Test: SELECT with different locking clauses
+-- Verify that FOR UPDATE/SHARE/etc apply UPDATE USING policies
SELECT * FROM rls_test_src;
SELECT * FROM rls_test_src FOR UPDATE;
SELECT * FROM rls_test_src FOR NO KEY UPDATE;
SELECT * FROM rls_test_src FOR SHARE;
SELECT * FROM rls_test_src FOR KEY SHARE;
+-- Test: Basic INSERT
+-- Verify INSERT CHECK is applied; with RETURNING also applies SELECT USING
INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
TRUNCATE rls_test_tgt;
INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+-- Test: Basic UPDATE
+-- Verify UPDATE USING (on old row) and UPDATE CHECK (on new row) are applied
+-- With WHERE clause, also applies SELECT USING when reading rows to update
+-- With RETURNING, applies SELECT USING on result
UPDATE rls_test_tgt SET b = 'tgt b';
UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+-- Test: Basic DELETE
+-- Verify DELETE USING is applied; with WHERE or RETURNING also applies SELECT USING
BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
DELETE FROM rls_test_tgt RETURNING *;
+-- Test: INSERT ON CONFLICT DO NOTHING
+-- Verify INSERT CHECK is applied for all rows (even those that conflict)
INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+-- Test: INSERT ON CONFLICT DO UPDATE
+-- Verify INSERT CHECK on all rows, then UPDATE policies on conflicting rows
BEGIN;
INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
@@ -122,6 +142,8 @@ INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = '
INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
ROLLBACK;
+-- Test: MERGE operations
+-- Verify policies for MERGE variants match the documented table
MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
WHEN NOT MATCHED THEN DO NOTHING;
--
2.48.1
Oops, looks like CI got mad as I didn’t include all patch files - trying again.
/Viktor
Show quoted text
On 20 Oct 2025 at 16:02 +0200, Viktor Holmberg <v@viktorh.net>, wrote:
I’ve had a look at this.
• The doc updates make it much clearer how things work.
• For the new test, I’ve verified that they pass. I also think that having them is very good, considering the complexity of the RLS system. I found the added test quite hectic to follow, but this could just be a me problem (not very used to reading the postgres test code). I’ve attached a patch with AI-generated comments, which helped me understand the test, if that is of any help. If you could add some yourself, that would of course be even better.Regardless of if those comments are included or not, which I leave to you, I think this is good to commit.
/Viktor
On 20 Oct 2025 at 15:29 +0200, Dean Rasheed <dean.a.rasheed@gmail.com>, wrote:While looking at the INSERT ... ON CONFLICT DO SELECT patch, I noticed
that the "Policies Applied by Command Type" table on the CREATE POLICY
page doesn't fully or accurately describe all the policies that are
actually checked in all cases:* INSERT ON CONFLICT checks the new row from the INSERT against SELECT
policy expressions, regardless of what ON CONFLICT action is
performed.* If an ON CONFLICT DO UPDATE is executed, the new row from the
auxiliary UPDATE command is also checked against SELECT policy
expressions.* MERGE always checks all candidate source and target rows against
SELECT policy expressions, even if no action is performed.* MERGE ... THEN INSERT checks the new row against SELECT policy
expressions, if there is a RETURNING clause.* MERGE ... THEN UPDATE always checks the new and existing rows
against SELECT policy expressions, even if there is no RETURNING
clause.* MERGE ... THEN DELETE isn't mentioned at all. It always checks the
existing row against SELECT policy expressions.I think having MERGE use the same row in the doc table as other
commands makes it harder to read, and it would be better to just list
each of the MERGE cases separately, even if that does involve some
repetition.In addition, a paragraph above the table for INSERT policies says:
"""
Note that INSERT with ON CONFLICT DO UPDATE checks INSERT policies'
WITH CHECK expressions only for rows appended to the relation by the
INSERT path.
"""Maybe that was once true, but it isn't true now, in any supported PG
version. The WITH CHECK expressions from INSERT policies are always
checked, regardless of which path it ends up taking.I think it would be good to have regression tests specifically
covering all these cases. Yes, there are a lot of existing RLS
regression tests, but they tend to cover more complex scenarios, and
focus on whether the result of the command was what was expected,
rather than precisely which policies were checked in the process.
Thus, it's not obvious whether they provide complete coverage.So patch 0001, attached, adds a new set of regression tests, near the
start of rowsecurity.sql, which specifically tests which policies are
applied for each command variant.Patch 0002 updates the doc table to try to be clearer and more
accurate, and consistent with the test results from 0001, and fixes
the paragraph mentioned above.Regards,
Dean
Attachments:
v1-0001-New-RLS-tests-to-test-policies-applied-by-command.patchapplication/octet-streamDownload
From c2c49cd10f001a5ee7a2d52083b2fcd3232fc53e Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 27 Mar 2025 14:08:09 +0000
Subject: [PATCH v1 1/2] New RLS tests to test policies applied by command
type.
The existing RLS tests focus on the outcome of various testing
scenarios, rather than the exact policies applied. These new tests
list out the policies applied for each command type, including the
different paths through INSERT ... ON CONFLICT and MERGE.
---
src/test/regress/expected/rowsecurity.out | 226 ++++++++++++++++++++++
src/test/regress/sql/rowsecurity.sql | 111 +++++++++++
2 files changed, 337 insertions(+)
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 87929191d06..ce80cbde938 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -31,6 +31,232 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
COST 0.0000001 LANGUAGE plpgsql
AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Test policies applied by command type
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
+ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
+INSERT INTO rls_test_src VALUES (1, 'src a');
+CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
+ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
+CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
+ $$ BEGIN new.c = upper(new.b); RETURN new; END; $$
+ LANGUAGE plpgsql;
+CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
+ FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION ins_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'INSERT CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE POLICY sel_pol ON rls_test_src FOR SELECT
+ USING (sel_using_fn('rls_test_src', rls_test_src));
+CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
+ USING (upd_using_fn('rls_test_src', rls_test_src))
+ WITH CHECK (upd_check_fn('rls_test_src', rls_test_src));
+CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
+ USING (sel_using_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
+ WITH CHECK (ins_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
+ USING (upd_using_fn('rls_test_tgt', rls_test_tgt))
+ WITH CHECK (upd_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
+ USING (del_using_fn('rls_test_tgt', rls_test_tgt));
+GRANT SELECT, UPDATE ON rls_test_src TO public;
+GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_test_src;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR UPDATE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR NO KEY UPDATE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR SHARE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR KEY SHARE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+TRUNCATE rls_test_tgt;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+ a | b | c
+---+-------+-------
+ 1 | tgt a | TGT A
+(1 row)
+
+UPDATE rls_test_tgt SET b = 'tgt b';
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 1 | tgt d | TGT D
+(1 row)
+
+BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+DELETE FROM rls_test_tgt RETURNING *;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 1 | tgt d | TGT D
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
+NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A")
+INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
+NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt c","TGT C")
+NOTICE: UPDATE USING on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(2,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt d","TGT D")
+INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A")
+ a | b | c
+---+-------+-------
+ 3 | tgt a | TGT A
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt c","TGT C")
+NOTICE: UPDATE USING on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(3,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 3 | tgt d | TGT D
+(1 row)
+
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN DO NOTHING;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a');
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a')
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt a | TGT A
+(1 row)
+
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt b';
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt c'
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt c | TGT C
+(1 row)
+
+BEGIN;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt c | TGT C
+(1 row)
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE rls_test_src, rls_test_tgt;
+DROP FUNCTION rls_test_tgt_set_c;
+DROP FUNCTION sel_using_fn, ins_check_fn, upd_using_fn, upd_check_fn, del_using_fn;
-- BASIC Row-Level Security Scenario
SET SESSION AUTHORIZATION regress_rls_alice;
CREATE TABLE uaccount (
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index f61dbbf9581..4bec2672a4f 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -41,6 +41,117 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Test policies applied by command type
+SET SESSION AUTHORIZATION regress_rls_alice;
+
+CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
+ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
+INSERT INTO rls_test_src VALUES (1, 'src a');
+
+CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
+ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
+
+CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
+ $$ BEGIN new.c = upper(new.b); RETURN new; END; $$
+ LANGUAGE plpgsql;
+CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
+ FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+
+CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION ins_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'INSERT CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'UPDATE CHECK on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+
+CREATE POLICY sel_pol ON rls_test_src FOR SELECT
+ USING (sel_using_fn('rls_test_src', rls_test_src));
+CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
+ USING (upd_using_fn('rls_test_src', rls_test_src))
+ WITH CHECK (upd_check_fn('rls_test_src', rls_test_src));
+
+CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
+ USING (sel_using_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
+ WITH CHECK (ins_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
+ USING (upd_using_fn('rls_test_tgt', rls_test_tgt))
+ WITH CHECK (upd_check_fn('rls_test_tgt', rls_test_tgt));
+CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
+ USING (del_using_fn('rls_test_tgt', rls_test_tgt));
+
+GRANT SELECT, UPDATE ON rls_test_src TO public;
+GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+
+SELECT * FROM rls_test_src;
+SELECT * FROM rls_test_src FOR UPDATE;
+SELECT * FROM rls_test_src FOR NO KEY UPDATE;
+SELECT * FROM rls_test_src FOR SHARE;
+SELECT * FROM rls_test_src FOR KEY SHARE;
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
+TRUNCATE rls_test_tgt;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+
+UPDATE rls_test_tgt SET b = 'tgt b';
+UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
+UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+
+BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
+BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
+DELETE FROM rls_test_tgt RETURNING *;
+
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
+INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
+INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *;
+INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
+ROLLBACK;
+
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN DO NOTHING;
+
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a');
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a')
+ RETURNING *;
+
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt b';
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt c'
+ RETURNING *;
+
+BEGIN;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE;
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE
+ RETURNING *;
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE rls_test_src, rls_test_tgt;
+DROP FUNCTION rls_test_tgt_set_c;
+DROP FUNCTION sel_using_fn, ins_check_fn, upd_using_fn, upd_check_fn, del_using_fn;
+
-- BASIC Row-Level Security Scenario
SET SESSION AUTHORIZATION regress_rls_alice;
--
2.43.0
v1-0002-doc-Improve-the-Policies-Applied-by-Command-Type-.patchapplication/octet-streamDownload
From 441f55244b19b7ffdadf77dc0c9bcd311ec56d00 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 27 Mar 2025 14:19:15 +0000
Subject: [PATCH v1 2/2] doc: Improve the "Policies Applied by Command Type"
table.
This table was missing MERGE ... THEN DELETE and some of the policies
applied during INSERT ... ON CONFLICT and MERGE. Fix that, and try to
improve readability by listing the various MERGE cases separately,
rather than together with INSERT/UPDATE/DELETE.
In addition, correct the paragraph above the table to note that an
INSERT ... ON CONFLICT DO NOTHING/UPDATE checks all new rows against
the INSERT policy expressions, regardless of whether the rows end up
being inserted.
---
doc/src/sgml/ref/create_policy.sgml | 70 +++++++++++++++++++++++------
1 file changed, 57 insertions(+), 13 deletions(-)
diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
index e76c342d3da..2196fbb1174 100644
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -293,9 +293,9 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</para>
<para>
Note that <literal>INSERT</literal> with <literal>ON CONFLICT DO
- UPDATE</literal> checks <literal>INSERT</literal> policies'
- <literal>WITH CHECK</literal> expressions only for rows appended
- to the relation by the <literal>INSERT</literal> path.
+ NOTHING/UPDATE</literal> checks <literal>INSERT</literal> policies'
+ <literal>WITH CHECK</literal> expressions for all rows, regardless
+ of whether they end up being inserted.
</para>
</listitem>
</varlistentry>
@@ -440,15 +440,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>INSERT</command> / <command>MERGE ... THEN INSERT</command></entry>
- <entry>—</entry>
- <entry>New row</entry>
- <entry>—</entry>
- <entry>—</entry>
- <entry>—</entry>
- </row>
- <row>
- <entry><command>INSERT ... RETURNING</command></entry>
+ <entry><command>INSERT</command></entry>
<entry>
New row <footnote id="rls-select-priv">
<para>
@@ -464,7 +456,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>UPDATE</command> / <command>MERGE ... THEN UPDATE</command></entry>
+ <entry><command>UPDATE</command></entry>
<entry>
Existing & new rows <footnoteref linkend="rls-select-priv"/>
</entry>
@@ -483,14 +475,66 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
<entry>Existing row</entry>
</row>
+ <row>
+ <entry><command>INSERT ... ON CONFLICT</command></entry>
+ <entry>New row</entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
<row>
<entry><command>ON CONFLICT DO UPDATE</command></entry>
+ <entry>
+ Existing & new rows <footnote id="rls-on-conflict-update-priv">
+ <para>
+ New row of the auxiliary <command>UPDATE</command> command, which
+ might be different from the new row of the original
+ <command>INSERT</command> command.
+ </para>
+ </footnote>
+ </entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ <entry>
+ New row <footnoteref linkend="rls-on-conflict-update-priv"/>
+ </entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE</command></entry>
+ <entry>All candidate source & target rows</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN INSERT</command></entry>
+ <entry>
+ New row <footnoteref linkend="rls-select-priv"/>
+ </entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN UPDATE</command></entry>
<entry>Existing & new rows</entry>
<entry>—</entry>
<entry>Existing row</entry>
<entry>New row</entry>
<entry>—</entry>
</row>
+ <row>
+ <entry><command>MERGE ... THEN DELETE</command></entry>
+ <entry>Existing row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ </row>
</tbody>
</tgroup>
</table>
--
2.43.0
v1-0003-Adding-comments-to-new-RLS-tests.patchapplication/octet-streamDownload
From babf1cc1a3486adfbd78fc6f63f3b4559e9bc793 Mon Sep 17 00:00:00 2001
From: Viktor Holmberg <v@viktorh.net>
Date: Mon, 20 Oct 2025 15:52:24 +0200
Subject: [PATCH 3/3] Adding comments to new RLS tests
---
src/test/regress/expected/rowsecurity.out | 22 ++++++++++++++++++++++
src/test/regress/sql/rowsecurity.sql | 22 ++++++++++++++++++++++
2 files changed, 44 insertions(+)
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index a32f8f07e43..f945115ece8 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -33,9 +33,12 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
-- Test policies applied by command type
SET SESSION AUTHORIZATION regress_rls_alice;
+-- Setup: Create a source table (for MERGE operations)
CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
INSERT INTO rls_test_src VALUES (1, 'src a');
+-- Setup: Create a target table with a trigger that sets column c = UPPER(b)
+-- This trigger ensures that policy functions see the "final" row state
CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
@@ -43,6 +46,8 @@ CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
LANGUAGE plpgsql;
CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+-- Setup: Create policy functions that emit NOTICE messages
+-- These let us verify which policies are applied and when
CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
@@ -58,6 +63,7 @@ CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
+-- Setup: Create policies on both tables
CREATE POLICY sel_pol ON rls_test_src FOR SELECT
USING (sel_using_fn('rls_test_src', rls_test_src));
CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
@@ -75,6 +81,8 @@ CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
GRANT SELECT, UPDATE ON rls_test_src TO public;
GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
SET SESSION AUTHORIZATION regress_rls_bob;
+-- Test: SELECT with different locking clauses
+-- Verify that FOR UPDATE/SHARE/etc apply UPDATE USING policies
SELECT * FROM rls_test_src;
NOTICE: SELECT USING on rls_test_src.(1,"src a")
a | b
@@ -114,6 +122,8 @@ NOTICE: SELECT USING on rls_test_src.(1,"src a")
1 | src a
(1 row)
+-- Test: Basic INSERT
+-- Verify INSERT CHECK is applied; with RETURNING also applies SELECT USING
INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
TRUNCATE rls_test_tgt;
@@ -125,6 +135,10 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
1 | tgt a | TGT A
(1 row)
+-- Test: Basic UPDATE
+-- Verify UPDATE USING (on old row) and UPDATE CHECK (on new row) are applied
+-- With WHERE clause, also applies SELECT USING when reading rows to update
+-- With RETURNING, applies SELECT USING on result
UPDATE rls_test_tgt SET b = 'tgt b';
NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
@@ -143,6 +157,8 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
1 | tgt d | TGT D
(1 row)
+-- Test: Basic DELETE
+-- Verify DELETE USING is applied; with WHERE or RETURNING also applies SELECT USING
BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
@@ -156,12 +172,16 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
1 | tgt d | TGT D
(1 row)
+-- Test: INSERT ON CONFLICT DO NOTHING
+-- Verify INSERT CHECK is applied for all rows (even those that conflict)
INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+-- Test: INSERT ON CONFLICT DO UPDATE
+-- Verify INSERT CHECK on all rows, then UPDATE policies on conflicting rows
BEGIN;
INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt a","TGT A")
@@ -194,6 +214,8 @@ NOTICE: SELECT USING on rls_test_tgt.(3,"tgt d","TGT D")
(1 row)
ROLLBACK;
+-- Test: MERGE operations
+-- Verify policies for MERGE variants match the documented table
MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
WHEN NOT MATCHED THEN DO NOTHING;
NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 36046426048..d893bbf71e3 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -44,10 +44,13 @@ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
-- Test policies applied by command type
SET SESSION AUTHORIZATION regress_rls_alice;
+-- Setup: Create a source table (for MERGE operations)
CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
INSERT INTO rls_test_src VALUES (1, 'src a');
+-- Setup: Create a target table with a trigger that sets column c = UPPER(b)
+-- This trigger ensures that policy functions see the "final" row state
CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
@@ -57,6 +60,8 @@ CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+-- Setup: Create policy functions that emit NOTICE messages
+-- These let us verify which policies are applied and when
CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
@@ -73,6 +78,7 @@ CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
$$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
LANGUAGE plpgsql;
+-- Setup: Create policies on both tables
CREATE POLICY sel_pol ON rls_test_src FOR SELECT
USING (sel_using_fn('rls_test_src', rls_test_src));
CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
@@ -94,27 +100,41 @@ GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
SET SESSION AUTHORIZATION regress_rls_bob;
+-- Test: SELECT with different locking clauses
+-- Verify that FOR UPDATE/SHARE/etc apply UPDATE USING policies
SELECT * FROM rls_test_src;
SELECT * FROM rls_test_src FOR UPDATE;
SELECT * FROM rls_test_src FOR NO KEY UPDATE;
SELECT * FROM rls_test_src FOR SHARE;
SELECT * FROM rls_test_src FOR KEY SHARE;
+-- Test: Basic INSERT
+-- Verify INSERT CHECK is applied; with RETURNING also applies SELECT USING
INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
TRUNCATE rls_test_tgt;
INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+-- Test: Basic UPDATE
+-- Verify UPDATE USING (on old row) and UPDATE CHECK (on new row) are applied
+-- With WHERE clause, also applies SELECT USING when reading rows to update
+-- With RETURNING, applies SELECT USING on result
UPDATE rls_test_tgt SET b = 'tgt b';
UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+-- Test: Basic DELETE
+-- Verify DELETE USING is applied; with WHERE or RETURNING also applies SELECT USING
BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
DELETE FROM rls_test_tgt RETURNING *;
+-- Test: INSERT ON CONFLICT DO NOTHING
+-- Verify INSERT CHECK is applied for all rows (even those that conflict)
INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+-- Test: INSERT ON CONFLICT DO UPDATE
+-- Verify INSERT CHECK on all rows, then UPDATE policies on conflicting rows
BEGIN;
INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
@@ -122,6 +142,8 @@ INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = '
INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
ROLLBACK;
+-- Test: MERGE operations
+-- Verify policies for MERGE variants match the documented table
MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
WHEN NOT MATCHED THEN DO NOTHING;
--
2.48.1
On Tue, Oct 21, 2025 at 12:01 AM Viktor Holmberg <v@viktorh.net> wrote:
So patch 0001, attached, adds a new set of regression tests, near the
start of rowsecurity.sql, which specifically tests which policies are
applied for each command variant.
hi.
I only applied the 0001.
it would be better to add some comments to the regress tests, IMHO.
for example, for below:
+SELECT * FROM rls_test_src FOR UPDATE;
+SELECT * FROM rls_test_src FOR NO KEY UPDATE;
+SELECT * FROM rls_test_src FOR SHARE;
+SELECT * FROM rls_test_src FOR KEY SHARE;
we could add a comment such as:
"Expect both UPDATE and the SELECT command policies to be invoked for
these four below query".
seems missing tests for INSERT ... ON CONFLICT DO NOTHING
which only INSERT policy to be invoked.
The 0001 regess tests define several functions: sel_using_fn,
ins_check_fn, upd_using_fn,
upd_check_fn, and del_using_fn.
IMHO, these could be simplified (we probably only need two functions).
see the attached version for my attempt to reduce them.
Attachments:
v2-0001-refactor-New-RLS-tests-to-test-policies-applied-by-command-typ.no-cfbotapplication/octet-stream; name=v2-0001-refactor-New-RLS-tests-to-test-policies-applied-by-command-typ.no-cfbotDownload
From 1a34324021bc6b30d519188cbe6f5d7a0f0e0859 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 23 Oct 2025 16:20:43 +0800
Subject: [PATCH v2 1/1] refactor New RLS tests to test policies applied by
command type.
discussion: https://postgr.es/m/CAEZATCWqnfeChjK=n1V_dYZT4rt4mnq+ybf9c0qXDYTVMsy8pg@mail.gmail.com
---
src/test/regress/expected/rowsecurity.out | 37 +++++++++--------------
src/test/regress/sql/rowsecurity.sql | 37 +++++++++--------------
2 files changed, 30 insertions(+), 44 deletions(-)
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 52da80bcf3a..f21d457c1ec 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -43,35 +43,26 @@ CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
LANGUAGE plpgsql;
CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
-CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
+CREATE FUNCTION policy_using_fn(text, text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE '% USING on %.%', $1, $2, $3; RETURN true; END; $$
LANGUAGE plpgsql;
-CREATE FUNCTION ins_check_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'INSERT CHECK on %.%', $1, $2; RETURN true; END; $$
- LANGUAGE plpgsql;
-CREATE FUNCTION upd_using_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'UPDATE USING on %.%', $1, $2; RETURN true; END; $$
- LANGUAGE plpgsql;
-CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'UPDATE CHECK on %.%', $1, $2; RETURN true; END; $$
- LANGUAGE plpgsql;
-CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
+CREATE FUNCTION policy_check_fn(text, text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE '% CHECK on %.%', $1, $2, $3; RETURN true; END; $$
LANGUAGE plpgsql;
CREATE POLICY sel_pol ON rls_test_src FOR SELECT
- USING (sel_using_fn('rls_test_src', rls_test_src));
+ USING (policy_using_fn('SELECT', 'rls_test_src', rls_test_src));
CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
- USING (upd_using_fn('rls_test_src', rls_test_src))
- WITH CHECK (upd_check_fn('rls_test_src', rls_test_src));
+ USING (policy_using_fn('UPDATE', 'rls_test_src', rls_test_src))
+ WITH CHECK (policy_check_fn('UPDATE', 'rls_test_src', rls_test_src));
CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
- USING (sel_using_fn('rls_test_tgt', rls_test_tgt));
+ USING (policy_using_fn('SELECT', 'rls_test_tgt', rls_test_tgt));
CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
- WITH CHECK (ins_check_fn('rls_test_tgt', rls_test_tgt));
+ WITH CHECK (policy_check_fn('INSERT', 'rls_test_tgt', rls_test_tgt));
CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
- USING (upd_using_fn('rls_test_tgt', rls_test_tgt))
- WITH CHECK (upd_check_fn('rls_test_tgt', rls_test_tgt));
+ USING (policy_using_fn('UPDATE', 'rls_test_tgt', rls_test_tgt))
+ WITH CHECK (policy_check_fn('UPDATE', 'rls_test_tgt', rls_test_tgt));
CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
- USING (del_using_fn('rls_test_tgt', rls_test_tgt));
+ USING (policy_using_fn('DELETE', 'rls_test_tgt', rls_test_tgt));
GRANT SELECT, UPDATE ON rls_test_src TO public;
GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
SET SESSION AUTHORIZATION regress_rls_bob;
@@ -162,6 +153,8 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
BEGIN;
INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt a","TGT A")
@@ -256,7 +249,7 @@ NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
RESET SESSION AUTHORIZATION;
DROP TABLE rls_test_src, rls_test_tgt;
DROP FUNCTION rls_test_tgt_set_c;
-DROP FUNCTION sel_using_fn, ins_check_fn, upd_using_fn, upd_check_fn, del_using_fn;
+DROP FUNCTION policy_using_fn, policy_check_fn;
-- BASIC Row-Level Security Scenario
SET SESSION AUTHORIZATION regress_rls_alice;
CREATE TABLE uaccount (
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 92423187dd5..7dfdf00a62e 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -57,37 +57,29 @@ CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
-CREATE FUNCTION sel_using_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'SELECT USING on %.%', $1, $2; RETURN true; END; $$
+CREATE FUNCTION policy_using_fn(text, text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE '% USING on %.%', $1, $2, $3; RETURN true; END; $$
LANGUAGE plpgsql;
-CREATE FUNCTION ins_check_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'INSERT CHECK on %.%', $1, $2; RETURN true; END; $$
- LANGUAGE plpgsql;
-CREATE FUNCTION upd_using_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'UPDATE USING on %.%', $1, $2; RETURN true; END; $$
- LANGUAGE plpgsql;
-CREATE FUNCTION upd_check_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'UPDATE CHECK on %.%', $1, $2; RETURN true; END; $$
- LANGUAGE plpgsql;
-CREATE FUNCTION del_using_fn(text, record) RETURNS bool AS
- $$ BEGIN RAISE NOTICE 'DELETE USING on %.%', $1, $2; RETURN true; END; $$
+
+CREATE FUNCTION policy_check_fn(text, text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE '% CHECK on %.%', $1, $2, $3; RETURN true; END; $$
LANGUAGE plpgsql;
CREATE POLICY sel_pol ON rls_test_src FOR SELECT
- USING (sel_using_fn('rls_test_src', rls_test_src));
+ USING (policy_using_fn('SELECT', 'rls_test_src', rls_test_src));
CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
- USING (upd_using_fn('rls_test_src', rls_test_src))
- WITH CHECK (upd_check_fn('rls_test_src', rls_test_src));
+ USING (policy_using_fn('UPDATE', 'rls_test_src', rls_test_src))
+ WITH CHECK (policy_check_fn('UPDATE', 'rls_test_src', rls_test_src));
CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
- USING (sel_using_fn('rls_test_tgt', rls_test_tgt));
+ USING (policy_using_fn('SELECT', 'rls_test_tgt', rls_test_tgt));
CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
- WITH CHECK (ins_check_fn('rls_test_tgt', rls_test_tgt));
+ WITH CHECK (policy_check_fn('INSERT', 'rls_test_tgt', rls_test_tgt));
CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
- USING (upd_using_fn('rls_test_tgt', rls_test_tgt))
- WITH CHECK (upd_check_fn('rls_test_tgt', rls_test_tgt));
+ USING (policy_using_fn('UPDATE', 'rls_test_tgt', rls_test_tgt))
+ WITH CHECK (policy_check_fn('UPDATE', 'rls_test_tgt', rls_test_tgt));
CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
- USING (del_using_fn('rls_test_tgt', rls_test_tgt));
+ USING (policy_using_fn('DELETE', 'rls_test_tgt', rls_test_tgt));
GRANT SELECT, UPDATE ON rls_test_src TO public;
GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
@@ -114,6 +106,7 @@ DELETE FROM rls_test_tgt RETURNING *;
INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT DO NOTHING;
BEGIN;
INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
@@ -150,7 +143,7 @@ MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
RESET SESSION AUTHORIZATION;
DROP TABLE rls_test_src, rls_test_tgt;
DROP FUNCTION rls_test_tgt_set_c;
-DROP FUNCTION sel_using_fn, ins_check_fn, upd_using_fn, upd_check_fn, del_using_fn;
+DROP FUNCTION policy_using_fn, policy_check_fn;
-- BASIC Row-Level Security Scenario
--
2.34.1
On Thu, 23 Oct 2025 at 09:23, jian he <jian.universality@gmail.com> wrote:
On Tue, Oct 21, 2025 at 12:01 AM Viktor Holmberg <v@viktorh.net> wrote:
So patch 0001, attached, adds a new set of regression tests, near the
start of rowsecurity.sql, which specifically tests which policies are
applied for each command variant.hi.
I only applied the 0001.it would be better to add some comments to the regress tests, IMHO. for example, for below: +SELECT * FROM rls_test_src FOR UPDATE; +SELECT * FROM rls_test_src FOR NO KEY UPDATE; +SELECT * FROM rls_test_src FOR SHARE; +SELECT * FROM rls_test_src FOR KEY SHARE;we could add a comment such as:
"Expect both UPDATE and the SELECT command policies to be invoked for
these four below query".
Thank you both for the reviews.
Attached is a new version with more comments in the tests, focusing on
what is expected from each test.
The 0001 regess tests define several functions: sel_using_fn,
ins_check_fn, upd_using_fn,
upd_check_fn, and del_using_fn.
IMHO, these could be simplified (we probably only need two functions).
Good point. Actually it can be done with just one function, further
reducing the amount of test code.
A recent commit reminded me that COPY ... TO also applies RLS SELECT
policies (and so does TABLE, though I doubt many people use that), so
I think it's worth testing and documenting those too. Updated patches
attached.
Regards,
Dean
Attachments:
v2-0002-doc-Improve-the-Policies-Applied-by-Command-Type-.patchapplication/x-patch; name=v2-0002-doc-Improve-the-Policies-Applied-by-Command-Type-.patchDownload
From e5f3fe63011b9912f7297d8f18077308ac91c2be Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 27 Mar 2025 14:19:15 +0000
Subject: [PATCH v2 2/2] doc: Improve the "Policies Applied by Command Type"
table.
This table was missing MERGE ... THEN DELETE and some of the policies
applied during INSERT ... ON CONFLICT and MERGE. Fix that, and try to
improve readability by listing the various MERGE cases separately,
rather than together with INSERT/UPDATE/DELETE.
In addition, correct the paragraph above the table to note that an
INSERT ... ON CONFLICT DO NOTHING/UPDATE checks all new rows against
the INSERT policy expressions, regardless of whether the rows end up
being inserted.
Author: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Viktor Holmberg <v@viktorh.net>
Discussion: https://postgr.es/m/CAEZATCWqnfeChjK=n1V_dYZT4rt4mnq+ybf9c0qXDYTVMsy8pg@mail.gmail.com
---
doc/src/sgml/ref/create_policy.sgml | 72 +++++++++++++++++++++++------
1 file changed, 58 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
index e76c342d3da..d6299a40844 100644
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -293,9 +293,9 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</para>
<para>
Note that <literal>INSERT</literal> with <literal>ON CONFLICT DO
- UPDATE</literal> checks <literal>INSERT</literal> policies'
- <literal>WITH CHECK</literal> expressions only for rows appended
- to the relation by the <literal>INSERT</literal> path.
+ NOTHING/UPDATE</literal> checks <literal>INSERT</literal> policies'
+ <literal>WITH CHECK</literal> expressions for all rows, regardless
+ of whether they end up being inserted.
</para>
</listitem>
</varlistentry>
@@ -424,7 +424,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</thead>
<tbody>
<row>
- <entry><command>SELECT</command></entry>
+ <entry><command>SELECT</command> / <command>COPY ... TO</command> / <command>TABLE</command></entry>
<entry>Existing row</entry>
<entry>—</entry>
<entry>—</entry>
@@ -440,15 +440,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>INSERT</command> / <command>MERGE ... THEN INSERT</command></entry>
- <entry>—</entry>
- <entry>New row</entry>
- <entry>—</entry>
- <entry>—</entry>
- <entry>—</entry>
- </row>
- <row>
- <entry><command>INSERT ... RETURNING</command></entry>
+ <entry><command>INSERT</command></entry>
<entry>
New row <footnote id="rls-select-priv">
<para>
@@ -464,7 +456,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>UPDATE</command> / <command>MERGE ... THEN UPDATE</command></entry>
+ <entry><command>UPDATE</command></entry>
<entry>
Existing & new rows <footnoteref linkend="rls-select-priv"/>
</entry>
@@ -483,14 +475,66 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
<entry>Existing row</entry>
</row>
+ <row>
+ <entry><command>INSERT ... ON CONFLICT</command></entry>
+ <entry>New row</entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
<row>
<entry><command>ON CONFLICT DO UPDATE</command></entry>
+ <entry>
+ Existing & new rows <footnote id="rls-on-conflict-update-priv">
+ <para>
+ New row of the auxiliary <command>UPDATE</command> command, which
+ might be different from the new row of the original
+ <command>INSERT</command> command.
+ </para>
+ </footnote>
+ </entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ <entry>
+ New row <footnoteref linkend="rls-on-conflict-update-priv"/>
+ </entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE</command></entry>
+ <entry>All candidate source & target rows</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN INSERT</command></entry>
+ <entry>
+ New row <footnoteref linkend="rls-select-priv"/>
+ </entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN UPDATE</command></entry>
<entry>Existing & new rows</entry>
<entry>—</entry>
<entry>Existing row</entry>
<entry>New row</entry>
<entry>—</entry>
</row>
+ <row>
+ <entry><command>MERGE ... THEN DELETE</command></entry>
+ <entry>Existing row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ </row>
</tbody>
</tgroup>
</table>
--
2.51.0
v2-0001-New-RLS-tests-to-test-policies-applied-by-command.patchapplication/x-patch; name=v2-0001-New-RLS-tests-to-test-policies-applied-by-command.patchDownload
From 076102e057dd301e23791eac0956193d6428ac3a Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 27 Mar 2025 14:08:09 +0000
Subject: [PATCH v2 1/2] New RLS tests to test policies applied by command
type.
The existing RLS tests focus on the outcomes of various testing
scenarios, rather than the exact policies applied. These new tests
list out the policies applied for each command type, including the
different paths through INSERT ... ON CONFLICT and MERGE, making it
easier to verify correct behaviour.
Author: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Viktor Holmberg <v@viktorh.net>
Reviewed-by: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/CAEZATCWqnfeChjK=n1V_dYZT4rt4mnq+ybf9c0qXDYTVMsy8pg@mail.gmail.com
---
src/test/regress/expected/rowsecurity.out | 259 ++++++++++++++++++++++
src/test/regress/sql/rowsecurity.sql | 139 ++++++++++++
2 files changed, 398 insertions(+)
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 42b78a24603..c958ef4d70a 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -31,6 +31,265 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
COST 0.0000001 LANGUAGE plpgsql
AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+--
+-- Test policies applied by command type
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+-- setup source table (for MERGE operations)
+CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
+ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
+GRANT SELECT, UPDATE ON rls_test_src TO public;
+INSERT INTO rls_test_src VALUES (1, 'src a');
+-- setup target table with a column set by a BEFORE ROW trigger
+-- (policies should always see values set by the trigger)
+CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
+ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
+GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
+CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
+ $$ BEGIN new.c = upper(new.b); RETURN new; END; $$
+ LANGUAGE plpgsql;
+CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
+ FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+-- setup a complete set of policies that emit NOTICE messages when applied
+CREATE FUNCTION rls_test_policy_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE '%.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+CREATE POLICY sel_pol ON rls_test_src FOR SELECT
+ USING (rls_test_policy_fn('SELECT USING on rls_test_src', rls_test_src));
+CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
+ USING (rls_test_policy_fn('UPDATE USING on rls_test_src', rls_test_src))
+ WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_src', rls_test_src));
+CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
+ USING (rls_test_policy_fn('SELECT USING on rls_test_tgt', rls_test_tgt));
+CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
+ WITH CHECK (rls_test_policy_fn('INSERT CHECK on rls_test_tgt', rls_test_tgt));
+CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
+ USING (rls_test_policy_fn('UPDATE USING on rls_test_tgt', rls_test_tgt))
+ WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_tgt', rls_test_tgt));
+CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
+ USING (rls_test_policy_fn('DELETE USING on rls_test_tgt', rls_test_tgt));
+-- test policies applied to regress_rls_bob
+SET SESSION AUTHORIZATION regress_rls_bob;
+-- SELECT, COPY ... TO, and TABLE should only apply SELECT USING policy clause
+SELECT * FROM rls_test_src;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+COPY rls_test_src TO stdout;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+1 src a
+TABLE rls_test_src;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+-- SELECT ... FOR UPDATE/SHARE should also apply UPDATE USING policy clause
+SELECT * FROM rls_test_src FOR UPDATE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR NO KEY UPDATE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR SHARE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+SELECT * FROM rls_test_src FOR KEY SHARE;
+NOTICE: UPDATE USING on rls_test_src.(1,"src a")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+ a | b
+---+-------
+ 1 | src a
+(1 row)
+
+-- plain INSERT should apply INSERT CHECK policy clause
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+-- INSERT ... RETURNING should also apply SELECT USING policy clause
+TRUNCATE rls_test_tgt;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+ a | b | c
+---+-------+-------
+ 1 | tgt a | TGT A
+(1 row)
+
+-- UPDATE without WHERE or RETURNING should only apply UPDATE policy clauses
+UPDATE rls_test_tgt SET b = 'tgt b';
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+-- UPDATE with WHERE or RETURNING should also apply SELECT USING policy clause
+-- (to both old and new values)
+UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 1 | tgt d | TGT D
+(1 row)
+
+-- DELETE without WHERE or RETURNING should only apply DELETE USING policy clause
+BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+-- DELETE with WHERE or RETURNING should also apply SELECT USING policy clause
+BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+DELETE FROM rls_test_tgt RETURNING *;
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 1 | tgt d | TGT D
+(1 row)
+
+-- INSERT ... ON CONFLICT DO NOTHING should apply INSERT CHECK and SELECT USING
+-- policy clauses (to new value, whether it conflicts or not)
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+-- INSERT ... ON CONFLICT DO NOTHING without an arbiter clause only applies
+-- INSERT CHECK policy clause
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT DO NOTHING;
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+-- INSERT ... ON CONFLICT DO UPDATE should apply INSERT CHECK and SELECT USING
+-- policy clauses to values proposed for insert. In the event of a conflict it
+-- should also apply UPDATE and SELECT policies to old and new values, like
+-- UPDATE ... WHERE.
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
+NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A")
+INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
+NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt c","TGT C")
+NOTICE: UPDATE USING on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(2,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(2,"tgt d","TGT D")
+INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A")
+ a | b | c
+---+-------+-------
+ 3 | tgt a | TGT A
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
+NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt c","TGT C")
+NOTICE: UPDATE USING on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(3,"tgt d","TGT D")
+NOTICE: SELECT USING on rls_test_tgt.(3,"tgt d","TGT D")
+ a | b | c
+---+-------+-------
+ 3 | tgt d | TGT D
+(1 row)
+
+ROLLBACK;
+-- MERGE should always apply SELECT USING policy clauses to both source and
+-- target rows
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN DO NOTHING;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+-- MERGE ... INSERT should behave like INSERT on target table
+-- (SELECT policy applied to target, if RETURNING is specified)
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a');
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a')
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt a | TGT A
+(1 row)
+
+-- MERGE ... UPDATE should behave like UPDATE ... WHERE on target table
+-- (join clause is like WHERE, so SELECT policies are always applied)
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt b';
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt c'
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B")
+NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt c | TGT C
+(1 row)
+
+-- MERGE ... DELETE should behave like DELETE ... WHERE on target table
+-- (join clause is like WHERE, so SELECT policies are always applied)
+BEGIN;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE
+ RETURNING *;
+NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C")
+NOTICE: SELECT USING on rls_test_src.(1,"src a")
+NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
+ a | b | a | b | c
+---+-------+---+-------+-------
+ 1 | src a | 1 | tgt c | TGT C
+(1 row)
+
+-- Tidy up
+RESET SESSION AUTHORIZATION;
+DROP TABLE rls_test_src, rls_test_tgt;
+DROP FUNCTION rls_test_tgt_set_c;
+DROP FUNCTION rls_test_policy_fn;
-- BASIC Row-Level Security Scenario
SET SESSION AUTHORIZATION regress_rls_alice;
CREATE TABLE uaccount (
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 2d1be543391..5d923c5ca3b 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -41,6 +41,145 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+--
+-- Test policies applied by command type
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+
+-- setup source table (for MERGE operations)
+CREATE TABLE rls_test_src (a int PRIMARY KEY, b text);
+ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY;
+GRANT SELECT, UPDATE ON rls_test_src TO public;
+INSERT INTO rls_test_src VALUES (1, 'src a');
+
+-- setup target table with a column set by a BEFORE ROW trigger
+-- (policies should always see values set by the trigger)
+CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text);
+ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY;
+GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public;
+
+CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS
+ $$ BEGIN new.c = upper(new.b); RETURN new; END; $$
+ LANGUAGE plpgsql;
+CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt
+ FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c();
+
+-- setup a complete set of policies that emit NOTICE messages when applied
+CREATE FUNCTION rls_test_policy_fn(text, record) RETURNS bool AS
+ $$ BEGIN RAISE NOTICE '%.%', $1, $2; RETURN true; END; $$
+ LANGUAGE plpgsql;
+
+CREATE POLICY sel_pol ON rls_test_src FOR SELECT
+ USING (rls_test_policy_fn('SELECT USING on rls_test_src', rls_test_src));
+CREATE POLICY upd_pol ON rls_test_src FOR UPDATE
+ USING (rls_test_policy_fn('UPDATE USING on rls_test_src', rls_test_src))
+ WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_src', rls_test_src));
+
+CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT
+ USING (rls_test_policy_fn('SELECT USING on rls_test_tgt', rls_test_tgt));
+CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT
+ WITH CHECK (rls_test_policy_fn('INSERT CHECK on rls_test_tgt', rls_test_tgt));
+CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
+ USING (rls_test_policy_fn('UPDATE USING on rls_test_tgt', rls_test_tgt))
+ WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_tgt', rls_test_tgt));
+CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
+ USING (rls_test_policy_fn('DELETE USING on rls_test_tgt', rls_test_tgt));
+
+-- test policies applied to regress_rls_bob
+SET SESSION AUTHORIZATION regress_rls_bob;
+
+-- SELECT, COPY ... TO, and TABLE should only apply SELECT USING policy clause
+SELECT * FROM rls_test_src;
+COPY rls_test_src TO stdout;
+TABLE rls_test_src;
+
+-- SELECT ... FOR UPDATE/SHARE should also apply UPDATE USING policy clause
+SELECT * FROM rls_test_src FOR UPDATE;
+SELECT * FROM rls_test_src FOR NO KEY UPDATE;
+SELECT * FROM rls_test_src FOR SHARE;
+SELECT * FROM rls_test_src FOR KEY SHARE;
+
+-- plain INSERT should apply INSERT CHECK policy clause
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a');
+
+-- INSERT ... RETURNING should also apply SELECT USING policy clause
+TRUNCATE rls_test_tgt;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *;
+
+-- UPDATE without WHERE or RETURNING should only apply UPDATE policy clauses
+UPDATE rls_test_tgt SET b = 'tgt b';
+
+-- UPDATE with WHERE or RETURNING should also apply SELECT USING policy clause
+-- (to both old and new values)
+UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1;
+UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *;
+
+-- DELETE without WHERE or RETURNING should only apply DELETE USING policy clause
+BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
+
+-- DELETE with WHERE or RETURNING should also apply SELECT USING policy clause
+BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
+DELETE FROM rls_test_tgt RETURNING *;
+
+-- INSERT ... ON CONFLICT DO NOTHING should apply INSERT CHECK and SELECT USING
+-- policy clauses (to new value, whether it conflicts or not)
+INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING;
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING;
+
+-- INSERT ... ON CONFLICT DO NOTHING without an arbiter clause only applies
+-- INSERT CHECK policy clause
+INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT DO NOTHING;
+
+-- INSERT ... ON CONFLICT DO UPDATE should apply INSERT CHECK and SELECT USING
+-- policy clauses to values proposed for insert. In the event of a conflict it
+-- should also apply UPDATE and SELECT policies to old and new values, like
+-- UPDATE ... WHERE.
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b';
+INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d';
+INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *;
+INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
+ROLLBACK;
+
+-- MERGE should always apply SELECT USING policy clauses to both source and
+-- target rows
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN DO NOTHING;
+
+-- MERGE ... INSERT should behave like INSERT on target table
+-- (SELECT policy applied to target, if RETURNING is specified)
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a');
+TRUNCATE rls_test_tgt;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a')
+ RETURNING *;
+
+-- MERGE ... UPDATE should behave like UPDATE ... WHERE on target table
+-- (join clause is like WHERE, so SELECT policies are always applied)
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt b';
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN UPDATE SET b = 'tgt c'
+ RETURNING *;
+
+-- MERGE ... DELETE should behave like DELETE ... WHERE on target table
+-- (join clause is like WHERE, so SELECT policies are always applied)
+BEGIN;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE;
+ROLLBACK;
+MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
+ WHEN MATCHED THEN DELETE
+ RETURNING *;
+
+-- Tidy up
+RESET SESSION AUTHORIZATION;
+DROP TABLE rls_test_src, rls_test_tgt;
+DROP FUNCTION rls_test_tgt_set_c;
+DROP FUNCTION rls_test_policy_fn;
+
-- BASIC Row-Level Security Scenario
SET SESSION AUTHORIZATION regress_rls_alice;
--
2.51.0
Looks great. The test is easy to understand now. I’ve set the commitfest entry to “ready for committer”.
/Viktor
Show quoted text
On 20 Oct 2025 at 15:29 +0200, Dean Rasheed <dean.a.rasheed@gmail.com>, wrote:
While looking at the INSERT ... ON CONFLICT DO SELECT patch, I noticed
that the "Policies Applied by Command Type" table on the CREATE POLICY
page doesn't fully or accurately describe all the policies that are
actually checked in all cases:* INSERT ON CONFLICT checks the new row from the INSERT against SELECT
policy expressions, regardless of what ON CONFLICT action is
performed.* If an ON CONFLICT DO UPDATE is executed, the new row from the
auxiliary UPDATE command is also checked against SELECT policy
expressions.* MERGE always checks all candidate source and target rows against
SELECT policy expressions, even if no action is performed.* MERGE ... THEN INSERT checks the new row against SELECT policy
expressions, if there is a RETURNING clause.* MERGE ... THEN UPDATE always checks the new and existing rows
against SELECT policy expressions, even if there is no RETURNING
clause.* MERGE ... THEN DELETE isn't mentioned at all. It always checks the
existing row against SELECT policy expressions.I think having MERGE use the same row in the doc table as other
commands makes it harder to read, and it would be better to just list
each of the MERGE cases separately, even if that does involve some
repetition.In addition, a paragraph above the table for INSERT policies says:
"""
Note that INSERT with ON CONFLICT DO UPDATE checks INSERT policies'
WITH CHECK expressions only for rows appended to the relation by the
INSERT path.
"""Maybe that was once true, but it isn't true now, in any supported PG
version. The WITH CHECK expressions from INSERT policies are always
checked, regardless of which path it ends up taking.I think it would be good to have regression tests specifically
covering all these cases. Yes, there are a lot of existing RLS
regression tests, but they tend to cover more complex scenarios, and
focus on whether the result of the command was what was expected,
rather than precisely which policies were checked in the process.
Thus, it's not obvious whether they provide complete coverage.So patch 0001, attached, adds a new set of regression tests, near the
start of rowsecurity.sql, which specifically tests which policies are
applied for each command variant.Patch 0002 updates the doc table to try to be clearer and more
accurate, and consistent with the test results from 0001, and fixes
the paragraph mentioned above.Regards,
Dean
On Thu, Oct 23, 2025 at 11:15 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Attached is a new version with more comments in the tests, focusing on
what is expected from each test.The 0001 regess tests define several functions: sel_using_fn,
ins_check_fn, upd_using_fn,
upd_check_fn, and del_using_fn.
IMHO, these could be simplified (we probably only need two functions).Good point. Actually it can be done with just one function, further
reducing the amount of test code.
hi.
v2-0001 looks good to me.
A recent commit reminded me that COPY ... TO also applies RLS SELECT
policies (and so does TABLE, though I doubt many people use that), so
I think it's worth testing and documenting those too. Updated patches
attached.
other Utility commands will also invoke the SELECT/UPDATE policy.
The below several commands will invoke SELECT or UPDATE policy,
if rls_test_src have SELECT or UPDATE policy on it.
create table sss as SELECT * FROM rls_test_src FOR UPDATE;
explain analyze SELECT * FROM rls_test_src FOR UPDATE;
PREPARE q1 AS SELECT * FROM rls_test_src FOR UPDATE;
EXECUTE q1;
create MATERIALIZED view mv as SELECT * FROM rls_test_src FOR UPDATE
with no data;
REFRESH MATERIALIZED VIEW mv;
create MATERIALIZED view mv1 as SELECT * FROM rls_test_src FOR UPDATE;
DECLARE curs1 CURSOR WITH HOLD FOR SELECT * FROM rls_test_src;
While at it, I found out that
table "Policies Applied by Command Type" was missing SELECT FOR NO KEY UPDATE
and SELECT FOR KEY SHARE.
While at it create_policy.sgml, I am not sure the below sentence is
not fully accurate.
""
If an INSERT or UPDATE command attempts to add rows to the table that do not
pass the ALL policy's WITH CHECK expression, the entire command will be aborted.
""
The above sentence fails to mention the case when the WITH CHECK
expression does not exist.
for example:
create table tts(x int);
CREATE POLICY p1 ON tts FOR all using (x = 1);
grant select, insert on tts to alice;
alter table tts ENABLE ROW LEVEL SECURITY;
set role alice;
insert into tts values (2);
On Mon, 27 Oct 2025 at 05:03, jian he <jian.universality@gmail.com> wrote:
v2-0001 looks good to me.
Thanks. I've pushed that one.
A recent commit reminded me that COPY ... TO also applies RLS SELECT
policies (and so does TABLE, though I doubt many people use that), so
I think it's worth testing and documenting those too. Updated patches
attached.other Utility commands will also invoke the SELECT/UPDATE policy.
The below several commands will invoke SELECT or UPDATE policy,
if rls_test_src have SELECT or UPDATE policy on it.
I don't think it's worth documenting every single command that
includes a SELECT somewhere in it. Adding too much to the docs makes
them harder to read, not easier, and I think it's pretty clear that
these examples end up executing a SELECT, and so it should be clear
that they apply the RLS SELECT/UPDATE policies.
In fact, on reflection, I don't think it's worth mentioning TABLE here
either, since it's not really a separate command. It doesn't have its
own doc page, but instead is only mentioned on the SELECT doc page,
which says that it's equivalent to SELECT * FROM table, so it should
be clear that it applies SELECT policies.
So I think I'll stick to just mentioning COPY .. TO, since it might
not otherwise be obvious that it does apply RLS SELECT policies.
While at it, I found out that
table "Policies Applied by Command Type" was missing SELECT FOR NO KEY UPDATE
and SELECT FOR KEY SHARE.
I don't think that's necessary. We could try to say something like
"SELECT ... FOR [NO KEY] UPDATE / [KEY] SHARE", but I think that would
make it harder to read, given the lack of space in that table. Several
other places in the documentation already use the text "FOR
UPDATE/SHARE" to include all 4 variants of SELECT row locking,
including the SELECT doc page itself, so I think it should be
sufficient here too.
While at it create_policy.sgml, I am not sure the below sentence is
not fully accurate.
""
If an INSERT or UPDATE command attempts to add rows to the table that do not
pass the ALL policy's WITH CHECK expression, the entire command will be aborted.
""
The above sentence fails to mention the case when the WITH CHECK
expression does not exist.
Hmm, the sentence immediately before that explains that the USING
expression will be used to check new rows, if there is no WITH CHECK
expression, but that's using UPDATE as an example, so I guess it's
worth being clear that the same applies to an INSERT.
Updated patch attached.
Regards,
Dean
Attachments:
v3-0001-doc-Improve-the-Policies-Applied-by-Command-Type-.patchtext/x-patch; charset=US-ASCII; name=v3-0001-doc-Improve-the-Policies-Applied-by-Command-Type-.patchDownload
From 8b5c0e3db182809c21694f313333f260f2a19258 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Mon, 27 Oct 2025 11:09:35 +0000
Subject: [PATCH v3] doc: Improve the "Policies Applied by Command Type" table.
This table was missing MERGE ... THEN DELETE and some of the policies
applied during INSERT ... ON CONFLICT and MERGE. Fix that, and try to
improve readability by listing the various MERGE cases separately,
rather than together with INSERT/UPDATE/DELETE. In addition, mention
COPY ... TO along with SELECT, since it behaves in the same way.
In addition, correct the paragraph above the table to note that an
INSERT ... ON CONFLICT DO NOTHING/UPDATE checks all new rows against
the INSERT policy expressions, regardless of whether the rows end up
being inserted.
Also, in the description of ALL policies, clarify that an ALL policy
applied to an INSERT will use the policy's USING expression to check
new rows, if it lacks a WITH CHECK expression.
Author: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Viktor Holmberg <v@viktorh.net>
Reviewed-by: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/CAEZATCWqnfeChjK=n1V_dYZT4rt4mnq+ybf9c0qXDYTVMsy8pg@mail.gmail.com
---
doc/src/sgml/ref/create_policy.sgml | 76 +++++++++++++++++++++++------
1 file changed, 61 insertions(+), 15 deletions(-)
diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
index e76c342d3da..13adb5ecae8 100644
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -251,7 +251,9 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
otherwise). If an <command>INSERT</command>
or <command>UPDATE</command> command attempts to add rows to the
table that do not pass the <literal>ALL</literal>
- policy's <literal>WITH CHECK</literal> expression, the entire
+ policy's <literal>WITH CHECK</literal> expression (or its
+ <literal>USING</literal> expression, if it does not have a
+ <literal>WITH CHECK</literal> expression), the entire
command will be aborted.
</para>
</listitem>
@@ -293,9 +295,9 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</para>
<para>
Note that <literal>INSERT</literal> with <literal>ON CONFLICT DO
- UPDATE</literal> checks <literal>INSERT</literal> policies'
- <literal>WITH CHECK</literal> expressions only for rows appended
- to the relation by the <literal>INSERT</literal> path.
+ NOTHING/UPDATE</literal> checks <literal>INSERT</literal> policies'
+ <literal>WITH CHECK</literal> expressions for all rows, regardless
+ of whether they end up being inserted.
</para>
</listitem>
</varlistentry>
@@ -424,7 +426,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</thead>
<tbody>
<row>
- <entry><command>SELECT</command></entry>
+ <entry><command>SELECT</command> / <command>COPY ... TO</command></entry>
<entry>Existing row</entry>
<entry>—</entry>
<entry>—</entry>
@@ -440,15 +442,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>INSERT</command> / <command>MERGE ... THEN INSERT</command></entry>
- <entry>—</entry>
- <entry>New row</entry>
- <entry>—</entry>
- <entry>—</entry>
- <entry>—</entry>
- </row>
- <row>
- <entry><command>INSERT ... RETURNING</command></entry>
+ <entry><command>INSERT</command></entry>
<entry>
New row <footnote id="rls-select-priv">
<para>
@@ -464,7 +458,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
</row>
<row>
- <entry><command>UPDATE</command> / <command>MERGE ... THEN UPDATE</command></entry>
+ <entry><command>UPDATE</command></entry>
<entry>
Existing & new rows <footnoteref linkend="rls-select-priv"/>
</entry>
@@ -483,14 +477,66 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>—</entry>
<entry>Existing row</entry>
</row>
+ <row>
+ <entry><command>INSERT ... ON CONFLICT</command></entry>
+ <entry>New row</entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
<row>
<entry><command>ON CONFLICT DO UPDATE</command></entry>
+ <entry>
+ Existing & new rows <footnote id="rls-on-conflict-update-priv">
+ <para>
+ New row of the auxiliary <command>UPDATE</command> command, which
+ might be different from the new row of the original
+ <command>INSERT</command> command.
+ </para>
+ </footnote>
+ </entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ <entry>
+ New row <footnoteref linkend="rls-on-conflict-update-priv"/>
+ </entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE</command></entry>
+ <entry>All candidate source & target rows</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN INSERT</command></entry>
+ <entry>
+ New row <footnoteref linkend="rls-select-priv"/>
+ </entry>
+ <entry>New row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN UPDATE</command></entry>
<entry>Existing & new rows</entry>
<entry>—</entry>
<entry>Existing row</entry>
<entry>New row</entry>
<entry>—</entry>
</row>
+ <row>
+ <entry><command>MERGE ... THEN DELETE</command></entry>
+ <entry>Existing row</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>Existing row</entry>
+ </row>
</tbody>
</tgroup>
</table>
--
2.51.0
On Mon, Oct 27, 2025 at 7:26 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Updated patch attached.
hi.
TABLE, Policies Applied by Command Type
MERGE related change looks very similar to standalone INSERT/UPDATE/DELETE.
overall v3-0001 looks good to me.
while reading the doc, this description in UPDATE section:
"""
Note, however, that unlike a standalone UPDATE command, if the existing row does
not pass the USING expressions, an error will be thrown (the UPDATE path will
never be silently avoided).
""
I think the above statement also applies to MERGE ... THEN UPDATE.
Perhaps the table “Policies Applied by Command Type” already conveys this,
but I’m not sure.
below shows that MERGE ... THEN UPDATE USING expression does not pass will
result error
begin;
reset role;
drop table if exists tts;
create table tts(a int , b int);
insert into tts values (4, 5), (2,5), (3, 5);
CREATE POLICY p1 ON tts FOR SELECT USING (a < 3);
CREATE POLICY p3 ON tts FOR UPDATE USING (a > 3) WITH CHECK (b = 5);
grant all on tts to alice;
ALTER TABLE tts ENABLE ROW LEVEL SECURITY;
commit;
BEGIN;
SET ROLE alice;
MERGE INTO tts d USING (SELECT 2 as sdid) s ON a = s.sdid
WHEN MATCHED THEN
UPDATE SET b = 5;
ROLLBACK;
On Tue, 28 Oct 2025 at 03:34, jian he <jian.universality@gmail.com> wrote:
while reading the doc, this description in UPDATE section:
"""
Note, however, that unlike a standalone UPDATE command, if the existing row does
not pass the USING expressions, an error will be thrown (the UPDATE path will
never be silently avoided).
""I think the above statement also applies to MERGE ... THEN UPDATE.
Perhaps the table “Policies Applied by Command Type” already conveys this,
but I’m not sure.
Yeah, reading through the text on that page in more detail, there are
a number of other omissions, or places that aren't quite fully
correct, so I've gone through those and attempted to improve things.
Also, I think it would be better if the table made the distinction
between policy checks that just filter out rows, without throwing an
error, and checks that do cause an error to be thrown.
v4 attached.
Regards,
Dean
Attachments:
v4-0001-doc-Improve-description-of-RLS-policies-applied-b.patchtext/x-patch; charset=US-ASCII; name=v4-0001-doc-Improve-description-of-RLS-policies-applied-b.patchDownload
From 808a53af2eb883d257c7d49ebdc3eccf40825703 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Mon, 27 Oct 2025 11:09:35 +0000
Subject: [PATCH v4] doc: Improve description of RLS policies applied by
command type.
On the CREATE POLICY page, the "Policies Applied by Command Type"
table was missing MERGE ... THEN DELETE and some of the policies
applied during INSERT ... ON CONFLICT and MERGE. Fix that, and try to
improve readability by listing the various MERGE cases separately,
rather than together with INSERT/UPDATE/DELETE. Mention COPY ... TO
along with SELECT, since it behaves in the same way. In addition,
document which policy violations cause errors to be thrown, and which
just cause rows to be silently ignored.
A paragraph above states that INSERT ... ON CONFLICT DO UPDATE only
checks the WITH CHECK expressions of INSERT policies for rows appended
to the relation by the INSERT path, which is incorrect - all rows
proposed for insertion are checked, regardless of whether they end up
being inserted. Fix that, and also mention that the same applies to
INSERT ... ON CONFLICT DO NOTHING.
In addition, in various other places on that page, clarify how the
different types of policy are applied to different commands, and
whether or not errors are thrown when policy checks do not pass.
Author: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Viktor Holmberg <v@viktorh.net>
Reviewed-by: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/CAEZATCWqnfeChjK=n1V_dYZT4rt4mnq+ybf9c0qXDYTVMsy8pg@mail.gmail.com
---
doc/src/sgml/ref/create_policy.sgml | 203 ++++++++++++++++++++++------
1 file changed, 159 insertions(+), 44 deletions(-)
diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
index e76c342d3da..42d43ad7bf4 100644
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -49,6 +49,8 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
in <literal>WITH CHECK</literal>. When a <literal>USING</literal>
expression returns true for a given row then that row is visible to the
user, while if false or null is returned then the row is not visible.
+ Typically, no error occurs when a row is not visible, but see
+ <xref linkend="sql-createpolicy-summary"/> for exceptions.
When a <literal>WITH CHECK</literal> expression returns true for a row
then that row is inserted or updated, while if false or null is returned
then an error occurs.
@@ -194,8 +196,9 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
rows for which the expression returns false or null will not be
visible to the user (in a <command>SELECT</command>), and will not be
available for modification (in an <command>UPDATE</command>
- or <command>DELETE</command>). Such rows are silently suppressed; no error
- is reported.
+ or <command>DELETE</command>). Typically, such rows are silently
+ suppressed; no error is reported (but see
+ <xref linkend="sql-createpolicy-summary"/> for exceptions).
</para>
</listitem>
</varlistentry>
@@ -251,8 +254,10 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
otherwise). If an <command>INSERT</command>
or <command>UPDATE</command> command attempts to add rows to the
table that do not pass the <literal>ALL</literal>
- policy's <literal>WITH CHECK</literal> expression, the entire
- command will be aborted.
+ policy's <literal>WITH CHECK</literal> expression (or its
+ <literal>USING</literal> expression, if it does not have a
+ <literal>WITH CHECK</literal> expression), the entire command will
+ be aborted.
</para>
</listitem>
</varlistentry>
@@ -268,11 +273,50 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
relation that pass the <literal>SELECT</literal> policy will be
returned during a <literal>SELECT</literal> query, and that queries
that require <literal>SELECT</literal> permissions, such as
- <literal>UPDATE</literal>, will also only see those records
+ <literal>UPDATE</literal>, <literal>DELETE</literal>, and
+ <literal>MERGE</literal>, will also only see those records
that are allowed by the <literal>SELECT</literal> policy.
A <literal>SELECT</literal> policy cannot have a <literal>WITH
CHECK</literal> expression, as it only applies in cases where
- records are being retrieved from the relation.
+ records are being retrieved from the relation, except as described
+ below.
+ </para>
+ <para>
+ If a data-modifying query has a <literal>RETURNING</literal> clause,
+ <literal>SELECT</literal> permissions are required on the relation,
+ and any newly inserted or updated rows from the relation must satisfy
+ the relation's <literal>SELECT</literal> policies in order to be
+ available to the <literal>RETURNING</literal> clause. If a newly
+ inserted or updated row does not satisfy the relation's
+ <literal>SELECT</literal> policies, an error will be thrown (inserted
+ or updated rows to be returned are <emphasis>never</emphasis>
+ silently ignored).
+ </para>
+ <para>
+ If an <literal>INSERT</literal> has an <literal>ON CONFLICT DO
+ NOTHING/UPDATE</literal> clause, <literal>SELECT</literal>
+ permissions are required on the relation, and the rows proposed for
+ insertion are checked using the relation's <literal>SELECT</literal>
+ policies. If a row proposed for insertion does not satisfy the
+ relation's <literal>SELECT</literal> policies, an error is thrown
+ (the <literal>INSERT</literal> is <emphasis>never</emphasis> silently
+ avoided). In addition, if the <literal>UPDATE</literal> path is
+ taken, the row to be updated and the new updated row are checked
+ against the relation's <literal>SELECT</literal> policies, and an
+ error is thrown if they are not satisfied (an auxiliary
+ <literal>UPDATE</literal> is <emphasis>never</emphasis> silently
+ avoided).
+ </para>
+ <para>
+ A <literal>MERGE</literal> command requires <literal>SELECT</literal>
+ permissions on both the source and target relations, and so each
+ relation's <literal>SELECT</literal> policies are applied before they
+ are joined, and the <literal>MERGE</literal> actions will only see
+ those records that are allowed by those policies. In addition, if
+ an <literal>UPDATE</literal> action is executed, the target relation's
+ <literal>SELECT</literal> policies are applied to the updated row, as
+ for a standalone <literal>UPDATE</literal>, except that an error is
+ thrown if they are not satisfied.
</para>
</listitem>
</varlistentry>
@@ -292,10 +336,11 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
where records are being added to the relation.
</para>
<para>
- Note that <literal>INSERT</literal> with <literal>ON CONFLICT DO
- UPDATE</literal> checks <literal>INSERT</literal> policies'
- <literal>WITH CHECK</literal> expressions only for rows appended
- to the relation by the <literal>INSERT</literal> path.
+ Note that an <literal>INSERT</literal> with an <literal>ON CONFLICT
+ DO NOTHING/UPDATE</literal> clause will check the
+ <literal>INSERT</literal> policies' <literal>WITH CHECK</literal>
+ expressions for all rows proposed for insertion, regardless of
+ whether or not they end up being inserted.
</para>
</listitem>
</varlistentry>
@@ -305,12 +350,12 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<listitem>
<para>
Using <literal>UPDATE</literal> for a policy means that it will apply
- to <literal>UPDATE</literal>, <literal>SELECT FOR UPDATE</literal>
+ to <literal>UPDATE</literal>, <literal>SELECT FOR UPDATE</literal>,
and <literal>SELECT FOR SHARE</literal> commands, as well as
auxiliary <literal>ON CONFLICT DO UPDATE</literal> clauses of
- <literal>INSERT</literal> commands.
- <literal>MERGE</literal> commands containing <literal>UPDATE</literal>
- actions are affected as well. Since <literal>UPDATE</literal>
+ <literal>INSERT</literal> commands, and <literal>MERGE</literal>
+ commands containing <literal>UPDATE</literal> actions.
+ Since an <literal>UPDATE</literal> command
involves pulling an existing record and replacing it with a new
modified record, <literal>UPDATE</literal>
policies accept both a <literal>USING</literal> expression and
@@ -356,7 +401,8 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
command, if the existing row does not pass the
<literal>USING</literal> expressions, an error will be thrown (the
<literal>UPDATE</literal> path will <emphasis>never</emphasis> be silently
- avoided).
+ avoided). The same applies to an <literal>UPDATE</literal> action
+ of a <command>MERGE</command> command.
</para>
</listitem>
</varlistentry>
@@ -366,12 +412,18 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<listitem>
<para>
Using <literal>DELETE</literal> for a policy means that it will apply
- to <literal>DELETE</literal> commands. Only rows that pass this
- policy will be seen by a <literal>DELETE</literal> command. There can
- be rows that are visible through a <literal>SELECT</literal> that are
- not available for deletion, if they do not pass the
- <literal>USING</literal> expression for
- the <literal>DELETE</literal> policy.
+ to <literal>DELETE</literal> commands and <literal>MERGE</literal>
+ commands containing <literal>DELETE</literal> actions. For a
+ <literal>DELETE</literal> command, only rows that pass this policy
+ will be seen by the <literal>DELETE</literal> command. There can
+ be rows that are visible through a <literal>SELECT</literal> policy
+ that are not available for deletion, if they do not pass the
+ <literal>USING</literal> expression for the <literal>DELETE</literal>
+ policy. Note, however, that a <literal>DELETE</literal> action in a
+ <literal>MERGE</literal> command will see rows that are visible
+ through <literal>SELECT</literal> policies, and if the
+ <literal>DELETE</literal> policy does not pass for such a row, an
+ error will be thrown.
</para>
<para>
@@ -400,6 +452,15 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</variablelist>
+ <para>
+ <xref linkend="sql-createpolicy-summary"/> summarizes how the different
+ types of policy apply to specific commands. In the table,
+ <quote>check</quote> means that the policy expression is checked and an
+ error is thrown if it returns false or null, whereas <quote>filter</quote>
+ means that the row is silently ignored if the policy expression returns
+ false or null.
+ </para>
+
<table id="sql-createpolicy-summary">
<title>Policies Applied by Command Type</title>
<tgroup cols="6">
@@ -424,8 +485,8 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</thead>
<tbody>
<row>
- <entry><command>SELECT</command></entry>
- <entry>Existing row</entry>
+ <entry><command>SELECT</command> / <command>COPY ... TO</command></entry>
+ <entry>Filter existing row</entry>
<entry>—</entry>
<entry>—</entry>
<entry>—</entry>
@@ -433,63 +494,117 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
</row>
<row>
<entry><command>SELECT FOR UPDATE/SHARE</command></entry>
- <entry>Existing row</entry>
+ <entry>Filter existing row</entry>
<entry>—</entry>
- <entry>Existing row</entry>
+ <entry>Filter existing row</entry>
<entry>—</entry>
<entry>—</entry>
</row>
<row>
- <entry><command>INSERT</command> / <command>MERGE ... THEN INSERT</command></entry>
+ <entry><command>INSERT</command></entry>
+ <entry>
+ Check new row <footnote id="rls-select-priv">
+ <para>
+ If read access is required to either the existing or new row (for
+ example, a <literal>WHERE</literal> or <literal>RETURNING</literal>
+ clause that refers to columns from the relation).
+ </para>
+ </footnote>
+ </entry>
+ <entry>Check new row</entry>
<entry>—</entry>
- <entry>New row</entry>
<entry>—</entry>
<entry>—</entry>
+ </row>
+ <row>
+ <entry><command>UPDATE</command></entry>
+ <entry>
+ Filter existing row <footnoteref linkend="rls-select-priv"/> &
+ check new row <footnoteref linkend="rls-select-priv"/>
+ </entry>
+ <entry>—</entry>
+ <entry>Filter existing row</entry>
+ <entry>Check new row</entry>
<entry>—</entry>
</row>
<row>
- <entry><command>INSERT ... RETURNING</command></entry>
+ <entry><command>DELETE</command></entry>
<entry>
- New row <footnote id="rls-select-priv">
+ Filter existing row <footnoteref linkend="rls-select-priv"/>
+ </entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>Filter existing row</entry>
+ </row>
+ <row>
+ <entry><command>INSERT ... ON CONFLICT</command></entry>
+ <entry>
+ Check new row <footnote id="rls-on-conflict-priv">
<para>
- If read access is required to the existing or new row (for example,
- a <literal>WHERE</literal> or <literal>RETURNING</literal> clause
- that refers to columns from the relation).
+ Row proposed for insertion is checked regardless of whether or not a
+ conflict occurs.
</para>
</footnote>
</entry>
- <entry>New row</entry>
+ <entry>
+ Check new row <footnoteref linkend="rls-on-conflict-priv"/>
+ </entry>
<entry>—</entry>
<entry>—</entry>
<entry>—</entry>
</row>
<row>
- <entry><command>UPDATE</command> / <command>MERGE ... THEN UPDATE</command></entry>
+ <entry><command>ON CONFLICT DO UPDATE</command></entry>
<entry>
- Existing & new rows <footnoteref linkend="rls-select-priv"/>
+ Check existing & new rows <footnote id="rls-on-conflict-update-priv">
+ <para>
+ New row of the auxiliary <command>UPDATE</command> command, which
+ might be different from the new row of the original
+ <command>INSERT</command> command.
+ </para>
+ </footnote>
</entry>
<entry>—</entry>
- <entry>Existing row</entry>
- <entry>New row</entry>
+ <entry>Check existing row</entry>
+ <entry>
+ Check new row <footnoteref linkend="rls-on-conflict-update-priv"/>
+ </entry>
<entry>—</entry>
</row>
<row>
- <entry><command>DELETE</command></entry>
+ <entry><command>MERGE</command></entry>
+ <entry>Filter source & target rows</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN INSERT</command></entry>
<entry>
- Existing row <footnoteref linkend="rls-select-priv"/>
+ Check new row <footnoteref linkend="rls-select-priv"/>
</entry>
+ <entry>Check new row</entry>
<entry>—</entry>
<entry>—</entry>
<entry>—</entry>
- <entry>Existing row</entry>
</row>
<row>
- <entry><command>ON CONFLICT DO UPDATE</command></entry>
- <entry>Existing & new rows</entry>
+ <entry><command>MERGE ... THEN UPDATE</command></entry>
+ <entry>Check new row</entry>
+ <entry>—</entry>
+ <entry>Check existing row</entry>
+ <entry>Check new row</entry>
+ <entry>—</entry>
+ </row>
+ <row>
+ <entry><command>MERGE ... THEN DELETE</command></entry>
+ <entry>—</entry>
+ <entry>—</entry>
<entry>—</entry>
- <entry>Existing row</entry>
- <entry>New row</entry>
<entry>—</entry>
+ <entry>Check existing row</entry>
</row>
</tbody>
</tgroup>
--
2.51.0
On Mon, Nov 3, 2025 at 7:22 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Yeah, reading through the text on that page in more detail, there are
a number of other omissions, or places that aren't quite fully
correct, so I've gone through those and attempted to improve things.Also, I think it would be better if the table made the distinction
between policy checks that just filter out rows, without throwing an
error, and checks that do cause an error to be thrown.v4 attached.
some of the <literal> can be replaced by <command>, for example:
+ A <literal>MERGE</literal> command requires <literal>SELECT</literal>
+ permissions on both the source and target relations, and so each
currently the visual appearance is the same, I guess it's not a big deal.
(Table 300. Policies Applied by Command Type) is way more intuitive.
overall looks good to me.