-- -- Test of Row-level security feature -- -- Clean up in case a prior regression run failed -- Suppress NOTICE messages when users/groups don't exist SET client_min_messages TO 'warning'; DROP USER IF EXISTS rls_regress_user0; DROP USER IF EXISTS rls_regress_user1; DROP USER IF EXISTS rls_regress_user2; DROP USER IF EXISTS rls_regress_exempt_user; DROP ROLE IF EXISTS rls_regress_group1; DROP ROLE IF EXISTS rls_regress_group2; DROP SCHEMA IF EXISTS rls_regress_schema CASCADE; RESET client_min_messages; -- initial setup CREATE USER rls_regress_user0; CREATE USER rls_regress_user1; CREATE USER rls_regress_user2; CREATE USER rls_regress_exempt_user BYPASSRLS; CREATE ROLE rls_regress_group1 NOLOGIN; CREATE ROLE rls_regress_group2 NOLOGIN; GRANT rls_regress_group1 TO rls_regress_user1; GRANT rls_regress_group2 TO rls_regress_user2; CREATE SCHEMA rls_regress_schema; GRANT ALL ON SCHEMA rls_regress_schema to public; SET search_path = rls_regress_schema; -- setup of malicious function 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; -- BASIC Row-Level Security Scenario SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE uaccount ( pguser name primary key, seclv int ); GRANT SELECT ON uaccount TO public; INSERT INTO uaccount VALUES ('rls_regress_user0', 99), ('rls_regress_user1', 1), ('rls_regress_user2', 2), ('rls_regress_user3', 3); CREATE TABLE category ( cid int primary key, cname text ); GRANT ALL ON category TO public; INSERT INTO category VALUES (11, 'novel'), (22, 'science fiction'), (33, 'technology'), (44, 'manga'); CREATE TABLE document ( did int primary key, cid int references category(cid), dlevel int not null, dauthor name, dtitle text ); GRANT ALL ON document TO public; INSERT INTO document VALUES ( 1, 11, 1, 'rls_regress_user1', 'my first novel'), ( 2, 11, 2, 'rls_regress_user1', 'my second novel'), ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'), ( 4, 44, 1, 'rls_regress_user1', 'my first manga'), ( 5, 44, 2, 'rls_regress_user1', 'my second manga'), ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'), ( 7, 33, 2, 'rls_regress_user2', 'great technology book'), ( 8, 44, 1, 'rls_regress_user2', 'great manga'); ALTER TABLE document ENABLE ROW LEVEL SECURITY; -- user's security level must be higher than or equal to document's CREATE POLICY p1 ON document USING (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user)); -- viewpoint from rls_regress_user1 SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO ON; SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => my first novel NOTICE: f_leak => my first manga NOTICE: f_leak => great science fiction NOTICE: f_leak => great manga did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 4 | 44 | 1 | rls_regress_user1 | my first manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 8 | 44 | 1 | rls_regress_user2 | great manga (4 rows) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => my first novel NOTICE: f_leak => my first manga NOTICE: f_leak => great science fiction NOTICE: f_leak => great manga cid | did | dlevel | dauthor | dtitle | cname -----+-----+--------+-------------------+-----------------------+----------------- 11 | 1 | 1 | rls_regress_user1 | my first novel | novel 44 | 4 | 1 | rls_regress_user1 | my first manga | manga 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction 44 | 8 | 1 | rls_regress_user2 | great manga | manga (4 rows) -- try a sampled version SELECT * FROM document TABLESAMPLE BERNOULLI(50) REPEATABLE(0) WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => my first manga NOTICE: f_leak => great science fiction NOTICE: f_leak => great manga did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 4 | 44 | 1 | rls_regress_user1 | my first manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 8 | 44 | 1 | rls_regress_user2 | great manga (3 rows) -- viewpoint from rls_regress_user2 SET SESSION AUTHORIZATION rls_regress_user2; SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => my first novel NOTICE: f_leak => my second novel NOTICE: f_leak => my science fiction NOTICE: f_leak => my first manga NOTICE: f_leak => my second manga NOTICE: f_leak => great science fiction NOTICE: f_leak => great technology book NOTICE: f_leak => great manga did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 2 | 11 | 2 | rls_regress_user1 | my second novel 3 | 22 | 2 | rls_regress_user1 | my science fiction 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 7 | 33 | 2 | rls_regress_user2 | great technology book 8 | 44 | 1 | rls_regress_user2 | great manga (8 rows) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => my first novel NOTICE: f_leak => my second novel NOTICE: f_leak => my science fiction NOTICE: f_leak => my first manga NOTICE: f_leak => my second manga NOTICE: f_leak => great science fiction NOTICE: f_leak => great technology book NOTICE: f_leak => great manga cid | did | dlevel | dauthor | dtitle | cname -----+-----+--------+-------------------+-----------------------+----------------- 11 | 1 | 1 | rls_regress_user1 | my first novel | novel 11 | 2 | 2 | rls_regress_user1 | my second novel | novel 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction 44 | 4 | 1 | rls_regress_user1 | my first manga | manga 44 | 5 | 2 | rls_regress_user1 | my second manga | manga 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction 33 | 7 | 2 | rls_regress_user2 | great technology book | technology 44 | 8 | 1 | rls_regress_user2 | great manga | manga (8 rows) -- try a sampled version SELECT * FROM document TABLESAMPLE BERNOULLI(50) REPEATABLE(0) WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => my first manga NOTICE: f_leak => my second manga NOTICE: f_leak => great science fiction NOTICE: f_leak => great manga did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 8 | 44 | 1 | rls_regress_user2 | great manga (4 rows) EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); QUERY PLAN ---------------------------------------------------------- Subquery Scan on document Filter: f_leak(document.dtitle) -> Seq Scan on document document_1 Filter: (dlevel <= $0) InitPlan 1 (returns $0) -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = "current_user"()) (7 rows) EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); QUERY PLAN ---------------------------------------------------------------------- Hash Join Hash Cond: (category.cid = document.cid) -> Seq Scan on category -> Hash -> Subquery Scan on document Filter: f_leak(document.dtitle) -> Seq Scan on document document_1 Filter: (dlevel <= $0) InitPlan 1 (returns $0) -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = "current_user"()) (11 rows) -- only owner can change policies ALTER POLICY p1 ON document USING (true); --fail ERROR: must be owner of relation document DROP POLICY p1 ON document; --fail ERROR: must be owner of relation document SET SESSION AUTHORIZATION rls_regress_user0; ALTER POLICY p1 ON document USING (dauthor = current_user); -- viewpoint from rls_regress_user1 again SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => my first novel NOTICE: f_leak => my second novel NOTICE: f_leak => my science fiction NOTICE: f_leak => my first manga NOTICE: f_leak => my second manga did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+-------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 2 | 11 | 2 | rls_regress_user1 | my second novel 3 | 22 | 2 | rls_regress_user1 | my science fiction 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga (5 rows) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER by did; NOTICE: f_leak => my first novel NOTICE: f_leak => my second novel NOTICE: f_leak => my science fiction NOTICE: f_leak => my first manga NOTICE: f_leak => my second manga cid | did | dlevel | dauthor | dtitle | cname -----+-----+--------+-------------------+--------------------+----------------- 11 | 1 | 1 | rls_regress_user1 | my first novel | novel 11 | 2 | 2 | rls_regress_user1 | my second novel | novel 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction 44 | 4 | 1 | rls_regress_user1 | my first manga | manga 44 | 5 | 2 | rls_regress_user1 | my second manga | manga (5 rows) -- viewpoint from rls_regres_user2 again SET SESSION AUTHORIZATION rls_regress_user2; SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; NOTICE: f_leak => great science fiction NOTICE: f_leak => great technology book NOTICE: f_leak => great manga did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 6 | 22 | 1 | rls_regress_user2 | great science fiction 7 | 33 | 2 | rls_regress_user2 | great technology book 8 | 44 | 1 | rls_regress_user2 | great manga (3 rows) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER by did; NOTICE: f_leak => great science fiction NOTICE: f_leak => great technology book NOTICE: f_leak => great manga cid | did | dlevel | dauthor | dtitle | cname -----+-----+--------+-------------------+-----------------------+----------------- 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction 33 | 7 | 2 | rls_regress_user2 | great technology book | technology 44 | 8 | 1 | rls_regress_user2 | great manga | manga (3 rows) EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); QUERY PLAN ---------------------------------------------- Subquery Scan on document Filter: f_leak(document.dtitle) -> Seq Scan on document document_1 Filter: (dauthor = "current_user"()) (4 rows) EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); QUERY PLAN ---------------------------------------------------- Nested Loop -> Subquery Scan on document Filter: f_leak(document.dtitle) -> Seq Scan on document document_1 Filter: (dauthor = "current_user"()) -> Index Scan using category_pkey on category Index Cond: (cid = document.cid) (7 rows) -- interaction of FK/PK constraints SET SESSION AUTHORIZATION rls_regress_user0; CREATE POLICY p2 ON category USING (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33) WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44) ELSE false END); ALTER TABLE category ENABLE ROW LEVEL SECURITY; -- cannot delete PK referenced by invisible FK SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid; did | cid | dlevel | dauthor | dtitle | cid | cname -----+-----+--------+-------------------+--------------------+-----+------------ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel | | | | | 33 | technology 5 | 44 | 2 | rls_regress_user1 | my second manga | | 4 | 44 | 1 | rls_regress_user1 | my first manga | | 3 | 22 | 2 | rls_regress_user1 | my science fiction | | (6 rows) DELETE FROM category WHERE cid = 33; -- fails with FK violation ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document" DETAIL: Key is still referenced from table "document". -- can insert FK referencing invisible PK SET SESSION AUTHORIZATION rls_regress_user2; SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid; did | cid | dlevel | dauthor | dtitle | cid | cname -----+-----+--------+-------------------+-----------------------+-----+----------------- 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga 7 | 33 | 2 | rls_regress_user2 | great technology book | | (3 rows) INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- UNIQUE or PRIMARY KEY constraint violation DOES reveal presence of row SET SESSION AUTHORIZATION rls_regress_user1; INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user1', 'my third manga'); -- Must fail with unique violation, revealing presence of did we can't see ERROR: duplicate key value violates unique constraint "document_pkey" SELECT * FROM document WHERE did = 8; -- and confirm we can't see it did | cid | dlevel | dauthor | dtitle -----+-----+--------+---------+-------- (0 rows) -- RLS policies are checked before constraints INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation ERROR: new row violates row level security policy for "document" UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation ERROR: new row violates row level security policy for "document" -- database superuser does bypass RLS policy when enabled RESET SESSION AUTHORIZATION; SET row_security TO ON; SELECT * FROM document; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 2 | 11 | 2 | rls_regress_user1 | my second novel 3 | 22 | 2 | rls_regress_user1 | my science fiction 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 7 | 33 | 2 | rls_regress_user2 | great technology book 8 | 44 | 1 | rls_regress_user2 | great manga 10 | 33 | 1 | rls_regress_user2 | hoge (9 rows) SELECT * FROM category; cid | cname -----+----------------- 11 | novel 22 | science fiction 33 | technology 44 | manga (4 rows) -- database superuser does not bypass RLS policy when FORCE enabled. RESET SESSION AUTHORIZATION; SET row_security TO FORCE; SELECT * FROM document; did | cid | dlevel | dauthor | dtitle -----+-----+--------+---------+-------- (0 rows) SELECT * FROM category; cid | cname -----+------- (0 rows) -- database superuser does bypass RLS policy when disabled RESET SESSION AUTHORIZATION; SET row_security TO OFF; SELECT * FROM document; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 2 | 11 | 2 | rls_regress_user1 | my second novel 3 | 22 | 2 | rls_regress_user1 | my science fiction 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 7 | 33 | 2 | rls_regress_user2 | great technology book 8 | 44 | 1 | rls_regress_user2 | great manga 10 | 33 | 1 | rls_regress_user2 | hoge (9 rows) SELECT * FROM category; cid | cname -----+----------------- 11 | novel 22 | science fiction 33 | technology 44 | manga (4 rows) -- database non-superuser with bypass privilege can bypass RLS policy when disabled SET SESSION AUTHORIZATION rls_regress_exempt_user; SET row_security TO OFF; SELECT * FROM document; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 2 | 11 | 2 | rls_regress_user1 | my second novel 3 | 22 | 2 | rls_regress_user1 | my science fiction 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 7 | 33 | 2 | rls_regress_user2 | great technology book 8 | 44 | 1 | rls_regress_user2 | great manga 10 | 33 | 1 | rls_regress_user2 | hoge (9 rows) SELECT * FROM category; cid | cname -----+----------------- 11 | novel 22 | science fiction 33 | technology 44 | manga (4 rows) -- RLS policy applies to table owner when FORCE enabled. SET SESSION AUTHORIZATION rls_regress_user0; SET row_security TO FORCE; SELECT * FROM document; did | cid | dlevel | dauthor | dtitle -----+-----+--------+---------+-------- (0 rows) SELECT * FROM category; cid | cname -----+------- (0 rows) -- RLS policy does not apply to table owner when RLS enabled. SET SESSION AUTHORIZATION rls_regress_user0; SET row_security TO ON; SELECT * FROM document; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 2 | 11 | 2 | rls_regress_user1 | my second novel 3 | 22 | 2 | rls_regress_user1 | my science fiction 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 7 | 33 | 2 | rls_regress_user2 | great technology book 8 | 44 | 1 | rls_regress_user2 | great manga 10 | 33 | 1 | rls_regress_user2 | hoge (9 rows) SELECT * FROM category; cid | cname -----+----------------- 11 | novel 22 | science fiction 33 | technology 44 | manga (4 rows) -- RLS policy does not apply to table owner when RLS disabled. SET SESSION AUTHORIZATION rls_regress_user0; SET row_security TO OFF; SELECT * FROM document; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 1 | 11 | 1 | rls_regress_user1 | my first novel 2 | 11 | 2 | rls_regress_user1 | my second novel 3 | 22 | 2 | rls_regress_user1 | my science fiction 4 | 44 | 1 | rls_regress_user1 | my first manga 5 | 44 | 2 | rls_regress_user1 | my second manga 6 | 22 | 1 | rls_regress_user2 | great science fiction 7 | 33 | 2 | rls_regress_user2 | great technology book 8 | 44 | 1 | rls_regress_user2 | great manga 10 | 33 | 1 | rls_regress_user2 | hoge (9 rows) SELECT * FROM category; cid | cname -----+----------------- 11 | novel 22 | science fiction 33 | technology 44 | manga (4 rows) -- -- Table inheritance and RLS policy -- SET SESSION AUTHORIZATION rls_regress_user0; SET row_security TO ON; CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS; ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor GRANT ALL ON t1 TO public; COPY t1 FROM stdin WITH (oids); CREATE TABLE t2 (c float) INHERITS (t1); GRANT ALL ON t2 TO public; COPY t2 FROM stdin WITH (oids); CREATE TABLE t3 (c text, b text, a int) WITH OIDS; ALTER TABLE t3 INHERIT t1; GRANT ALL ON t3 TO public; COPY t3(a,b,c) FROM stdin WITH (oids); CREATE POLICY p1 ON t1 FOR ALL TO PUBLIC USING (a % 2 = 0); -- be even number CREATE POLICY p2 ON t2 FOR ALL TO PUBLIC USING (a % 2 = 1); -- be odd number ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; ALTER TABLE t2 ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM t1; a | b ---+----- 2 | bbb 4 | ddd 2 | bcd 4 | def 2 | yyy (5 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN ------------------------------- Append -> Seq Scan on t1 Filter: ((a % 2) = 0) -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Seq Scan on t3 Filter: ((a % 2) = 0) (7 rows) SELECT * FROM t1 WHERE f_leak(b); NOTICE: f_leak => bbb NOTICE: f_leak => ddd NOTICE: f_leak => bcd NOTICE: f_leak => def NOTICE: f_leak => yyy a | b ---+----- 2 | bbb 4 | ddd 2 | bcd 4 | def 2 | yyy (5 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); QUERY PLAN ------------------------------------- Subquery Scan on t1 Filter: f_leak(t1.b) -> Append -> Seq Scan on t1 t1_1 Filter: ((a % 2) = 0) -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Seq Scan on t3 Filter: ((a % 2) = 0) (9 rows) -- reference to system column SELECT oid, * FROM t1; oid | a | b -----+---+----- 102 | 2 | bbb 104 | 4 | ddd 202 | 2 | bcd 204 | 4 | def 302 | 2 | yyy (5 rows) EXPLAIN (COSTS OFF) SELECT *, t1 FROM t1; QUERY PLAN ------------------------------- Append -> Seq Scan on t1 Filter: ((a % 2) = 0) -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Seq Scan on t3 Filter: ((a % 2) = 0) (7 rows) -- reference to whole-row reference SELECT *, t1 FROM t1; a | b | t1 ---+-----+--------- 2 | bbb | (2,bbb) 4 | ddd | (4,ddd) 2 | bcd | (2,bcd) 4 | def | (4,def) 2 | yyy | (2,yyy) (5 rows) EXPLAIN (COSTS OFF) SELECT *, t1 FROM t1; QUERY PLAN ------------------------------- Append -> Seq Scan on t1 Filter: ((a % 2) = 0) -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Seq Scan on t3 Filter: ((a % 2) = 0) (7 rows) -- for share/update lock SELECT * FROM t1 FOR SHARE; a | b ---+----- 2 | bbb 4 | ddd 2 | bcd 4 | def 2 | yyy (5 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1 FOR SHARE; QUERY PLAN ------------------------------------------------------- LockRows -> Subquery Scan on t1 -> LockRows -> Result -> Append -> Seq Scan on t1 t1_1 Filter: ((a % 2) = 0) -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Seq Scan on t3 Filter: ((a % 2) = 0) (11 rows) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE; NOTICE: f_leak => bbb NOTICE: f_leak => ddd NOTICE: f_leak => bcd NOTICE: f_leak => def NOTICE: f_leak => yyy a | b ---+----- 2 | bbb 4 | ddd 2 | bcd 4 | def 2 | yyy (5 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE; QUERY PLAN ------------------------------------------------------- LockRows -> Subquery Scan on t1 Filter: f_leak(t1.b) -> LockRows -> Result -> Append -> Seq Scan on t1 t1_1 Filter: ((a % 2) = 0) -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Seq Scan on t3 Filter: ((a % 2) = 0) (12 rows) -- superuser is allowed to bypass RLS checks RESET SESSION AUTHORIZATION; SET row_security TO OFF; SELECT * FROM t1 WHERE f_leak(b); NOTICE: f_leak => aaa NOTICE: f_leak => bbb NOTICE: f_leak => ccc NOTICE: f_leak => ddd NOTICE: f_leak => abc NOTICE: f_leak => bcd NOTICE: f_leak => cde NOTICE: f_leak => def NOTICE: f_leak => xxx NOTICE: f_leak => yyy NOTICE: f_leak => zzz a | b ---+----- 1 | aaa 2 | bbb 3 | ccc 4 | ddd 1 | abc 2 | bcd 3 | cde 4 | def 1 | xxx 2 | yyy 3 | zzz (11 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); QUERY PLAN --------------------------- Append -> Seq Scan on t1 Filter: f_leak(b) -> Seq Scan on t2 Filter: f_leak(b) -> Seq Scan on t3 Filter: f_leak(b) (7 rows) -- non-superuser with bypass privilege can bypass RLS policy when disabled SET SESSION AUTHORIZATION rls_regress_exempt_user; SET row_security TO OFF; SELECT * FROM t1 WHERE f_leak(b); NOTICE: f_leak => aaa NOTICE: f_leak => bbb NOTICE: f_leak => ccc NOTICE: f_leak => ddd NOTICE: f_leak => abc NOTICE: f_leak => bcd NOTICE: f_leak => cde NOTICE: f_leak => def NOTICE: f_leak => xxx NOTICE: f_leak => yyy NOTICE: f_leak => zzz a | b ---+----- 1 | aaa 2 | bbb 3 | ccc 4 | ddd 1 | abc 2 | bcd 3 | cde 4 | def 1 | xxx 2 | yyy 3 | zzz (11 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); QUERY PLAN --------------------------- Append -> Seq Scan on t1 Filter: f_leak(b) -> Seq Scan on t2 Filter: f_leak(b) -> Seq Scan on t3 Filter: f_leak(b) (7 rows) ----- Dependencies ----- SET SESSION AUTHORIZATION rls_regress_user0; SET row_security TO ON; CREATE TABLE dependee (x integer, y integer); CREATE TABLE dependent (x integer, y integer); CREATE POLICY d1 ON dependent FOR ALL TO PUBLIC USING (x = (SELECT d.x FROM dependee d WHERE d.y = y)); DROP TABLE dependee; -- Should fail without CASCADE due to dependency on row security qual? ERROR: cannot drop table dependee because other objects depend on it DETAIL: policy d1 on table dependent depends on table dependee HINT: Use DROP ... CASCADE to drop the dependent objects too. DROP TABLE dependee CASCADE; NOTICE: drop cascades to policy d1 on table dependent EXPLAIN (COSTS OFF) SELECT * FROM dependent; -- After drop, should be unqualified QUERY PLAN ----------------------- Seq Scan on dependent (1 row) ----- RECURSION ---- -- -- Simple recursion -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE rec1 (x integer, y integer); CREATE POLICY r1 ON rec1 USING (x = (SELECT r.x FROM rec1 r WHERE y = r.y)); ALTER TABLE rec1 ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM rec1; -- fail, direct recursion ERROR: infinite recursion detected in policy for relation "rec1" -- -- Mutual recursion -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE rec2 (a integer, b integer); ALTER POLICY r1 ON rec1 USING (x = (SELECT a FROM rec2 WHERE b = y)); CREATE POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1 WHERE y = b)); ALTER TABLE rec2 ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM rec1; -- fail, mutual recursion ERROR: infinite recursion detected in policy for relation "rec1" -- -- Mutual recursion via views -- SET SESSION AUTHORIZATION rls_regress_user1; CREATE VIEW rec1v AS SELECT * FROM rec1; CREATE VIEW rec2v AS SELECT * FROM rec2; SET SESSION AUTHORIZATION rls_regress_user0; ALTER POLICY r1 ON rec1 USING (x = (SELECT a FROM rec2v WHERE b = y)); ALTER POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1v WHERE y = b)); SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM rec1; -- fail, mutual recursion via views ERROR: infinite recursion detected in policy for relation "rec1" -- -- Mutual recursion via .s.b views -- SET SESSION AUTHORIZATION rls_regress_user1; -- Suppress NOTICE messages when doing a cascaded drop. SET client_min_messages TO 'warning'; DROP VIEW rec1v, rec2v CASCADE; RESET client_min_messages; CREATE VIEW rec1v WITH (security_barrier) AS SELECT * FROM rec1; CREATE VIEW rec2v WITH (security_barrier) AS SELECT * FROM rec2; SET SESSION AUTHORIZATION rls_regress_user0; CREATE POLICY r1 ON rec1 USING (x = (SELECT a FROM rec2v WHERE b = y)); CREATE POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1v WHERE y = b)); SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM rec1; -- fail, mutual recursion via s.b. views ERROR: infinite recursion detected in policy for relation "rec1" -- -- recursive RLS and VIEWs in policy -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE s1 (a int, b text); INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); CREATE TABLE s2 (x int, y text); INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x); GRANT SELECT ON s1, s2 TO rls_regress_user1; CREATE POLICY p1 ON s1 USING (a in (select x from s2 where y like '%2f%')); CREATE POLICY p2 ON s2 USING (x in (select a from s1 where b like '%22%')); CREATE POLICY p3 ON s1 FOR INSERT WITH CHECK (a = (SELECT a FROM s1)); ALTER TABLE s1 ENABLE ROW LEVEL SECURITY; ALTER TABLE s2 ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%'; SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion) ERROR: infinite recursion detected in policy for relation "s1" INSERT INTO s1 VALUES (1, 'foo'); -- fail (infinite recursion) ERROR: infinite recursion detected in policy for relation "s1" SET SESSION AUTHORIZATION rls_regress_user0; DROP POLICY p3 on s1; ALTER POLICY p2 ON s2 USING (x % 2 = 0); SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM s1 WHERE f_leak(b); -- OK NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c a | b ---+---------------------------------- 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM only s1 WHERE f_leak(b); QUERY PLAN ---------------------------------------------------------- Subquery Scan on s1 Filter: f_leak(s1.b) -> Hash Join Hash Cond: (s1_1.a = s2.x) -> Seq Scan on s1 s1_1 -> Hash -> HashAggregate Group Key: s2.x -> Subquery Scan on s2 Filter: (s2.y ~~ '%2f%'::text) -> Seq Scan on s2 s2_1 Filter: ((x % 2) = 0) (12 rows) SET SESSION AUTHORIZATION rls_regress_user0; ALTER POLICY p1 ON s1 USING (a in (select x from v2)); -- using VIEW in RLS policy SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM s1 WHERE f_leak(b); -- OK NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3 NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc a | b ----+---------------------------------- -4 | 0267aaf632e87a63288a08331f22c7c3 6 | 1679091c5a880faf6fb5e6087eb1b2dc (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b); QUERY PLAN ---------------------------------------------------------- Subquery Scan on s1 Filter: f_leak(s1.b) -> Hash Join Hash Cond: (s1_1.a = s2.x) -> Seq Scan on s1 s1_1 -> Hash -> HashAggregate Group Key: s2.x -> Subquery Scan on s2 Filter: (s2.y ~~ '%af%'::text) -> Seq Scan on s2 s2_1 Filter: ((x % 2) = 0) (12 rows) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; xx | x | y ----+----+---------------------------------- -6 | -6 | 596a3d04481816330f07e4f97510c28f -4 | -4 | 0267aaf632e87a63288a08331f22c7c3 2 | 2 | c81e728d9d4c2f636f067f89cc14862c (3 rows) EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; QUERY PLAN -------------------------------------------------------------------- Subquery Scan on s2 Filter: (s2.y ~~ '%28%'::text) -> Seq Scan on s2 s2_1 Filter: ((x % 2) = 0) SubPlan 1 -> Limit -> Subquery Scan on s1 -> Nested Loop Semi Join Join Filter: (s1_1.a = s2_2.x) -> Seq Scan on s1 s1_1 -> Materialize -> Subquery Scan on s2_2 Filter: (s2_2.y ~~ '%af%'::text) -> Seq Scan on s2 s2_3 Filter: ((x % 2) = 0) (15 rows) SET SESSION AUTHORIZATION rls_regress_user0; ALTER POLICY p2 ON s2 USING (x in (select a from s1 where b like '%d2%')); SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view) ERROR: infinite recursion detected in policy for relation "s1" -- prepared statement with rls_regress_user0 privilege PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1; EXECUTE p1(2); a | b ---+----- 2 | bbb 2 | bcd 2 | yyy (3 rows) EXPLAIN (COSTS OFF) EXECUTE p1(2); QUERY PLAN ---------------------------------------------- Append -> Seq Scan on t1 Filter: ((a <= 2) AND ((a % 2) = 0)) -> Seq Scan on t2 Filter: ((a <= 2) AND ((a % 2) = 0)) -> Seq Scan on t3 Filter: ((a <= 2) AND ((a % 2) = 0)) (7 rows) -- superuser is allowed to bypass RLS checks RESET SESSION AUTHORIZATION; SET row_security TO OFF; SELECT * FROM t1 WHERE f_leak(b); NOTICE: f_leak => aaa NOTICE: f_leak => bbb NOTICE: f_leak => ccc NOTICE: f_leak => ddd NOTICE: f_leak => abc NOTICE: f_leak => bcd NOTICE: f_leak => cde NOTICE: f_leak => def NOTICE: f_leak => xxx NOTICE: f_leak => yyy NOTICE: f_leak => zzz a | b ---+----- 1 | aaa 2 | bbb 3 | ccc 4 | ddd 1 | abc 2 | bcd 3 | cde 4 | def 1 | xxx 2 | yyy 3 | zzz (11 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); QUERY PLAN --------------------------- Append -> Seq Scan on t1 Filter: f_leak(b) -> Seq Scan on t2 Filter: f_leak(b) -> Seq Scan on t3 Filter: f_leak(b) (7 rows) -- plan cache should be invalidated EXECUTE p1(2); a | b ---+----- 1 | aaa 2 | bbb 1 | abc 2 | bcd 1 | xxx 2 | yyy (6 rows) EXPLAIN (COSTS OFF) EXECUTE p1(2); QUERY PLAN -------------------------- Append -> Seq Scan on t1 Filter: (a <= 2) -> Seq Scan on t2 Filter: (a <= 2) -> Seq Scan on t3 Filter: (a <= 2) (7 rows) PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1; EXECUTE p2(2); a | b ---+----- 2 | bbb 2 | bcd 2 | yyy (3 rows) EXPLAIN (COSTS OFF) EXECUTE p2(2); QUERY PLAN ------------------------- Append -> Seq Scan on t1 Filter: (a = 2) -> Seq Scan on t2 Filter: (a = 2) -> Seq Scan on t3 Filter: (a = 2) (7 rows) -- also, case when privilege switch from superuser SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO ON; EXECUTE p2(2); a | b ---+----- 2 | bbb 2 | bcd 2 | yyy (3 rows) EXPLAIN (COSTS OFF) EXECUTE p2(2); QUERY PLAN --------------------------------------------- Append -> Seq Scan on t1 Filter: ((a = 2) AND ((a % 2) = 0)) -> Seq Scan on t2 Filter: ((a = 2) AND ((a % 2) = 0)) -> Seq Scan on t3 Filter: ((a = 2) AND ((a % 2) = 0)) (7 rows) -- -- UPDATE / DELETE and Row-level security -- SET SESSION AUTHORIZATION rls_regress_user1; EXPLAIN (COSTS OFF) UPDATE t1 SET b = b || b WHERE f_leak(b); QUERY PLAN ------------------------------------------- Update on t1 t1_3 Update on t1 t1_3 Update on t2 t1 Update on t3 t1 -> Subquery Scan on t1 Filter: f_leak(t1.b) -> LockRows -> Seq Scan on t1 t1_4 Filter: ((a % 2) = 0) -> Subquery Scan on t1_1 Filter: f_leak(t1_1.b) -> LockRows -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Subquery Scan on t1_2 Filter: f_leak(t1_2.b) -> LockRows -> Seq Scan on t3 Filter: ((a % 2) = 0) (19 rows) UPDATE t1 SET b = b || b WHERE f_leak(b); NOTICE: f_leak => bbb NOTICE: f_leak => ddd NOTICE: f_leak => bcd NOTICE: f_leak => def NOTICE: f_leak => yyy EXPLAIN (COSTS OFF) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b); QUERY PLAN ------------------------------------------- Update on t1 t1_1 -> Subquery Scan on t1 Filter: f_leak(t1.b) -> LockRows -> Seq Scan on t1 t1_2 Filter: ((a % 2) = 0) (6 rows) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b); NOTICE: f_leak => bbbbbb NOTICE: f_leak => dddddd -- returning clause with system column UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; NOTICE: f_leak => bbbbbb_updt NOTICE: f_leak => dddddd_updt oid | a | b | t1 -----+---+-------------+----------------- 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt) 104 | 4 | dddddd_updt | (4,dddddd_updt) (2 rows) UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *; NOTICE: f_leak => bbbbbb_updt NOTICE: f_leak => dddddd_updt NOTICE: f_leak => bcdbcd NOTICE: f_leak => defdef NOTICE: f_leak => yyyyyy a | b ---+------------- 2 | bbbbbb_updt 4 | dddddd_updt 2 | bcdbcd 4 | defdef 2 | yyyyyy (5 rows) UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; NOTICE: f_leak => bbbbbb_updt NOTICE: f_leak => dddddd_updt NOTICE: f_leak => bcdbcd NOTICE: f_leak => defdef NOTICE: f_leak => yyyyyy oid | a | b | t1 -----+---+-------------+----------------- 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt) 104 | 4 | dddddd_updt | (4,dddddd_updt) 202 | 2 | bcdbcd | (2,bcdbcd) 204 | 4 | defdef | (4,defdef) 302 | 2 | yyyyyy | (2,yyyyyy) (5 rows) -- updates with from clause EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t3 WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b); QUERY PLAN --------------------------------------------------------------- Update on t2 t2_1 -> Nested Loop -> Subquery Scan on t2 Filter: f_leak(t2.b) -> LockRows -> Seq Scan on t2 t2_2 Filter: ((a = 3) AND ((a % 2) = 1)) -> Seq Scan on t3 Filter: (f_leak(b) AND (a = 2)) (9 rows) UPDATE t2 SET b=t2.b FROM t3 WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b); NOTICE: f_leak => cde NOTICE: f_leak => xxx NOTICE: f_leak => zzz NOTICE: f_leak => yyyyyy EXPLAIN (COSTS OFF) UPDATE t1 SET b=t1.b FROM t2 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); QUERY PLAN --------------------------------------------------------------- Update on t1 t1_3 Update on t1 t1_3 Update on t2 t1 Update on t3 t1 -> Nested Loop -> Subquery Scan on t1 Filter: f_leak(t1.b) -> LockRows -> Seq Scan on t1 t1_4 Filter: ((a = 3) AND ((a % 2) = 0)) -> Subquery Scan on t2 Filter: f_leak(t2.b) -> Seq Scan on t2 t2_3 Filter: ((a = 3) AND ((a % 2) = 1)) -> Nested Loop -> Subquery Scan on t1_1 Filter: f_leak(t1_1.b) -> LockRows -> Seq Scan on t2 t2_4 Filter: ((a = 3) AND ((a % 2) = 0)) -> Subquery Scan on t2_1 Filter: f_leak(t2_1.b) -> Seq Scan on t2 t2_5 Filter: ((a = 3) AND ((a % 2) = 1)) -> Nested Loop -> Subquery Scan on t1_2 Filter: f_leak(t1_2.b) -> LockRows -> Seq Scan on t3 Filter: ((a = 3) AND ((a % 2) = 0)) -> Subquery Scan on t2_2 Filter: f_leak(t2_2.b) -> Seq Scan on t2 t2_6 Filter: ((a = 3) AND ((a % 2) = 1)) (34 rows) UPDATE t1 SET b=t1.b FROM t2 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t1 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); QUERY PLAN --------------------------------------------------------------------- Update on t2 t2_1 -> Nested Loop -> Subquery Scan on t2 Filter: f_leak(t2.b) -> LockRows -> Seq Scan on t2 t2_2 Filter: ((a = 3) AND ((a % 2) = 1)) -> Subquery Scan on t1 Filter: f_leak(t1.b) -> Result -> Append -> Seq Scan on t1 t1_1 Filter: ((a = 3) AND ((a % 2) = 0)) -> Seq Scan on t2 t2_3 Filter: ((a = 3) AND ((a % 2) = 0)) -> Seq Scan on t3 Filter: ((a = 3) AND ((a % 2) = 0)) (17 rows) UPDATE t2 SET b=t2.b FROM t1 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); NOTICE: f_leak => cde -- updates with from clause self join EXPLAIN (COSTS OFF) UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2 WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2; QUERY PLAN --------------------------------------------------------------------- Update on t2 t2_1_1 -> Nested Loop Join Filter: (t2_1.b = t2_2.b) -> Subquery Scan on t2_1 Filter: f_leak(t2_1.b) -> Subquery Scan on t2_1_2 Filter: ((t2_1_2.a % 2) = 1) -> LockRows -> Seq Scan on t2 t2_1_3 Filter: ((a = 3) AND ((a % 2) = 1)) -> Subquery Scan on t2_2 Filter: f_leak(t2_2.b) -> Seq Scan on t2 t2_2_1 Filter: ((a = 3) AND ((a % 2) = 1)) (14 rows) UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2 WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2; NOTICE: f_leak => cde NOTICE: f_leak => cde a | b | c | a | b | c | t2_1 | t2_2 ---+-----+-----+---+-----+-----+-------------+------------- 3 | cde | 3.3 | 3 | cde | 3.3 | (3,cde,3.3) | (3,cde,3.3) (1 row) EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2 WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2; QUERY PLAN --------------------------------------------------------------------- Update on t1 t1_1_3 Update on t1 t1_1_3 Update on t2 t1_1 Update on t3 t1_1 -> Nested Loop Join Filter: (t1_1.b = t1_2.b) -> Subquery Scan on t1_1 Filter: f_leak(t1_1.b) -> Subquery Scan on t1_1_4 Filter: ((t1_1_4.a % 2) = 0) -> LockRows -> Seq Scan on t1 t1_1_5 Filter: ((a = 4) AND ((a % 2) = 0)) -> Subquery Scan on t1_2 Filter: f_leak(t1_2.b) -> Append -> Seq Scan on t1 t1_2_3 Filter: ((a = 4) AND ((a % 2) = 0)) -> Seq Scan on t2 t1_2_4 Filter: ((a = 4) AND ((a % 2) = 0)) -> Seq Scan on t3 t1_2_5 Filter: ((a = 4) AND ((a % 2) = 0)) -> Nested Loop Join Filter: (t1_1_1.b = t1_2_1.b) -> Subquery Scan on t1_1_1 Filter: f_leak(t1_1_1.b) -> Subquery Scan on t1_1_6 Filter: ((t1_1_6.a % 2) = 0) -> LockRows -> Seq Scan on t2 t1_1_7 Filter: ((a = 4) AND ((a % 2) = 0)) -> Subquery Scan on t1_2_1 Filter: f_leak(t1_2_1.b) -> Append -> Seq Scan on t1 t1_2_6 Filter: ((a = 4) AND ((a % 2) = 0)) -> Seq Scan on t2 t1_2_7 Filter: ((a = 4) AND ((a % 2) = 0)) -> Seq Scan on t3 t1_2_8 Filter: ((a = 4) AND ((a % 2) = 0)) -> Nested Loop Join Filter: (t1_1_2.b = t1_2_2.b) -> Subquery Scan on t1_1_2 Filter: f_leak(t1_1_2.b) -> Subquery Scan on t1_1_8 Filter: ((t1_1_8.a % 2) = 0) -> LockRows -> Seq Scan on t3 t1_1_9 Filter: ((a = 4) AND ((a % 2) = 0)) -> Subquery Scan on t1_2_2 Filter: f_leak(t1_2_2.b) -> Append -> Seq Scan on t1 t1_2_9 Filter: ((a = 4) AND ((a % 2) = 0)) -> Seq Scan on t2 t1_2_10 Filter: ((a = 4) AND ((a % 2) = 0)) -> Seq Scan on t3 t1_2_11 Filter: ((a = 4) AND ((a % 2) = 0)) (58 rows) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2 WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2; NOTICE: f_leak => dddddd_updt NOTICE: f_leak => dddddd_updt NOTICE: f_leak => defdef NOTICE: f_leak => defdef NOTICE: f_leak => dddddd_updt NOTICE: f_leak => defdef a | b | a | b | t1_1 | t1_2 ---+-------------+---+-------------+-----------------+----------------- 4 | dddddd_updt | 4 | dddddd_updt | (4,dddddd_updt) | (4,dddddd_updt) 4 | defdef | 4 | defdef | (4,defdef) | (4,defdef) (2 rows) RESET SESSION AUTHORIZATION; SET row_security TO OFF; SELECT * FROM t1 ORDER BY a,b; a | b ---+------------- 1 | aaa 1 | abc 1 | xxx 2 | bbbbbb_updt 2 | bcdbcd 2 | yyyyyy 3 | ccc 3 | cde 3 | zzz 4 | dddddd_updt 4 | defdef (11 rows) SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO ON; EXPLAIN (COSTS OFF) DELETE FROM only t1 WHERE f_leak(b); QUERY PLAN ------------------------------------------- Delete on t1 t1_1 -> Subquery Scan on t1 Filter: f_leak(t1.b) -> LockRows -> Seq Scan on t1 t1_2 Filter: ((a % 2) = 0) (6 rows) EXPLAIN (COSTS OFF) DELETE FROM t1 WHERE f_leak(b); QUERY PLAN ------------------------------------------- Delete on t1 t1_3 Delete on t1 t1_3 Delete on t2 t1 Delete on t3 t1 -> Subquery Scan on t1 Filter: f_leak(t1.b) -> LockRows -> Seq Scan on t1 t1_4 Filter: ((a % 2) = 0) -> Subquery Scan on t1_1 Filter: f_leak(t1_1.b) -> LockRows -> Seq Scan on t2 Filter: ((a % 2) = 0) -> Subquery Scan on t1_2 Filter: f_leak(t1_2.b) -> LockRows -> Seq Scan on t3 Filter: ((a % 2) = 0) (19 rows) DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1; NOTICE: f_leak => bbbbbb_updt NOTICE: f_leak => dddddd_updt oid | a | b | t1 -----+---+-------------+----------------- 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt) 104 | 4 | dddddd_updt | (4,dddddd_updt) (2 rows) DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1; NOTICE: f_leak => bcdbcd NOTICE: f_leak => defdef NOTICE: f_leak => yyyyyy oid | a | b | t1 -----+---+--------+------------ 202 | 2 | bcdbcd | (2,bcdbcd) 204 | 4 | defdef | (4,defdef) 302 | 2 | yyyyyy | (2,yyyyyy) (3 rows) -- -- S.b. view on top of Row-level security -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE b1 (a int, b text); INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); CREATE POLICY p1 ON b1 USING (a % 2 = 0); ALTER TABLE b1 ENABLE ROW LEVEL SECURITY; GRANT ALL ON b1 TO rls_regress_user1; SET SESSION AUTHORIZATION rls_regress_user1; CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION; GRANT ALL ON bv1 TO rls_regress_user2; SET SESSION AUTHORIZATION rls_regress_user2; EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b); QUERY PLAN --------------------------------------------- Subquery Scan on bv1 Filter: f_leak(bv1.b) -> Seq Scan on b1 Filter: ((a > 0) AND ((a % 2) = 0)) (4 rows) SELECT * FROM bv1 WHERE f_leak(b); NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d NOTICE: f_leak => d3d9446802a44259755d38e6d163e820 a | b ----+---------------------------------- 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 (5 rows) INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO ERROR: new row violates row level security policy for "b1" INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check ERROR: new row violates row level security policy for "b1" INSERT INTO bv1 VALUES (12, 'xxx'); -- ok EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b); QUERY PLAN --------------------------------------------------------------------------- Update on b1 b1_1 -> Subquery Scan on b1 Filter: f_leak(b1.b) -> Subquery Scan on b1_2 -> LockRows -> Seq Scan on b1 b1_3 Filter: ((a > 0) AND (a = 4) AND ((a % 2) = 0)) (7 rows) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b); NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b); QUERY PLAN --------------------------------------------------------------------------- Delete on b1 b1_1 -> Subquery Scan on b1 Filter: f_leak(b1.b) -> Subquery Scan on b1_2 -> LockRows -> Seq Scan on b1 b1_3 Filter: ((a > 0) AND (a = 6) AND ((a % 2) = 0)) (7 rows) DELETE FROM bv1 WHERE a = 6 AND f_leak(b); NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc SET SESSION AUTHORIZATION rls_regress_user0; SELECT * FROM b1; a | b -----+---------------------------------- -10 | 1b0fd9efa5279c4203b7c70233f86dbf -9 | 252e691406782824eec43d7eadc3d256 -8 | a8d2ec85eaf98407310b72eb73dda247 -7 | 74687a12d3915d3c4d83f1af7b3683d5 -6 | 596a3d04481816330f07e4f97510c28f -5 | 47c1b025fa18ea96c33fbb6718688c0f -4 | 0267aaf632e87a63288a08331f22c7c3 -3 | b3149ecea4628efd23d2f86e5a723472 -2 | 5d7b9adcbe1c629ec722529dd12e5129 -1 | 6bb61e3b7bce0931da574d19d1d82c88 0 | cfcd208495d565ef66e7dff9f98764da 1 | c4ca4238a0b923820dcc509a6f75849b 2 | c81e728d9d4c2f636f067f89cc14862c 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 5 | e4da3b7fbbce2345d7772b0674a318d5 7 | 8f14e45fceea167a5a36dedd4bea2543 8 | c9f0f895fb98ab9159f51fd0297e236d 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 10 | d3d9446802a44259755d38e6d163e820 12 | xxx 4 | yyy (21 rows) -- -- INSERT ... ON CONFLICT DO UPDATE and Row-level security -- SET SESSION AUTHORIZATION rls_regress_user0; DROP POLICY p1 ON document; CREATE POLICY p1 ON document FOR SELECT USING (true); CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user); CREATE POLICY p3 ON document FOR UPDATE USING (cid = (SELECT cid from category WHERE cname = 'novel')) WITH CHECK (dauthor = current_user); SET SESSION AUTHORIZATION rls_regress_user1; -- Exists... SELECT * FROM document WHERE did = 2; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------- 2 | 11 | 2 | rls_regress_user1 | my second novel (1 row) -- ...so violates actual WITH CHECK OPTION within UPDATE (not INSERT, since -- alternative UPDATE path happens to be taken): INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle, dauthor = EXCLUDED.dauthor; ERROR: new row violates row level security policy for "document" -- Violates USING qual for UPDATE policy p3. -- -- UPDATE path is taken, but UPDATE fails purely because *existing* row to be -- updated is not a "novel"/cid 11 (row is not leaked, even though we have -- SELECT privileges sufficient to see the row in this instance): INSERT INTO document VALUES (33, 22, 1, 'rls_regress_user1', 'okay science fiction'); -- preparation for next statement INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'Some novel, replaces sci-fi') -- takes UPDATE path ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle; ERROR: new row violates row level security policy (USING expression) for "document" -- Fine (we UPDATE, since INSERT WCOs and UPDATE security barrier quals + WCOs -- not violated): INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+---------------- 2 | 11 | 2 | rls_regress_user1 | my first novel (1 row) -- Fine (we INSERT, so "cid = 33" ("technology") isn't evaluated): INSERT INTO document VALUES (78, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'some technology novel') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33 RETURNING *; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 78 | 11 | 1 | rls_regress_user1 | some technology novel (1 row) -- Fine (same query, but we UPDATE, so "cid = 33", ("technology") is not the -- case in respect of *existing* tuple): INSERT INTO document VALUES (78, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'some technology novel') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33 RETURNING *; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+----------------------- 78 | 33 | 1 | rls_regress_user1 | some technology novel (1 row) -- Same query a third time, but now fails due to existing tuple finally not -- passing quals: INSERT INTO document VALUES (78, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'some technology novel') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33 RETURNING *; ERROR: new row violates row level security policy (USING expression) for "document" -- Don't fail just because INSERT doesn't satisfy WITH CHECK option that -- originated as a barrier/USING() qual from the UPDATE. Note that the UPDATE -- path *isn't* taken, and so UPDATE-related policy does not apply: INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *; did | cid | dlevel | dauthor | dtitle -----+-----+--------+-------------------+---------------------------------- 79 | 33 | 1 | rls_regress_user1 | technology book, can only insert (1 row) -- But this time, the same statement fails, because the UPDATE path is taken, -- and updating the row just inserted falls afoul of security barrier qual -- (enforced as WCO) -- what we might have updated target tuple to is -- irrelevant, in fact. INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *; ERROR: new row violates row level security policy (USING expression) for "document" -- Test default USING qual enforced as WCO SET SESSION AUTHORIZATION rls_regress_user0; DROP POLICY p1 ON document; DROP POLICY p2 ON document; DROP POLICY p3 ON document; CREATE POLICY p3_with_default ON document FOR UPDATE USING (cid = (SELECT cid from category WHERE cname = 'novel')); SET SESSION AUTHORIZATION rls_regress_user1; -- Just because WCO-style enforcement of USING quals occurs with -- existing/target tuple does not mean that the implementation can be allowed -- to fail to also enforce this qual against the final tuple appended to -- relation (since in the absence of an explicit WCO, this is also interpreted -- as an UPDATE/ALL WCO in general). -- -- UPDATE path is taken here (fails due to existing tuple). Note that this is -- not reported as a "USING expression", because it's an RLS UPDATE check that originated as -- a USING qual for the purposes of RLS in general, as opposed to an explicit -- USING qual that is ordinarily a security barrier. We leave it up to the -- UPDATE to make this fail: INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *; ERROR: new row violates row level security policy for "document" -- UPDATE path is taken here. Existing tuple passes, since it's cid -- corresponds to "novel", but default USING qual is enforced against -- post-UPDATE tuple too (as always when updating with a policy that lacks an -- explicit WCO), and so this fails: INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'my first novel') ON CONFLICT (did) DO UPDATE SET cid = EXCLUDED.cid, dtitle = EXCLUDED.dtitle RETURNING *; ERROR: new row violates row level security policy for "document" SET SESSION AUTHORIZATION rls_regress_user0; DROP POLICY p3_with_default ON document; -- -- Test ALL policies with ON CONFLICT DO UPDATE (much the same as existing UPDATE -- tests) -- CREATE POLICY p3_with_all ON document FOR ALL USING (cid = (SELECT cid from category WHERE cname = 'novel')) WITH CHECK (dauthor = current_user); SET SESSION AUTHORIZATION rls_regress_user1; -- Fails, since ALL WCO is enforced in insert path: INSERT INTO document VALUES (80, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33; ERROR: new row violates row level security policy for "document" -- Fails, since ALL policy USING qual is enforced (existing, target tuple is in -- violation, since it has the "manga" cid): INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel') ON CONFLICT (did) DO UPDATE SET dtitle = EXCLUDED.dtitle; ERROR: new row violates row level security policy (USING expression) for "document" -- Fails, since ALL WCO are enforced: INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel') ON CONFLICT (did) DO UPDATE SET dauthor = 'rls_regress_user2'; ERROR: new row violates row level security policy for "document" -- -- ROLE/GROUP -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE z1 (a int, b text); GRANT SELECT ON z1 TO rls_regress_group1, rls_regress_group2, rls_regress_user1, rls_regress_user2; INSERT INTO z1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'), (4, 'ddd'); CREATE POLICY p1 ON z1 TO rls_regress_group1 USING (a % 2 = 0); CREATE POLICY p2 ON z1 TO rls_regress_group2 USING (a % 2 = 1); ALTER TABLE z1 ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM z1 WHERE f_leak(b); NOTICE: f_leak => bbb NOTICE: f_leak => ddd a | b ---+----- 2 | bbb 4 | ddd (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); QUERY PLAN ------------------------------- Subquery Scan on z1 Filter: f_leak(z1.b) -> Seq Scan on z1 z1_1 Filter: ((a % 2) = 0) (4 rows) SET ROLE rls_regress_group1; SELECT * FROM z1 WHERE f_leak(b); NOTICE: f_leak => bbb NOTICE: f_leak => ddd a | b ---+----- 2 | bbb 4 | ddd (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); QUERY PLAN ------------------------------- Subquery Scan on z1 Filter: f_leak(z1.b) -> Seq Scan on z1 z1_1 Filter: ((a % 2) = 0) (4 rows) SET SESSION AUTHORIZATION rls_regress_user2; SELECT * FROM z1 WHERE f_leak(b); NOTICE: f_leak => aaa NOTICE: f_leak => ccc a | b ---+----- 1 | aaa 3 | ccc (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); QUERY PLAN ------------------------------- Subquery Scan on z1 Filter: f_leak(z1.b) -> Seq Scan on z1 z1_1 Filter: ((a % 2) = 1) (4 rows) SET ROLE rls_regress_group2; SELECT * FROM z1 WHERE f_leak(b); NOTICE: f_leak => aaa NOTICE: f_leak => ccc a | b ---+----- 1 | aaa 3 | ccc (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); QUERY PLAN ------------------------------- Subquery Scan on z1 Filter: f_leak(z1.b) -> Seq Scan on z1 z1_1 Filter: ((a % 2) = 1) (4 rows) -- -- Views should follow policy for view owner. -- -- View and Table owner are the same. SET SESSION AUTHORIZATION rls_regress_user0; CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); GRANT SELECT ON rls_view TO rls_regress_user1; -- Query as role that is not owner of view or table. Should return all records. SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM rls_view; NOTICE: f_leak => aaa NOTICE: f_leak => bbb NOTICE: f_leak => ccc NOTICE: f_leak => ddd a | b ---+----- 1 | aaa 2 | bbb 3 | ccc 4 | ddd (4 rows) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; QUERY PLAN --------------------- Seq Scan on z1 Filter: f_leak(b) (2 rows) -- Query as view/table owner. Should return all records. SET SESSION AUTHORIZATION rls_regress_user0; SELECT * FROM rls_view; NOTICE: f_leak => aaa NOTICE: f_leak => bbb NOTICE: f_leak => ccc NOTICE: f_leak => ddd a | b ---+----- 1 | aaa 2 | bbb 3 | ccc 4 | ddd (4 rows) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; QUERY PLAN --------------------- Seq Scan on z1 Filter: f_leak(b) (2 rows) DROP VIEW rls_view; -- View and Table owners are different. SET SESSION AUTHORIZATION rls_regress_user1; CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); GRANT SELECT ON rls_view TO rls_regress_user0; -- Query as role that is not owner of view but is owner of table. -- Should return records based on view owner policies. SET SESSION AUTHORIZATION rls_regress_user0; SELECT * FROM rls_view; NOTICE: f_leak => bbb NOTICE: f_leak => ddd a | b ---+----- 2 | bbb 4 | ddd (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; QUERY PLAN ------------------------------- Subquery Scan on z1 Filter: f_leak(z1.b) -> Seq Scan on z1 z1_1 Filter: ((a % 2) = 0) (4 rows) -- Query as role that is not owner of table but is owner of view. -- Should return records based on view owner policies. SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM rls_view; NOTICE: f_leak => bbb NOTICE: f_leak => ddd a | b ---+----- 2 | bbb 4 | ddd (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; QUERY PLAN ------------------------------- Subquery Scan on z1 Filter: f_leak(z1.b) -> Seq Scan on z1 z1_1 Filter: ((a % 2) = 0) (4 rows) -- Query as role that is not the owner of the table or view without permissions. SET SESSION AUTHORIZATION rls_regress_user2; SELECT * FROM rls_view; --fail - permission denied. ERROR: permission denied for relation rls_view EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied. ERROR: permission denied for relation rls_view -- Query as role that is not the owner of the table or view with permissions. SET SESSION AUTHORIZATION rls_regress_user1; GRANT SELECT ON rls_view TO rls_regress_user2; SELECT * FROM rls_view; NOTICE: f_leak => bbb NOTICE: f_leak => ddd a | b ---+----- 2 | bbb 4 | ddd (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; QUERY PLAN ------------------------------- Subquery Scan on z1 Filter: f_leak(z1.b) -> Seq Scan on z1 z1_1 Filter: ((a % 2) = 0) (4 rows) SET SESSION AUTHORIZATION rls_regress_user1; DROP VIEW rls_view; -- -- Command specific -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE x1 (a int, b text, c text); GRANT ALL ON x1 TO PUBLIC; INSERT INTO x1 VALUES (1, 'abc', 'rls_regress_user1'), (2, 'bcd', 'rls_regress_user1'), (3, 'cde', 'rls_regress_user2'), (4, 'def', 'rls_regress_user2'), (5, 'efg', 'rls_regress_user1'), (6, 'fgh', 'rls_regress_user1'), (7, 'fgh', 'rls_regress_user2'), (8, 'fgh', 'rls_regress_user2'); CREATE POLICY p0 ON x1 FOR ALL USING (c = current_user); CREATE POLICY p1 ON x1 FOR SELECT USING (a % 2 = 0); CREATE POLICY p2 ON x1 FOR INSERT WITH CHECK (a % 2 = 1); CREATE POLICY p3 ON x1 FOR UPDATE USING (a % 2 = 0); CREATE POLICY p4 ON x1 FOR DELETE USING (a < 8); ALTER TABLE x1 ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM x1 WHERE f_leak(b) ORDER BY a ASC; NOTICE: f_leak => abc NOTICE: f_leak => bcd NOTICE: f_leak => def NOTICE: f_leak => efg NOTICE: f_leak => fgh NOTICE: f_leak => fgh a | b | c ---+-----+------------------- 1 | abc | rls_regress_user1 2 | bcd | rls_regress_user1 4 | def | rls_regress_user2 5 | efg | rls_regress_user1 6 | fgh | rls_regress_user1 8 | fgh | rls_regress_user2 (6 rows) UPDATE x1 SET b = b || '_updt' WHERE f_leak(b) RETURNING *; NOTICE: f_leak => abc NOTICE: f_leak => bcd NOTICE: f_leak => def NOTICE: f_leak => efg NOTICE: f_leak => fgh NOTICE: f_leak => fgh a | b | c ---+----------+------------------- 1 | abc_updt | rls_regress_user1 2 | bcd_updt | rls_regress_user1 4 | def_updt | rls_regress_user2 5 | efg_updt | rls_regress_user1 6 | fgh_updt | rls_regress_user1 8 | fgh_updt | rls_regress_user2 (6 rows) SET SESSION AUTHORIZATION rls_regress_user2; SELECT * FROM x1 WHERE f_leak(b) ORDER BY a ASC; NOTICE: f_leak => cde NOTICE: f_leak => fgh NOTICE: f_leak => bcd_updt NOTICE: f_leak => def_updt NOTICE: f_leak => fgh_updt NOTICE: f_leak => fgh_updt a | b | c ---+----------+------------------- 2 | bcd_updt | rls_regress_user1 3 | cde | rls_regress_user2 4 | def_updt | rls_regress_user2 6 | fgh_updt | rls_regress_user1 7 | fgh | rls_regress_user2 8 | fgh_updt | rls_regress_user2 (6 rows) UPDATE x1 SET b = b || '_updt' WHERE f_leak(b) RETURNING *; NOTICE: f_leak => cde NOTICE: f_leak => fgh NOTICE: f_leak => bcd_updt NOTICE: f_leak => def_updt NOTICE: f_leak => fgh_updt NOTICE: f_leak => fgh_updt a | b | c ---+---------------+------------------- 3 | cde_updt | rls_regress_user2 7 | fgh_updt | rls_regress_user2 2 | bcd_updt_updt | rls_regress_user1 4 | def_updt_updt | rls_regress_user2 6 | fgh_updt_updt | rls_regress_user1 8 | fgh_updt_updt | rls_regress_user2 (6 rows) DELETE FROM x1 WHERE f_leak(b) RETURNING *; NOTICE: f_leak => cde_updt NOTICE: f_leak => fgh_updt NOTICE: f_leak => bcd_updt_updt NOTICE: f_leak => def_updt_updt NOTICE: f_leak => fgh_updt_updt NOTICE: f_leak => fgh_updt_updt a | b | c ---+---------------+------------------- 3 | cde_updt | rls_regress_user2 7 | fgh_updt | rls_regress_user2 2 | bcd_updt_updt | rls_regress_user1 4 | def_updt_updt | rls_regress_user2 6 | fgh_updt_updt | rls_regress_user1 8 | fgh_updt_updt | rls_regress_user2 (6 rows) -- -- Duplicate Policy Names -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE y1 (a int, b text); CREATE TABLE y2 (a int, b text); GRANT ALL ON y1, y2 TO rls_regress_user1; CREATE POLICY p1 ON y1 FOR ALL USING (a % 2 = 0); CREATE POLICY p2 ON y1 FOR SELECT USING (a > 2); CREATE POLICY p1 ON y1 FOR SELECT USING (a % 2 = 1); --fail ERROR: policy "p1" for table "y1" already exists CREATE POLICY p1 ON y2 FOR ALL USING (a % 2 = 0); --OK ALTER TABLE y1 ENABLE ROW LEVEL SECURITY; ALTER TABLE y2 ENABLE ROW LEVEL SECURITY; -- -- Expression structure with SBV -- -- Create view as table owner. RLS should NOT be applied. SET SESSION AUTHORIZATION rls_regress_user0; CREATE VIEW rls_sbv WITH (security_barrier) AS SELECT * FROM y1 WHERE f_leak(b); EXPLAIN (COSTS OFF) SELECT * FROM rls_sbv WHERE (a = 1); QUERY PLAN ----------------------------------- Seq Scan on y1 Filter: (f_leak(b) AND (a = 1)) (2 rows) DROP VIEW rls_sbv; -- Create view as role that does not own table. RLS should be applied. SET SESSION AUTHORIZATION rls_regress_user1; CREATE VIEW rls_sbv WITH (security_barrier) AS SELECT * FROM y1 WHERE f_leak(b); EXPLAIN (COSTS OFF) SELECT * FROM rls_sbv WHERE (a = 1); QUERY PLAN ---------------------------------------------------------- Subquery Scan on y1 Filter: f_leak(y1.b) -> Seq Scan on y1 y1_1 Filter: ((a = 1) AND ((a > 2) OR ((a % 2) = 0))) (4 rows) DROP VIEW rls_sbv; -- -- Expression structure -- SET SESSION AUTHORIZATION rls_regress_user0; INSERT INTO y2 (SELECT x, md5(x::text) FROM generate_series(0,20) x); CREATE POLICY p2 ON y2 USING (a % 3 = 0); CREATE POLICY p3 ON y2 USING (a % 4 = 0); SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM y2 WHERE f_leak(b); NOTICE: f_leak => cfcd208495d565ef66e7dff9f98764da NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c NOTICE: f_leak => eccbc87e4b5ce2fe28308fd9f2a7baf3 NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d NOTICE: f_leak => 45c48cce2e2d7fbdea1afc51c7c6ad26 NOTICE: f_leak => d3d9446802a44259755d38e6d163e820 NOTICE: f_leak => c20ad4d76fe97759aa27a0c99bff6710 NOTICE: f_leak => aab3238922bcc25a6f606eb525ffdc56 NOTICE: f_leak => 9bf31c7ff062936a96d3c8bd1f8f2ff3 NOTICE: f_leak => c74d97b01eae257e44aa9d5bade97baf NOTICE: f_leak => 6f4922f45568161a8cdf4ad2299f6d23 NOTICE: f_leak => 98f13708210194c475687be6106a3b84 a | b ----+---------------------------------- 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 (14 rows) EXPLAIN (COSTS OFF) SELECT * FROM y2 WHERE f_leak(b); QUERY PLAN ------------------------------------------------------------------- Subquery Scan on y2 Filter: f_leak(y2.b) -> Seq Scan on y2 y2_1 Filter: (((a % 4) = 0) OR ((a % 3) = 0) OR ((a % 2) = 0)) (4 rows) -- -- Qual push-down of leaky functions, when not referring to table -- SELECT * FROM y2 WHERE f_leak('abc'); NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc NOTICE: f_leak => abc a | b ----+---------------------------------- 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 (14 rows) EXPLAIN (COSTS OFF) SELECT * FROM y2 WHERE f_leak('abc'); QUERY PLAN --------------------------------------------------------------------------------------- Seq Scan on y2 Filter: (f_leak('abc'::text) AND (((a % 4) = 0) OR ((a % 3) = 0) OR ((a % 2) = 0))) (2 rows) CREATE TABLE test_qual_pushdown ( abc text ); INSERT INTO test_qual_pushdown VALUES ('abc'),('def'); SELECT * FROM y2 JOIN test_qual_pushdown ON (b = abc) WHERE f_leak(abc); NOTICE: f_leak => abc NOTICE: f_leak => def a | b | abc ---+---+----- (0 rows) EXPLAIN (COSTS OFF) SELECT * FROM y2 JOIN test_qual_pushdown ON (b = abc) WHERE f_leak(abc); QUERY PLAN ------------------------------------------------------------------------- Hash Join Hash Cond: (test_qual_pushdown.abc = y2.b) -> Seq Scan on test_qual_pushdown Filter: f_leak(abc) -> Hash -> Seq Scan on y2 Filter: (((a % 4) = 0) OR ((a % 3) = 0) OR ((a % 2) = 0)) (7 rows) SELECT * FROM y2 JOIN test_qual_pushdown ON (b = abc) WHERE f_leak(b); NOTICE: f_leak => cfcd208495d565ef66e7dff9f98764da NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c NOTICE: f_leak => eccbc87e4b5ce2fe28308fd9f2a7baf3 NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d NOTICE: f_leak => 45c48cce2e2d7fbdea1afc51c7c6ad26 NOTICE: f_leak => d3d9446802a44259755d38e6d163e820 NOTICE: f_leak => c20ad4d76fe97759aa27a0c99bff6710 NOTICE: f_leak => aab3238922bcc25a6f606eb525ffdc56 NOTICE: f_leak => 9bf31c7ff062936a96d3c8bd1f8f2ff3 NOTICE: f_leak => c74d97b01eae257e44aa9d5bade97baf NOTICE: f_leak => 6f4922f45568161a8cdf4ad2299f6d23 NOTICE: f_leak => 98f13708210194c475687be6106a3b84 a | b | abc ---+---+----- (0 rows) EXPLAIN (COSTS OFF) SELECT * FROM y2 JOIN test_qual_pushdown ON (b = abc) WHERE f_leak(b); QUERY PLAN ------------------------------------------------------------------------------- Hash Join Hash Cond: (test_qual_pushdown.abc = y2.b) -> Seq Scan on test_qual_pushdown -> Hash -> Subquery Scan on y2 Filter: f_leak(y2.b) -> Seq Scan on y2 y2_1 Filter: (((a % 4) = 0) OR ((a % 3) = 0) OR ((a % 2) = 0)) (8 rows) DROP TABLE test_qual_pushdown; -- -- Plancache invalidate on user change. -- RESET SESSION AUTHORIZATION; -- Suppress NOTICE messages when doing a cascaded drop. SET client_min_messages TO 'warning'; DROP TABLE t1 CASCADE; RESET client_min_messages; CREATE TABLE t1 (a integer); GRANT SELECT ON t1 TO rls_regress_user1, rls_regress_user2; CREATE POLICY p1 ON t1 TO rls_regress_user1 USING ((a % 2) = 0); CREATE POLICY p2 ON t1 TO rls_regress_user2 USING ((a % 4) = 0); ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; SET ROLE rls_regress_user1; PREPARE role_inval AS SELECT * FROM t1; EXPLAIN (COSTS OFF) EXECUTE role_inval; QUERY PLAN ------------------------- Seq Scan on t1 Filter: ((a % 2) = 0) (2 rows) SET ROLE rls_regress_user2; EXPLAIN (COSTS OFF) EXECUTE role_inval; QUERY PLAN ------------------------- Seq Scan on t1 Filter: ((a % 4) = 0) (2 rows) -- -- CTE and RLS -- RESET SESSION AUTHORIZATION; DROP TABLE t1 CASCADE; CREATE TABLE t1 (a integer, b text); CREATE POLICY p1 ON t1 USING (a % 2 = 0); ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; GRANT ALL ON t1 TO rls_regress_user1; INSERT INTO t1 (SELECT x, md5(x::text) FROM generate_series(0,20) x); SET SESSION AUTHORIZATION rls_regress_user1; WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FROM cte1; NOTICE: f_leak => cfcd208495d565ef66e7dff9f98764da NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d NOTICE: f_leak => d3d9446802a44259755d38e6d163e820 NOTICE: f_leak => c20ad4d76fe97759aa27a0c99bff6710 NOTICE: f_leak => aab3238922bcc25a6f606eb525ffdc56 NOTICE: f_leak => c74d97b01eae257e44aa9d5bade97baf NOTICE: f_leak => 6f4922f45568161a8cdf4ad2299f6d23 NOTICE: f_leak => 98f13708210194c475687be6106a3b84 a | b ----+---------------------------------- 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 (11 rows) EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FROM cte1; QUERY PLAN --------------------------------------- CTE Scan on cte1 CTE cte1 -> Subquery Scan on t1 Filter: f_leak(t1.b) -> Seq Scan on t1 t1_1 Filter: ((a % 2) = 0) (6 rows) WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail ERROR: new row violates row level security policy for "t1" WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok a | b ----+---------------------------------- 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 (11 rows) WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail ERROR: new row violates row level security policy for "t1" WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok a | b ----+--------- 20 | Success (1 row) -- -- Rename Policy -- RESET SESSION AUTHORIZATION; ALTER POLICY p1 ON t1 RENAME TO p1; --fail ERROR: policy "p1" for table "t1" already exists SELECT polname, relname FROM pg_policy pol JOIN pg_class pc ON (pc.oid = pol.polrelid) WHERE relname = 't1'; polname | relname ---------+--------- p1 | t1 (1 row) ALTER POLICY p1 ON t1 RENAME TO p2; --ok SELECT polname, relname FROM pg_policy pol JOIN pg_class pc ON (pc.oid = pol.polrelid) WHERE relname = 't1'; polname | relname ---------+--------- p2 | t1 (1 row) -- -- Check INSERT SELECT -- SET SESSION AUTHORIZATION rls_regress_user1; CREATE TABLE t2 (a integer, b text); INSERT INTO t2 (SELECT * FROM t1); EXPLAIN (COSTS OFF) INSERT INTO t2 (SELECT * FROM t1); QUERY PLAN ------------------------------- Insert on t2 -> Seq Scan on t1 Filter: ((a % 2) = 0) (3 rows) SELECT * FROM t2; a | b ----+---------------------------------- 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 20 | Success (12 rows) EXPLAIN (COSTS OFF) SELECT * FROM t2; QUERY PLAN ---------------- Seq Scan on t2 (1 row) CREATE TABLE t3 AS SELECT * FROM t1; SELECT * FROM t3; a | b ----+---------------------------------- 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 20 | Success (12 rows) SELECT * INTO t4 FROM t1; SELECT * FROM t4; a | b ----+---------------------------------- 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 20 | Success (12 rows) -- -- RLS with JOIN -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE blog (id integer, author text, post text); CREATE TABLE comment (blog_id integer, message text); GRANT ALL ON blog, comment TO rls_regress_user1; CREATE POLICY blog_1 ON blog USING (id % 2 = 0); ALTER TABLE blog ENABLE ROW LEVEL SECURITY; INSERT INTO blog VALUES (1, 'alice', 'blog #1'), (2, 'bob', 'blog #1'), (3, 'alice', 'blog #2'), (4, 'alice', 'blog #3'), (5, 'john', 'blog #1'); INSERT INTO comment VALUES (1, 'cool blog'), (1, 'fun blog'), (3, 'crazy blog'), (5, 'what?'), (4, 'insane!'), (2, 'who did it?'); SET SESSION AUTHORIZATION rls_regress_user1; -- Check RLS JOIN with Non-RLS. SELECT id, author, message FROM blog JOIN comment ON id = blog_id; id | author | message ----+--------+------------- 4 | alice | insane! 2 | bob | who did it? (2 rows) -- Check Non-RLS JOIN with RLS. SELECT id, author, message FROM comment JOIN blog ON id = blog_id; id | author | message ----+--------+------------- 4 | alice | insane! 2 | bob | who did it? (2 rows) SET SESSION AUTHORIZATION rls_regress_user0; CREATE POLICY comment_1 ON comment USING (blog_id < 4); ALTER TABLE comment ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; -- Check RLS JOIN RLS SELECT id, author, message FROM blog JOIN comment ON id = blog_id; id | author | message ----+--------+------------- 2 | bob | who did it? (1 row) SELECT id, author, message FROM comment JOIN blog ON id = blog_id; id | author | message ----+--------+------------- 2 | bob | who did it? (1 row) SET SESSION AUTHORIZATION rls_regress_user0; DROP TABLE blog, comment; -- -- Default Deny Policy -- RESET SESSION AUTHORIZATION; DROP POLICY p2 ON t1; ALTER TABLE t1 OWNER TO rls_regress_user0; -- Check that default deny does not apply to superuser. RESET SESSION AUTHORIZATION; SELECT * FROM t1; a | b ----+---------------------------------- 1 | c4ca4238a0b923820dcc509a6f75849b 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 5 | e4da3b7fbbce2345d7772b0674a318d5 7 | 8f14e45fceea167a5a36dedd4bea2543 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 11 | 6512bd43d9caa6e02c990b0a82652dca 13 | c51ce410c124a10e0db5e4b97fc2af39 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 17 | 70efdf2ec9b086079795c442636b55fb 19 | 1f0e3dad99908345f7439f8ffabdffc4 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 20 | Success (22 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN ---------------- Seq Scan on t1 (1 row) -- Check that default deny does not apply to table owner. SET SESSION AUTHORIZATION rls_regress_user0; SELECT * FROM t1; a | b ----+---------------------------------- 1 | c4ca4238a0b923820dcc509a6f75849b 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 5 | e4da3b7fbbce2345d7772b0674a318d5 7 | 8f14e45fceea167a5a36dedd4bea2543 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 11 | 6512bd43d9caa6e02c990b0a82652dca 13 | c51ce410c124a10e0db5e4b97fc2af39 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 17 | 70efdf2ec9b086079795c442636b55fb 19 | 1f0e3dad99908345f7439f8ffabdffc4 0 | cfcd208495d565ef66e7dff9f98764da 2 | c81e728d9d4c2f636f067f89cc14862c 4 | a87ff679a2f3e71d9181a67b7542122c 6 | 1679091c5a880faf6fb5e6087eb1b2dc 8 | c9f0f895fb98ab9159f51fd0297e236d 10 | d3d9446802a44259755d38e6d163e820 12 | c20ad4d76fe97759aa27a0c99bff6710 14 | aab3238922bcc25a6f606eb525ffdc56 16 | c74d97b01eae257e44aa9d5bade97baf 18 | 6f4922f45568161a8cdf4ad2299f6d23 20 | 98f13708210194c475687be6106a3b84 20 | Success (22 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN ---------------- Seq Scan on t1 (1 row) -- Check that default deny does apply to superuser when RLS force. SET row_security TO FORCE; RESET SESSION AUTHORIZATION; SELECT * FROM t1; a | b ---+--- (0 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN -------------------------- Result One-Time Filter: false (2 rows) -- Check that default deny does apply to table owner when RLS force. SET SESSION AUTHORIZATION rls_regress_user0; SELECT * FROM t1; a | b ---+--- (0 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN -------------------------- Result One-Time Filter: false (2 rows) -- Check that default deny applies to non-owner/non-superuser when RLS on. SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO ON; SELECT * FROM t1; a | b ---+--- (0 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN -------------------------- Result One-Time Filter: false (2 rows) SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM t1; a | b ---+--- (0 rows) EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN -------------------------- Result One-Time Filter: false (2 rows) -- -- COPY TO/FROM -- RESET SESSION AUTHORIZATION; DROP TABLE copy_t CASCADE; ERROR: table "copy_t" does not exist CREATE TABLE copy_t (a integer, b text); CREATE POLICY p1 ON copy_t USING (a % 2 = 0); ALTER TABLE copy_t ENABLE ROW LEVEL SECURITY; GRANT ALL ON copy_t TO rls_regress_user1, rls_regress_exempt_user; INSERT INTO copy_t (SELECT x, md5(x::text) FROM generate_series(0,10) x); -- Check COPY TO as Superuser/owner. RESET SESSION AUTHORIZATION; SET row_security TO OFF; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; 0,cfcd208495d565ef66e7dff9f98764da 1,c4ca4238a0b923820dcc509a6f75849b 2,c81e728d9d4c2f636f067f89cc14862c 3,eccbc87e4b5ce2fe28308fd9f2a7baf3 4,a87ff679a2f3e71d9181a67b7542122c 5,e4da3b7fbbce2345d7772b0674a318d5 6,1679091c5a880faf6fb5e6087eb1b2dc 7,8f14e45fceea167a5a36dedd4bea2543 8,c9f0f895fb98ab9159f51fd0297e236d 9,45c48cce2e2d7fbdea1afc51c7c6ad26 10,d3d9446802a44259755d38e6d163e820 SET row_security TO ON; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; 0,cfcd208495d565ef66e7dff9f98764da 1,c4ca4238a0b923820dcc509a6f75849b 2,c81e728d9d4c2f636f067f89cc14862c 3,eccbc87e4b5ce2fe28308fd9f2a7baf3 4,a87ff679a2f3e71d9181a67b7542122c 5,e4da3b7fbbce2345d7772b0674a318d5 6,1679091c5a880faf6fb5e6087eb1b2dc 7,8f14e45fceea167a5a36dedd4bea2543 8,c9f0f895fb98ab9159f51fd0297e236d 9,45c48cce2e2d7fbdea1afc51c7c6ad26 10,d3d9446802a44259755d38e6d163e820 SET row_security TO FORCE; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; 0,cfcd208495d565ef66e7dff9f98764da 2,c81e728d9d4c2f636f067f89cc14862c 4,a87ff679a2f3e71d9181a67b7542122c 6,1679091c5a880faf6fb5e6087eb1b2dc 8,c9f0f895fb98ab9159f51fd0297e236d 10,d3d9446802a44259755d38e6d163e820 -- Check COPY TO as user with permissions. SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO OFF; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - insufficient to bypass rls ERROR: insufficient privilege to bypass row security. SET row_security TO ON; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok 0,cfcd208495d565ef66e7dff9f98764da 2,c81e728d9d4c2f636f067f89cc14862c 4,a87ff679a2f3e71d9181a67b7542122c 6,1679091c5a880faf6fb5e6087eb1b2dc 8,c9f0f895fb98ab9159f51fd0297e236d 10,d3d9446802a44259755d38e6d163e820 SET row_security TO FORCE; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok 0,cfcd208495d565ef66e7dff9f98764da 2,c81e728d9d4c2f636f067f89cc14862c 4,a87ff679a2f3e71d9181a67b7542122c 6,1679091c5a880faf6fb5e6087eb1b2dc 8,c9f0f895fb98ab9159f51fd0297e236d 10,d3d9446802a44259755d38e6d163e820 -- Check COPY TO as user with permissions and BYPASSRLS SET SESSION AUTHORIZATION rls_regress_exempt_user; SET row_security TO OFF; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok 0,cfcd208495d565ef66e7dff9f98764da 1,c4ca4238a0b923820dcc509a6f75849b 2,c81e728d9d4c2f636f067f89cc14862c 3,eccbc87e4b5ce2fe28308fd9f2a7baf3 4,a87ff679a2f3e71d9181a67b7542122c 5,e4da3b7fbbce2345d7772b0674a318d5 6,1679091c5a880faf6fb5e6087eb1b2dc 7,8f14e45fceea167a5a36dedd4bea2543 8,c9f0f895fb98ab9159f51fd0297e236d 9,45c48cce2e2d7fbdea1afc51c7c6ad26 10,d3d9446802a44259755d38e6d163e820 SET row_security TO ON; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok 0,cfcd208495d565ef66e7dff9f98764da 2,c81e728d9d4c2f636f067f89cc14862c 4,a87ff679a2f3e71d9181a67b7542122c 6,1679091c5a880faf6fb5e6087eb1b2dc 8,c9f0f895fb98ab9159f51fd0297e236d 10,d3d9446802a44259755d38e6d163e820 SET row_security TO FORCE; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok 0,cfcd208495d565ef66e7dff9f98764da 2,c81e728d9d4c2f636f067f89cc14862c 4,a87ff679a2f3e71d9181a67b7542122c 6,1679091c5a880faf6fb5e6087eb1b2dc 8,c9f0f895fb98ab9159f51fd0297e236d 10,d3d9446802a44259755d38e6d163e820 -- Check COPY TO as user without permissions. SET row_security TO OFF; SET SESSION AUTHORIZATION rls_regress_user2; SET row_security TO OFF; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - insufficient to bypass rls ERROR: insufficient privilege to bypass row security. SET row_security TO ON; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - permission denied ERROR: permission denied for relation copy_t SET row_security TO FORCE; COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - permission denied ERROR: permission denied for relation copy_t -- Check COPY relation TO; keep it just one row to avoid reordering issues RESET SESSION AUTHORIZATION; SET row_security TO ON; CREATE TABLE copy_rel_to (a integer, b text); CREATE POLICY p1 ON copy_rel_to USING (a % 2 = 0); ALTER TABLE copy_rel_to ENABLE ROW LEVEL SECURITY; GRANT ALL ON copy_rel_to TO rls_regress_user1, rls_regress_exempt_user; INSERT INTO copy_rel_to VALUES (1, md5('1')); -- Check COPY TO as Superuser/owner. RESET SESSION AUTHORIZATION; SET row_security TO OFF; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; 1,c4ca4238a0b923820dcc509a6f75849b SET row_security TO ON; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; 1,c4ca4238a0b923820dcc509a6f75849b SET row_security TO FORCE; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; -- Check COPY TO as user with permissions. SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO OFF; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --fail - insufficient to bypass rls ERROR: insufficient privilege to bypass row security. SET row_security TO ON; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --ok SET row_security TO FORCE; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --ok -- Check COPY TO as user with permissions and BYPASSRLS SET SESSION AUTHORIZATION rls_regress_exempt_user; SET row_security TO OFF; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --ok 1,c4ca4238a0b923820dcc509a6f75849b SET row_security TO ON; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --ok SET row_security TO FORCE; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --ok -- Check COPY TO as user without permissions. SET row_security TO OFF; SET SESSION AUTHORIZATION rls_regress_user2; SET row_security TO OFF; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --fail - permission denied ERROR: permission denied for relation copy_rel_to SET row_security TO ON; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --fail - permission denied ERROR: permission denied for relation copy_rel_to SET row_security TO FORCE; COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --fail - permission denied ERROR: permission denied for relation copy_rel_to -- Check COPY FROM as Superuser/owner. RESET SESSION AUTHORIZATION; SET row_security TO OFF; COPY copy_t FROM STDIN; --ok SET row_security TO ON; COPY copy_t FROM STDIN; --ok SET row_security TO FORCE; COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. ERROR: COPY FROM not supported with row level security. HINT: Use direct INSERT statements instead. -- Check COPY FROM as user with permissions. SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO OFF; COPY copy_t FROM STDIN; --fail - insufficient privilege to bypass rls. ERROR: insufficient privilege to bypass row security. SET row_security TO ON; COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. ERROR: COPY FROM not supported with row level security. HINT: Use direct INSERT statements instead. SET row_security TO FORCE; COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. ERROR: COPY FROM not supported with row level security. HINT: Use direct INSERT statements instead. -- Check COPY TO as user with permissions and BYPASSRLS SET SESSION AUTHORIZATION rls_regress_exempt_user; SET row_security TO OFF; COPY copy_t FROM STDIN; --ok SET row_security TO ON; COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. ERROR: COPY FROM not supported with row level security. HINT: Use direct INSERT statements instead. SET row_security TO FORCE; COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. ERROR: COPY FROM not supported with row level security. HINT: Use direct INSERT statements instead. -- Check COPY FROM as user without permissions. SET SESSION AUTHORIZATION rls_regress_user2; SET row_security TO OFF; COPY copy_t FROM STDIN; --fail - permission denied. ERROR: permission denied for relation copy_t SET row_security TO ON; COPY copy_t FROM STDIN; --fail - permission denied. ERROR: permission denied for relation copy_t SET row_security TO FORCE; COPY copy_t FROM STDIN; --fail - permission denied. ERROR: permission denied for relation copy_t RESET SESSION AUTHORIZATION; DROP TABLE copy_t; DROP TABLE copy_rel_to CASCADE; -- Check WHERE CURRENT OF SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE current_check (currentid int, payload text, rlsuser text); GRANT ALL ON current_check TO PUBLIC; INSERT INTO current_check VALUES (1, 'abc', 'rls_regress_user1'), (2, 'bcd', 'rls_regress_user1'), (3, 'cde', 'rls_regress_user1'), (4, 'def', 'rls_regress_user1'); CREATE POLICY p1 ON current_check FOR SELECT USING (currentid % 2 = 0); CREATE POLICY p2 ON current_check FOR DELETE USING (currentid = 4 AND rlsuser = current_user); CREATE POLICY p3 ON current_check FOR UPDATE USING (currentid = 4) WITH CHECK (rlsuser = current_user); ALTER TABLE current_check ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; -- Can SELECT even rows SELECT * FROM current_check; currentid | payload | rlsuser -----------+---------+------------------- 2 | bcd | rls_regress_user1 4 | def | rls_regress_user1 (2 rows) -- Cannot UPDATE row 2 UPDATE current_check SET payload = payload || '_new' WHERE currentid = 2 RETURNING *; currentid | payload | rlsuser -----------+---------+--------- (0 rows) BEGIN; DECLARE current_check_cursor SCROLL CURSOR FOR SELECT * FROM current_check; -- Returns rows that can be seen according to SELECT policy, like plain SELECT -- above (even rows) FETCH ABSOLUTE 1 FROM current_check_cursor; currentid | payload | rlsuser -----------+---------+------------------- 2 | bcd | rls_regress_user1 (1 row) -- Still cannot UPDATE row 2 through cursor UPDATE current_check SET payload = payload || '_new' WHERE CURRENT OF current_check_cursor RETURNING *; currentid | payload | rlsuser -----------+---------+--------- (0 rows) -- Can update row 4 through cursor, which is the next visible row FETCH RELATIVE 1 FROM current_check_cursor; currentid | payload | rlsuser -----------+---------+------------------- 4 | def | rls_regress_user1 (1 row) UPDATE current_check SET payload = payload || '_new' WHERE CURRENT OF current_check_cursor RETURNING *; currentid | payload | rlsuser -----------+---------+------------------- 4 | def_new | rls_regress_user1 (1 row) SELECT * FROM current_check; currentid | payload | rlsuser -----------+---------+------------------- 2 | bcd | rls_regress_user1 4 | def_new | rls_regress_user1 (2 rows) -- Plan should be a subquery TID scan EXPLAIN (COSTS OFF) UPDATE current_check SET payload = payload WHERE CURRENT OF current_check_cursor; QUERY PLAN --------------------------------------------------------------- Update on current_check current_check_1 -> Subquery Scan on current_check -> LockRows -> Tid Scan on current_check current_check_2 TID Cond: CURRENT OF current_check_cursor Filter: (currentid = 4) (6 rows) -- Similarly can only delete row 4 FETCH ABSOLUTE 1 FROM current_check_cursor; currentid | payload | rlsuser -----------+---------+------------------- 2 | bcd | rls_regress_user1 (1 row) DELETE FROM current_check WHERE CURRENT OF current_check_cursor RETURNING *; currentid | payload | rlsuser -----------+---------+--------- (0 rows) FETCH RELATIVE 1 FROM current_check_cursor; currentid | payload | rlsuser -----------+---------+------------------- 4 | def | rls_regress_user1 (1 row) DELETE FROM current_check WHERE CURRENT OF current_check_cursor RETURNING *; currentid | payload | rlsuser -----------+---------+------------------- 4 | def_new | rls_regress_user1 (1 row) SELECT * FROM current_check; currentid | payload | rlsuser -----------+---------+------------------- 2 | bcd | rls_regress_user1 (1 row) COMMIT; -- -- check pg_stats view filtering -- SET row_security TO ON; SET SESSION AUTHORIZATION rls_regress_user0; ANALYZE current_check; -- Stats visible SELECT row_security_active('current_check'); row_security_active --------------------- f (1 row) SELECT attname, most_common_vals FROM pg_stats WHERE tablename = 'current_check' ORDER BY 1; attname | most_common_vals -----------+--------------------- currentid | payload | rlsuser | {rls_regress_user1} (3 rows) SET SESSION AUTHORIZATION rls_regress_user1; -- Stats not visible SELECT row_security_active('current_check'); row_security_active --------------------- t (1 row) SELECT attname, most_common_vals FROM pg_stats WHERE tablename = 'current_check' ORDER BY 1; attname | most_common_vals ---------+------------------ (0 rows) -- -- Collation support -- BEGIN; SET row_security TO FORCE; CREATE TABLE coll_t (c) AS VALUES ('bar'::text); CREATE POLICY coll_p ON coll_t USING (c < ('foo'::text COLLATE "C")); ALTER TABLE coll_t ENABLE ROW LEVEL SECURITY; SELECT (string_to_array(polqual, ':'))[7] AS inputcollid FROM pg_policy WHERE polrelid = 'coll_t'::regclass; inputcollid ------------------ inputcollid 950 (1 row) SELECT * FROM coll_t; c ----- bar (1 row) ROLLBACK; -- -- Shared Object Dependencies -- RESET SESSION AUTHORIZATION; BEGIN; CREATE ROLE alice; CREATE ROLE bob; CREATE TABLE tbl1 (c) AS VALUES ('bar'::text); GRANT SELECT ON TABLE tbl1 TO alice; CREATE POLICY P ON tbl1 TO alice, bob USING (true); SELECT refclassid::regclass, deptype FROM pg_depend WHERE classid = 'pg_policy'::regclass AND refobjid = 'tbl1'::regclass; refclassid | deptype ------------+--------- pg_class | a (1 row) SELECT refclassid::regclass, deptype FROM pg_shdepend WHERE classid = 'pg_policy'::regclass AND refobjid IN ('alice'::regrole, 'bob'::regrole); refclassid | deptype ------------+--------- pg_authid | r pg_authid | r (2 rows) SAVEPOINT q; DROP ROLE alice; --fails due to dependency on POLICY p ERROR: role "alice" cannot be dropped because some objects depend on it DETAIL: target of policy p on table tbl1 privileges for table tbl1 ROLLBACK TO q; ALTER POLICY p ON tbl1 TO bob USING (true); SAVEPOINT q; DROP ROLE alice; --fails due to dependency on GRANT SELECT ERROR: role "alice" cannot be dropped because some objects depend on it DETAIL: privileges for table tbl1 ROLLBACK TO q; REVOKE ALL ON TABLE tbl1 FROM alice; SAVEPOINT q; DROP ROLE alice; --succeeds ROLLBACK TO q; SAVEPOINT q; DROP ROLE bob; --fails due to dependency on POLICY p ERROR: role "bob" cannot be dropped because some objects depend on it DETAIL: target of policy p on table tbl1 ROLLBACK TO q; DROP POLICY p ON tbl1; SAVEPOINT q; DROP ROLE bob; -- succeeds ROLLBACK TO q; ROLLBACK; -- cleanup -- -- Converting table to view -- BEGIN; SET ROW_SECURITY = FORCE; CREATE TABLE t (c int); CREATE POLICY p ON t USING (c % 2 = 1); ALTER TABLE t ENABLE ROW LEVEL SECURITY; SAVEPOINT q; CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD SELECT * FROM generate_series(1,5) t0(c); -- fails due to row level security enabled ERROR: could not convert table "t" to a view because it has row security enabled ROLLBACK TO q; ALTER TABLE t DISABLE ROW LEVEL SECURITY; SAVEPOINT q; CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD SELECT * FROM generate_series(1,5) t0(c); -- fails due to policy p on t ERROR: could not convert table "t" to a view because it has row security policies ROLLBACK TO q; DROP POLICY p ON t; CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD SELECT * FROM generate_series(1,5) t0(c); -- succeeds ROLLBACK; -- -- Policy expression handling -- BEGIN; SET row_security = FORCE; CREATE TABLE t (c) AS VALUES ('bar'::text); CREATE POLICY p ON t USING (max(c)); -- fails: aggregate functions are not allowed in policy expressions ERROR: aggregate functions are not allowed in policy expressions ROLLBACK; -- -- Non-target relations are only subject to SELECT policies -- SET SESSION AUTHORIZATION rls_regress_user0; CREATE TABLE r1 (a int); CREATE TABLE r2 (a int); INSERT INTO r1 VALUES (10), (20); INSERT INTO r2 VALUES (10), (20); GRANT ALL ON r1, r2 TO rls_regress_user1; CREATE POLICY p1 ON r1 USING (true); ALTER TABLE r1 ENABLE ROW LEVEL SECURITY; CREATE POLICY p1 ON r2 FOR SELECT USING (true); CREATE POLICY p2 ON r2 FOR INSERT WITH CHECK (false); CREATE POLICY p3 ON r2 FOR UPDATE USING (false); CREATE POLICY p4 ON r2 FOR DELETE USING (false); ALTER TABLE r2 ENABLE ROW LEVEL SECURITY; SET SESSION AUTHORIZATION rls_regress_user1; SELECT * FROM r1; a ---- 10 20 (2 rows) SELECT * FROM r2; a ---- 10 20 (2 rows) -- r2 is read-only INSERT INTO r2 VALUES (2); -- Not allowed ERROR: new row violates row level security policy for "r2" UPDATE r2 SET a = 2 RETURNING *; -- Updates nothing a --- (0 rows) DELETE FROM r2 RETURNING *; -- Deletes nothing a --- (0 rows) -- r2 can be used as a non-target relation in DML INSERT INTO r1 SELECT a + 1 FROM r2 RETURNING *; -- OK a ---- 11 21 (2 rows) UPDATE r1 SET a = r2.a + 2 FROM r2 WHERE r1.a = r2.a RETURNING *; -- OK a | a ----+---- 12 | 10 22 | 20 (2 rows) DELETE FROM r1 USING r2 WHERE r1.a = r2.a + 2 RETURNING *; -- OK a | a ----+---- 12 | 10 22 | 20 (2 rows) SELECT * FROM r1; a ---- 11 21 (2 rows) SELECT * FROM r2; a ---- 10 20 (2 rows) SET SESSION AUTHORIZATION rls_regress_user0; DROP TABLE r1; DROP TABLE r2; -- -- Clean up objects -- RESET SESSION AUTHORIZATION; -- Suppress NOTICE messages when doing a cascaded drop. SET client_min_messages TO 'warning'; DROP SCHEMA rls_regress_schema CASCADE; RESET client_min_messages; DROP USER rls_regress_user0; DROP USER rls_regress_user1; DROP USER rls_regress_user2; DROP USER rls_regress_exempt_user; DROP ROLE rls_regress_group1; DROP ROLE rls_regress_group2;