Zwei Fehler, beide erst im Betrieb sichtbar. 1. Schritt 3 der Ersteinrichtung meldete "Bitte pruefe die markierten Felder" - und markierte keines. Man kam dort nicht weiter. Das kurze Formular schickt neun Felder, das vollstaendige unter Stammdaten alle. Beide gehen durch dasselbe Schema, und die optionalen Felder waren zwar nullable, aber nicht optional: Ein Feld, das gar nicht abgeschickt wurde, ist undefined, nicht null. Beanstandet wurden also zwei Dutzend Felder, die auf dem Bildschirm nie standen. Damit das nicht wiederkommt, unterscheidet die Aktion jetzt zwischen Eingabefehler und Formular-Schema-Bruch: Beanstandet die Pruefung ein Feld, das nicht abgeschickt wurde, sagt sie das ausdruecklich und schreibt es ins Serverlog, statt es dem Nutzer als Eingabefehler unterzuschieben, den er nirgends finden kann. 2. Eine betreute Person liess sich nicht endgueltig loeschen. Postgres setzt beim Loeschen den Verweis im Audit-Log auf NULL, und genau dieses UPDATE hat der Unveraenderlichkeits-Trigger abgewiesen. Damit scheiterte auch das Leeren des Papierkorbs nach 30 Tagen. Der Trigger erlaubt jetzt genau eine Ausnahme: das Nullen der beiden Fremdschluessel. Jede andere Aenderung bleibt verboten, auch das Setzen eines Fremdschluessels auf einen anderen Wert. Der Nachweis bleibt vollstaendig - es verschwindet nur der Verweis auf einen Datensatz, den es nicht mehr gibt. Geprueft: sieben neue Tests gegen die tatsaechlichen Formularinhalte, der Ablauf einmal komplett im Browser bis zum Dashboard, und der Trigger gegen eine laufende Datenbank - Loeschen gelingt, Faelschen scheitert. Etappe: 8 Phase: Haertung
113 lines
4.7 KiB
PL/PgSQL
113 lines
4.7 KiB
PL/PgSQL
-- =============================================================================
|
|
-- Audit-Log unveraenderlich machen
|
|
-- =============================================================================
|
|
--
|
|
-- Aufruf: npm run db:audit-schutz
|
|
--
|
|
-- Das Audit-Log ist mein Nachweis gegenueber Betreuungsgericht und moeglichen
|
|
-- Miterben. Es muss deshalb nur anhaengend sein: UPDATE und DELETE werden auf
|
|
-- Datenbankebene verweigert - auch dann, wenn die Anwendung selbst es
|
|
-- versuchen wuerde oder jemand mit psql direkt an der Datenbank sitzt.
|
|
--
|
|
-- Einzige Ausnahme: das Aufraeumen von Eintraegen, die aelter als die
|
|
-- Aufbewahrungsfrist sind. Dafuer gibt es die Funktion
|
|
-- audit_log_aufraeumen(intervall), die den Schutz kontrolliert umgeht.
|
|
--
|
|
-- Das Skript ist idempotent.
|
|
-- =============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION audit_log_ist_unveraenderlich()
|
|
RETURNS trigger AS $$
|
|
BEGIN
|
|
-- Die Aufraeum-Funktion setzt diese Sitzungsvariable, um gezielt loeschen
|
|
-- zu duerfen. Alles andere wird abgewiesen.
|
|
IF current_setting('betreuung.audit_aufraeumen', true) = 'ja' THEN
|
|
RETURN OLD;
|
|
END IF;
|
|
|
|
/*
|
|
* Eine einzige Ausnahme beim UPDATE: das Nullen der beiden
|
|
* Fremdschluessel.
|
|
*
|
|
* Wird eine betreute Person oder ein Benutzer endgueltig geloescht
|
|
* (Papierkorb nach 30 Tagen), setzt Postgres wegen ON DELETE SET NULL
|
|
* die Spalten betreuteId bzw. userId auf NULL. Ohne diese Ausnahme
|
|
* scheitert das Loeschen - und der Papierkorb liesse sich nie leeren.
|
|
*
|
|
* Der Nachweis bleibt dabei vollstaendig: Zeitpunkt, Aktion, Entitaet,
|
|
* Bezeichnung und die verschluesselte Aenderung sind unberuehrt, ebenso
|
|
* die Momentaufnahmen userEmail und userName. Es verschwindet nur der
|
|
* Verweis auf einen Datensatz, den es nicht mehr gibt.
|
|
*
|
|
* Jede andere Aenderung wird abgewiesen - auch das Zurueckschreiben
|
|
* eines Fremdschluessels auf einen anderen Wert.
|
|
*/
|
|
IF TG_OP = 'UPDATE'
|
|
AND NEW.id = OLD.id
|
|
AND NEW.zeitpunkt = OLD.zeitpunkt
|
|
AND NEW.aktion = OLD.aktion
|
|
AND NEW.entitaet = OLD.entitaet
|
|
AND NEW."entitaetId" IS NOT DISTINCT FROM OLD."entitaetId"
|
|
AND NEW.bezeichnung IS NOT DISTINCT FROM OLD.bezeichnung
|
|
AND NEW."userEmail" IS NOT DISTINCT FROM OLD."userEmail"
|
|
AND NEW."userName" IS NOT DISTINCT FROM OLD."userName"
|
|
AND NEW."aenderungVerschluesselt" IS NOT DISTINCT FROM OLD."aenderungVerschluesselt"
|
|
AND NEW."ipHash" IS NOT DISTINCT FROM OLD."ipHash"
|
|
AND NEW."userAgent" IS NOT DISTINCT FROM OLD."userAgent"
|
|
-- Die beiden Fremdschluessel duerfen sich aendern, aber NUR nach NULL.
|
|
AND (NEW."betreuteId" IS NULL OR NEW."betreuteId" IS NOT DISTINCT FROM OLD."betreuteId")
|
|
AND (NEW."userId" IS NULL OR NEW."userId" IS NOT DISTINCT FROM OLD."userId")
|
|
THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
RAISE EXCEPTION
|
|
'Das Audit-Log ist unveraenderlich. % auf audit_log ist nicht zulaessig.',
|
|
TG_OP
|
|
USING ERRCODE = 'insufficient_privilege';
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS audit_log_kein_update ON audit_log;
|
|
CREATE TRIGGER audit_log_kein_update
|
|
BEFORE UPDATE ON audit_log
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION audit_log_ist_unveraenderlich();
|
|
|
|
DROP TRIGGER IF EXISTS audit_log_kein_delete ON audit_log;
|
|
CREATE TRIGGER audit_log_kein_delete
|
|
BEFORE DELETE ON audit_log
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION audit_log_ist_unveraenderlich();
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Kontrolliertes Aufraeumen alter Eintraege.
|
|
-- Beispiel: SELECT audit_log_aufraeumen('10 years');
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION audit_log_aufraeumen(aufbewahrung interval)
|
|
RETURNS bigint AS $$
|
|
DECLARE
|
|
geloescht bigint;
|
|
BEGIN
|
|
PERFORM set_config('betreuung.audit_aufraeumen', 'ja', true);
|
|
DELETE FROM audit_log WHERE zeitpunkt < now() - aufbewahrung;
|
|
GET DIAGNOSTICS geloescht = ROW_COUNT;
|
|
PERFORM set_config('betreuung.audit_aufraeumen', 'nein', true);
|
|
RETURN geloescht;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Aufraeumen der Login-Versuche (Rate-Limiting-Historie, 30 Tage genuegen).
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION login_versuche_aufraeumen()
|
|
RETURNS bigint AS $$
|
|
DECLARE
|
|
geloescht bigint;
|
|
BEGIN
|
|
DELETE FROM login_versuch WHERE zeitpunkt < now() - interval '30 days';
|
|
GET DIAGNOSTICS geloescht = ROW_COUNT;
|
|
RETURN geloescht;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|