Alle Fachentitaeten der Betreuung in einem Schema - bewusst komplett, damit spaetere Etappen keine Migration am Fundament brauchen. Kernentscheidungen: - Geldbetraege durchgaengig als BigInt in Cent. Eine Rechnungslegung nach § 1865 BGB darf keine Rundungsfehler erben. - Felder mit Suffix Verschluesselt tragen einen AES-256-GCM-Umschlag. Wo trotzdem gesucht werden muss (IBAN), steht ein HMAC-Blindindex daneben. - Fristen und Stichtage sind @db.Date, nicht DateTime - sonst verschiebt die Zeitzone eine Frist um einen Tag. - Frist.systemSchluessel macht die aus dem Bestellungsdatum abgeleiteten Pflichtfristen idempotent. - Genehmigung.beschlussDokumentId ist die technische Voraussetzung dafuer, dass ein Vorgang ohne Beschluss nicht auf VOLLZOGEN gesetzt werden kann. - DokumentVerknuepfung mit echten Fremdschluesseln statt loser Typ/ID- Spalte: eine Akte muss referenzsicher sein. - Wertstand und Saldostand sind Historien - Werte werden nie ueberschrieben. Dazu zwei SQL-Skripte, die Prisma nicht abbilden kann: - volltextsuche.sql: tsvector-Trigger (deutsch) mit Gewichtung A/B/C, plus Trigram-Indizes fuer unscharfe Absendersuche. - audit-schutz.sql: Trigger, die UPDATE und DELETE auf audit_log verweigern. Das Log ist Nachweis gegenueber Gericht und Miterben, also nur anhaengend - auch gegen psql. Etappe: 1 Phase: Fundament
77 lines
2.8 KiB
PL/PgSQL
77 lines
2.8 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;
|
|
|
|
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;
|