From bda9a3f090a3a9a6786f87550d4c1e629ff5de83 Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Tue, 2 Jun 2026 14:42:45 -0700 Subject: [PATCH 1/2] test: pin pgque_reader can call get_queue_info Failing regression: a pgque_reader session calling pgque.get_queue_info() / get_queue_info(text) hits "permission denied for function seq_getval" because the function is SECURITY INVOKER but calls the admin-only pgque.seq_getval(text). Reproduces issue #265. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/run_all.sql | 3 ++ tests/test_get_queue_info_reader.sql | 57 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/test_get_queue_info_reader.sql diff --git a/tests/run_all.sql b/tests/run_all.sql index 7ed6a1bf..565164a9 100644 --- a/tests/run_all.sql +++ b/tests/run_all.sql @@ -118,6 +118,9 @@ \echo 'Running: test_e2e_role_split' \i tests/test_e2e_role_split.sql +\echo 'Running: test_get_queue_info_reader' +\i tests/test_get_queue_info_reader.sql + \echo 'Running: test_cooperative_consumers' \i tests/test_cooperative_consumers.sql diff --git a/tests/test_get_queue_info_reader.sql b/tests/test_get_queue_info_reader.sql new file mode 100644 index 00000000..9181e973 --- /dev/null +++ b/tests/test_get_queue_info_reader.sql @@ -0,0 +1,57 @@ +-- test_get_queue_info_reader.sql -- pgque_reader can call get_queue_info +-- Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license. +-- +-- get_queue_info() and get_queue_info(text) are granted to pgque_reader and +-- documented as reader-usable monitoring functions. They internally call +-- pgque.seq_getval(text), whose ACL is admin-only, so a SECURITY INVOKER +-- get_queue_info fails at runtime for a reader with: +-- ERROR: permission denied for function seq_getval +-- This pins the fix: get_queue_info must be callable by pgque_reader, the +-- exact role it is granted to. See +-- https://github.com/NikolayS/pgque/issues/265 + +\set ON_ERROR_STOP on + +-- Idempotent preamble. +do $$ begin + if exists (select 1 from pgque.queue where queue_name = 'gqi_reader_q') then + perform pgque.drop_queue('gqi_reader_q', true); + end if; +end $$; + +select pgque.create_queue('gqi_reader_q'); + +-- A tick must exist so the function exercises the seq_getval() code path. +select pgque.subscribe('gqi_reader_q', 'gqi_consumer'); +select pgque.send('gqi_reader_q', 'gqi.msg', '{"n":1}'::jsonb); +select pgque.ticker(); + +-- Exercise both overloads under the role they are granted to. +set role pgque_reader; + +do $$ +declare + v_count integer; + v_name text; + v_ev bigint; +begin + -- get_queue_info(text): the overload that calls seq_getval(). + select queue_name, ev_new + into v_name, v_ev + from pgque.get_queue_info('gqi_reader_q'); + assert v_name = 'gqi_reader_q', 'get_queue_info(text) must return the queue row'; + assert v_ev is not null, 'get_queue_info(text) ev_new must be populated'; + raise notice 'PASS: pgque_reader can call get_queue_info(text), ev_new=%', v_ev; + + -- get_queue_info(): the zero-arg overload fans out to the text overload. + select count(*) into v_count from pgque.get_queue_info(); + assert v_count >= 1, 'get_queue_info() must return at least the one queue'; + raise notice 'PASS: pgque_reader can call get_queue_info(), rows=%', v_count; +end $$; + +reset role; + +-- Cleanup. +select pgque.drop_queue('gqi_reader_q', true); + +\echo 'PASS: test_get_queue_info_reader' From 7fcda8ee95bec7a3de5f8142e1dc96afaba55bbc Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Tue, 2 Jun 2026 14:43:19 -0700 Subject: [PATCH 2/2] fix: make get_queue_info reader-callable get_queue_info(text) calls the admin-only pgque.seq_getval(text) but ships as SECURITY INVOKER, so a pgque_reader session -- the exact role it is granted to and documented for -- fails at runtime with "permission denied for function seq_getval". Promote both get_queue_info overloads to SECURITY DEFINER with SET search_path = pgque, pg_catalog (mandatory per CLAUDE.md), mirroring the sibling get_consumer_info / get_batch_info. Patch added to build/transform.sh and the generated sql/pgque.sql and sql/pgque-tle.sql regenerated so source and generated stay in sync. Grants no privilege beyond reading queue metadata. Fixes #265 Co-Authored-By: Claude Opus 4.8 (1M context) --- build/transform.sh | 25 +++++++++++++++++++++++++ sql/pgque-tle.sql | 4 ++-- sql/pgque.sql | 4 ++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/build/transform.sh b/build/transform.sh index c8825580..3914c82d 100755 --- a/build/transform.sh +++ b/build/transform.sh @@ -607,6 +607,31 @@ END { echo "PASS: get_batch_cursor SECURITY header injected (extra_where is trusted SQL)" +# Promote get_queue_info() / get_queue_info(text) to SECURITY DEFINER. +# Upstream PgQ ships these as SECURITY INVOKER, but get_queue_info(text) +# internally calls pgque.seq_getval(text), whose ACL is admin-only. Both +# overloads are granted to pgque_reader and documented as reader-usable +# monitoring functions, so a reader session would hit +# ERROR: permission denied for function seq_getval +# at runtime. The sibling get_consumer_info / get_batch_info are already +# SECURITY DEFINER for exactly this reason; mirror that pattern here. +# SECURITY DEFINER MUST pin search_path (CLAUDE.md), so attach +# "set search_path = pgque, pg_catalog" in the same step. This grants no +# privilege beyond reading queue metadata + the queue event sequence. +GET_QUEUE_INFO_FILE="${OUTPUT_DIR}/functions/pgque.get_queue_info.sql" +sedi -E \ + 's/^(\$\$ language plpgsql);$/\1 security definer set search_path = pgque, pg_catalog;/' \ + "${GET_QUEUE_INFO_FILE}" + +defcount=$(grep -c 'language plpgsql security definer set search_path = pgque, pg_catalog;' \ + "${GET_QUEUE_INFO_FILE}") +if [[ "${defcount}" -ne 2 ]]; then + echo "ERROR: expected 2 SECURITY DEFINER get_queue_info overloads, got ${defcount}" >&2 + exit 1 +fi + +echo "PASS: get_queue_info promoted to SECURITY DEFINER (reader-safe seq_getval)" + # -- Assembly: build sql/pgque.sql ------------------------------------ echo "" diff --git a/sql/pgque-tle.sql b/sql/pgque-tle.sql index 8d4de44f..46c7ac90 100644 --- a/sql/pgque-tle.sql +++ b/sql/pgque-tle.sql @@ -2656,7 +2656,7 @@ begin end loop; return; end; -$$ language plpgsql; +$$ language plpgsql security definer set search_path = pgque, pg_catalog; create or replace function pgque.get_queue_info( in i_queue_name text, @@ -2737,7 +2737,7 @@ begin end loop; return; end; -$$ language plpgsql; +$$ language plpgsql security definer set search_path = pgque, pg_catalog; create or replace function pgque.get_consumer_info( diff --git a/sql/pgque.sql b/sql/pgque.sql index bf70020b..080c719d 100644 --- a/sql/pgque.sql +++ b/sql/pgque.sql @@ -2568,7 +2568,7 @@ begin end loop; return; end; -$$ language plpgsql; +$$ language plpgsql security definer set search_path = pgque, pg_catalog; create or replace function pgque.get_queue_info( in i_queue_name text, @@ -2649,7 +2649,7 @@ begin end loop; return; end; -$$ language plpgsql; +$$ language plpgsql security definer set search_path = pgque, pg_catalog; create or replace function pgque.get_consumer_info(