Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions build/transform.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
4 changes: 2 additions & 2 deletions sql/pgque-tle.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions sql/pgque.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions tests/run_all.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 57 additions & 0 deletions tests/test_get_queue_info_reader.sql
Original file line number Diff line number Diff line change
@@ -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'
Loading