Files
nexarch/archive/migrations/0008_legal_hold_destruction.up.sql
T
sysops e23f514850 RET-03: löschworkflow-und-aufbewahrungssperre-legal-hold
- migrations/0008_legal_hold_destruction: legal_holds (historisiert,
  Partial-Unique-Index gegen doppelte aktive Sperre), destruction_log
  (append-only, per Postgres-Trigger gegen UPDATE/DELETE geschuetzt)
- archive/internal/deletionworkflow: SetLegalHold (Begruendungspflicht),
  ReleaseLegalHold (Aufheben selbst protokolliert, keine Loeschung der
  Zeile), ReleaseExpired (Freigabeprozess active->expired, keine
  Sofortloeschung, Sperre wird respektiert), Destroy (verlangt
  vorherigen expired-Status, prueft Sperre erneut, transaktional mit
  Protokolleintrag)
- 6 Tests, alle Pflichtpruefungen real bestanden (Sperre widersteht
  Loeschversuch, Protokoll real unveraenderlich per Trigger, Aufheben
  real protokolliert)
- Migration real auf dms_tenant_test angewendet

Pruefungen siehe archive/docs/RET-03-PRUEFPROTOKOLL.md
2026-08-30 14:27:21 +02:00

47 lines
2.1 KiB
PL/PgSQL

-- RET-03: Löschworkflow & Aufbewahrungssperre (Legal Hold).
--
-- legal_holds ist historisiert (kein UPDATE der Sperr-Zeile selbst
-- ausser released_at/released_by beim Aufheben) — jede Sperre und jede
-- Aufhebung bleibt dauerhaft nachvollziehbar (Akzeptanzkriterium 3:
-- Aufheben ist selbst protokolliert). Ein Partial-Unique-Index erlaubt
-- je Objekt hoechstens EINE AKTIVE Sperre gleichzeitig.
CREATE TABLE IF NOT EXISTS legal_holds (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
retention_object_id UUID NOT NULL REFERENCES retention_objects(id) ON DELETE CASCADE,
reason TEXT NOT NULL,
set_by TEXT NOT NULL,
set_at TIMESTAMPTZ NOT NULL DEFAULT now(),
released_at TIMESTAMPTZ,
released_by TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_legal_holds_active
ON legal_holds (retention_object_id) WHERE released_at IS NULL;
-- destruction_log ist APPEND-ONLY, real durchgesetzt ueber Trigger (nicht
-- nur Anwendungscode) — Akzeptanzkriterium 3 / Pflichtpruefung 2 verlangt
-- ausdruecklich "DB-Constraint oder Trigger".
CREATE TABLE IF NOT EXISTS destruction_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
retention_object_id UUID NOT NULL REFERENCES retention_objects(id) ON DELETE RESTRICT,
object_type TEXT NOT NULL,
object_reference TEXT NOT NULL,
destroyed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
destroyed_by TEXT NOT NULL
);
CREATE OR REPLACE FUNCTION destruction_log_immutable() RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'destruction_log ist unveraenderlich (RET-03) - % auf id=% nicht erlaubt', TG_OP, OLD.id;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_destruction_log_no_update ON destruction_log;
CREATE TRIGGER trg_destruction_log_no_update
BEFORE UPDATE ON destruction_log
FOR EACH ROW EXECUTE FUNCTION destruction_log_immutable();
DROP TRIGGER IF EXISTS trg_destruction_log_no_delete ON destruction_log;
CREATE TRIGGER trg_destruction_log_no_delete
BEFORE DELETE ON destruction_log
FOR EACH ROW EXECUTE FUNCTION destruction_log_immutable();