Skip to content

Commit d205f26

Browse files
jrgemignaniCopilot
andcommitted
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 <223556219+Copilot@users.noreply.github.com>
1 parent 5de33f6 commit d205f26

3 files changed

Lines changed: 92 additions & 0 deletions

File tree

‎regress/expected/rls_vle.out‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,6 +1384,47 @@ RESET ROLE;
13841384
DROP POLICY node_none ON rls_vle."Node";
13851385
ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
13861386
-- ============================================================================
1387+
-- PART 12: Parallel operations. The policies are evaluated by the process
1388+
-- that loads the graph, and a parallel worker lacks part of the caller's
1389+
-- session state, so loading an RLS-protected label during a parallel
1390+
-- operation is refused. Roles that bypass the policy, and serial plans, are
1391+
-- unaffected.
1392+
-- ============================================================================
1393+
ALTER TABLE rls_vle."LINK" ENABLE ROW LEVEL SECURITY;
1394+
CREATE POLICY link_all ON rls_vle."LINK" FOR SELECT USING (true);
1395+
SET debug_parallel_query = on;
1396+
\set VERBOSITY terse
1397+
SET ROLE rls_vle_user;
1398+
SELECT * FROM cypher('rls_vle', $$
1399+
MATCH (n:Node) RETURN vertex_stats(n)
1400+
$$) AS (stats agtype);
1401+
ERROR: cannot load row-level security protected graph data during a parallel operation
1402+
SELECT age_graph_stats('"rls_vle"');
1403+
ERROR: cannot load row-level security protected graph data during a parallel operation
1404+
RESET ROLE;
1405+
\set VERBOSITY default
1406+
SELECT * FROM cypher('rls_vle', $$
1407+
MATCH (n:Node {name: 'n1'}) WITH vertex_stats(n) AS s RETURN s.out_degree
1408+
$$) AS (out_degree agtype);
1409+
out_degree
1410+
------------
1411+
1
1412+
(1 row)
1413+
1414+
RESET debug_parallel_query;
1415+
SET ROLE rls_vle_user;
1416+
SELECT * FROM cypher('rls_vle', $$
1417+
MATCH (n:Node {name: 'n1'}) WITH vertex_stats(n) AS s RETURN s.out_degree
1418+
$$) AS (out_degree agtype);
1419+
out_degree
1420+
------------
1421+
1
1422+
(1 row)
1423+
1424+
RESET ROLE;
1425+
DROP POLICY link_all ON rls_vle."LINK";
1426+
ALTER TABLE rls_vle."LINK" DISABLE ROW LEVEL SECURITY;
1427+
-- ============================================================================
13871428
-- cleanup
13881429
-- ============================================================================
13891430
SELECT drop_graph('rls_vle', true);

‎regress/sql/rls_vle.sql‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,36 @@ RESET ROLE;
873873
DROP POLICY node_none ON rls_vle."Node";
874874
ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
875875

876+
-- ============================================================================
877+
-- PART 12: Parallel operations. The policies are evaluated by the process
878+
-- that loads the graph, and a parallel worker lacks part of the caller's
879+
-- session state, so loading an RLS-protected label during a parallel
880+
-- operation is refused. Roles that bypass the policy, and serial plans, are
881+
-- unaffected.
882+
-- ============================================================================
883+
ALTER TABLE rls_vle."LINK" ENABLE ROW LEVEL SECURITY;
884+
CREATE POLICY link_all ON rls_vle."LINK" FOR SELECT USING (true);
885+
SET debug_parallel_query = on;
886+
\set VERBOSITY terse
887+
SET ROLE rls_vle_user;
888+
SELECT * FROM cypher('rls_vle', $$
889+
MATCH (n:Node) RETURN vertex_stats(n)
890+
$$) AS (stats agtype);
891+
SELECT age_graph_stats('"rls_vle"');
892+
RESET ROLE;
893+
\set VERBOSITY default
894+
SELECT * FROM cypher('rls_vle', $$
895+
MATCH (n:Node {name: 'n1'}) WITH vertex_stats(n) AS s RETURN s.out_degree
896+
$$) AS (out_degree agtype);
897+
RESET debug_parallel_query;
898+
SET ROLE rls_vle_user;
899+
SELECT * FROM cypher('rls_vle', $$
900+
MATCH (n:Node {name: 'n1'}) WITH vertex_stats(n) AS s RETURN s.out_degree
901+
$$) AS (out_degree agtype);
902+
RESET ROLE;
903+
DROP POLICY link_all ON rls_vle."LINK";
904+
ALTER TABLE rls_vle."LINK" DISABLE ROW LEVEL SECURITY;
905+
876906
-- ============================================================================
877907
-- cleanup
878908
-- ============================================================================

‎src/backend/utils/adt/age_global_graph.c‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ static void load_GRAPH_global_hashtables(GRAPH_global_context *ggctx);
278278
static void load_vertex_hashtable(GRAPH_global_context *ggctx);
279279
static void load_edge_hashtable(GRAPH_global_context *ggctx);
280280
static void enforce_label_table_select_acl(Oid label_table_oid);
281+
static void prevent_rls_load_in_parallel_mode(void);
281282
static void register_ggctx_security_callbacks(void);
282283
static void ggctx_security_relcache_callback(Datum arg, Oid relid);
283284
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)
13701371
}
13711372
}
13721373

1374+
/*
1375+
* The RLS loaders evaluate the policies in whichever process loads the graph.
1376+
* A parallel worker does not have all of the caller's session state (for
1377+
* example, inet_client_addr() is NULL there), so a policy could admit rows in
1378+
* a worker that it hides from the caller. Refuse to load an RLS-protected label
1379+
* during a parallel operation.
1380+
*/
1381+
static void prevent_rls_load_in_parallel_mode(void)
1382+
{
1383+
if (IsInParallelMode())
1384+
{
1385+
ereport(ERROR,
1386+
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
1387+
errmsg("cannot load row-level security protected graph data during a parallel operation"),
1388+
errhint("Run the query without parallelism, for example with SET max_parallel_workers_per_gather = 0.")));
1389+
}
1390+
}
1391+
13731392
/* helper routine to load all vertices into the GRAPH global vertex hashtable */
13741393
static void load_vertex_hashtable(GRAPH_global_context *ggctx)
13751394
{
@@ -1434,6 +1453,7 @@ static void load_vertex_hashtable(GRAPH_global_context *ggctx)
14341453
check_enable_rls(vertex_label_table_oid, InvalidOid, true) ==
14351454
RLS_ENABLED)
14361455
{
1456+
prevent_rls_load_in_parallel_mode();
14371457
load_vertex_label_rls(ggctx, vertex_label_table_oid,
14381458
vertex_label_name);
14391459
/*
@@ -1594,6 +1614,7 @@ static void load_edge_hashtable(GRAPH_global_context *ggctx)
15941614
check_enable_rls(edge_label_table_oid, InvalidOid, true) ==
15951615
RLS_ENABLED)
15961616
{
1617+
prevent_rls_load_in_parallel_mode();
15971618
load_edge_label_rls(ggctx, edge_label_table_oid, edge_label_name);
15981619
ggctx->loaded_with_rls = true;
15991620
continue;

0 commit comments

Comments
 (0)