Festes Enddatum des Passwortpfads zuruecknehmen
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m38s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m7s

This commit is contained in:
2026-09-08 17:25:32 +02:00
parent c6cff9656e
commit e1b69fb022

View File

@@ -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;
$$;