1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

Fix RLS checks for FOR PORTION OF leftover rows

UPDATE/DELETE FOR PORTION OF may insert leftover rows to preserve the
parts of the old row that are outside the target range.  Those inserts
go through ExecInsert(), which checks RLS policies using
WCO_RLS_INSERT_CHECK.

However, the rewriter only added RLS WITH CHECK options for the
original statement command.  For UPDATE, that meant only
WCO_RLS_UPDATE_CHECK options were available, so ExecInsert() skipped
them.  For DELETE, no RLS WITH CHECK options were added at all.  As a
result, leftover rows could be inserted even when they violated INSERT
RLS policies.

Fix this by adding INSERT RLS WITH CHECK options for UPDATE/DELETE FOR
PORTION OF target relations.  Also add regression coverage for both
UPDATE and DELETE, including cases where allowed leftovers still
succeed and disallowed leftovers are rejected.

Author: Chao Li <lic@highgo.com>
Co-authored-by: Paul A Jungwirth <pj@illuminatedcomputing.com>
Reviewed-by: Paul A Jungwirth <pj@illuminatedcomputing.com>
Reviewed-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/6C34A987-AC50-4477-BD71-2D4AFEE1A589%40gmail.com
Discussion: https://www.postgresql.org/message-id/flat/CAJTYsWWdeBkoH5g8D-k9LDw9ciqsMxb21EJSiFXAzP4J%3DXyxOQ%40mail.gmail.com
This commit is contained in:
Peter Eisentraut
2026-07-20 08:38:08 +02:00
parent 45cf7b1e5b
commit 20fe90bbb9
8 changed files with 264 additions and 4 deletions

View File

@@ -530,6 +530,26 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>Check new row</entry>
<entry>&mdash;</entry>
</row>
<row>
<entry><command>UPDATE ... FOR PORTION OF</command></entry>
<entry>
Filter existing row &amp; check new row
</entry>
<entry>
Check leftover rows&nbsp;<footnote id="rls-fpo-leftover">
<para>
<literal>FOR PORTION OF</literal> re-inserts the portions of the
affected row that fall outside the targeted range, to preserve them.
These leftover rows are checked against the <command>INSERT</command>
policy's <literal>WITH CHECK</literal> expression, even though no
<literal>INSERT</literal> privilege is required to store them.
</para>
</footnote>
</entry>
<entry>Filter existing row</entry>
<entry>Check new row</entry>
<entry>&mdash;</entry>
</row>
<row>
<entry><command>DELETE</command></entry>
<entry>
@@ -540,6 +560,18 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
<entry>&mdash;</entry>
<entry>Filter existing row</entry>
</row>
<row>
<entry><command>DELETE ... FOR PORTION OF</command></entry>
<entry>
Filter existing row
</entry>
<entry>
Check leftover rows&nbsp;<footnoteref linkend="rls-fpo-leftover"/>
</entry>
<entry>&mdash;</entry>
<entry>&mdash;</entry>
<entry>Filter existing row</entry>
</row>
<row>
<entry><command>INSERT ... ON CONFLICT</command></entry>
<entry>

View File

@@ -97,7 +97,8 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
When <literal>FOR PORTION OF</literal> is used, the secondary inserts do
not require <literal>INSERT</literal> privilege on the table. (This is
because conceptually no new information is being added; the inserted rows
only preserve existing data about the untargeted time period.)
only preserve existing data about the untargeted time period.) Row-level
security INSERT policies are still checked for these leftover inserts.
</para>
</refsect1>

View File

@@ -101,7 +101,8 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
When <literal>FOR PORTION OF</literal> is used, the secondary inserts do
not require <literal>INSERT</literal> privilege on the table. (This is
because conceptually no new information is being added; the inserted rows
only preserve existing data about the untargeted time period.)
only preserve existing data about the untargeted time period.) Row-level
security INSERT policies are still checked for these leftover inserts.
</para>
</refsect1>

View File

