-- ============================================================ -- Hotfix template: lock down role / email columns on a "profiles"-style table. -- -- Apply this when `audit.js` reports: -- 🚨 policy:. — UPDATE policy lacks WITH CHECK on role/email -- -- HOW TO USE: -- 1. Replace `public.profiles` below with the affected table name if -- different (typically it really is `profiles` / `users` / `members`). -- 2. Confirm the column name is `role` (the rest of the policy assumes it). -- 3. Run via: node ~/.openclaw/workspace/projects/oganim/deploy/run-migration.cjs -- ...or the equivalent migration tool for the project. -- -- The migration is idempotent. Safe to re-run. -- ============================================================ reset role; -- 1. Replace the existing self-update policy with one that locks role + email. drop policy if exists profiles_self_update on public.profiles; create policy profiles_self_update on public.profiles for update using (auth.uid() = id) with check ( auth.uid() = id and role is not distinct from (select role from public.profiles where id = auth.uid()) and email is not distinct from (select email from public.profiles where id = auth.uid()) ); -- 2. Belt-and-suspenders DB trigger: even if the policy is later changed by -- mistake, this trigger rejects role mutations from any non-staff caller. create or replace function public.tg_profile_role_lock() returns trigger language plpgsql security definer set search_path = public as $$ declare caller_is_staff boolean; begin -- service_role / postgres bypass: auth.uid() is null for those. if auth.uid() is null then return NEW; end if; if NEW.role is distinct from OLD.role then select coalesce( (select role in ('admin','agent') from public.profiles where id = auth.uid()), false) into caller_is_staff; if not caller_is_staff then raise exception 'profile.role can only be changed by staff (caller=%)', auth.uid() using errcode = '42501'; end if; end if; return NEW; end; $$; drop trigger if exists trg_profile_role_lock on public.profiles; create trigger trg_profile_role_lock before update of role on public.profiles for each row execute function public.tg_profile_role_lock();