fix(admin): preserve revoked owners and 503 status
Owner recovery lived in the identity-only migration path and could clear historical revocations on conflict. Admin layout also rethrew authorization 503s, turning service-unavailable failures into 500 responses.\n\nMove recovery behind the RBAC migration sequence, exclude revoked candidates without mutating their history, and terminate layout redirects at a no-store 503 route.
This commit is contained in:
@@ -1,88 +0,0 @@
|
||||
begin;
|
||||
|
||||
do $$
|
||||
declare
|
||||
v_active_owner_count integer;
|
||||
v_candidate_count integer;
|
||||
v_candidate_id uuid;
|
||||
v_owner_role_id uuid;
|
||||
begin
|
||||
-- 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
|
||||
where (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 update set
|
||||
revoked_at = null,
|
||||
revoked_by = null,
|
||||
updated_at = now();
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user