Files
Jyotisha/frontend/db/migrations/20260807010000_recover_initial_admin_owner.sql
T
Jesse_Chen 66a6c96a5c fix(admin): fail safe across migration and auth boundaries
The recovery migration crossed the identity and RBAC ledgers without guarding schema prerequisites, while unknown configuration, provider, and database failures escaped the admin authorization boundary as 500s. Keep recovery in the DB ledger with explicit prerequisite no-ops, and sanitize unknown authorization failures to the existing 503 path.
2026-08-07 12:08:58 +08:00

101 lines
3.3 KiB
PL/PgSQL

begin;
do $$
declare
v_active_owner_count integer;
v_candidate_count integer;
v_candidate_id uuid;
v_owner_role_id uuid;
begin
-- This migration also belongs to the identity-only db migration ledger. When
-- the Supabase RBAC schema is not part of that migration flow, record a safe
-- no-op without referencing any missing relation or trigger function.
if to_regclass('public.admin_users') is null
or to_regclass('public.admin_roles') is null
or to_regclass('public.admin_user_roles') is null
or to_regclass('identity.users') is null
or to_regclass('auth.users') is null
or to_regprocedure('public.admin_permission_keys(uuid)') is null
or to_regprocedure('public.assert_active_admin_owner_exists()') is null then
return;
end if;
-- Serialize recovery with the last-Owner protection used by the RBAC functions.
perform pg_catalog.pg_advisory_xact_lock(1096040772, 1);
select count(*)
into v_active_owner_count
from public.admin_users au
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
join public.admin_roles ar on ar.id = aur.role_id
where au.revoked_at is null
and ar.code = 'owner';
-- Once an active Owner exists, role management remains the only authority.
if v_active_owner_count > 0 then
return;
end if;
-- Keep the candidate set stable while recovery decides and writes.
lock table identity.users, auth.users in share mode;
-- A pristine database has no account to recover yet. Any populated system
-- without an Owner must have exactly one active identity-admin candidate.
if not exists (select 1 from identity.users)
and not exists (select 1 from auth.users) then
return;
end if;
select count(*), (array_agg(u.id order by u.id))[1]
into v_candidate_count, v_candidate_id
from identity.users u
join auth.users a on a.id = u.id
left join public.admin_users existing_admin on existing_admin.user_id = u.id
where (existing_admin.user_id is null or existing_admin.revoked_at is null)
and (not u.banned or (u.ban_expires is not null and u.ban_expires <= clock_timestamp()))
and exists (
select 1
from unnest(string_to_array(u.role, ',')) as role_part(value)
where btrim(role_part.value) = 'admin'
);
if v_candidate_count <> 1 then
raise exception 'admin_owner_recovery_requires_exactly_one_active_identity_admin: found %',
v_candidate_count
using errcode = '23514';
end if;
select id
into v_owner_role_id
from public.admin_roles
where code = 'owner';
if v_owner_role_id is null then
raise exception 'admin_owner_recovery_owner_role_missing'
using errcode = '23514';
end if;
insert into public.admin_users (user_id, created_by)
values (v_candidate_id, v_candidate_id)
on conflict on constraint admin_users_pkey do nothing;
insert into public.admin_user_roles (admin_user_id, role_id, assigned_by)
values (v_candidate_id, v_owner_role_id, v_candidate_id)
on conflict do nothing;
if not exists (
select 1
from public.admin_users au
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
where au.user_id = v_candidate_id
and au.revoked_at is null
and aur.role_id = v_owner_role_id
) then
raise exception 'admin_owner_recovery_failed'
using errcode = '23514';
end if;
end;
$$;
commit;