-
Notifications
You must be signed in to change notification settings - Fork 135
fix(supabase): add pgmq_public wrapper migration so local stack starts #717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
93 changes: 93 additions & 0 deletions
93
supabase/migrations/20260517144641_pgmq_public_wrapper.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
|
|
||
| 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; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: gridaco/grida
Length of output: 140
🏁 Script executed:
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
EXECUTEgranted toPUBLICby default. This migration creates wrapper functions without revoking the defaultPUBLIC EXECUTEprivilege, which contradicts the stated intent ("deny-by-default for anon/authenticated"). AddREVOKE 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