66a6c96a5c
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.
101 lines
3.3 KiB
PL/PgSQL
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;
|