-
Notifications
You must be signed in to change notification settings - Fork 0
fix: replace GUC storage with config table in kit_sync_setup_cron (#383) #385
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| -- Fix: replace GUC-based secret storage with a private config table. | ||
| -- ALTER DATABASE SET app.kit_cron_secret is blocked on hosted Supabase (error 42501) | ||
| -- even from a SECURITY DEFINER function. This migration introduces a singleton | ||
| -- config table and rewrites kit_sync_setup_cron() to upsert there instead. | ||
| -- The cron job body reads URL and secret from the table at runtime, so the | ||
| -- secret is never inlined in cron.job.command (same privacy guarantee as before). | ||
|
|
||
| CREATE TABLE IF NOT EXISTS public.kit_sync_runtime_config ( | ||
| singleton boolean PRIMARY KEY DEFAULT true, | ||
| worker_url text NOT NULL, | ||
| cron_secret text NOT NULL, | ||
| updated_at timestamptz NOT NULL DEFAULT now(), | ||
| CONSTRAINT singleton_only CHECK (singleton IS TRUE) | ||
| ); | ||
|
|
||
| ALTER TABLE public.kit_sync_runtime_config ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| -- Supabase's default privileges grant SELECT/INSERT/UPDATE/DELETE to anon and | ||
| -- authenticated on all new public tables. Revoke explicitly so the table is | ||
| -- inaccessible via PostgREST regardless of RLS policy state. | ||
| -- service_role is intentionally excluded: the SECURITY DEFINER function runs as | ||
| -- the owner and needs no table grant; omitting the service_role grant prevents | ||
| -- direct REST reads of cron_secret via the service key. | ||
| REVOKE ALL ON TABLE public.kit_sync_runtime_config FROM PUBLIC; | ||
| REVOKE ALL ON TABLE public.kit_sync_runtime_config FROM anon, authenticated; | ||
|
|
||
| -- ── Updated cron registration helper ───────────────────────────────────────── | ||
| -- Replaces ALTER DATABASE SET with an upsert into kit_sync_runtime_config. | ||
| -- Cron job body selects worker_url and cron_secret from the table at runtime. | ||
|
|
||
| CREATE OR REPLACE FUNCTION kit_sync_setup_cron( | ||
| p_url text, | ||
| p_secret text | ||
| ) | ||
| RETURNS void | ||
| LANGUAGE plpgsql | ||
| SECURITY DEFINER | ||
| SET search_path = extensions, net, public | ||
| AS $$ | ||
| BEGIN | ||
| -- Existing databases may still have app.kit_cron_secret set by the prior GUC-based | ||
| -- migration. Best-effort reset so the stale value doesn't linger in pg_db_role_setting; | ||
| -- silently ignored if ALTER DATABASE RESET is blocked on hosted Supabase. | ||
| BEGIN | ||
| EXECUTE format('ALTER DATABASE %I RESET app.kit_cron_secret', current_database()); | ||
| EXCEPTION WHEN OTHERS THEN | ||
| NULL; | ||
| END; | ||
|
|
||
| INSERT INTO public.kit_sync_runtime_config (singleton, worker_url, cron_secret, updated_at) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth a one-line comment that existing databases may still have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added a comment inside the function body about the stale GUC, plus a best-effort nested BEGIN...EXCEPTION WHEN OTHERS block that executes ALTER DATABASE ... RESET app.kit_cron_secret. The exception handler makes it a no-op on hosted Supabase where RESET may also be restricted, so it cannot break the deploy. |
||
| VALUES (true, p_url, p_secret, now()) | ||
| ON CONFLICT (singleton) DO UPDATE SET | ||
| worker_url = EXCLUDED.worker_url, | ||
| cron_secret = EXCLUDED.cron_secret, | ||
| updated_at = EXCLUDED.updated_at; | ||
|
|
||
| PERFORM cron.unschedule('kit-sync-worker') | ||
| WHERE EXISTS ( | ||
| SELECT 1 FROM cron.job WHERE jobname = 'kit-sync-worker' | ||
| ); | ||
|
|
||
| PERFORM cron.schedule( | ||
| 'kit-sync-worker', | ||
| '*/5 * * * *', | ||
| $sql$ | ||
| SELECT net.http_post( | ||
| url := c.worker_url, | ||
| headers := jsonb_build_object( | ||
| 'Content-Type', 'application/json', | ||
| 'Authorization', 'Bearer ' || c.cron_secret | ||
| ), | ||
| body := '{}'::jsonb | ||
| ) | ||
| FROM public.kit_sync_runtime_config c | ||
| WHERE c.singleton IS TRUE | ||
| LIMIT 1 | ||
| $sql$ | ||
| ); | ||
| END; | ||
| $$; | ||
|
|
||
| REVOKE ALL ON FUNCTION kit_sync_setup_cron(text, text) FROM PUBLIC; | ||
| GRANT EXECUTE ON FUNCTION kit_sync_setup_cron(text, text) TO service_role; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| BEGIN; | ||
|
|
||
| SELECT plan(8); | ||
| SELECT plan(17); | ||
|
|
||
| -- 1. pg_cron extension exists | ||
| SELECT has_extension('pg_cron', 'pg_cron extension is installed'); | ||
|
|
@@ -73,6 +73,76 @@ SELECT ok( | |
| 'kit_sync_dequeue returns pending row as processing' | ||
| ); | ||
|
|
||
| -- 9. kit_sync_runtime_config table exists | ||
| SELECT has_table('public', 'kit_sync_runtime_config', 'kit_sync_runtime_config table exists'); | ||
|
|
||
| -- 10. RLS is enabled on kit_sync_runtime_config | ||
| SELECT ok( | ||
| (SELECT c.relrowsecurity | ||
| FROM pg_class c | ||
| JOIN pg_namespace n ON n.oid = c.relnamespace | ||
| WHERE n.nspname = 'public' AND c.relname = 'kit_sync_runtime_config'), | ||
| 'kit_sync_runtime_config has RLS enabled' | ||
| ); | ||
|
|
||
| -- 11. anon role has no SELECT on kit_sync_runtime_config | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch revoking NOT has_table_privilege('authenticated', 'public.kit_sync_runtime_config', 'SELECT')Supabase default privileges grant both roles on new public tables; testing only
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added test 12 asserting authenticated has no SELECT (mirrors the anon check). Plan is now 17. |
||
| SELECT ok( | ||
| NOT has_table_privilege('anon', 'public.kit_sync_runtime_config', 'SELECT'), | ||
| 'anon has no SELECT on kit_sync_runtime_config' | ||
| ); | ||
|
|
||
| -- 12. authenticated role has no SELECT on kit_sync_runtime_config | ||
| -- Supabase default privileges grant both anon and authenticated; test both. | ||
| SELECT ok( | ||
| NOT has_table_privilege('authenticated', 'public.kit_sync_runtime_config', 'SELECT'), | ||
| 'authenticated has no SELECT on kit_sync_runtime_config' | ||
| ); | ||
|
|
||
| -- 13. kit_sync_setup_cron executes without error and upserts config row | ||
| SELECT lives_ok( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling Consider an explicit
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added PERFORM cron.unschedule("kit-sync-worker") after the assertions (still inside the pgTAP block, before SELECT * FROM finish()). This cleans up the cron.job row even if pg_cron committed independently of the test transaction. |
||
| $$ SELECT kit_sync_setup_cron('https://test.supabase.co/functions/v1/kit-sync', 'test-secret-abc') $$, | ||
| 'kit_sync_setup_cron executes without error' | ||
| ); | ||
|
|
||
| -- 14. config row contains the expected worker_url and cron_secret | ||
| SELECT ok( | ||
| EXISTS ( | ||
| SELECT 1 FROM public.kit_sync_runtime_config | ||
| WHERE singleton = true | ||
| AND worker_url = 'https://test.supabase.co/functions/v1/kit-sync' | ||
| AND cron_secret = 'test-secret-abc' | ||
| ), | ||
| 'kit_sync_runtime_config row has correct worker_url and cron_secret after setup' | ||
| ); | ||
|
|
||
| -- 15. cron.job row exists for kit-sync-worker | ||
| SELECT ok( | ||
| EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'kit-sync-worker'), | ||
| 'kit-sync-worker cron job row exists after setup' | ||
| ); | ||
|
|
||
| -- 16. cron.job.command for kit-sync-worker does not contain the literal secret | ||
| SELECT ok( | ||
| NOT EXISTS ( | ||
| SELECT 1 FROM cron.job | ||
| WHERE jobname = 'kit-sync-worker' | ||
| AND command LIKE '%test-secret-abc%' | ||
| ), | ||
| 'kit-sync-worker cron command does not inline the bearer secret' | ||
| ); | ||
|
|
||
| -- 17. service_role has no SELECT on kit_sync_runtime_config via table grant | ||
| -- The SECURITY DEFINER function accesses the table as owner; no grant needed. | ||
| SELECT ok( | ||
| NOT has_table_privilege('service_role', 'public.kit_sync_runtime_config', 'SELECT'), | ||
| 'service_role has no direct SELECT on kit_sync_runtime_config' | ||
| ); | ||
|
|
||
| -- Cleanup: unschedule the test cron job to avoid leaving stale rows in | ||
| -- cron.job on non-ephemeral databases (pg_cron commits independently of | ||
| -- the outer pgTAP transaction). | ||
| PERFORM cron.unschedule('kit-sync-worker'); | ||
|
|
||
| SELECT * FROM finish(); | ||
|
|
||
| ROLLBACK; | ||
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.
Good catch — fixed in f04ae3d. Added
ALTER TABLE public.kit_sync_runtime_config ENABLE ROW LEVEL SECURITY;immediately after the table definition, matching the pattern frommarketing_email_v1.sql.