Making security barrier views automatically updatable
This pages describes an attempt to make security barrier views automatically updatable. See also Automatically updatable security barrier views which describes a different approach and some of the fundamental problems that make this difficult.
- Original proposal with most of the work done in the rewriter.
- Refined patch with the security barrier expansion code moved to the planner to solve issues with inheritance.
- Further update with fixes for rules
More up to date information will appear on the commitfest entry for this patch.
How it works
The patch has 2 main components:
- A new field securityQuals on the RangeTblEntry structure.
This is a list, allowing the quals from nested security barrier views to be kept separate from one another and from the user-supplied quals in the query's jointree. This field is populated in rewriteTargetView(), if the target view has the security barrier option, instead of merging the view's quals with any user-supplied quals. Ultimately these quals will be used to build security barrier subqueries to fetch the rows to update, but they are kept separate initially so that the remaining rewriter code, and the prep stages of the planner don't have to deal with a subquery result relation. - New code in the preprocessing stage of the planner to handle any RTEs with securityQuals.
Such RTEs are turned into (possibly nested) subquery RTEs with the security_barrier property set to prevent push-down of leaky user-supplied quals.
The first part is fairly straightforward. The new code in the planner, however, has to happen at just the right time, in order to work properly with inheritance and targetlist expansion. The following is an outline of the path through the planner, showing where the new code slots in (changes to existing code in bold):
- standard_planner():
- subquery_planner():
- Process CTEs.
- Pull up sublinks [ANY/EXISTS].
- Inline set-returning functions.
- Pull up subqueries.
NOTE: This excludes subqueries with the security_barrier property. - Flatten a simple UNION ALL query into one with an append_rel_list.
- Preprocess RowMarks. This includes adding rowmarks for all non-target relations not otherwise locked.
This code treats RTEs with securityQuals as subqueries, because such RTEs will become subquery RTEs later. - Expand inherited tables, producing entries in the append_rel_list.
- Preprocess expressions in the targetlist, WITH CHECK OPTION clauses, RETURNING list, WHERE quals, HAVING clause, window clauses, LIMIT and OFFSET clauses, the append_rel_list and functions and VALUES in the rangetable. This preprocessing includes recursive planning of any sublink subqueries contained in any of those expressions (recursive invocation of subquery_planner()). Thus all SubLink nodes in those parts of the Query are turned into SubPlan nodes.
NOTE: This preprocessing excludes RTE subqueries in the rangetable, since those are processed by later recursion. For the same reason, this preprocessing also excludes RTE securityQuals. - Maybe turn HAVING into a WHERE clause.
- Reduce outer joins where possible.
- If the result relation has no inheritance, jump straight into grouping_planner().
- Else inheritance_planner():
- For each inheritance child:
- Copy the whole query and use adjust_appendrel_attrs() to fix up any Vars to refer to the current child relation.
NOTE: adjust_appendrel_attrs() skips subqueries in the rangetable, since they cannot contain references to the result relation. This would be broken if LATERAL references to the result relation were allowed.
adjust_appendrel_attrs() does descend into RTE securityQuals, because they may contain up-references to the result relation. They may also contain sublink subqueries, so new code is needed to make adjust_appendrel_attrs() able to handle this (previously this was not possible, since all other SubLinks have been turned into SubPlans during expression preprocessing). - Careful rangetable mangling so that each copy of the query can share the same rangetable.
- Plan the child query using grouping_planner():
- Plan set operations using recursion.
- Preprocess the targetlist. In the case of an UPDATE, this will add targetlist entries for each of the result relation's attributes. By this point, the result relation may be an inheritance child of the original result relation, so it may have additional attributes.
- Expand any RTE securityQuals. This turns any RTEs with securityQuals into subquery RTEs with the security_barrier property. The targetlists of the new subqueries are built using the (now fully known) list of attributes of the result relation that the outer query refers to. If the RTE in question is the result relation, a copy of it is made and left unexpanded, to act as the new result relation. The expanded subquery RTE acts as the source of rows to update.
- Find the best paths using query_planner():
- ...
- make_one_rel():
- set_base_rel_size():
- set_rel_size():
- For subquery RTEs, set_subquery_pathlist():
- Push down any quals into the subquery where it is safe to do so.
If the subquery has the security_barrier property, exclude quals that contain references to leaky functions. Why not examine the function arguments ??? - Recursively invoke subquery_planner() to plan the RTE's subquery.
- Push down any quals into the subquery where it is safe to do so.
- For subquery RTEs, set_subquery_pathlist():
- set_rel_size():
- set_base_rel_size():
- Copy the whole query and use adjust_appendrel_attrs() to fix up any Vars to refer to the current child relation.
- For each inheritance child:
- subquery_planner():
The problem with LATERAL
In 9.3.2, and 9.4devel prior to commit 158b7fa6a34006bdc70b515e14e120d3e896589b, it was possible to write an UPDATE with a FROM clause containing LATERAL references to the result relation. The problem with that in the current code is illustrated above — if the result relation is the parent of an inheritance set, then it falls over because adjust_appendrel_attrs() does not descend into subqueries in the rangetable, and so those LATERAL references are not rewritten in the inheritance planner to refer to the correct append_rel child entry. This is illustrated by the following example:
CREATE TABLE t1(x int); CREATE TABLE t2() INHERITS(t1); CREATE TABLE t3(a int, b int); UPDATE t1 SET x=b FROM LATERAL (SELECT * FROM t3 WHERE a=x OFFSET 0) t3;
which, if it's allowed, produces
ERROR: no relation entry for relid 1
If adjust_appendrel_attrs() were to descend into subqueries in the rangetable to fixup such references, then it would have to be modified to handle any SubLinks that it might encounter there (just as this patch does), since subqueries in the rangetable are not planned in the initial preprocessing.
Test cases
These tests make use of the following function which can snoop on values hidden behind a view's quals, if the view doesn't have the security_barrier property:
CREATE OR REPLACE FUNCTION snoop(anyelement) RETURNS boolean AS $$ BEGIN RAISE NOTICE 'Snooped value: %', $1; RETURN true; END; $$ LANGUAGE plpgsql STRICT IMMUTABLE COST 0.000001;
and the following LEAKPROOF function, which doesn't reveal anything about the data passed to it, and which should therefore be safe to push down into security barrier views:
CREATE OR REPLACE FUNCTION leakproof(anyelement) RETURNS boolean AS $$ BEGIN RETURN true; END; $$ LANGUAGE plpgsql STRICT IMMUTABLE LEAKPROOF;
Confirm non-security barrier views leak
Without the security_barrier property, the low cost of the snoop() function causes it to be executed first, leaking information that the view is supposed to hide:
CREATE TABLE t1(person text, visibility text); INSERT INTO t1 VALUES ('Tom', 'public'), ('Dick', 'private'), ('Harry', 'public'); CREATE VIEW v1 AS SELECT person FROM t1 WHERE visibility = 'public'; EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET person=person WHERE NOT snoop(person); UPDATE v1 SET person=person WHERE NOT snoop(person); EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM v1 WHERE NOT snoop(person); DELETE FROM v1 WHERE NOT snoop(person); EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM v1 WHERE snoop(person); SELECT * FROM v1 WHERE snoop(person); DROP TABLE t1 CASCADE;
Results:
QUERY PLAN ------------------------------------------------------------------------------- Update on public.t1 -> Seq Scan on public.t1 Output: t1.person, t1.visibility, t1.ctid Filter: ((NOT snoop(t1.person)) AND (t1.visibility = 'public'::text)) (4 rows) NOTICE: Snooped value: Tom NOTICE: Snooped value: Dick NOTICE: Snooped value: Harry UPDATE 0 QUERY PLAN ------------------------------------------------------------------------------- Delete on public.t1 -> Seq Scan on public.t1 Output: t1.ctid Filter: ((NOT snoop(t1.person)) AND (t1.visibility = 'public'::text)) (4 rows) NOTICE: Snooped value: Tom NOTICE: Snooped value: Dick NOTICE: Snooped value: Harry DELETE 0 QUERY PLAN ------------------------------------------------------------------- Seq Scan on public.t1 Output: t1.person Filter: (snoop(t1.person) AND (t1.visibility = 'public'::text)) (3 rows) NOTICE: Snooped value: Tom NOTICE: Snooped value: Dick NOTICE: Snooped value: Harry person -------- Tom Harry (2 rows)
So a person with access to the view can see the person 'Dick' in the underlying table, even though he is not exposed through the view.
Confirm security barrier views don't leak
Setting the security_barrier option on the view should result in plans with subquery scans, and quals containing leaky functions should not be pushed down, but leakproof functions may be:
CREATE TABLE t1(person text, visibility text); INSERT INTO t1 VALUES ('Tom', 'public'), ('Dick', 'private'), ('Harry', 'public'); CREATE VIEW v1 WITH (security_barrier=true) AS SELECT person FROM t1 WHERE visibility = 'public'; EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET person=person WHERE NOT snoop(person) AND leakproof(person); UPDATE v1 SET person=person WHERE NOT snoop(person) AND leakproof(person); EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM v1 WHERE NOT snoop(person) AND leakproof(person); DELETE FROM v1 WHERE NOT snoop(person) AND leakproof(person); EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM v1 WHERE snoop(person) AND leakproof(person); SELECT * FROM v1 WHERE snoop(person) AND leakproof(person); DROP TABLE t1 CASCADE;
Results:
QUERY PLAN ------------------------------------------------------------------------------------------------------- Update on public.t1 t1_1 -> Subquery Scan on t1 Output: t1.person, t1.visibility, t1.ctid Filter: (NOT snoop(t1.person)) -> Seq Scan on public.t1 t1_2 Output: t1_2.person, t1_2.ctid, t1_2.visibility Filter: ((t1_2.visibility = 'public'::text) AND leakproof(t1_2.person)) (7 rows) NOTICE: Snooped value: Tom NOTICE: Snooped value: Harry UPDATE 0 QUERY PLAN ------------------------------------------------------------------------------------------------------- Delete on public.t1 t1_1 -> Subquery Scan on t1 Output: t1.ctid Filter: (NOT snoop(t1.person)) -> Seq Scan on public.t1 t1_2 Output: t1_2.ctid, t1_2.person Filter: ((t1_2.visibility = 'public'::text) AND leakproof(t1_2.person)) (7 rows) NOTICE: Snooped value: Tom NOTICE: Snooped value: Harry DELETE 0 QUERY PLAN ------------------------------------------------------------------------------------------- Subquery Scan on v1 Output: v1.person Filter: snoop(v1.person) -> Seq Scan on public.t1 Output: t1.person Filter: ((t1.visibility = 'public'::text) AND leakproof(t1.person)) (6 rows) NOTICE: Snooped value: Tom NOTICE: Snooped value: Harry person -------- Tom Harry (2 rows)
So no information is leaked, since the snoop function isn't pushed down into the subquery, but the leakproof function is.
Nested security barrier views
Nothing stops a user defining their own security barrier views and putting malicious functions in them, so the quals from each view must be kept separate and only merged when it is safe to do so:
CREATE TABLE t1(a int, b text, c text); INSERT INTO t1 VALUES (-1, 'Private', 'PRIVATE'), (1, 'Public', 'PUBLIC'); CREATE VIEW v1 WITH (security_barrier=true) AS SELECT * FROM t1 WHERE a > 0; CREATE VIEW v2 AS SELECT * FROM v1 WHERE snoop(b) and leakproof(b); CREATE VIEW v3 WITH (security_barrier=true) AS SELECT * FROM v1 WHERE snoop(b) and leakproof(b); EXPLAIN (VERBOSE, COSTS OFF) UPDATE v2 SET a=a WHERE NOT snoop(c) AND leakproof(c); UPDATE v2 SET a=a WHERE NOT snoop(c) AND leakproof(c); EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM v2 WHERE NOT snoop(c) AND leakproof(c); DELETE FROM v2 WHERE NOT snoop(c) AND leakproof(c); EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM v2 WHERE snoop(c) AND leakproof(c); SELECT * FROM v2 WHERE snoop(c) AND leakproof(c); EXPLAIN (VERBOSE, COSTS OFF) UPDATE v3 SET a=a WHERE NOT snoop(c) AND leakproof(c); UPDATE v3 SET a=a WHERE NOT snoop(c) AND leakproof(c); EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM v3 WHERE NOT snoop(c) AND leakproof(c); DELETE FROM v3 WHERE NOT snoop(c) AND leakproof(c); EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM v3 WHERE snoop(c) AND leakproof(c); SELECT * FROM v3 WHERE snoop(c) AND leakproof(c); DROP TABLE t1 CASCADE;
Results:
QUERY PLAN -------------------------------------------------------------------------------------------------------- Update on public.t1 t1_1 -> Subquery Scan on t1 Output: t1.a, t1.b, t1.c, t1.ctid Filter: ((NOT snoop(t1.c)) AND snoop(t1.b)) -> Seq Scan on public.t1 t1_2 Output: t1_2.a, t1_2.ctid, t1_2.c, t1_2.b Filter: ((t1_2.a > 0) AND leakproof(t1_2.c) AND leakproof(t1_2.b)) (7 rows) NOTICE: Snooped value: PUBLIC UPDATE 0 QUERY PLAN -------------------------------------------------------------------------------------------------------- Delete on public.t1 t1_1 -> Subquery Scan on t1 Output: t1.ctid Filter: ((NOT snoop(t1.c)) AND snoop(t1.b)) -> Seq Scan on public.t1 t1_2 Output: t1_2.ctid, t1_2.c, t1_2.b Filter: ((t1_2.a > 0) AND leakproof(t1_2.c) AND leakproof(t1_2.b)) (7 rows) NOTICE: Snooped value: PUBLIC DELETE 0 QUERY PLAN ---------------------------------------------------------------------------------------- Subquery Scan on v1 Output: v1.a, v1.b, v1.c Filter: (snoop(v1.b) AND snoop(v1.c)) -> Seq Scan on public.t1 Output: t1.a, t1.b, t1.c Filter: ((t1.a > 0) AND leakproof(t1.b) AND leakproof(t1.c)) (6 rows) NOTICE: Snooped value: Public NOTICE: Snooped value: PUBLIC a | b | c ---+--------+-------- 1 | Public | PUBLIC (1 row) QUERY PLAN -------------------------------------------------------------------------------------------------------------- Update on public.t1 t1_1 -> Subquery Scan on t1 Output: t1.a, t1.b, t1.c, t1.ctid Filter: (NOT snoop(t1.c)) -> Subquery Scan on t1_2 Output: t1_2.a, t1_2.ctid, t1_2.c, t1_2.b Filter: snoop(t1_2.b) -> Seq Scan on public.t1 t1_3 Output: t1_3.a, t1_3.ctid, t1_3.c, t1_3.b Filter: ((t1_3.a > 0) AND leakproof(t1_3.b) AND leakproof(t1_3.c)) (10 rows) NOTICE: Snooped value: Public NOTICE: Snooped value: PUBLIC UPDATE 0 QUERY PLAN -------------------------------------------------------------------------------------------------------------- Delete on public.t1 t1_1 -> Subquery Scan on t1 Output: t1.ctid Filter: (NOT snoop(t1.c)) -> Subquery Scan on t1_2 Output: t1_2.ctid, t1_2.c, t1_2.b Filter: snoop(t1_2.b) -> Seq Scan on public.t1 t1_3 Output: t1_3.ctid, t1_3.c, t1_3.b Filter: ((t1_3.a > 0) AND leakproof(t1_3.b) AND leakproof(t1_3.c)) (10 rows) NOTICE: Snooped value: Public NOTICE: Snooped value: PUBLIC DELETE 0 QUERY PLAN ---------------------------------------------------------------------------------------------- Subquery Scan on v3 Output: v3.a, v3.b, v3.c Filter: snoop(v3.c) -> Subquery Scan on v1 Output: v1.a, v1.b, v1.c Filter: snoop(v1.b) -> Seq Scan on public.t1 Output: t1.a, t1.b, t1.c Filter: ((t1.a > 0) AND leakproof(t1.b) AND leakproof(t1.c)) (9 rows) NOTICE: Snooped value: Public NOTICE: Snooped value: PUBLIC a | b | c ---+--------+-------- 1 | Public | PUBLIC (1 row)
So the unsafe top-level quals are kept separate from the outer view's quals, and the unsafe quals from the outer view are kept separate from the inner view's quals, but all safe quals are pushed all the way down to give the most efficient plan.
Security barrier view WITH CHECK OPTION
CREATE TABLE t1(a int, b text); INSERT INTO t1 VALUES (-1, 'Private'), (1, 'Public'); CREATE VIEW v1 WITH (security_barrier=true) AS SELECT * FROM t1 WHERE a > 0 WITH CHECK OPTION; -- Should succeed INSERT INTO v1 VALUES (2, 'Public 2'); UPDATE v1 SET a=20 WHERE a=2; -- Should fail INSERT INTO v1 VALUES (-2, 'Private 2'); UPDATE v1 SET a=-10 WHERE a=1; DROP TABLE t1 CASCADE;
Results:
-- Should succeed INSERT 0 1 UPDATE 1 -- Should fail ERROR: new row violates WITH CHECK OPTION for view "v1" DETAIL: Failing row contains (-2, Private 2). ERROR: new row violates WITH CHECK OPTION for view "v1" DETAIL: Failing row contains (-10, Public).
Security barrier view WITH CHECK OPTION and BEFORE trigger on base table
The WITH CHECK OPTION should apply to NEW rows (after the action of the before trigger):
CREATE TABLE t1(a int, b text); CREATE FUNCTION t1_before_trig_fn() RETURNS trigger AS $$ BEGIN NEW.a := -NEW.a; NEW.b := NEW.b || ' (modified by trigger)'; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER t1_before_trig BEFORE INSERT OR UPDATE ON t1 FOR EACH ROW EXECUTE PROCEDURE t1_before_trig_fn(); CREATE VIEW v1 WITH (security_barrier=true) AS SELECT * FROM t1 WHERE a > 0 WITH CHECK OPTION; -- Should succeed INSERT INTO v1 VALUES (-1, 'Test row'); SELECT * FROM t1; UPDATE v1 SET a=-2 WHERE a=1; SELECT * FROM t1; -- Should fail INSERT INTO v1 VALUES (2, 'Failing row'); UPDATE v1 SET a=3, b='Failing row' WHERE a=2; DROP TABLE t1 CASCADE; DROP FUNCTION t1_before_trig_fn();
Results:
-- Should succeed INSERT 0 1 a | b ---+-------------------------------- 1 | Test row (modified by trigger) (1 row) UPDATE 1 a | b ---+------------------------------------------------------ 2 | Test row (modified by trigger) (modified by trigger) (1 row) -- Should fail ERROR: new row violates WITH CHECK OPTION for view "v1" DETAIL: Failing row contains (-2, Failing row (modified by trigger)). ERROR: new row violates WITH CHECK OPTION for view "v1" DETAIL: Failing row contains (-3, Failing row (modified by trigger)).
Security barrier view with ORDER BY and non-updatable columns
CREATE TABLE t1 (a int, b int); INSERT INTO t1 VALUES (1,2), (4,5), (3,-3); CREATE VIEW v1 WITH (security_barrier=true) AS SELECT a, b, a-b AS c FROM t1 WHERE a*a+b*b != 0 ORDER BY a+b; SELECT * FROM v1; EXPLAIN (VERBOSE, COSTS OFF) INSERT INTO v1(a, b) VALUES (7,-8); INSERT INTO v1(a, b) VALUES (7,-8); SELECT * FROM v1; EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET b=b+1 WHERE snoop(a) AND leakproof(a) RETURNING *; UPDATE v1 SET b=b+1 WHERE snoop(a) AND leakproof(a) RETURNING *; DROP TABLE t1 CASCADE;
Results:
a | b | c ---+----+---- 3 | -3 | 6 1 | 2 | -1 4 | 5 | -1 (3 rows) QUERY PLAN ------------------------- Insert on public.t1 -> Result Output: 7, (-8) (3 rows) INSERT 0 1 a | b | c ---+----+---- 7 | -8 | 15 3 | -3 | 6 1 | 2 | -1 4 | 5 | -1 (4 rows) QUERY PLAN --------------------------------------------------------------------------------------------------------- Update on public.t1 t1_1 Output: t1_1.a, t1_1.b, (t1_1.a - t1_1.b) -> Subquery Scan on t1 Output: t1.a, (t1.b + 1), t1.ctid Filter: snoop(t1.a) -> Seq Scan on public.t1 t1_2 Output: t1_2.b, t1_2.ctid, t1_2.a Filter: ((((t1_2.a * t1_2.a) + (t1_2.b * t1_2.b)) <> 0) AND leakproof(t1_2.a)) (8 rows) NOTICE: Snooped value: 1 NOTICE: Snooped value: 4 NOTICE: Snooped value: 3 NOTICE: Snooped value: 7 a | b | c ---+----+---- 1 | 3 | -2 4 | 6 | -2 3 | -2 | 5 7 | -7 | 14 (4 rows) UPDATE 4
Security barrier view with subqueries
CREATE TABLE t1 (a int); INSERT INTO t1 VALUES (1), (2), (2); CREATE TABLE t2 (a int); INSERT INTO t2 SELECT * FROM generate_series(1, 5); CREATE VIEW v1 WITH (security_barrier=true) AS SELECT a, (SELECT count(*) FROM t1 WHERE t1.a = t2.a) AS c FROM t2 WHERE EXISTS(SELECT 1 FROM t1 WHERE t1.a = t2.a); SELECT * FROM v1; EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET a=a+1 WHERE snoop(a) AND snoop(c) AND leakproof(a) RETURNING *; UPDATE v1 SET a=a+1 WHERE snoop(a) AND snoop(c) AND leakproof(a) RETURNING *; DROP TABLE t1, t2 CASCADE;
Results:
a | c ---+--- 1 | 1 2 | 2 (2 rows) QUERY PLAN ---------------------------------------------------------------------- Update on public.t2 t2_1 Output: t2_1.a, (SubPlan 1) -> Subquery Scan on t2 Output: (t2.a + 1), t2.ctid Filter: (snoop(t2.a) AND snoop((SubPlan 2))) -> Hash Join Output: t2_2.a, t2_2.ctid Hash Cond: (t2_2.a = t1.a) -> Seq Scan on public.t2 t2_2 Output: t2_2.a, t2_2.ctid Filter: leakproof(t2_2.a) -> Hash Output: t1.a -> HashAggregate Output: t1.a Group Key: t1.a -> Seq Scan on public.t1 Output: t1.a SubPlan 2 -> Aggregate Output: count(*) -> Seq Scan on public.t1 t1_2 Output: t1_2.a Filter: (t1_2.a = t2.a) SubPlan 1 -> Aggregate Output: count(*) -> Seq Scan on public.t1 t1_1 Output: t1_1.a Filter: (t1_1.a = t2_1.a) (30 rows) NOTICE: Snooped value: 1 NOTICE: Snooped value: 1 NOTICE: Snooped value: 2 NOTICE: Snooped value: 2 a | c ---+--- 2 | 2 3 | 0 (2 rows) UPDATE 2
Security barrier view based on an inheritance set
CREATE TABLE t1 (a int, b float, c text); CREATE INDEX t1_a_idx ON t1(a); INSERT INTO t1 SELECT i,i,'t1' FROM generate_series(1,1000) g(i); ANALYSE t1; CREATE TABLE t11 (d text) INHERITS (t1); CREATE INDEX t11_a_idx ON t11(a); INSERT INTO t11 SELECT i,i,'t11','t11d' FROM generate_series(1,1000) g(i); ANALYSE t11; CREATE TABLE t12 (e int[]) INHERITS (t1); CREATE INDEX t12_a_idx ON t12(a); INSERT INTO t12 SELECT i,i,'t12','{1,2}'::int[] FROM generate_series(1,1000) g(i); ANALYSE t12; CREATE TABLE t111 () INHERITS (t11, t12); CREATE INDEX t111_a_idx ON t111(a); INSERT INTO t111 SELECT i,i,'t111','t111d','{1,1,1}'::int[] FROM generate_series(1,1000) g(i); ANALYSE t111; CREATE VIEW v1 WITH (security_barrier=true) AS SELECT *, (SELECT d FROM t11 WHERE t11.a = t1.a LIMIT 1) AS d FROM t1 WHERE a > 0 AND EXISTS(SELECT 1 FROM t12 WHERE t12.a = t1.a); SELECT * FROM v1 WHERE a=123; EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a=123; UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a=123; SELECT * FROM v1 WHERE b=123; DROP TABLE t1, t11, t12, t111 CASCADE;
Results:
a | b | c | d -----+-----+------+------ 123 | 123 | t1 | t11d 123 | 123 | t11 | t11d 123 | 123 | t12 | t11d 123 | 123 | t111 | t11d (4 rows) QUERY PLAN ------------------------------------------------------------------------------------------- Update on public.t1 t1_4 -> Subquery Scan on t1 Output: (t1.a + 1), t1.b, t1.c, t1.ctid Filter: snoop(t1.a) -> Nested Loop Semi Join Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c -> Index Scan using t1_a_idx on public.t1 t1_5 Output: t1_5.a, t1_5.ctid, t1_5.b, t1_5.c Index Cond: ((t1_5.a > 0) AND (t1_5.a = 123)) Filter: leakproof(t1_5.a) -> Append -> Index Only Scan using t12_a_idx on public.t12 Output: t12.a Index Cond: (t12.a = t1_5.a) -> Index Only Scan using t111_a_idx on public.t111 Output: t111.a Index Cond: (t111.a = t1_5.a) -> Subquery Scan on t1_1 Output: (t1_1.a + 1), t1_1.b, t1_1.c, t1_1.d, t1_1.ctid Filter: snoop(t1_1.a) -> Nested Loop Semi Join Output: t11.a, t11.ctid, t11.b, t11.c, t11.d -> Index Scan using t11_a_idx on public.t11 Output: t11.a, t11.ctid, t11.b, t11.c, t11.d Index Cond: ((t11.a > 0) AND (t11.a = 123)) Filter: leakproof(t11.a) -> Append -> Index Only Scan using t12_a_idx on public.t12 t12_1 Output: t12_1.a Index Cond: (t12_1.a = t11.a) -> Index Only Scan using t111_a_idx on public.t111 t111_1 Output: t111_1.a Index Cond: (t111_1.a = t11.a) -> Subquery Scan on t1_2 Output: (t1_2.a + 1), t1_2.b, t1_2.c, t1_2.e, t1_2.ctid Filter: snoop(t1_2.a) -> Nested Loop Semi Join Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e -> Index Scan using t12_a_idx on public.t12 t12_2 Output: t12_2.a, t12_2.ctid, t12_2.b, t12_2.c, t12_2.e Index Cond: ((t12_2.a > 0) AND (t12_2.a = 123)) Filter: leakproof(t12_2.a) -> Append -> Index Only Scan using t12_a_idx on public.t12 t12_3 Output: t12_3.a Index Cond: (t12_3.a = t12_2.a) -> Index Only Scan using t111_a_idx on public.t111 t111_2 Output: t111_2.a Index Cond: (t111_2.a = t12_2.a) -> Subquery Scan on t1_3 Output: (t1_3.a + 1), t1_3.b, t1_3.c, t1_3.d, t1_3.e, t1_3.ctid Filter: snoop(t1_3.a) -> Nested Loop Semi Join Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e -> Index Scan using t111_a_idx on public.t111 t111_3 Output: t111_3.a, t111_3.ctid, t111_3.b, t111_3.c, t111_3.d, t111_3.e Index Cond: ((t111_3.a > 0) AND (t111_3.a = 123)) Filter: leakproof(t111_3.a) -> Append -> Index Only Scan using t12_a_idx on public.t12 t12_4 Output: t12_4.a Index Cond: (t12_4.a = t111_3.a) -> Index Only Scan using t111_a_idx on public.t111 t111_4 Output: t111_4.a Index Cond: (t111_4.a = t111_3.a) (65 rows) NOTICE: Snooped value: 123 NOTICE: Snooped value: 123 NOTICE: Snooped value: 123 NOTICE: Snooped value: 123 UPDATE 4 a | b | c | d -----+-----+------+------ 124 | 123 | t1 | t11d 124 | 123 | t11 | t11d 124 | 123 | t12 | t11d 124 | 123 | t111 | t11d (4 rows)
Update on security barrier view joined to subquery
CREATE TABLE t1 (a int, b int); INSERT INTO t1 VALUES (1, 10); ANALYSE t1; CREATE TABLE t2 () INHERITS (t1); INSERT INTO t2 VALUES (2, 20); ANALYSE t2; CREATE TABLE t3 (x int, y int); INSERT INTO t3 VALUES (1,100); ANALYSE t3; CREATE TABLE t4 (x int, y int); INSERT INTO t4 VALUES (2,200); ANALYSE t4; CREATE VIEW v1 WITH (security_barrier=true) AS SELECT * FROM t1 WHERE a > 0; EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET b = y FROM (SELECT * FROM t3 UNION ALL SELECT * FROM t4) s WHERE a = x; UPDATE v1 SET b = y FROM (SELECT * FROM t3 UNION ALL SELECT * FROM t4) s WHERE a = x; SELECT * FROM t1; DROP TABLE t1, t2, t3, t4 CASCADE;
Results:
QUERY PLAN ------------------------------------------------------------ Update on public.t1 -> Nested Loop Output: t1_1.a, t3.y, t1_1.ctid, (ROW(t3.x, t3.y)) Join Filter: (t1_1.a = t3.x) -> Seq Scan on public.t1 t1_1 Output: t1_1.ctid, t1_1.a Filter: (t1_1.a > 0) -> Append -> Seq Scan on public.t3 Output: t3.y, ROW(t3.x, t3.y), t3.x -> Seq Scan on public.t4 Output: t4.y, ROW(t4.x, t4.y), t4.x -> Nested Loop Output: t2.a, t3.y, t2.ctid, (ROW(t3.x, t3.y)) Join Filter: (t2.a = t3.x) -> Seq Scan on public.t2 Output: t2.ctid, t2.a Filter: (t2.a > 0) -> Append -> Seq Scan on public.t3 Output: t3.y, ROW(t3.x, t3.y), t3.x -> Seq Scan on public.t4 Output: t4.y, ROW(t4.x, t4.y), t4.x (23 rows) UPDATE 2 a | b ---+----- 1 | 100 2 | 200 (2 rows)
Update on security barrier view with LATERAL join to subquery
Test removed --- LATERAL references to the result relation are no longer allowed.
Update on security barrier view with LATERAL join to JOIN subquery
Test removed --- LATERAL references to the result relation are no longer allowed.
Security barrier views with subqueries based on other security barrier views
CREATE TABLE t1 (a int, b int, c int); CREATE INDEX t1_a_idx ON t1(a); INSERT INTO t1 SELECT i, i, i FROM generate_series(1,1000) g(i); ANALYSE t1; CREATE TABLE t2 () INHERITS (t1); CREATE INDEX t2_a_idx ON t2(a); INSERT INTO t2 SELECT i, i, i FROM generate_series(1001,2000) g(i); ANALYSE t2; CREATE VIEW v1 WITH (security_barrier=true) AS SELECT * FROM t1 WHERE a%2 = 0; CREATE VIEW v2 WITH (security_barrier=true) AS SELECT * FROM v1 WHERE EXISTS(SELECT 1 FROM t1 WHERE t1.a = v1.a) AND a%3 = 0 AND snoop('b='||b); CREATE VIEW v3 WITH (security_barrier=true) AS SELECT * FROM v2 WHERE EXISTS(SELECT 1 FROM v1 WHERE v1.a = v2.a) AND a%5 = 0 AND snoop('c='||c); EXPLAIN (VERBOSE, COSTS OFF) UPDATE v3 SET a=-a WHERE a >= 990 AND a <= 1020 AND snoop('a='||a); UPDATE v3 SET a=-a WHERE a >= 990 AND a <= 1020 AND snoop('a='||a); DROP TABLE t1 CASCADE;
Results:
QUERY PLAN -------------------------------------------------------------------------------------------------------- Update on public.t1 t1_2 -> Subquery Scan on t1 Output: (- t1.a), t1.b, t1.c, t1.ctid Filter: snoop(('a='::text || (t1.a)::text)) -> Nested Loop Semi Join Output: t1_3.a, t1_3.ctid, t1_3.b, t1_3.c Join Filter: (t1_3.a = t1_7.a) -> Subquery Scan on t1_3 Output: t1_3.a, t1_3.ctid, t1_3.b, t1_3.c Filter: (((t1_3.a % 5) = 0) AND snoop(('c='::text || (t1_3.c)::text))) -> Nested Loop Semi Join Output: t1_4.a, t1_4.ctid, t1_4.b, t1_4.c -> Subquery Scan on t1_4 Output: t1_4.a, t1_4.ctid, t1_4.b, t1_4.c Filter: (((t1_4.a % 3) = 0) AND snoop(('b='::text || (t1_4.b)::text))) -> Index Scan using t1_a_idx on public.t1 t1_6 Output: t1_6.a, t1_6.ctid, t1_6.b, t1_6.c Index Cond: ((t1_6.a >= 990) AND (t1_6.a <= 1020)) Filter: ((t1_6.a % 2) = 0) -> Append -> Index Only Scan using t1_a_idx on public.t1 t1_5 Output: t1_5.a Index Cond: (t1_5.a = t1_4.a) -> Index Only Scan using t2_a_idx on public.t2 Output: t2.a Index Cond: (t2.a = t1_4.a) -> Append -> Seq Scan on public.t1 t1_7 Output: t1_7.a, t1_7.b, t1_7.c Filter: ((t1_7.a % 2) = 0) -> Seq Scan on public.t2 t2_1 Output: t2_1.a, t2_1.b, t2_1.c Filter: ((t2_1.a % 2) = 0) -> Subquery Scan on t1_1 Output: (- t1_1.a), t1_1.b, t1_1.c, t1_1.ctid Filter: snoop(('a='::text || (t1_1.a)::text)) -> Nested Loop Semi Join Output: t1_8.a, t1_8.ctid, t1_8.b, t1_8.c Join Filter: (t1_8.a = t1_11.a) -> Subquery Scan on t1_8 Output: t1_8.a, t1_8.ctid, t1_8.b, t1_8.c Filter: (((t1_8.a % 5) = 0) AND snoop(('c='::text || (t1_8.c)::text))) -> Nested Loop Semi Join Output: t1_9.a, t1_9.ctid, t1_9.b, t1_9.c -> Subquery Scan on t1_9 Output: t1_9.a, t1_9.ctid, t1_9.b, t1_9.c Filter: (((t1_9.a % 3) = 0) AND snoop(('b='::text || (t1_9.b)::text))) -> Index Scan using t2_a_idx on public.t2 t2_3 Output: t2_3.a, t2_3.ctid, t2_3.b, t2_3.c Index Cond: ((t2_3.a >= 990) AND (t2_3.a <= 1020)) Filter: ((t2_3.a % 2) = 0) -> Append -> Index Only Scan using t1_a_idx on public.t1 t1_10 Output: t1_10.a Index Cond: (t1_10.a = t1_9.a) -> Index Only Scan using t2_a_idx on public.t2 t2_2 Output: t2_2.a Index Cond: (t2_2.a = t1_9.a) -> Append -> Seq Scan on public.t1 t1_11 Output: t1_11.a, t1_11.b, t1_11.c Filter: ((t1_11.a % 2) = 0) -> Seq Scan on public.t2 t2_4 Output: t2_4.a, t2_4.b, t2_4.c Filter: ((t2_4.a % 2) = 0) (65 rows) NOTICE: Snooped value: b=990 NOTICE: Snooped value: c=990 NOTICE: Snooped value: a=990 NOTICE: Snooped value: b=996 NOTICE: Snooped value: b=1002 NOTICE: Snooped value: b=1008 NOTICE: Snooped value: b=1014 NOTICE: Snooped value: b=1020 NOTICE: Snooped value: c=1020 NOTICE: Snooped value: a=1020 UPDATE 2
Update on security barrier view with rules on the base relation
Here the rules on the base table turn selected INSERTs and all DELETEs into UPDATEs. They also change the resultRelation index, so that the securityQuals are no longer attached to the resultRelation.
CREATE TABLE t1 (id int PRIMARY KEY, data text, deleted boolean); INSERT INTO t1 VALUES (1, 'Row 1', false), (2, 'Row 2', true); CREATE RULE t1_ins_rule AS ON INSERT TO t1 WHERE EXISTS (SELECT 1 FROM t1 WHERE id = new.id) DO INSTEAD UPDATE t1 SET data = new.data, deleted = false WHERE id = new.id; CREATE RULE t1_del_rule AS ON DELETE TO t1 DO INSTEAD UPDATE t1 SET deleted = true WHERE id = old.id; CREATE VIEW v1 WITH (security_barrier=true) AS SELECT id, data FROM t1 WHERE NOT deleted; SELECT * FROM v1; EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM v1 WHERE id = 1 AND snoop(data); DELETE FROM v1 WHERE id = 1 AND snoop(data); EXPLAIN (VERBOSE, COSTS OFF) INSERT INTO v1 VALUES (2, 'New row 2'); INSERT INTO v1 VALUES (2, 'New row 2'); SELECT * FROM t1; DROP TABLE t1 CASCADE;
Results:
id | data ----+------- 1 | Row 1 (1 row) QUERY PLAN ------------------------------------------------------------------ Update on public.t1 t1_1 -> Nested Loop Output: t1_1.id, t1_1.data, true, t1_1.ctid, t1.wholerow -> Index Scan using t1_pkey on public.t1 t1_1 Output: t1_1.id, t1_1.data, t1_1.ctid Index Cond: (t1_1.id = 1) -> Subquery Scan on t1 Output: t1.id, t1.data, t1.wholerow Filter: snoop(t1.data) -> Index Scan using t1_pkey on public.t1 t1_2 Output: t1_2.id, t1_2.data, t1_2.* Index Cond: (t1_2.id = 1) Filter: (NOT t1_2.deleted) (13 rows) NOTICE: Snooped value: Row 1 DELETE 0 QUERY PLAN ---------------------------------------------------------- Insert on public.t1 InitPlan 1 (returns $0) -> Index Only Scan using t1_pkey on public.t1 t1_1 Index Cond: (t1_1.id = 2) -> Result Output: 2, 'New row 2'::text, NULL::boolean One-Time Filter: ($0 IS NOT TRUE) Update on public.t1 InitPlan 1 (returns $0) -> Index Only Scan using t1_pkey on public.t1 t1_1 Index Cond: (t1_1.id = 2) -> Result Output: t1.id, 'New row 2'::text, false, t1.ctid One-Time Filter: $0 -> Index Scan using t1_pkey on public.t1 Output: t1.id, t1.ctid Index Cond: (t1.id = 2) (18 rows) INSERT 0 0 id | data | deleted ----+-----------+--------- 1 | Row 1 | t 2 | New row 2 | f (2 rows)
Update on security barrier view with rules on the base relation (#2)
A much more artificial example — the rule on the base relation turns DELETEs INTO SELECTs:
CREATE TABLE t1 (id int, data text); INSERT INTO t1 VALUES (1, 'Public row'), (-1, 'Private row'); CREATE RULE t1_del_rule AS ON DELETE TO t1 DO INSTEAD SELECT * FROM t1 WHERE id = old.id; CREATE VIEW v1 WITH (security_barrier=true) AS SELECT * FROM t1 WHERE id > 0; EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM v1 WHERE snoop(data); DELETE FROM v1 WHERE snoop(data); SELECT * FROM t1; DROP TABLE t1 CASCADE;
Results:
QUERY PLAN ------------------------------------------------ Hash Join Output: t1_1.id, t1_1.data Hash Cond: (t1_1.id = t1.id) -> Seq Scan on public.t1 t1_1 Output: t1_1.id, t1_1.data -> Hash Output: t1.id -> Subquery Scan on t1 Output: t1.id Filter: snoop(t1.data) -> Seq Scan on public.t1 t1_2 Output: t1_2.id, t1_2.data Filter: (t1_2.id > 0) (13 rows) NOTICE: Snooped value: Public row id | data ----+------------ 1 | Public row (1 row) DELETE 0 id | data ----+------------- 1 | Public row -1 | Private row (2 rows)