From e1b69fb022964c95660d9591cac8a677b7e15cbf Mon Sep 17 00:00:00 2001 From: Andrei Laas Date: Tue, 8 Sep 2026 17:25:32 +0200 Subject: [PATCH] Festes Enddatum des Passwortpfads zuruecknehmen --- ..._passwort_gueltig_bis_ohne_feste_frist.sql | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 db/migrations/20260908140000_passwort_gueltig_bis_ohne_feste_frist.sql diff --git a/db/migrations/20260908140000_passwort_gueltig_bis_ohne_feste_frist.sql b/db/migrations/20260908140000_passwort_gueltig_bis_ohne_feste_frist.sql new file mode 100644 index 0000000..4b3dc0f --- /dev/null +++ b/db/migrations/20260908140000_passwort_gueltig_bis_ohne_feste_frist.sql @@ -0,0 +1,164 @@ +-- Das feste Enddatum des Passwortpfads fällt weg. +-- +-- ═══ Was hier zurückgenommen wird ═══ +-- +-- 20260908120000 hat den Passwortpfad mit einem eingebauten Ende versehen: +-- `gueltig_bis` mit Vorgabewert 08.10.2026 und einer Prüfbedingung, die keinen +-- späteren Wert zuliess. Der Gedanke war, dass die Übergangslösung von selbst +-- aufhört, ohne dass sich jemand erinnern muss. +-- +-- Die Entscheidung ist bewusst revidiert: was hier entsteht, ist ein +-- Testaufbau. Echte Personaldaten stehen nicht im System und sind auch nicht +-- kurzfristig vorgesehen, die Anwendung soll später ohnehin nicht mehr aus dem +-- Internet erreichbar sein, und ein Stichtag im Schema, der mitten in einen +-- laufenden Test fällt, kostet mehr, als er in dieser Lage einbringt. Ein +-- abgelaufener Zugang sieht für die Testerin aus wie ein Fehler in der +-- Anwendung, nicht wie eine Schutzmassnahme. +-- +-- ═══ Was bleibt ═══ +-- +-- Die **Spalte** bleibt, und die Prüfung in app_passwort_pruefen() bleibt Wort +-- für Wort stehen. Nur steht in der Spalte künftig nichts: +-- +-- if current_date > k.gueltig_bis then return null; end if; +-- +-- Bei `gueltig_bis is null` ergibt der Vergleich „unbekannt", und ein `if` auf +-- unbekannt verzweigt nicht — die Zeile ist also wirkungslos, ohne dass etwas +-- daran geändert werden müsste. Sobald ein Ende gebraucht wird, genügt ein +-- update; eine Migration braucht es dafür nicht mehr: +-- +-- update app_passwoerter set gueltig_bis = current_date; -- ab morgen zu +-- update app_passwoerter set gueltig_bis = null; -- wieder offen +-- +-- Der Abschaltknopf bleibt damit erhalten, er löst nur nicht mehr von selbst +-- aus. Dass der Passwortpfad eine Übergangslösung ist und vor echten Daten +-- verschwindet, ist ab jetzt eine Entscheidung und keine Zusicherung des +-- Schemas — so festgehalten in CLAUDE.md. +-- +-- Die vier übrigen Schranken sind unberührt: Zwang zum Wechsel beim ersten +-- Mal, sieben Tage Frist für einen nie benutzten Zugang, Sperre nach fünf +-- Fehlversuchen, und die Freischaltung je Person. + +-- ── 1. Die Prüfbedingung weg ────────────────────────────────────────── +alter table app_passwoerter drop constraint if exists chk_passwort_pfad_endet; + +-- ── 2. Die Spalte leer und leerbar ──────────────────────────────────── +-- +-- Erst den Vorgabewert weg, dann die Pflicht: in dieser Reihenfolge kann keine +-- Zeile mehr entstehen, die den alten Stichtag trägt, während die Migration +-- noch läuft. +alter table app_passwoerter alter column gueltig_bis drop default; +alter table app_passwoerter alter column gueltig_bis drop not null; + +-- Der Bestand: heute sind das null Zeilen (es wurde noch kein Zugang mit +-- Passwort angelegt), aber die Anweisung gehört trotzdem hierher — dieselbe +-- Migration läuft später auf einer Datenbank, in der es welche gibt. +update app_passwoerter set gueltig_bis = null where gueltig_bis is not null; + +comment on table app_passwoerter is + 'Anmeldung mit Passwort — Übergangslösung für die Testphase. gueltig_bis ist leer und begrenzt damit nichts; ein Datum darin schaltet den Zugang ab dem Folgetag ab.'; + +comment on column app_passwoerter.gueltig_bis is + 'Letzter Tag, an dem dieses Passwort angenommen wird. Leer heisst unbefristet — der Abschaltknopf, nicht die Frist.'; + +-- ── 3. Den Kommentar in der Funktion nachziehen ─────────────────────── +-- +-- Im Rumpf von app_passwort_pruefen() steht an dieser Verzweigung „Läuft ohne +-- Zutun ab". Das stimmt seit dieser Migration nicht mehr, und ein Kommentar, +-- der eine Zusicherung beschreibt, die es nicht gibt, ist schlimmer als keiner +-- — er wird beim nächsten Lesen für den Stand gehalten. +-- +-- Ersetzt wird nur der Kommentartext, über pg_get_functiondef und replace(). +-- Vorbild ist 20260805110000: der Rumpf bleibt dabei nachweislich unangetastet, +-- weil er nicht abgetippt wird. Findet sich der Text nicht, bricht die +-- Migration ab, statt still nichts zu tun. +do $$ +declare + v_def text; + v_alt text := 'Das Ende des Passwortpfads. Läuft ohne Zutun ab; siehe Kopf der Datei.'; + v_neu text := 'Das Ende des Passwortpfads. Greift nur, wenn gueltig_bis gesetzt ist — ' + || 'bei null ist der Vergleich unbekannt und die Verzweigung wirkungslos ' + || '(siehe 20260908140000).'; +begin + select pg_get_functiondef(p.oid) into v_def + from pg_proc p + join pg_namespace n on n.oid = p.pronamespace + where n.nspname = 'public' + and p.proname = 'app_passwort_pruefen'; + + if v_def is null then + raise exception 'app_passwort_pruefen() nicht gefunden — Abbruch.'; + end if; + if position(v_alt in v_def) = 0 then + raise exception 'Der erwartete Kommentar steht nicht in app_passwort_pruefen(). Der Rumpf wurde zwischenzeitlich geändert; bitte von Hand nachsehen.'; + end if; + + execute replace(v_def, v_alt, v_neu); +end; +$$; + +comment on function app_passwort_pruefen(text, text) is + 'Prüft E-Mail und Passwort und liefert app_users.id oder null. Nennt nie einen Grund. gueltig_bis wird beachtet, ist aber standardmässig leer.'; + +-- ── 4. Gegenprobe ───────────────────────────────────────────────────── +-- +-- Zwei Fragen, und die zweite ist die eigentliche: verhält sich die Anmeldung +-- bei leerem gueltig_bis so, wie diese Migration behauptet? Der Vergleich mit +-- null ist genau die Art Annahme, die stimmt, bis sie es nicht tut. +do $$ +declare + v_id uuid := gen_random_uuid(); + v_treffer uuid; +begin + -- 1. Die Prüfbedingung ist weg, und ein weit entferntes Datum geht durch. + if exists ( + select 1 from pg_constraint + where conrelid = 'app_passwoerter'::regclass and conname = 'chk_passwort_pfad_endet' + ) then + raise exception 'chk_passwort_pfad_endet besteht weiterhin — Abbruch.'; + end if; + + if exists ( + select 1 from information_schema.columns + where table_schema = 'public' and table_name = 'app_passwoerter' + and column_name = 'gueltig_bis' + and (is_nullable = 'NO' or column_default is not null) + ) then + raise exception 'gueltig_bis ist weiterhin pflichtig oder trägt einen Vorgabewert — Abbruch.'; + end if; + + if exists (select 1 from app_passwoerter where gueltig_bis is not null) then + raise exception 'Es stehen noch Zeilen mit einem Enddatum in app_passwoerter — Abbruch.'; + end if; + + -- 2. Eine Probeperson mit Passwort. Der Hash mit gen_salt('bf', 4) statt 12: + -- crypt() liest die Stärke aus dem gespeicherten Hash, die Prüfung läuft + -- also genauso — nur schneller, und eine Migration soll nicht rechnen. + insert into app_users (id, external_id, email, full_name) + values (v_id, 'probe:' || v_id::text, 'probe-passwort@example.invalid', 'Probe'); + insert into app_passwoerter (user_id, hash, muss_wechseln, gueltig_bis) + values (v_id, crypt('Nur-fuer-die-Probe1', gen_salt('bf', 4)), true, current_date - 1); + + -- 2a. Mit abgelaufenem Datum: abgewiesen. + v_treffer := app_passwort_pruefen('probe-passwort@example.invalid', 'Nur-fuer-die-Probe1'); + if v_treffer is not null then + raise exception 'Ein abgelaufenes gueltig_bis wird nicht mehr beachtet — die Abschaltung wäre wirkungslos. Abbruch.'; + end if; + + -- 2b. Ohne Datum: angenommen. Das ist der Zustand, den diese Migration + -- herstellt, und die Aussage, auf die sich alles Weitere stützt. + update app_passwoerter set gueltig_bis = null where user_id = v_id; + v_treffer := app_passwort_pruefen('probe-passwort@example.invalid', 'Nur-fuer-die-Probe1'); + if v_treffer is distinct from v_id then + raise exception 'Bei leerem gueltig_bis kommt keine Anmeldung zustande — Abbruch.'; + end if; + + -- Aufräumen. Der erfolgreiche Versuch oben hat eine Protokollzeile + -- geschrieben; sie muss mit weg, sonst scheitert das Löschen des Kontos am + -- Fremdschlüssel — und stehenbleiben darf sie erst recht nicht, sie gehört + -- zu einer Person, die es nie gab. Eng begrenzt auf diese eine Kennung. + delete from audit_log where actor_user_id = v_id; + delete from app_passwoerter where user_id = v_id; + delete from app_users where id = v_id; +end; +$$;