From 7db0dc225fa874a2579fdaced92c5d6e91591c91 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 23 Sep 2026 19:16:56 +0000 Subject: [PATCH 1/3] fix(db): make legacy Data API grants explicit --- supabase/README.md | 1 + ...923191320_explicit_legacy_table_grants.sql | 32 ++++++++++++++++ supabase/tests/legacy_table_grants_test.sql | 38 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 supabase/migrations/20260923191320_explicit_legacy_table_grants.sql create mode 100644 supabase/tests/legacy_table_grants_test.sql diff --git a/supabase/README.md b/supabase/README.md index 00e4f597a..d35c3a832 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -97,6 +97,7 @@ Do not use wildcard origins to hide a failed browser test. Before protected stag ## Migrations and database tests Migration filenames define execution order. Never edit an applied migration to change production history; add a new forward migration. +When creating a table in `public`, grant only the Data API roles and operations it needs in the same migration, alongside RLS and its policies. Fresh projects and database resets cannot rely on automatic table or sequence grants. Local gate: diff --git a/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql b/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql new file mode 100644 index 000000000..e0fcefedb --- /dev/null +++ b/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql @@ -0,0 +1,32 @@ +-- Older table-creation migrations relied on Supabase's automatic public grants. +-- Make their final Data API permissions explicit for new projects and db reset. + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE + public.user_settings, + public.cases, public.attacks, public.brainstorms, public.factoids, + public.chats, public.messages, public.sources, public.source_files, + public.ai_requests, public.api_keys, + public.user_roles, public.rate_limits, public.global_prompts, + public.flowcharts, + public.subscriptions, public.promo_codes, public.user_promos, + public.admin_grants, public.shared_items, public.source_folders, + public.daily_usage_counters +TO service_role; + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE + public.cases, public.attacks, public.brainstorms, public.factoids, + public.chats, public.messages, public.sources, public.source_files, + public.api_keys, public.flowcharts, public.source_folders +TO authenticated; + +GRANT SELECT ON TABLE + public.user_roles, public.rate_limits, public.subscriptions, + public.user_promos, public.admin_grants +TO authenticated; + +-- Settings keep their existing SELECT and column-limited UPDATE grants; +-- trusted usage and shared items remain server-owned. +GRANT SELECT ON TABLE public.global_prompts TO anon, authenticated; + +-- The existing anonymous keepalive insert uses this sequence's nextval(). +GRANT USAGE ON SEQUENCE public.keepalive_events_id_seq TO anon, service_role; diff --git a/supabase/tests/legacy_table_grants_test.sql b/supabase/tests/legacy_table_grants_test.sql new file mode 100644 index 000000000..1b52b1324 --- /dev/null +++ b/supabase/tests/legacy_table_grants_test.sql @@ -0,0 +1,38 @@ +BEGIN; +SELECT plan(5); + +SELECT ok( + has_table_privilege('authenticated', 'public.flowcharts', 'SELECT') + AND has_table_privilege('authenticated', 'public.flowcharts', 'INSERT') + AND has_table_privilege('authenticated', 'public.flowcharts', 'UPDATE') + AND has_table_privilege('authenticated', 'public.flowcharts', 'DELETE') + AND has_table_privilege('authenticated', 'public.source_folders', 'SELECT') + AND has_table_privilege('authenticated', 'public.source_folders', 'INSERT'), + 'client-owned legacy tables remain writable after a fresh migration replay' +); +SELECT ok( + has_table_privilege('service_role', 'public.promo_codes', 'SELECT') + AND has_table_privilege('service_role', 'public.promo_codes', 'INSERT') + AND has_table_privilege('service_role', 'public.promo_codes', 'UPDATE') + AND has_table_privilege('service_role', 'public.daily_usage_counters', 'INSERT') + AND has_table_privilege('service_role', 'public.daily_usage_counters', 'UPDATE'), + 'server-owned legacy tables remain reachable to the service role' +); +SELECT ok( + has_table_privilege('authenticated', 'public.user_settings', 'SELECT') + AND has_column_privilege('authenticated', 'public.user_settings', 'language', 'UPDATE') + AND NOT has_table_privilege('authenticated', 'public.user_settings', 'INSERT,UPDATE,DELETE'), + 'settings keep their column-limited client permissions' +); +SELECT ok( + has_table_privilege('authenticated', 'public.subscriptions', 'SELECT') + AND has_table_privilege('authenticated', 'public.user_promos', 'SELECT'), + 'subscription status and redeemed promos remain client-readable' +); +SELECT ok( + has_sequence_privilege('anon', 'public.keepalive_events_id_seq', 'USAGE'), + 'anonymous keepalive inserts can generate an event id' +); + +SELECT * FROM finish(); +ROLLBACK; From 4899759190feec931be1d14e257117925a3e059b Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 25 Sep 2026 07:23:21 +0000 Subject: [PATCH 2/3] fix(db): grant signed-in users usage reads --- ...923191320_explicit_legacy_table_grants.sql | 2 +- supabase/tests/legacy_table_grants_test.sql | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql b/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql index e0fcefedb..4af15bc25 100644 --- a/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql +++ b/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql @@ -21,7 +21,7 @@ TO authenticated; GRANT SELECT ON TABLE public.user_roles, public.rate_limits, public.subscriptions, - public.user_promos, public.admin_grants + public.user_promos, public.admin_grants, public.ai_requests TO authenticated; -- Settings keep their existing SELECT and column-limited UPDATE grants; diff --git a/supabase/tests/legacy_table_grants_test.sql b/supabase/tests/legacy_table_grants_test.sql index 1b52b1324..ce0f81fa2 100644 --- a/supabase/tests/legacy_table_grants_test.sql +++ b/supabase/tests/legacy_table_grants_test.sql @@ -1,5 +1,5 @@ BEGIN; -SELECT plan(5); +SELECT plan(6); SELECT ok( has_table_privilege('authenticated', 'public.flowcharts', 'SELECT') @@ -26,13 +26,29 @@ SELECT ok( ); SELECT ok( has_table_privilege('authenticated', 'public.subscriptions', 'SELECT') - AND has_table_privilege('authenticated', 'public.user_promos', 'SELECT'), - 'subscription status and redeemed promos remain client-readable' + AND has_table_privilege('authenticated', 'public.user_promos', 'SELECT') + AND has_table_privilege('authenticated', 'public.ai_requests', 'SELECT'), + 'subscription status, redeemed promos, and usage remain client-readable' ); SELECT ok( has_sequence_privilege('anon', 'public.keepalive_events_id_seq', 'USAGE'), 'anonymous keepalive inserts can generate an event id' ); +INSERT INTO auth.users (id) VALUES + ('f1111111-1111-4111-8111-111111111111'), + ('f2222222-2222-4222-8222-222222222222'); +INSERT INTO public.ai_requests (user_id, feature, provider, model) VALUES + ('f1111111-1111-4111-8111-111111111111', 'ask', 'test', 'test'), + ('f2222222-2222-4222-8222-222222222222', 'ask', 'test', 'test'); +SET LOCAL ROLE authenticated; +SELECT set_config('request.jwt.claim.sub', 'f1111111-1111-4111-8111-111111111111', true); +SELECT is( + (SELECT count(*) FROM public.ai_requests), + 1::bigint, + 'signed-in usage reads see only their own requests' +); +RESET ROLE; + SELECT * FROM finish(); ROLLBACK; From 1b074b71e8515f4c5e8d65f6f8e032e1c7d0ffa0 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 25 Sep 2026 07:48:21 +0000 Subject: [PATCH 3/3] fix(db): preserve server-only global prompts --- .../migrations/20260923191320_explicit_legacy_table_grants.sql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql b/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql index 4af15bc25..a8ef35aa3 100644 --- a/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql +++ b/supabase/migrations/20260923191320_explicit_legacy_table_grants.sql @@ -25,8 +25,7 @@ GRANT SELECT ON TABLE TO authenticated; -- Settings keep their existing SELECT and column-limited UPDATE grants; --- trusted usage and shared items remain server-owned. -GRANT SELECT ON TABLE public.global_prompts TO anon, authenticated; +-- shared items and global prompts remain server-owned. -- The existing anonymous keepalive insert uses this sequence's nextval(). GRANT USAGE ON SEQUENCE public.keepalive_events_id_seq TO anon, service_role;