Die Migration ist an ihrer eigenen Pruefung gescheitert:
FEHLGESCHLAGEN 20260924220000_reporting_lines_ohne_zeilenschutz.sql
Nicht berechtigt: nur aktive HR-Benutzer:innen duerfen diese Aktion
ausfuehren.
Die Pruefung verglich die Zeilenzahl der neuen Funktion mit der Zahl der
laufenden Besetzungen -- und rief sie dafuer auf. Die Funktion verlangt
seit dieser Migration HR-Rechte, die Migration selbst laeuft aber als
Administrator ohne app.user_id. require_hr_admin() hat sie also zu Recht
abgewiesen.
Nichts wurde eingespielt, die Transaktion ist vollstaendig
zurueckgerollt.
Jetzt wird der Koerper gelesen statt ausgefuehrt: geprueft wird, dass
require_hr_admin, security definer und der search_path stehen und dass
die Bestandteile der Abfrage vollzaehlig uebertragen sind. Eine
Selbstpruefung darf nichts aufrufen, was einen angemeldeten
Anwendungsbenutzer voraussetzt.
Die Akte brauchte 1,3 bis 1,5 s, davon ueber 1,1 s in dieser Funktion.
Als postgres -- am Zeilenschutz vorbei -- kostet dieselbe Funktion 60 ms.
als postgres 60 ms
als alpenwerk_app, vor 20260924200000 1457 ms
als alpenwerk_app, danach 1153 ms
Die vorige Migration fasste die Policies so, dass is_hr_user() je Abfrage
einmal laeuft. Das half um ein Fuenftel -- die Pruefung war also nicht der
Hauptposten.
Die Teile der Funktion sind einzeln billig, unter Zeilenschutz gemessen:
Besetzungen 12 ms, Vorfahrenkette 7 ms, Leitungen 9 ms. Zusammen 28 ms,
die Funktion daraus 1153. Die Kosten entstehen beim Zusammensetzen: unter
Zeilenschutz wird aus jeder Tabellenreferenz eine Unterabfrage mit
Sicherheitsschranke, und der Planer verliert die Freiheit, die
Zwischenmengen einmal zu berechnen -- die beiden Unterabfragen am Ende
laufen dann fuer jede der 784 Zeilen erneut.
Jetzt security definer mit require_hr_admin() in der ersten Zeile. Das ist
dieselbe Bedingung, die in den Policies aller vier gelesenen Tabellen
steht -- require_hr_admin() ist woertlich "if not is_hr_user() then
raise". Sie wird einmal gestellt statt hunderttausendfach. Wer Daten
bekommt, aendert sich nicht; eine unberechtigte Anfrage bekommt jetzt eine
Meldung statt einer leeren Antwort.
Der Preis: diese Funktion liest die vier Tabellen am Zeilenschutz vorbei.
Kaeme je eine feinere Rechteordnung, muesste sie eigens nachgezogen
werden. Mit dem Eigentuemer besprochen -- solche Rollen sind nicht
vorgesehen.
Der Koerper ist unveraendert aus 20260727120100, der einzigen textlichen
Fassung. Geaendert sind Sprache, security definer, search_path und die
erste Zeile.