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;