Files
PolyWeather/scripts/supabase/production_remediation_20260529.sql
T
2026-05-29 18:40:29 +08:00

139 lines
4.8 KiB
SQL

-- PolyWeather production remediation from Supabase MCP diagnostics.
-- Intended for Supabase SQL Editor or a privileged database session.
-- The Supabase MCP session available to Codex was read-only, so this file
-- contains the exact write-side changes to run with project write access.
-- 1) Restrict SECURITY DEFINER functions from public RPC execution.
-- These functions are used as event/row triggers and should not be callable
-- directly by anon/authenticated clients unless intentionally exposed.
revoke execute on function public.rls_auto_enable() from public, anon, authenticated;
revoke execute on function public.sync_profile_from_auth() from public, anon, authenticated;
-- 2) Remove broad direct public-table privileges from client roles.
-- RLS currently blocks row access because no policies exist, but these grants
-- create a large blast radius if a permissive policy is added later.
revoke all privileges on all tables in schema public from anon, authenticated;
revoke all privileges on all sequences in schema public from anon, authenticated;
-- Keep future objects created by this role from inheriting broad access.
alter default privileges in schema public revoke all on tables from anon, authenticated;
alter default privileges in schema public revoke all on sequences from anon, authenticated;
-- 3) Add missing foreign-key helper indexes reported by Supabase advisor.
create index if not exists idx_entitlement_events_user_id
on public.entitlement_events(user_id);
create index if not exists idx_payments_user_id
on public.payments(user_id);
create index if not exists idx_wallet_link_challenges_user_id
on public.wallet_link_challenges(user_id);
-- 4) Normalize stale business state observed in read-only diagnostics.
update public.subscriptions
set status = 'expired',
updated_at = now()
where status = 'active'
and expires_at <= now();
update public.payment_intents
set status = 'expired',
updated_at = now()
where status in ('created', 'submitted')
and expires_at <= now();
delete from public.wallet_link_challenges
where consumed_at is null
and expires_at <= now();
-- Backfill profiles for auth users created while the auth trigger was absent
-- or failed. Existing profiles are not modified.
insert into public.profiles (id, email)
select u.id, coalesce(u.email, '')
from auth.users u
left join public.profiles p on p.id = u.id
where p.id is null;
analyze public.entitlement_events;
analyze public.payments;
analyze public.wallet_link_challenges;
analyze public.subscriptions;
analyze public.payment_intents;
analyze public.profiles;
-- 5) Verification queries. Expected after this script:
-- - exposed_security_definer_functions = 0
-- - public_client_table_grants = 0
-- - missing_public_fk_indexes = 0
-- - active_expired_subscriptions = 0
-- - open_expired_payment_intents = 0
-- - expired_unconsumed_wallet_challenges = 0
-- - auth_users_without_profile = 0
select 'exposed_security_definer_functions' as check_name,
count(*)::bigint as count
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
and p.proname in ('rls_auto_enable', 'sync_profile_from_auth')
and p.prosecdef
and (
has_function_privilege('anon', p.oid, 'execute')
or has_function_privilege('authenticated', p.oid, 'execute')
)
union all
select 'public_client_table_grants',
count(*)::bigint
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
cross join lateral aclexplode(coalesce(c.relacl, acldefault('r', c.relowner))) a
left join pg_roles grantee on grantee.oid = a.grantee
where n.nspname = 'public'
and c.relkind = 'r'
and coalesce(grantee.rolname, 'PUBLIC') in ('anon', 'authenticated')
union all
select 'missing_public_fk_indexes',
count(*)::bigint
from (
select con.conrelid, con.conkey
from pg_constraint con
join pg_namespace n on n.oid = con.connamespace
where con.contype = 'f'
and n.nspname = 'public'
and not exists (
select 1
from pg_index i
where i.indrelid = con.conrelid
and i.indisvalid
and i.indisready
and (
select array_agg(k order by ord)
from unnest(i.indkey) with ordinality as x(k, ord)
where ord <= array_length(con.conkey, 1)
) = con.conkey
)
) missing_fks
union all
select 'active_expired_subscriptions',
count(*)::bigint
from public.subscriptions
where status = 'active'
and expires_at <= now()
union all
select 'open_expired_payment_intents',
count(*)::bigint
from public.payment_intents
where status in ('created', 'submitted')
and expires_at <= now()
union all
select 'expired_unconsumed_wallet_challenges',
count(*)::bigint
from public.wallet_link_challenges
where consumed_at is null
and expires_at <= now()
union all
select 'auth_users_without_profile',
count(*)::bigint
from auth.users u
left join public.profiles p on p.id = u.id
where p.id is null;