Files
gongxue-base/supabase/migrations/202607110001_data_api_acl_rls_hardening.sql
2026-07-12 19:26:57 +08:00

103 lines
4.1 KiB
PL/PgSQL

-- Taro uses Supabase for Auth only. Business data is served by apps/api, so
-- public-schema Data API access must stay closed until a table/view/RPC has a
-- dedicated authorization model and an explicit grant migration.
revoke all privileges on all tables in schema public from public, anon, authenticated;
revoke all privileges on all sequences in schema public from public, anon, authenticated;
revoke all privileges on all functions in schema public from public, anon, authenticated;
revoke create on schema public from public, anon, authenticated;
-- A standard Supabase migration user cannot change ACLs on public extension
-- functions owned by supabase_admin. The privileged runtime-role bootstrap must
-- seal those base-image functions before migrations; never accept warning-only
-- REVOKE output as proof that the Data API RPC surface is closed.
do $$
begin
if exists (
select 1
from pg_proc function_row
join pg_namespace namespace on namespace.oid = function_row.pronamespace
cross join (values ('anon'), ('authenticated')) requested_role(role_name)
join pg_roles client_role on client_role.rolname = requested_role.role_name
where namespace.nspname = 'public'
and has_function_privilege(client_role.oid, function_row.oid, 'EXECUTE')
) then
raise exception
'public functions remain executable by anon/authenticated; run the privileged backend runtime role bootstrap before migrations';
end if;
end
$$;
do $$
declare
owner_name name;
begin
for owner_name in
select distinct owner_role.rolname
from (
select c.relowner as owner_oid
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind in ('r', 'p', 'S', 'v', 'm', 'f')
union
select p.proowner as owner_oid
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
) owners
join pg_roles owner_role on owner_role.oid = owners.owner_oid
where owner_role.rolname = current_user
or pg_has_role(current_user, owner_role.oid, 'MEMBER')
loop
execute format(
'alter default privileges for role %I in schema public revoke all privileges on tables from public, anon, authenticated',
owner_name
);
execute format(
'alter default privileges for role %I in schema public revoke all privileges on sequences from public, anon, authenticated',
owner_name
);
-- PostgreSQL grants PUBLIC EXECUTE to new functions through the global
-- default ACL. A schema-local revoke cannot subtract that global default.
execute format(
'alter default privileges for role %I revoke execute on functions from public, anon, authenticated',
owner_name
);
end loop;
end $$;
-- A platform role claim can outlive a staff-status change. Resolve platform
-- authority from the active database identity on every RLS evaluation instead.
create or replace function app.is_platform_admin()
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
select app.current_role() = 'service_role'
or exists (
select 1
from public.platform_users u
where u.auth_user_id = (select auth.uid())
and u.primary_role = 'platform_admin'
and u.status = 'active'
)
$$;
revoke all on function app.is_platform_admin() from public, anon, authenticated;
grant execute on function app.is_platform_admin() to anon, authenticated, service_role;
-- The original FOR ALL owner policy allowed a signed-in user to update their
-- own role, status and platform_permissions. Keep only a future-safe self-read
-- policy; no client table grant is provided by this migration.
drop policy if exists platform_admin_platform_users on public.platform_users;
drop policy if exists platform_users_self_read on public.platform_users;
create policy platform_users_self_read on public.platform_users
for select
to authenticated
using (auth_user_id = (select auth.uid()));
comment on policy platform_users_self_read on public.platform_users is
'Self-read policy only. anon/authenticated have no table grant; privileged updates must use apps/api.';