Skip to content
Merged
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
93 changes: 93 additions & 0 deletions supabase/migrations/20260517144641_pgmq_public_wrapper.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
-- pgmq_public: PostgREST-facing wrapper schema for pgmq queues.
--
-- config.toml [api].schemas exposes "pgmq_public" to PostgREST. On the hosted
-- project the Studio "Expose Queues via PostgREST" toggle creates this schema;
-- locally it is never auto-created, so without this migration PostgREST fails
-- its schema-cache load with `schema "pgmq_public" does not exist` and the
-- local stack never comes up.
--
-- This mirrors what the Supabase dashboard generates (Supabase discussions
-- #41729 / #32969) so local == prod. Grants are service_role-only: the sole
-- consumer (jobs/src/main.ts) uses the service-role key, and supabase/AGENTS.md
-- requires deny-by-default for anon/authenticated.
--
-- Idempotent (`if not exists` / `or replace`) and forward-only: safe on the
-- hosted project where these objects already exist.

create schema if not exists pgmq_public;

grant usage on schema pgmq_public to service_role;
grant usage on schema pgmq to service_role;
Comment on lines +17 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether this migration explicitly revokes PUBLIC privileges.
rg -n "revoke\\s+all\\s+on\\s+schema\\s+pgmq_public\\s+from\\s+public|revoke\\s+execute\\s+on\\s+all\\s+functions\\s+in\\s+schema\\s+pgmq_public\\s+from\\s+public|grant\\s+execute\\s+on\\s+function|grant\\s+usage\\s+on\\s+schema\\s+pgmq_public" supabase/migrations/20260517144641_pgmq_public_wrapper.sql

Repository: gridaco/grida

Length of output: 140


🏁 Script executed:

cat -n supabase/migrations/20260517144641_pgmq_public_wrapper.sql

Repository: gridaco/grida

Length of output: 4328


🌐 Web query:

PostgreSQL default permissions schema function PUBLIC EXECUTE

💡 Result:

In PostgreSQL, functions are created with the EXECUTE privilege granted to PUBLIC by default [1]. To change this behavior for future functions, you must use the ALTER DEFAULT PRIVILEGES command [2][3]. Crucially, if you want to revoke this default PUBLIC EXECUTE privilege, you must do so globally (without specifying an IN SCHEMA clause) [1][4][5]. Per-schema default privileges are additive to global default privileges; therefore, attempting to revoke privileges in a specific schema will not override the global default that grants EXECUTE to PUBLIC [4][5]. To revoke the default PUBLIC EXECUTE privilege for functions created by a specific role, use the following command: ALTER DEFAULT PRIVILEGES FOR ROLE role_name REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; If you omit the FOR ROLE clause, the command applies to the current role [3][6]. Once this global revocation is in place, you can selectively grant EXECUTE privileges to specific roles or schemas as needed [1]. Note that ALTER DEFAULT PRIVILEGES only affects objects created after the command is executed and does not change privileges on existing functions [2][3].

Citations:


Add explicit REVOKE statements to prevent PUBLIC access to functions.

Functions in PostgreSQL are created with EXECUTE granted to PUBLIC by default. This migration creates wrapper functions without revoking the default PUBLIC EXECUTE privilege, which contradicts the stated intent ("deny-by-default for anon/authenticated"). Add REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC; after the function grants to enforce deny-by-default at the DDL level.

Suggested fix
 grant execute on function
     pgmq_public.pop(text),
     pgmq_public.send(text, jsonb, integer),
     pgmq_public.read(text, integer, integer),
     pgmq_public.archive(text, bigint),
     pgmq_public.delete(text, bigint)
 to service_role;
+revoke execute on all functions in schema pgmq_public from public;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260517144641_pgmq_public_wrapper.sql` around lines 17 -
20, Add an explicit revoke for public execute privileges on the wrapper
functions so PUBLIC cannot call them by default: after you finish
creating/granting the wrapper functions in schema pgmq_public (and similarly for
pgmq if wrappers are created there), add a statement to revoke EXECUTE from
PUBLIC, e.g. use REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM
PUBLIC to enforce deny-by-default for anon/authenticated; ensure this revoke
comes after the grants that assign service_role privileges so service_role
retains access.


create or replace function pgmq_public.pop(queue_name text)
returns setof pgmq.message_record
language plpgsql
set search_path = ''
as $$
begin
return query select * from pgmq.pop(queue_name := queue_name);
end;
$$;
comment on function pgmq_public.pop(queue_name text) is
'Retrieves and locks the next message from the specified queue.';

create or replace function pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer default 0)
returns setof bigint
language plpgsql
set search_path = ''
as $$
begin
return query select * from pgmq.send(queue_name := queue_name, msg := message, delay := sleep_seconds);
end;
$$;
comment on function pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer) is
'Sends a message to the specified queue, optionally delaying its availability by a number of seconds.';

create or replace function pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
returns setof pgmq.message_record
language plpgsql
set search_path = ''
as $$
begin
return query select * from pgmq.read(queue_name := queue_name, vt := sleep_seconds, qty := n);
end;
$$;
comment on function pgmq_public.read(queue_name text, sleep_seconds integer, n integer) is
'Reads up to "n" messages from the specified queue with an optional "sleep_seconds" (visibility timeout).';

create or replace function pgmq_public.archive(queue_name text, message_id bigint)
returns boolean
language plpgsql
set search_path = ''
as $$
begin
return pgmq.archive(queue_name := queue_name, msg_id := message_id);
end;
$$;
comment on function pgmq_public.archive(queue_name text, message_id bigint) is
'Archives a message by moving it from the queue to a permanent archive.';

create or replace function pgmq_public.delete(queue_name text, message_id bigint)
returns boolean
language plpgsql
set search_path = ''
as $$
begin
return pgmq.delete(queue_name := queue_name, msg_id := message_id);
end;
$$;
comment on function pgmq_public.delete(queue_name text, message_id bigint) is
'Permanently deletes a message from the specified queue.';

grant execute on function
pgmq_public.pop(text),
pgmq_public.send(text, jsonb, integer),
pgmq_public.read(text, integer, integer),
pgmq_public.archive(text, bigint),
pgmq_public.delete(text, bigint)
to service_role;

grant all privileges on all tables in schema pgmq to service_role;
alter default privileges in schema pgmq grant all privileges on tables to service_role;
grant usage, select, update on all sequences in schema pgmq to service_role;
alter default privileges in schema pgmq grant usage, select, update on sequences to service_role;
68 changes: 68 additions & 0 deletions supabase/tests/pgmq_public_grants_test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-- pgTAP suite: pgmq_public wrapper schema grants
--
-- Proves the deny-by-default boundary created by
-- migrations/20260517144641_pgmq_public_wrapper.sql:
-- - the schema + the five wrapper functions exist (so PostgREST can
-- introspect "pgmq_public" declared in config.toml [api].schemas),
-- - service_role can use the schema and execute the functions
-- (the jobs/src/main.ts poller uses the service-role key),
-- - anon / authenticated have NO USAGE on the schema.
--
-- The security gate is schema-level USAGE, not function EXECUTE: Postgres
-- grants EXECUTE to PUBLIC by default, but without USAGE on pgmq_public a
-- role cannot resolve or call anything inside it. That matches the
-- canonical Supabase pattern this migration mirrors.

BEGIN;

SELECT plan(11);

-- ---------------------------------------------------------------------
-- 1. Schema + function existence (PostgREST schema-cache surface).
-- ---------------------------------------------------------------------

SELECT has_schema('pgmq_public', 'pgmq_public schema exists');

SELECT has_function('pgmq_public', 'pop',
ARRAY['text'], 'pgmq_public.pop(text) exists');
SELECT has_function('pgmq_public', 'send',
ARRAY['text', 'jsonb', 'integer'], 'pgmq_public.send(text,jsonb,integer) exists');
SELECT has_function('pgmq_public', 'read',
ARRAY['text', 'integer', 'integer'], 'pgmq_public.read(text,integer,integer) exists');
SELECT has_function('pgmq_public', 'archive',
ARRAY['text', 'bigint'], 'pgmq_public.archive(text,bigint) exists');
SELECT has_function('pgmq_public', 'delete',
ARRAY['text', 'bigint'], 'pgmq_public.delete(text,bigint) exists');

-- ---------------------------------------------------------------------
-- 2. service_role can use the schema + execute the poller's functions.
-- ---------------------------------------------------------------------

SELECT ok(
has_schema_privilege('service_role', 'pgmq_public', 'USAGE'),
'service_role has USAGE on pgmq_public'
);
SELECT ok(
has_function_privilege('service_role', 'pgmq_public.read(text,integer,integer)', 'EXECUTE'),
'service_role can EXECUTE pgmq_public.read'
);
SELECT ok(
has_function_privilege('service_role', 'pgmq_public.archive(text,bigint)', 'EXECUTE'),
'service_role can EXECUTE pgmq_public.archive'
);

-- ---------------------------------------------------------------------
-- 3. Deny-by-default: anon / authenticated cannot reach the schema.
-- ---------------------------------------------------------------------

SELECT ok(
NOT has_schema_privilege('anon', 'pgmq_public', 'USAGE'),
'anon has no USAGE on pgmq_public'
);
SELECT ok(
NOT has_schema_privilege('authenticated', 'pgmq_public', 'USAGE'),
'authenticated has no USAGE on pgmq_public'
);

SELECT * FROM finish();
ROLLBACK;
Loading