This is an automated email from the ASF dual-hosted git repository. MuhammadTahaNaveed pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/age.git
commit 026a0cbd83435fa619e8d69f3c0e3d6a776c1bc8 Author: John Gemignani <[email protected]> AuthorDate: Thu Oct 8 05:34:52 2026 +0000 Refuse RLS graph loads during parallel operations The traversal graph loads RLS-protected labels through an SPI query, so the policies are evaluated in whichever process loads the graph. vertex_stats() and age_graph_stats() are parallel safe, so under a parallel plan a worker loaded the graph. A worker does not have all of the caller's session state; inet_client_addr(), for one, is NULL there. A policy that hid edges from a client connection therefore admitted them in the worker, and the statistics counted edges the caller could not see. Raise an error (SQLSTATE 25000) instead of loading an RLS-protected label in parallel mode. Roles the policies do not apply to, labels without RLS, serial plans and age.enforce_rls_in_traversal = off are unaffected. The function declarations stay parallel safe, so other queries keep their parallel plans. The new rls_vle part forces a parallel plan and checks that the restricted role is refused while the owner and a serial plan still get statistics. Co-authored-by: Copilot <[email protected]> --- regress/expected/rls_vle.out | 41 ++++++++++++++++++++++++++++++++ regress/sql/rls_vle.sql | 30 +++++++++++++++++++++++ src/backend/utils/adt/age_global_graph.c | 21 ++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/regress/expected/rls_vle.out b/regress/expected/rls_vle.out index 3a6f7265..e567da5f 100644 --- a/regress/expected/rls_vle.out +++ b/regress/expected/rls_vle.out @@ -1384,6 +1384,47 @@ 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); +ERROR: cannot load row-level security protected graph data during a parallel operation +SELECT age_graph_stats('"rls_vle"'); +ERROR: cannot load row-level security protected graph data during a parallel operation +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); + out_degree +------------ + 1 +(1 row) + +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); + out_degree +------------ + 1 +(1 row) + +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); diff --git a/regress/sql/rls_vle.sql b/regress/sql/rls_vle.sql index b2738a62..11e6de50 100644 --- a/regress/sql/rls_vle.sql +++ b/regress/sql/rls_vle.sql @@ -873,6 +873,36 @@ 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 -- ============================================================================ diff --git a/src/backend/utils/adt/age_global_graph.c b/src/backend/utils/adt/age_global_graph.c index d7b861b2..fae12d79 100644 --- a/src/backend/utils/adt/age_global_graph.c +++ b/src/backend/utils/adt/age_global_graph.c @@ -278,6 +278,7 @@ static void load_GRAPH_global_hashtables(GRAPH_global_context *ggctx); static void load_vertex_hashtable(GRAPH_global_context *ggctx); static void load_edge_hashtable(GRAPH_global_context *ggctx); static void enforce_label_table_select_acl(Oid label_table_oid); +static void prevent_rls_load_in_parallel_mode(void); static void register_ggctx_security_callbacks(void); static void ggctx_security_relcache_callback(Datum arg, Oid relid); static void ggctx_security_syscache_callback(Datum arg, int cacheid, @@ -1370,6 +1371,24 @@ static void enforce_label_table_select_acl(Oid label_table_oid) } } +/* + * The RLS loaders evaluate the policies in whichever process loads the graph. + * A parallel worker does not have all of the caller's session state (for + * example, inet_client_addr() is NULL there), so a policy could admit rows in + * a worker that it hides from the caller. Refuse to load an RLS-protected label + * during a parallel operation. + */ +static void prevent_rls_load_in_parallel_mode(void) +{ + if (IsInParallelMode()) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_TRANSACTION_STATE), + errmsg("cannot load row-level security protected graph data during a parallel operation"), + errhint("Run the query without parallelism, for example with SET max_parallel_workers_per_gather = 0."))); + } +} + /* helper routine to load all vertices into the GRAPH global vertex hashtable */ static void load_vertex_hashtable(GRAPH_global_context *ggctx) { @@ -1434,6 +1453,7 @@ static void load_vertex_hashtable(GRAPH_global_context *ggctx) check_enable_rls(vertex_label_table_oid, InvalidOid, true) == RLS_ENABLED) { + prevent_rls_load_in_parallel_mode(); load_vertex_label_rls(ggctx, vertex_label_table_oid, vertex_label_name); /* @@ -1594,6 +1614,7 @@ static void load_edge_hashtable(GRAPH_global_context *ggctx) check_enable_rls(edge_label_table_oid, InvalidOid, true) == RLS_ENABLED) { + prevent_rls_load_in_parallel_mode(); load_edge_label_rls(ggctx, edge_label_table_oid, edge_label_name); ggctx->loaded_with_rls = true; continue;
