| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| LOAD 'age'; | |
| SET search_path TO ag_catalog; | |
| -- | |
| -- Row-level security enforcement during VLE / global-graph traversal | |
| -- (controlled by age.enforce_rls_in_traversal, default on). | |
| -- | |
| -- Chain: n1 -[LINK visible:true]-> n2 -[LINK visible:false]-> n3 -[LINK visible:true]-> n4 | |
| -- | |
| SELECT create_graph('rls_vle'); | |
| SELECT * FROM cypher('rls_vle', $$ CREATE (:Node {name: 'n1'}) $$) AS (a agtype); | |
| SELECT * FROM cypher('rls_vle', $$ CREATE (:Node {name: 'n2'}) $$) AS (a agtype); | |
| SELECT * FROM cypher('rls_vle', $$ CREATE (:Node {name: 'n3'}) $$) AS (a agtype); | |
| SELECT * FROM cypher('rls_vle', $$ CREATE (:Node {name: 'n4'}) $$) AS (a agtype); | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'}), (b:Node {name: 'n2'}) | |
| CREATE (a)-[:LINK {visible: true}]->(b) | |
| $$) AS (a agtype); | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n2'}), (b:Node {name: 'n3'}) | |
| CREATE (a)-[:LINK {visible: false}]->(b) | |
| $$) AS (a agtype); | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n3'}), (b:Node {name: 'n4'}) | |
| CREATE (a)-[:LINK {visible: true}]->(b) | |
| $$) AS (a agtype); | |
| -- restricted role: SELECT on the graph + ag_catalog, nothing more | |
| CREATE ROLE rls_vle_user LOGIN; | |
| GRANT USAGE ON SCHEMA rls_vle TO rls_vle_user; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA rls_vle TO rls_vle_user; | |
| GRANT USAGE ON SCHEMA ag_catalog TO rls_vle_user; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA ag_catalog TO rls_vle_user; | |
| -- ============================================================================ | |
| -- PART 1: Baseline (superuser, RLS bypassed) -- VLE from n1 reaches n2, n3, n4. | |
| -- ============================================================================ | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| -- ============================================================================ | |
| -- PART 2: Edge-label RLS -- hide edges whose "visible" property is false. | |
| -- ============================================================================ | |
| ALTER TABLE rls_vle."LINK" ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."LINK" FORCE ROW LEVEL SECURITY; | |
| CREATE POLICY link_visible ON rls_vle."LINK" | |
| FOR SELECT USING ((properties->>'"visible"')::boolean = true); | |
| -- Enforced (default): the restricted role reaches ONLY n2 (n2->n3 hidden). | |
| SET ROLE rls_vle_user; | |
| SHOW age.enforce_rls_in_traversal; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| -- Opt-out: with enforcement off, the legacy direct scan bypasses RLS, so the | |
| -- restricted role again reaches n2, n3, n4. | |
| SET age.enforce_rls_in_traversal = off; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| RESET age.enforce_rls_in_traversal; | |
| -- clear the edge policy for the next part | |
| DROP POLICY link_visible ON rls_vle."LINK"; | |
| ALTER TABLE rls_vle."LINK" NO FORCE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."LINK" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- PART 3: Vertex-label RLS -- exercises the vertex loader and the dropping of | |
| -- edges whose endpoint vertex is hidden (matches MATCH's inner-join behavior). | |
| -- All edges are visible here; only vertices are filtered. | |
| -- ============================================================================ | |
| ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."Node" FORCE ROW LEVEL SECURITY; | |
| -- 3.1: hide the INTERMEDIATE vertex n2. Its edges are dropped, so the traversal | |
| -- cannot pass through it -- expect no rows (and no "no vertex found" error). | |
| CREATE POLICY node_visible ON rls_vle."Node" | |
| FOR SELECT USING ((properties->>'"name"') <> 'n2'); | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| -- 3.2: hide only the LEAF vertex n4. The edge into n4 is dropped; the traversal | |
| -- still reaches n2 and n3. | |
| DROP POLICY node_visible ON rls_vle."Node"; | |
| CREATE POLICY node_visible ON rls_vle."Node" | |
| FOR SELECT USING ((properties->>'"name"') <> 'n4'); | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| DROP POLICY node_visible ON rls_vle."Node"; | |
| ALTER TABLE rls_vle."Node" NO FORCE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- PART 4: Per-role cache key -- two roles with different edge policies in one | |
| -- session must see different results (the cache is rebuilt when the role | |
| -- changes). user1 sees all edges; user2 sees none. | |
| -- ============================================================================ | |
| CREATE ROLE rls_vle_user2 LOGIN; | |
| GRANT USAGE ON SCHEMA rls_vle TO rls_vle_user2; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA rls_vle TO rls_vle_user2; | |
| GRANT USAGE ON SCHEMA ag_catalog TO rls_vle_user2; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA ag_catalog TO rls_vle_user2; | |
| ALTER TABLE rls_vle."LINK" ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."LINK" FORCE ROW LEVEL SECURITY; | |
| CREATE POLICY link_user1 ON rls_vle."LINK" FOR SELECT TO rls_vle_user USING (true); | |
| CREATE POLICY link_user2 ON rls_vle."LINK" FOR SELECT TO rls_vle_user2 USING (false); | |
| -- user1: all edges visible -> n2, n3, n4 | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| -- user2: no edges visible -> empty (cache must not be reused from user1) | |
| SET ROLE rls_vle_user2; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| -- tidy up Part 4's edge policies before the next parts | |
| DROP POLICY link_user1 ON rls_vle."LINK"; | |
| DROP POLICY link_user2 ON rls_vle."LINK"; | |
| ALTER TABLE rls_vle."LINK" NO FORCE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."LINK" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- PART 5: SELECT-ACL enforcement -- the low-level loader must not bypass table | |
| -- privileges, not only RLS policies. rls_vle_user3 can read every label table | |
| -- EXCEPT the edge table "LINK". | |
| -- ============================================================================ | |
| CREATE ROLE rls_vle_user3 LOGIN; | |
| GRANT USAGE ON SCHEMA rls_vle TO rls_vle_user3; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA rls_vle TO rls_vle_user3; | |
| GRANT USAGE ON SCHEMA ag_catalog TO rls_vle_user3; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA ag_catalog TO rls_vle_user3; | |
| REVOKE SELECT ON rls_vle."LINK" FROM rls_vle_user3; | |
| -- Enforced (default): traversal fails closed with permission denied on "LINK". | |
| -- The start-node MATCH on "Node" is allowed; only the edge load is denied. | |
| SET ROLE rls_vle_user3; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| -- Opt-out: with enforcement off, the legacy direct scan bypasses the ACL, so | |
| -- the same role again reaches n2, n3, n4 (the legacy behavior). | |
| SET age.enforce_rls_in_traversal = off; | |
| SET ROLE rls_vle_user3; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| RESET age.enforce_rls_in_traversal; | |
| -- ============================================================================ | |
| -- PART 6: Session-dependent RLS policy -- a policy whose result depends on a | |
| -- session GUC (current_setting) must be re-evaluated every statement, so an | |
| -- RLS-enforced cache is never reused across statements even when the role, the | |
| -- enforcement GUC, and the snapshot are unchanged. | |
| -- ============================================================================ | |
| ALTER TABLE rls_vle."LINK" ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."LINK" FORCE ROW LEVEL SECURITY; | |
| CREATE POLICY link_by_setting ON rls_vle."LINK" | |
| FOR SELECT USING (current_setting('rls_vle.mode', true) = 'all'); | |
| SET ROLE rls_vle_user; | |
| -- mode=all: every edge visible -> n2, n3, n4 | |
| SET rls_vle.mode = 'all'; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| -- mode changed in the same session (same role / GUC / snapshot): the cache must | |
| -- be rebuilt, so now no edge is visible -> empty. | |
| SET rls_vle.mode = 'none'; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET rls_vle.mode; | |
| RESET ROLE; | |
| DROP POLICY link_by_setting ON rls_vle."LINK"; | |
| ALTER TABLE rls_vle."LINK" NO FORCE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."LINK" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- PART 7: ACL-metadata cache invalidation -- an enforced (non-RLS) direct-scan | |
| -- cache must not survive a REVOKE on a label table. In DSM/SHMEM mode the graph | |
| -- version fast path only tracks data changes, so the cache is invalidated via | |
| -- catalog-invalidation callbacks; without them the REVOKE'd rows stay readable. | |
| -- Uses a fresh graph with no RLS so the direct-scan (ACL-only) path is taken. | |
| -- ============================================================================ | |
| SET age.enforce_rls_in_traversal = on; | |
| SELECT create_graph('rls_acl'); | |
| SELECT * FROM cypher('rls_acl', $$ | |
| CREATE (:Node {name: 'a'})-[:LINK]->(:Node {name: 'b'})-[:LINK]->(:Node {name: 'c'}) | |
| $$) AS (x agtype); | |
| GRANT USAGE ON SCHEMA rls_acl TO rls_vle_user; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA rls_acl TO rls_vle_user; | |
| -- build the enforced cache: rls_vle_user has SELECT -> reaches b, c | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_acl', $$ | |
| MATCH (a:Node {name: 'a'})-[:LINK*1..2]->(x) RETURN x.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| -- REVOKE SELECT on the edge label -- no graph data change, version counter same | |
| REVOKE SELECT ON rls_acl."LINK" FROM rls_vle_user; | |
| -- the cached traversal must now be rebuilt and denied | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_acl', $$ | |
| MATCH (a:Node {name: 'a'})-[:LINK*1..2]->(x) RETURN x.name | |
| $$) AS (name agtype) ORDER BY name; | |
| RESET ROLE; | |
| SELECT drop_graph('rls_acl', true); | |
| RESET age.enforce_rls_in_traversal; | |
| -- ============================================================================ | |
| -- PART 8: Traversal lifetime -- a traversal's graph context stays alive until | |
| -- its query ends, even when another traversal in the same query must load its | |
| -- own RLS-filtered context. Within one query an RLS-filtered context is reused | |
| -- under the rules PostgreSQL gives a statement's scans (see 8.3 to 8.7). | |
| -- ============================================================================ | |
| SELECT create_graph('rls_vle_chain'); | |
| SELECT * FROM cypher('rls_vle_chain', $$ | |
| CREATE (:Node {n: 1})-[:LINK]->(:Node {n: 2})-[:LINK]->(:Node {n: 3})-[:LINK]->(:Node {n: 4}) | |
| $$) AS (x agtype); | |
| SELECT create_graph('rls_vle_chain2'); | |
| SELECT * FROM cypher('rls_vle_chain2', $$ | |
| CREATE (:Node {n: 1})-[:LINK]->(:Node {n: 2})-[:LINK]->(:Node {n: 3}) | |
| $$) AS (x agtype); | |
| SELECT create_graph('rls_vle_many'); | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| UNWIND range(1, 20) AS i | |
| CREATE (:Source {i: i})-[:LINK]->(:Target {i: i}) | |
| $$) AS (x agtype); | |
| GRANT USAGE ON SCHEMA rls_vle_chain, rls_vle_chain2, rls_vle_many TO rls_vle_user; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA rls_vle_chain, rls_vle_chain2, rls_vle_many | |
| TO rls_vle_user; | |
| ALTER TABLE rls_vle_chain."LINK" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY link_all ON rls_vle_chain."LINK" FOR SELECT USING (true); | |
| ALTER TABLE rls_vle_chain2."LINK" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY link_all ON rls_vle_chain2."LINK" FOR SELECT USING (true); | |
| ALTER TABLE rls_vle_many."LINK" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY link_all ON rls_vle_many."LINK" FOR SELECT USING (true); | |
| SET ROLE rls_vle_user; | |
| -- 8.1: two traversals advanced row by row in one target list: 6 and 20 paths | |
| SELECT count(a) AS chain_paths, count(b) AS independent_paths | |
| FROM ( | |
| SELECT | |
| (age_vle('"rls_vle_chain"', NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '3', '1')).start_id AS a, | |
| (age_vle('"rls_vle_many"', NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '2', '1')).end_id AS b | |
| ) AS interleaved; | |
| -- 8.2: paths of the first graph are materialized after the second graph's | |
| -- traversal loaded its own context: 6 x 3 pairs of distinct paths | |
| SELECT count(*) AS pairs, count(DISTINCT p) AS chain_paths, | |
| count(DISTINCT q) AS chain2_paths | |
| FROM cypher('rls_vle_chain', $$ | |
| MATCH p = (a)-[:LINK*1..3]->(b) RETURN p | |
| $$) AS one(p agtype), | |
| cypher('rls_vle_chain2', $$ | |
| MATCH q = (a)-[:LINK*1..2]->(b) RETURN q | |
| $$) AS two(q agtype); | |
| RESET ROLE; | |
| -- 8.3: policy evaluations per query. Each traversal loads the 20 visible | |
| -- edges once and reuses its load when it is evaluated again for another row, | |
| -- as PostgreSQL rescans a materialized scan. Another traversal in the query | |
| -- loads the graph again, as a separate scan would evaluate the policy again, | |
| -- whichever functions the policy calls. | |
| SELECT age_delete_global_graphs(NULL) IS NOT NULL AS graph_caches_cleared; | |
| CREATE SEQUENCE public.rls_vle_policy_calls; | |
| GRANT USAGE, SELECT ON SEQUENCE public.rls_vle_policy_calls TO rls_vle_user; | |
| CREATE FUNCTION public.rls_vle_count_stable() RETURNS boolean | |
| LANGUAGE plpgsql STABLE | |
| AS $f$ | |
| BEGIN | |
| PERFORM nextval('public.rls_vle_policy_calls'); | |
| RETURN true; | |
| END | |
| $f$; | |
| ALTER POLICY link_all ON rls_vle_many."LINK" | |
| USING (public.rls_vle_count_stable()); | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) RETURN count(*) | |
| $$) AS (n agtype); | |
| RESET ROLE; | |
| SELECT last_value AS stable_one_hop_evaluations FROM public.rls_vle_policy_calls; | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..2]->(b:Target) RETURN count(*) | |
| $$) AS (n agtype); | |
| RESET ROLE; | |
| SELECT last_value AS stable_one_to_two_hop_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH p = (a:Source)-[:LINK*1..1]->(b:Target) RETURN count(p) | |
| $$) AS (n agtype); | |
| RESET ROLE; | |
| SELECT last_value AS stable_named_path_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) | |
| WITH count(*) AS first | |
| MATCH (c:Source)-[:LINK*1..1]->(d:Target) | |
| RETURN first, count(*) | |
| $$) AS (first agtype, second agtype); | |
| RESET ROLE; | |
| SELECT last_value AS stable_two_call_site_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| -- each traversal loaded its own context: the first one's was freed when the | |
| -- query ended, and the second one's stays in the backend until the next | |
| -- graph load frees it | |
| SELECT count(*) AS contexts_after_stable_query | |
| FROM pg_backend_memory_contexts WHERE name = 'AGE edge_table'; | |
| SELECT age_delete_global_graphs(NULL) IS NOT NULL AS graph_caches_cleared; | |
| -- a policy that calls only IMMUTABLE functions, alone and with current_user: | |
| -- each traversal still loads the graph. The counter's argument keeps it from | |
| -- being evaluated once at planning. | |
| CREATE FUNCTION public.rls_vle_count_fixed(id graphid) RETURNS boolean | |
| LANGUAGE plpgsql IMMUTABLE | |
| AS $f$ | |
| BEGIN | |
| PERFORM nextval('public.rls_vle_policy_calls'); | |
| RETURN true; | |
| END | |
| $f$; | |
| ALTER POLICY link_all ON rls_vle_many."LINK" | |
| USING (public.rls_vle_count_fixed(id)); | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) | |
| WITH count(*) AS first | |
| MATCH (c:Source)-[:LINK*1..1]->(d:Target) | |
| RETURN first, count(*) | |
| $$) AS (first agtype, second agtype); | |
| RESET ROLE; | |
| SELECT last_value AS immutable_two_call_site_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| ALTER POLICY link_all ON rls_vle_many."LINK" | |
| USING (public.rls_vle_count_fixed(id) AND current_user = 'rls_vle_user'); | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) | |
| WITH count(*) AS first | |
| MATCH (c:Source)-[:LINK*1..1]->(d:Target) | |
| RETURN first, count(*) | |
| $$) AS (first agtype, second agtype); | |
| RESET ROLE; | |
| SELECT last_value AS current_user_two_call_site_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| -- a policy that reads the search path, and one that calls a STABLE function | |
| -- in a subquery: each traversal loads the graph | |
| ALTER POLICY link_all ON rls_vle_many."LINK" | |
| USING (public.rls_vle_count_fixed(id) AND current_schema IS NOT NULL); | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) | |
| WITH count(*) AS first | |
| MATCH (c:Source)-[:LINK*1..1]->(d:Target) | |
| RETURN first, count(*) | |
| $$) AS (first agtype, second agtype); | |
| RESET ROLE; | |
| SELECT last_value AS current_schema_two_call_site_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| ALTER POLICY link_all ON rls_vle_many."LINK" | |
| USING (EXISTS (SELECT 1 WHERE id IS NOT NULL | |
| AND public.rls_vle_count_stable())); | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) | |
| WITH count(*) AS first | |
| MATCH (c:Source)-[:LINK*1..1]->(d:Target) | |
| RETURN first, count(*) | |
| $$) AS (first agtype, second agtype); | |
| RESET ROLE; | |
| SELECT last_value AS stable_subquery_two_call_site_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| SELECT age_delete_global_graphs(NULL) IS NOT NULL AS graph_caches_cleared; | |
| ALTER POLICY link_all ON rls_vle_many."LINK" | |
| USING (nextval('public.rls_vle_policy_calls') > 0); | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) RETURN count(*) | |
| $$) AS (n agtype); | |
| RESET ROLE; | |
| SELECT last_value AS volatile_one_hop_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| ALTER SEQUENCE public.rls_vle_policy_calls RESTART; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) | |
| WITH count(*) AS first | |
| MATCH (c:Source)-[:LINK*1..1]->(d:Target) | |
| RETURN first, count(*) | |
| $$) AS (first agtype, second agtype); | |
| RESET ROLE; | |
| SELECT last_value AS volatile_two_call_site_evaluations | |
| FROM public.rls_vle_policy_calls; | |
| -- the first call site's context was freed when the query ended; the second | |
| -- one is kept, as for the stable policy | |
| SELECT count(*) AS contexts_after_volatile_query | |
| FROM pg_backend_memory_contexts WHERE name = 'AGE edge_table'; | |
| ALTER POLICY link_all ON rls_vle_many."LINK" USING (true); | |
| -- a policy that reads a setting with current_setting(), which the statement | |
| -- itself changes: each traversal sets the limit through its graph argument | |
| -- and sees its own Targets, the first those up to 5 and the second those up | |
| -- to 10, as two scans of the label in PostgreSQL would | |
| ALTER TABLE rls_vle_many."Target" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY target_limit ON rls_vle_many."Target" FOR SELECT | |
| USING ((properties->>'"i"')::int <= | |
| coalesce(nullif(current_setting('rls_vle.max_target', true), ''), | |
| '0')::int); | |
| CREATE FUNCTION public.rls_vle_limit_targets(n int) RETURNS agtype | |
| LANGUAGE plpgsql VOLATILE | |
| AS $f$ | |
| BEGIN | |
| PERFORM set_config('rls_vle.max_target', n::text, false); | |
| RETURN '"rls_vle_many"'::agtype; | |
| END | |
| $f$; | |
| SET ROLE rls_vle_user; | |
| SELECT count(DISTINCT first.end_id) AS first_targets, | |
| count(DISTINCT second.end_id) AS second_targets | |
| FROM age_vle(public.rls_vle_limit_targets(5), NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1') AS first, | |
| LATERAL age_vle(public.rls_vle_limit_targets( | |
| CASE WHEN first.start_id IS NOT NULL THEN 10 END), | |
| NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1') AS second; | |
| RESET ROLE; | |
| RESET rls_vle.max_target; | |
| DROP POLICY target_limit ON rls_vle_many."Target"; | |
| ALTER TABLE rls_vle_many."Target" DISABLE ROW LEVEL SECURITY; | |
| DROP FUNCTION public.rls_vle_limit_targets(int); | |
| -- 8.4: a statement's own write is visible to its later traversal. The write | |
| -- advances the command id, so the later traversal does not reuse the graph | |
| -- loaded before it. | |
| BEGIN; | |
| GRANT INSERT ON rls_vle_many."LINK" TO rls_vle_user; | |
| GRANT USAGE ON SEQUENCE rls_vle_many."LINK_id_seq" TO rls_vle_user; | |
| CREATE POLICY link_insert ON rls_vle_many."LINK" FOR INSERT WITH CHECK (true); | |
| SET LOCAL ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source {i: 1})-[:LINK*1..1]->(b:Target) | |
| WITH a, count(b) AS before | |
| MATCH (t:Target {i: 2}) | |
| CREATE (a)-[:LINK]->(t) | |
| WITH a, before, count(*) AS created | |
| MATCH (a)-[:LINK*1..1]->(c:Target) | |
| RETURN before, created, count(c) | |
| $$) AS (before agtype, created agtype, after agtype); | |
| ROLLBACK; | |
| -- 8.5: a policy change made while a statement runs does not change what that | |
| -- statement's traversal sees, as for a PostgreSQL scan; the next statement | |
| -- sees it. The start vertex argument hides every edge when the traversal is | |
| -- evaluated for the second row, after the first row loaded the graph. | |
| CREATE FUNCTION public.rls_vle_start_hiding_links(id graphid, rn bigint) | |
| RETURNS ag_catalog.agtype | |
| LANGUAGE plpgsql SECURITY DEFINER | |
| AS $f$ | |
| BEGIN | |
| IF rn = 2 THEN | |
| ALTER POLICY link_all ON rls_vle_many."LINK" USING (false); | |
| END IF; | |
| RETURN (id::text)::ag_catalog.agtype; | |
| END | |
| $f$; | |
| SET ROLE rls_vle_user; | |
| SELECT count(*) AS paths_with_policy_changed_midway | |
| FROM (SELECT id, row_number() OVER (ORDER BY id) AS rn | |
| FROM rls_vle_many."Source") AS s, | |
| LATERAL age_vle('"rls_vle_many"', | |
| public.rls_vle_start_hiding_links(s.id, s.rn), NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1') AS v; | |
| SELECT count(*) AS paths_in_next_statement | |
| FROM rls_vle_many."Source" AS s, | |
| LATERAL age_vle('"rls_vle_many"', (s.id::text)::agtype, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1') AS v; | |
| RESET ROLE; | |
| -- 8.6: a graph loaded for one role is not reused for another, even within a | |
| -- statement. The start vertex argument switches to rls_vle_user2, from whom | |
| -- every edge is hidden, when the traversal is evaluated for the second row. | |
| ALTER POLICY link_all ON rls_vle_many."LINK" USING (true); | |
| GRANT USAGE ON SCHEMA rls_vle_many TO rls_vle_user2; | |
| GRANT SELECT ON ALL TABLES IN SCHEMA rls_vle_many TO rls_vle_user2; | |
| CREATE POLICY link_hidden_from_user2 ON rls_vle_many."LINK" AS RESTRICTIVE | |
| FOR SELECT TO rls_vle_user2 USING (false); | |
| CREATE FUNCTION public.rls_vle_start_as_user2(id graphid, rn bigint) | |
| RETURNS ag_catalog.agtype | |
| LANGUAGE plpgsql | |
| AS $f$ | |
| BEGIN | |
| IF rn = 2 THEN | |
| PERFORM set_config('role', 'rls_vle_user2', true); | |
| END IF; | |
| RETURN (id::text)::ag_catalog.agtype; | |
| END | |
| $f$; | |
| SET ROLE rls_vle_user; | |
| SELECT count(*) AS paths_with_role_changed_midway | |
| FROM (SELECT id, row_number() OVER (ORDER BY id) AS rn | |
| FROM rls_vle_many."Source") AS s, | |
| LATERAL age_vle('"rls_vle_many"', | |
| public.rls_vle_start_as_user2(s.id, s.rn), NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1') AS v; | |
| RESET ROLE; | |
| -- 8.7: a statement's own deletes do not make a traversal load its graph | |
| -- again. The traversal keeps the graph it loaded, as it does without RLS, so | |
| -- a policy that allows every row returns what no policy returns, and the | |
| -- query holds one copy of the graph instead of one for every row. | |
| SELECT create_graph('rls_vle_before_plain'); | |
| SELECT create_graph('rls_vle_before_rls'); | |
| SELECT create_graph('rls_vle_after_plain'); | |
| SELECT create_graph('rls_vle_after_rls'); | |
| SELECT * FROM cypher('rls_vle_before_plain', $$ | |
| CREATE (:Source {i: 1})-[:LINK]->(m:Mid {i: 0})-[:LINK]->(:Target {i: 9}), | |
| (:Source {i: 2})-[:LINK]->(m) | |
| $$) AS (x agtype); | |
| SELECT * FROM cypher('rls_vle_before_rls', $$ | |
| CREATE (:Source {i: 1})-[:LINK]->(m:Mid {i: 0})-[:LINK]->(:Target {i: 9}), | |
| (:Source {i: 2})-[:LINK]->(m) | |
| $$) AS (x agtype); | |
| SELECT * FROM cypher('rls_vle_after_plain', $$ | |
| CREATE (:Node {i: 1, ord: 0})-[:LINK]->(:Node {i: 2, ord: 2}) | |
| -[:LINK]->(:Node {i: 3, ord: 3}), | |
| (:Node {i: 9, ord: 1}) | |
| $$) AS (x agtype); | |
| SELECT * FROM cypher('rls_vle_after_rls', $$ | |
| CREATE (:Node {i: 1, ord: 0})-[:LINK]->(:Node {i: 2, ord: 2}) | |
| -[:LINK]->(:Node {i: 3, ord: 3}), | |
| (:Node {i: 9, ord: 1}) | |
| $$) AS (x agtype); | |
| GRANT USAGE ON SCHEMA rls_vle_before_plain, rls_vle_before_rls, | |
| rls_vle_after_plain, rls_vle_after_rls TO rls_vle_user; | |
| GRANT SELECT, DELETE ON ALL TABLES IN SCHEMA rls_vle_before_plain, | |
| rls_vle_before_rls, rls_vle_after_plain, rls_vle_after_rls | |
| TO rls_vle_user; | |
| ALTER TABLE rls_vle_before_rls."LINK" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY link_read ON rls_vle_before_rls."LINK" FOR SELECT USING (true); | |
| CREATE POLICY link_delete ON rls_vle_before_rls."LINK" FOR DELETE USING (true); | |
| ALTER TABLE rls_vle_after_rls."LINK" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY link_read ON rls_vle_after_rls."LINK" FOR SELECT USING (true); | |
| CREATE POLICY link_delete ON rls_vle_after_rls."LINK" FOR DELETE USING (true); | |
| SET ROLE rls_vle_user; | |
| -- a traversal before the delete, for each Source: s1 -> m <- s2, m -> t | |
| SELECT * FROM cypher('rls_vle_before_plain', $$ | |
| MATCH (a:Source)-[:LINK*1..2]->(b) | |
| DETACH DELETE b | |
| RETURN a.i, b.i | |
| $$) AS (source agtype, deleted agtype) ORDER BY source, deleted; | |
| SELECT * FROM cypher('rls_vle_before_rls', $$ | |
| MATCH (a:Source)-[:LINK*1..2]->(b) | |
| DETACH DELETE b | |
| RETURN a.i, b.i | |
| $$) AS (source agtype, deleted agtype) ORDER BY source, deleted; | |
| -- a traversal after the delete of each node but n1, in the order of ord: | |
| -- n1 -> n2 -> n3 and an unconnected n9, which is deleted first | |
| SELECT * FROM cypher('rls_vle_after_plain', $$ | |
| MATCH (d:Node) WHERE d.i > 1 | |
| WITH d ORDER BY d.ord | |
| DETACH DELETE d | |
| WITH d | |
| MATCH p = (n:Node {i: 1})-[:LINK*1..3]->() | |
| RETURN d.i, length(p) | |
| $$) AS (deleted agtype, path_length agtype) ORDER BY deleted, path_length; | |
| SELECT * FROM cypher('rls_vle_after_rls', $$ | |
| MATCH (d:Node) WHERE d.i > 1 | |
| WITH d ORDER BY d.ord | |
| DETACH DELETE d | |
| WITH d | |
| MATCH p = (n:Node {i: 1})-[:LINK*1..3]->() | |
| RETURN d.i, length(p) | |
| $$) AS (deleted agtype, path_length agtype) ORDER BY deleted, path_length; | |
| RESET ROLE; | |
| -- the copies of the graph held while a traversal for each Source is | |
| -- followed by a delete, counted after every row | |
| CREATE FUNCTION public.rls_vle_graph_copies(anyelement) RETURNS bigint | |
| LANGUAGE plpgsql VOLATILE SECURITY DEFINER | |
| AS $f$ | |
| BEGIN | |
| RETURN (SELECT count(*) FROM pg_backend_memory_contexts | |
| WHERE name = 'AGE edge_table'); | |
| END | |
| $f$; | |
| SELECT age_delete_global_graphs(NULL) IS NOT NULL AS graph_caches_cleared; | |
| BEGIN; | |
| GRANT DELETE ON ALL TABLES IN SCHEMA rls_vle_many TO rls_vle_user; | |
| CREATE POLICY link_delete ON rls_vle_many."LINK" FOR DELETE USING (true); | |
| SET LOCAL ROLE rls_vle_user; | |
| SELECT count(*) AS deleted, max(copies) AS most_graph_copies_held | |
| FROM (SELECT public.rls_vle_graph_copies(i) AS copies | |
| FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) | |
| DETACH DELETE b | |
| RETURN a.i | |
| $$) AS (i agtype)) AS per_row; | |
| ROLLBACK; | |
| -- 8.8: a path value kept after its query is built from a graph context that | |
| -- holds all of its vertices and edges. When no context does, building it | |
| -- raises an error: after the caches are deleted, and for a role whose | |
| -- context lacks the path's edges. | |
| CREATE TABLE public.rls_vle_stored_paths AS | |
| SELECT edges FROM age_vle('"rls_vle_many"', NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1'); | |
| SELECT count(age_materialize_vle_path(edges)) AS stored_paths_built | |
| FROM public.rls_vle_stored_paths; | |
| SELECT age_delete_global_graphs(NULL) IS NOT NULL AS graph_caches_cleared; | |
| SELECT count(age_materialize_vle_path(edges)) AS stored_paths_built | |
| FROM public.rls_vle_stored_paths; | |
| SELECT count(age_materialize_vle_edges(edges)) AS stored_edge_lists_built | |
| FROM public.rls_vle_stored_paths; | |
| GRANT SELECT ON public.rls_vle_stored_paths TO rls_vle_user2; | |
| SET ROLE rls_vle_user2; | |
| SELECT count(*) AS paths_visible_to_user2 | |
| FROM age_vle('"rls_vle_many"', NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), '1', '1', '1'); | |
| SELECT count(age_materialize_vle_path(edges)) AS stored_paths_built | |
| FROM public.rls_vle_stored_paths; | |
| SELECT count(age_materialize_vle_edges(edges)) AS stored_edge_lists_built | |
| FROM public.rls_vle_stored_paths; | |
| RESET ROLE; | |
| DROP TABLE public.rls_vle_stored_paths; | |
| -- 8.9: a query that fails after its traversal releases its graph context | |
| -- during the error cleanup, at the top level and in PL/pgSQL, so failed | |
| -- queries leave no contexts behind | |
| SET ROLE rls_vle_user; | |
| SELECT 1 / ((n::text)::int - 20) AS fails_after_traversal | |
| FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) RETURN count(*) | |
| $$) AS (n agtype); | |
| SELECT 1 / ((n::text)::int - 20) AS fails_after_traversal | |
| FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) RETURN count(*) | |
| $$) AS (n agtype); | |
| DO $d$ | |
| BEGIN | |
| PERFORM 1 / ((n::text)::int - 20) | |
| FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) RETURN count(*) | |
| $$) AS (n agtype); | |
| EXCEPTION WHEN division_by_zero THEN | |
| NULL; | |
| END | |
| $d$; | |
| SELECT * FROM cypher('rls_vle_many', $$ | |
| MATCH (a:Source)-[:LINK*1..1]->(b:Target) RETURN count(*) | |
| $$) AS (paths agtype); | |
| RESET ROLE; | |
| SELECT count(*) AS contexts_after_failed_queries | |
| FROM pg_backend_memory_contexts WHERE name = 'AGE edge_table'; | |
| -- 8.10: deleting the graph caches while a query holds its graph retires the | |
| -- context instead of freeing it. The query still builds its paths from it, | |
| -- and it is freed when the query ends, also when the query then fails. | |
| SET ROLE rls_vle_user; | |
| SELECT count(age_materialize_vle_path(v.edges)) AS paths_built_after_delete | |
| FROM (SELECT edges, row_number() OVER () AS rn | |
| FROM age_vle('"rls_vle_many"', NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1')) AS v | |
| WHERE CASE WHEN v.rn = 1 THEN age_delete_global_graphs(NULL) IS NOT NULL | |
| ELSE true END; | |
| RESET ROLE; | |
| SELECT count(*) AS contexts_after_query | |
| FROM pg_backend_memory_contexts WHERE name = 'AGE edge_table'; | |
| SET ROLE rls_vle_user; | |
| SELECT 1 / (count(age_materialize_vle_path(v.edges)) - 20) AS fails_after_paths | |
| FROM (SELECT edges, row_number() OVER () AS rn | |
| FROM age_vle('"rls_vle_many"', NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), | |
| '1', '1', '1')) AS v | |
| WHERE CASE WHEN v.rn = 1 | |
| THEN age_delete_global_graphs('"rls_vle_many"') IS NOT NULL | |
| ELSE true END; | |
| RESET ROLE; | |
| SELECT count(*) AS contexts_after_failed_query | |
| FROM pg_backend_memory_contexts WHERE name = 'AGE edge_table'; | |
| DROP FUNCTION public.rls_vle_graph_copies(anyelement); | |
| DROP FUNCTION public.rls_vle_start_as_user2(graphid, bigint); | |
| DROP FUNCTION public.rls_vle_start_hiding_links(graphid, bigint); | |
| DROP FUNCTION public.rls_vle_count_fixed(graphid); | |
| DROP FUNCTION public.rls_vle_count_stable(); | |
| DROP SEQUENCE public.rls_vle_policy_calls; | |
| SELECT drop_graph('rls_vle_chain', true); | |
| SELECT drop_graph('rls_vle_chain2', true); | |
| SELECT drop_graph('rls_vle_many', true); | |
| SELECT drop_graph('rls_vle_before_plain', true); | |
| SELECT drop_graph('rls_vle_before_rls', true); | |
| SELECT drop_graph('rls_vle_after_plain', true); | |
| SELECT drop_graph('rls_vle_after_rls', true); | |
| -- ============================================================================ | |
| -- PART 9: vertex_stats on a vertex hidden from traversals. An untyped MATCH | |
| -- scans the parent label table, which the child table's policy does not | |
| -- filter, so it can return a vertex that the traversal graph does not hold. | |
| -- vertex_stats returns null for it; visible vertices get statistics computed | |
| -- over the graph the caller can traverse. | |
| -- ============================================================================ | |
| ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY node_visible ON rls_vle."Node" | |
| FOR SELECT USING ((properties->>'"name"') <> 'n2'); | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (n:Node) RETURN n.name | |
| $$) AS (name agtype) ORDER BY name; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (n) | |
| WITH n, vertex_stats(n) AS s | |
| RETURN n.name, s IS NULL, s.in_degree, s.out_degree | |
| $$) AS (name agtype, no_stats agtype, in_degree agtype, out_degree agtype) | |
| ORDER BY name; | |
| RESET ROLE; | |
| DROP POLICY node_visible ON rls_vle."Node"; | |
| ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- PART 10: Column privileges. A role granted SELECT on every user column of | |
| -- an RLS-protected label, but not on the table, traverses it just as MATCH | |
| -- reads it. It cannot read ctid, so its graph keeps the rows' properties. | |
| -- ============================================================================ | |
| ALTER TABLE rls_vle."LINK" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY link_all ON rls_vle."LINK" FOR SELECT USING (true); | |
| REVOKE SELECT ON rls_vle."LINK" FROM rls_vle_user; | |
| GRANT SELECT (id, start_id, end_id, properties) ON rls_vle."LINK" | |
| TO rls_vle_user; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[:LINK]->(b) RETURN b.name | |
| $$) AS (name agtype); | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a:Node {name: 'n1'})-[e:LINK*1..3]->(b) | |
| RETURN b.name, [r IN e | r.visible] | |
| $$) AS (name agtype, visible agtype) ORDER BY name; | |
| RESET ROLE; | |
| REVOKE SELECT (id, start_id, end_id, properties) ON rls_vle."LINK" | |
| FROM rls_vle_user; | |
| GRANT SELECT ON rls_vle."LINK" TO rls_vle_user; | |
| DROP POLICY link_all ON rls_vle."LINK"; | |
| ALTER TABLE rls_vle."LINK" DISABLE ROW LEVEL SECURITY; | |
| ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY node_all ON rls_vle."Node" FOR SELECT USING (true); | |
| REVOKE SELECT ON rls_vle."Node" FROM rls_vle_user; | |
| GRANT SELECT (id, properties) ON rls_vle."Node" TO rls_vle_user; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH p = (a:Node {name: 'n1'})-[:LINK*1..3]->(b) | |
| RETURN b.name, [v IN nodes(p) | v.name] | |
| $$) AS (name agtype, names agtype) ORDER BY name; | |
| RESET ROLE; | |
| REVOKE SELECT (id, properties) ON rls_vle."Node" FROM rls_vle_user; | |
| GRANT SELECT ON rls_vle."Node" TO rls_vle_user; | |
| DROP POLICY node_all ON rls_vle."Node"; | |
| ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- PART 11: No visible vertices. When a policy hides every vertex, a traversal | |
| -- has nothing to traverse and returns no paths instead of an error. The | |
| -- untyped MATCH still returns the hidden vertices from the parent table, so | |
| -- each traversal starts from a vertex outside the visible graph. | |
| -- ============================================================================ | |
| ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY node_none ON rls_vle."Node" FOR SELECT USING (false); | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (n:Node) RETURN count(n) | |
| $$) AS (count agtype); | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a)-[:LINK*1..3]->(b) RETURN b.name | |
| $$) AS (name agtype); | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (a)-[:LINK*0..1]->(b) RETURN count(*) | |
| $$) AS (count agtype); | |
| SELECT count(*) AS paths | |
| FROM age_vle('"rls_vle"', NULL, NULL, | |
| age_build_vle_match_edge('"LINK"', NULL), '1', '3', '1'); | |
| RESET ROLE; | |
| DROP POLICY node_none ON rls_vle."Node"; | |
| ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- PART 12: Parallel operations. The policies are evaluated by the process | |
| -- that loads the graph, and a parallel worker lacks part of the caller's | |
| -- session state, so loading an RLS-protected label during a parallel | |
| -- operation is refused. Roles that bypass the policy, and serial plans, are | |
| -- unaffected. | |
| -- ============================================================================ | |
| ALTER TABLE rls_vle."LINK" ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY link_all ON rls_vle."LINK" FOR SELECT USING (true); | |
| SET debug_parallel_query = on; | |
| \set VERBOSITY terse | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (n:Node) RETURN vertex_stats(n) | |
| $$) AS (stats agtype); | |
| SELECT age_graph_stats('"rls_vle"'); | |
| RESET ROLE; | |
| \set VERBOSITY default | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (n:Node {name: 'n1'}) WITH vertex_stats(n) AS s RETURN s.out_degree | |
| $$) AS (out_degree agtype); | |
| RESET debug_parallel_query; | |
| SET ROLE rls_vle_user; | |
| SELECT * FROM cypher('rls_vle', $$ | |
| MATCH (n:Node {name: 'n1'}) WITH vertex_stats(n) AS s RETURN s.out_degree | |
| $$) AS (out_degree agtype); | |
| RESET ROLE; | |
| DROP POLICY link_all ON rls_vle."LINK"; | |
| ALTER TABLE rls_vle."LINK" DISABLE ROW LEVEL SECURITY; | |
| -- ============================================================================ | |
| -- cleanup | |
| -- ============================================================================ | |
| SELECT drop_graph('rls_vle', true); | |
| DROP OWNED BY rls_vle_user, rls_vle_user2, rls_vle_user3; | |
| DROP ROLE rls_vle_user, rls_vle_user2, rls_vle_user3; |