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;

Reply via email to