diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
index d8a036739c0..0a1699185c9 100644
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -530,6 +530,26 @@ CREATE POLICY name ON Check new row
—
+
+ UPDATE ... FOR PORTION OF
+
+ Filter existing row & check new row
+
+
+ Check leftover rows
+
+ FOR PORTION OF re-inserts the portions of the
+ affected row that fall outside the targeted range, to preserve them.
+ These leftover rows are checked against the INSERT
+ policy's WITH CHECK expression, even though no
+ INSERT privilege is required to store them.
+
+
+
+ Filter existing row
+ Check new row
+ —
+
DELETE
@@ -540,6 +560,18 @@ CREATE POLICY name ON —
Filter existing row
+
+ DELETE ... FOR PORTION OF
+
+ Filter existing row
+
+
+ Check leftover rows
+
+ —
+ —
+ Filter existing row
+
INSERT ... ON CONFLICT
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index ffdcd7fc4fa..3ab1f60525a 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -97,7 +97,8 @@ DELETE FROM [ ONLY ] table_name [ *
When FOR PORTION OF is used, the secondary inserts do
not require INSERT 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.
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 21a8fd8b037..3175671d475 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -101,7 +101,8 @@ UPDATE [ ONLY ] table_name [ * ]
When FOR PORTION OF is used, the secondary inserts do
not require INSERT 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.
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index e88a1bc1a89..46d88f79894 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -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
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index a050fc2dadf..271282c2d3b 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -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;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..f42085e53a0 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -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
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index a1ee1d5e501..f48644347d1 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -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;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..0bd24d1028b 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -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;