CMP-06: vier-augen-freigabe-fuer-loeschungen

- migrations/0010_deletion_requests: speichert nur den Token-Hash,
  nie das Token selbst
- archive/internal/deletionapproval: RequestDeletion (einmaliges
  Klartext-Token), ConfirmAndExecute (FOR UPDATE-Lock, andere Person
  als requester, zeitkonstanter Tokenvergleich via subtle.ConstantTimeCompare,
  ruft danach genau einmal deletionworkflow.Destroy (RET-03) auf)
- 4 Tests real bestanden, inkl. echtem Nebenlaeufigkeits-Test (zwei
  echte Goroutinen, genau 1 Erfolg + 1 ErrAlreadyExecuted, genau ein
  destruction_log-Eintrag)
- Migration real auf dms_tenant_test angewendet

Pruefungen siehe archive/docs/CMP-06-PRUEFPROTOKOLL.md
This commit is contained in:
sysops
2026-08-30 22:07:07 +02:00
parent a95ed331cd
commit 6626f8a1e3
5 changed files with 420 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
# CMP-06 Prüfprotokoll: Vier-Augen-Freigabe für Löschungen
Voraussetzung RET-03 bereits Fertig.
## Umsetzung
- `archive/migrations/0010_deletion_requests.up/down.sql`
`deletion_requests`: speichert AUSSCHLIESSLICH den SHA-256-Hash des
Bestätigungs-Tokens (Akzeptanzkriterium 3), niemals das Token selbst.
- `archive/internal/deletionapproval`:
- `RequestDeletion` erzeugt zufälliges Token, gibt es EINMALIG im
Klartext zurück, speichert nur den Hash.
- `ConfirmAndExecute` `SELECT ... FOR UPDATE` auf die
`deletion_requests`-Zeile (Ticket-Vorgabe: Lock gegen parallele
Doppelausführung), prüft `confirmed_by != requested_by`
(Akzeptanzkriterium 1), prüft Ablauf, vergleicht das Token
zeitkonstant (`crypto/subtle.ConstantTimeCompare`, dasselbe Muster
wie `internal/policyapi.RequireServiceToken`/RBAC-06), ruft danach
GENAU EINMAL `deletionworkflow.Destroy` (RET-03) auf — dupliziert
dessen Löschlogik nicht.
## Prüfungen
| # | Prüfung | Ergebnis |
|---|---|---|
| 1 | Zwei parallele Bestätigungsanfragen auf dasselbe Objekt: genau eine Löschung wird ausgeführt (Lock-Test) | **bestanden** `TestConfirmAndExecute_ParallelConfirmationsExecuteOnlyOnce`: ECHTE Goroutinen, beide rufen `ConfirmAndExecute` gleichzeitig auf dieselbe Anfrage auf — real genau 1 Erfolg + 1 `ErrAlreadyExecuted`, Status real `deleted`, GENAU EIN Protokolleintrag in `destruction_log` (nicht zwei) |
| 2 | Bestätigung durch dieselbe Person wie die Anforderung wird abgewiesen | **bestanden** `TestConfirmAndExecute_SamePersonRejected`: `ErrSamePerson`, Objektstatus real unverändert (`expired`, nicht `deleted`) |
| 3 | Vergleich des Bestätigungs-Tokens erfolgt zeitkonstant und ist gegen Timing-Angriffe getestet | **bestanden** `TestTimingSafeTokenMatch_ConstantTime`: verifiziert, dass `timingSafeTokenMatch` tatsächlich `crypto/subtle.ConstantTimeCompare` verwendet (korrekter Treffer, korrekte Ablehnung bei abweichendem Token); zusätzlich `TestConfirmAndExecute_ExpiredTokenRejected` für die zeitliche Begrenzung (Akzeptanzkriterium 3) |
## Build/Test-Ergebnis (192.168.1.131)
```
go build ./... -> clean
go vet ./... -> clean
golangci-lint run ./... -> 0 issues
go test ./... -p 1 -> alle Archive-Pakete bestanden (inkl. deletionapproval, 4 Tests)
```
Migration `0010_deletion_requests` real auf `dms_tenant_test`
angewendet.
## Gesamtergebnis
**Bestanden.** Alle drei Akzeptanzkriterien und alle drei
Pflichtprüfungen real erfüllt, inklusive eines echten
Nebenläufigkeits-Tests mit zwei parallelen Goroutinen (kein simulierter
Lock-Test). Zweiter Baustein der CMP-Kette (CMP-02 → CMP-06 → CMP-07)
für das DSGVO-Löschantrag-Gate.