@@ -393,6 +393,31 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
}
}
/*
* UPDATE/DELETE FOR PORTION OF may insert leftover rows to preserve the
* portions of the old row not covered by the target range. Those hidden
* inserts go through ExecInsert(), so they need the same INSERT RLS WITH
* CHECK options as ordinary INSERTs. SELECT rights are never needed for
* the leftover rows, because they are not considered by RETURNING.
*/
if (root->forPortionOf != NULL && rt_index == root->resultRelation &&
(commandType == CMD_UPDATE || commandType == CMD_DELETE))
{
List *insert_permissive_policies;
List *insert_restrictive_policies;
get_policies_for_relation(rel, CMD_INSERT, user_id,
&insert_permissive_policies,
&insert_restrictive_policies);
add_with_check_options(rel, rt_index,
WCO_RLS_INSERT_CHECK,
insert_permissive_policies,
insert_restrictive_policies,
withCheckOptions,
hasSubLinks,
false);
}
/*
* FOR MERGE, we fetch policies for UPDATE, DELETE and INSERT (and ALL)
* and set them up so that we can enforce the appropriate policy depending

View File

@@ -2561,4 +2561,79 @@ SELECT * FROM fpo_cursed;
(1 row)
DROP TABLE fpo_cursed;
-- UPDATE/DELETE FOR PORTION OF leftover rows must satisfy RLS INSERT checks.
CREATE ROLE regress_fpo_rls;
CREATE TABLE fpo_rls (
id int,
valid_at int4range
);
ALTER TABLE fpo_rls ENABLE ROW LEVEL SECURITY;
CREATE POLICY fpo_rls_select ON fpo_rls
FOR SELECT TO regress_fpo_rls
USING (true);
CREATE POLICY fpo_rls_update ON fpo_rls
FOR UPDATE TO regress_fpo_rls
USING (lower(valid_at) < 50)
WITH CHECK (lower(valid_at) < 50);
CREATE POLICY fpo_rls_delete ON fpo_rls
FOR DELETE TO regress_fpo_rls
USING (lower(valid_at) < 50);
CREATE POLICY fpo_rls_insert ON fpo_rls
FOR INSERT TO regress_fpo_rls
WITH CHECK (lower(valid_at) < 50);
GRANT SELECT, UPDATE, DELETE ON fpo_rls TO regress_fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
UPDATE fpo_rls
FOR PORTION OF valid_at FROM 30 TO 100
SET id = 2;
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
id | valid_at
----+----------
1 | [10,30)
2 | [30,100)
(2 rows)
TRUNCATE fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
DELETE FROM fpo_rls
FOR PORTION OF valid_at FROM 30 TO 100;
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
id | valid_at
----+----------
1 | [10,30)
(1 row)
TRUNCATE fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
UPDATE fpo_rls
FOR PORTION OF valid_at FROM 30 TO 70
SET id = 2;
ERROR: new row violates row-level security policy for table "fpo_rls"
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
id | valid_at
----+----------
1 | [10,100)
(1 row)
TRUNCATE fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
DELETE FROM fpo_rls
FOR PORTION OF valid_at FROM 30 TO 70;
ERROR: new row violates row-level security policy for table "fpo_rls"
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
id | valid_at
----+----------
1 | [10,100)
(1 row)
DROP TABLE fpo_rls;
DROP ROLE regress_fpo_rls;
RESET datestyle;

View File

@@ -68,6 +68,20 @@ CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
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));
-- setup temporal target table (for FOR PORTION OF operations)
CREATE TABLE rls_test_fpo_tgt (a int, b text, valid_at int4range);
ALTER TABLE rls_test_fpo_tgt ENABLE ROW LEVEL SECURITY;
GRANT SELECT, UPDATE, DELETE ON rls_test_fpo_tgt TO public;
INSERT INTO rls_test_fpo_tgt VALUES (1, 'fpo a', '[1,10)');
CREATE POLICY sel_pol ON rls_test_fpo_tgt FOR SELECT
USING (rls_test_policy_fn('SELECT USING on rls_test_fpo_tgt', rls_test_fpo_tgt));
CREATE POLICY ins_pol ON rls_test_fpo_tgt FOR INSERT
WITH CHECK (rls_test_policy_fn('INSERT CHECK on rls_test_fpo_tgt', rls_test_fpo_tgt));
CREATE POLICY upd_pol ON rls_test_fpo_tgt FOR UPDATE
USING (rls_test_policy_fn('UPDATE USING on rls_test_fpo_tgt', rls_test_fpo_tgt))
WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_fpo_tgt', rls_test_fpo_tgt));
CREATE POLICY del_pol ON rls_test_fpo_tgt FOR DELETE
USING (rls_test_policy_fn('DELETE USING on rls_test_fpo_tgt', rls_test_fpo_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
@@ -155,6 +169,19 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
1 | tgt d | TGT D
(1 row)
-- UPDATE ... FOR PORTION OF should also apply SELECT USING policy clauses to
-- the old and new rows, and INSERT CHECK policy clauses to leftover rows.
BEGIN;
UPDATE rls_test_fpo_tgt
FOR PORTION OF valid_at FROM 3 TO 7
SET b = 'fpo b';
NOTICE: UPDATE USING on rls_test_fpo_tgt.(1,"fpo a","[1,10)")
NOTICE: SELECT USING on rls_test_fpo_tgt.(1,"fpo a","[1,10)")
NOTICE: UPDATE CHECK on rls_test_fpo_tgt.(1,"fpo b","[3,7)")
NOTICE: SELECT USING on rls_test_fpo_tgt.(1,"fpo b","[3,7)")
NOTICE: INSERT CHECK on rls_test_fpo_tgt.(1,"fpo a","[1,3)")
NOTICE: INSERT CHECK on rls_test_fpo_tgt.(1,"fpo a","[7,10)")
ROLLBACK;
-- 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")
@@ -170,6 +197,16 @@ NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D")
1 | tgt d | TGT D
(1 row)
-- DELETE ... FOR PORTION OF should also apply SELECT USING policy clauses to
-- the old row, and INSERT CHECK policy clauses to leftover rows.
BEGIN;
DELETE FROM rls_test_fpo_tgt
FOR PORTION OF valid_at FROM 3 TO 7;
NOTICE: DELETE USING on rls_test_fpo_tgt.(1,"fpo a","[1,10)")
NOTICE: SELECT USING on rls_test_fpo_tgt.(1,"fpo a","[1,10)")
NOTICE: INSERT CHECK on rls_test_fpo_tgt.(1,"fpo a","[1,3)")
NOTICE: INSERT CHECK on rls_test_fpo_tgt.(1,"fpo a","[7,10)")
ROLLBACK;
-- INSERT ... ON CONFLICT DO NOTHING with an arbiter clause should apply
-- INSERT CHECK and SELECT USING policy clauses (to new value, whether it
-- conflicts or not)
@@ -332,7 +369,7 @@ NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C")
-- Tidy up
RESET SESSION AUTHORIZATION;
DROP TABLE rls_test_src, rls_test_tgt;
DROP TABLE rls_test_src, rls_test_tgt, rls_test_fpo_tgt;
DROP FUNCTION rls_test_tgt_set_c;
DROP FUNCTION rls_test_policy_fn;
-- BASIC Row-Level Security Scenario

View File

@@ -1676,4 +1676,62 @@ ROLLBACK;
SELECT * FROM fpo_cursed;
DROP TABLE fpo_cursed;
-- UPDATE/DELETE FOR PORTION OF leftover rows must satisfy RLS INSERT checks.
CREATE ROLE regress_fpo_rls;
CREATE TABLE fpo_rls (
id int,
valid_at int4range
);
ALTER TABLE fpo_rls ENABLE ROW LEVEL SECURITY;
CREATE POLICY fpo_rls_select ON fpo_rls
FOR SELECT TO regress_fpo_rls
USING (true);
CREATE POLICY fpo_rls_update ON fpo_rls
FOR UPDATE TO regress_fpo_rls
USING (lower(valid_at) < 50)
WITH CHECK (lower(valid_at) < 50);
CREATE POLICY fpo_rls_delete ON fpo_rls
FOR DELETE TO regress_fpo_rls
USING (lower(valid_at) < 50);
CREATE POLICY fpo_rls_insert ON fpo_rls
FOR INSERT TO regress_fpo_rls
WITH CHECK (lower(valid_at) < 50);
GRANT SELECT, UPDATE, DELETE ON fpo_rls TO regress_fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
UPDATE fpo_rls
FOR PORTION OF valid_at FROM 30 TO 100
SET id = 2;
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
TRUNCATE fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
DELETE FROM fpo_rls
FOR PORTION OF valid_at FROM 30 TO 100;
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
TRUNCATE fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
UPDATE fpo_rls
FOR PORTION OF valid_at FROM 30 TO 70
SET id = 2;
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
TRUNCATE fpo_rls;
INSERT INTO fpo_rls VALUES (1, '[10,100)');
SET ROLE regress_fpo_rls;
DELETE FROM fpo_rls
FOR PORTION OF valid_at FROM 30 TO 70;
RESET ROLE;
SELECT * FROM fpo_rls ORDER BY valid_at;
DROP TABLE fpo_rls;
DROP ROLE regress_fpo_rls;
RESET datestyle;

View File

@@ -85,6 +85,22 @@ CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE
CREATE POLICY del_pol ON rls_test_tgt FOR DELETE
USING (rls_test_policy_fn('DELETE USING on rls_test_tgt', rls_test_tgt));
-- setup temporal target table (for FOR PORTION OF operations)
CREATE TABLE rls_test_fpo_tgt (a int, b text, valid_at int4range);
ALTER TABLE rls_test_fpo_tgt ENABLE ROW LEVEL SECURITY;
GRANT SELECT, UPDATE, DELETE ON rls_test_fpo_tgt TO public;
INSERT INTO rls_test_fpo_tgt VALUES (1, 'fpo a', '[1,10)');
CREATE POLICY sel_pol ON rls_test_fpo_tgt FOR SELECT
USING (rls_test_policy_fn('SELECT USING on rls_test_fpo_tgt', rls_test_fpo_tgt));
CREATE POLICY ins_pol ON rls_test_fpo_tgt FOR INSERT
WITH CHECK (rls_test_policy_fn('INSERT CHECK on rls_test_fpo_tgt', rls_test_fpo_tgt));
CREATE POLICY upd_pol ON rls_test_fpo_tgt FOR UPDATE
USING (rls_test_policy_fn('UPDATE USING on rls_test_fpo_tgt', rls_test_fpo_tgt))
WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_fpo_tgt', rls_test_fpo_tgt));
CREATE POLICY del_pol ON rls_test_fpo_tgt FOR DELETE
USING (rls_test_policy_fn('DELETE USING on rls_test_fpo_tgt', rls_test_fpo_tgt));
-- test policies applied to regress_rls_bob
SET SESSION AUTHORIZATION regress_rls_bob;
@@ -114,6 +130,14 @@ 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 *;
-- UPDATE ... FOR PORTION OF should also apply SELECT USING policy clauses to
-- the old and new rows, and INSERT CHECK policy clauses to leftover rows.
BEGIN;
UPDATE rls_test_fpo_tgt
FOR PORTION OF valid_at FROM 3 TO 7
SET b = 'fpo b';
ROLLBACK;
-- DELETE without WHERE or RETURNING should only apply DELETE USING policy clause
BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
@@ -121,6 +145,13 @@ BEGIN; DELETE FROM rls_test_tgt; ROLLBACK;
BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK;
DELETE FROM rls_test_tgt RETURNING *;
-- DELETE ... FOR PORTION OF should also apply SELECT USING policy clauses to
-- the old row, and INSERT CHECK policy clauses to leftover rows.
BEGIN;
DELETE FROM rls_test_fpo_tgt
FOR PORTION OF valid_at FROM 3 TO 7;
ROLLBACK;
-- INSERT ... ON CONFLICT DO NOTHING with an arbiter clause should apply
-- INSERT CHECK and SELECT USING policy clauses (to new value, whether it
-- conflicts or not)
@@ -192,7 +223,7 @@ MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
-- Tidy up
RESET SESSION AUTHORIZATION;
DROP TABLE rls_test_src, rls_test_tgt;
DROP TABLE rls_test_src, rls_test_tgt, rls_test_fpo_tgt;
DROP FUNCTION rls_test_tgt_set_c;
DROP FUNCTION rls_test_policy_fn;