-- search_path für alle eigenen Funktionen festnageln.
--
-- Der Supabase-Linter meldet 34 Funktionen mit „Function Search Path Mutable".
-- Nachgezählt sind das alles SECURITY-INVOKER-Funktionen; die vier
-- SECURITY-DEFINER-Funktionen (is_hr_user, current_hr_user_id,
-- apply_due_pending_changes, fn_track_employee_assignment) setzen den Pfad
-- längst. Deshalb steht im Advisor auch 0 errors.
--
-- Warum das trotzdem behoben wird:
--
-- Der Angriff braucht SECURITY DEFINER. Wer in einem Schema, das im
-- search_path früher liegt, eine eigene Tabelle `employees` anlegt, bringt
-- eine unqualifiziert schreibende Funktion dazu, auf die untergeschobene
-- zuzugreifen — mit den Rechten der Eigentümerin der Funktion. Bei INVOKER
-- läuft alles mit den Rechten der aufrufenden Person, es gibt also nichts zu
-- gewinnen, und die RLS-Policies greifen unverändert.
--
-- Zur Lücke wird die Warnung erst, wenn eine dieser Funktionen später auf
-- SECURITY DEFINER umgestellt wird, etwa weil eine Mutation an RLS vorbei
-- schreiben muss. In dem Moment denkt niemand mehr an den search_path.
-- Einmal festnageln räumt die Falle weg und ändert kein Verhalten.
--
-- `pg_temp` steht ausdrücklich am Ende: ohne die Angabe durchsucht Postgres
-- das temporäre Schema *zuerst*, und dort darf jede Sitzung anlegen, was sie
-- will.

do $$
declare
  v_func record;
  v_count int := 0;
begin
  for v_func in
    select p.oid::regprocedure as signature
    from pg_proc p
    join pg_namespace n on n.oid = p.pronamespace
    where n.nspname = 'public'
      -- Nur Funktionen, keine Prozeduren oder Aggregate.
      and p.prokind = 'f'
      -- Erweiterungen gehören uns nicht: pg_trgm legt show_trgm und show_limit
      -- in public ab. Daran zu drehen bricht bei der nächsten Aktualisierung
      -- der Erweiterung oder wird stillschweigend zurückgesetzt.
      and not exists (
        select 1 from pg_depend d where d.objid = p.oid and d.deptype = 'e'
      )
      -- Bereits gesetzte nicht anfassen: die vier DEFINER-Funktionen stehen
      -- auf `search_path = public` und sollen so bleiben.
      and not exists (
        select 1 from unnest(coalesce(p.proconfig, '{}')) c where c like 'search_path=%'
      )
  loop
    execute format('alter function %s set search_path = public, pg_temp', v_func.signature);
    v_count := v_count + 1;
  end loop;

  raise notice 'search_path festgenagelt für % Funktion(en)', v_count;
end;
$$;

-- Gegenprobe: danach darf in public keine eigene Funktion ohne search_path
-- mehr stehen. Schlägt das an, hat die Schleife oben etwas übersehen — besser
-- hier, als es im Advisor stehen zu lassen.
do $$
declare v_offen int;
begin
  select count(*) into v_offen
  from pg_proc p
  join pg_namespace n on n.oid = p.pronamespace
  where n.nspname = 'public'
    and p.prokind = 'f'
    and not exists (select 1 from pg_depend d where d.objid = p.oid and d.deptype = 'e')
    and not exists (select 1 from unnest(coalesce(p.proconfig, '{}')) c where c like 'search_path=%');

  if v_offen > 0 then
    raise exception 'Es stehen noch % Funktion(en) ohne search_path in public.', v_offen;
  end if;
end;
$$;
