docs: GoBD-Checklist auf aktuellen Stand bringen, Spec für PROJ-56c nachtragen
Checklist war seit 2026-06-13 nicht aktualisiert: PROJ-48/49/50/51/52 waren laengst deployt, standen aber noch als fehlend/teilweise drin. PROJ-56c (Cron-Purge mit Markierungspflicht) war im Code implementiert, hatte aber nie eine eigene Feature-Spec — nachtraeglich dokumentiert.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# PROJ-56c: Cron-gesteuerte Löschung abgelaufener + markierter Mails (GoBD-Retention-Purge)
|
||||
|
||||
## Status: Deployed
|
||||
**Created:** 2026-06-22 (nachträglich dokumentiert 2026-07-04)
|
||||
**Last Updated:** 2026-07-04
|
||||
|
||||
## Dependencies
|
||||
- PROJ-34 (Retention-Policy + Löschsperre) — `retain_until`, `ErrRetentionLock`
|
||||
- PROJ-11/PROJ-48 (Audit-Log) — Audit-Eintrag pro Löschung
|
||||
- Verwandtes Cron-Muster: PROJ-56 (Last-Entzerrung Hintergrundjobs), PROJ-58 (Cron-Batch-Jobs)
|
||||
|
||||
## Hintergrund
|
||||
Analog zu Pilers `purge.sh`: unbeaufsichtigte, automatisierte Löschung nach Ablauf der
|
||||
Aufbewahrungsfrist ist ein GoBD-Risiko, wenn sie rein auf `retain_until < NOW()` basiert —
|
||||
ein Datum allein darf eine unwiderrufliche Löschung nicht auslösen, ohne dass ein Mensch
|
||||
die konkrete Mail zuvor geprüft und freigegeben hat (Vier-Augen-Prinzip: Frist + Mensch).
|
||||
|
||||
Deshalb ist der Cron-Job bewusst **restriktiver** als der manuelle Admin-Button
|
||||
(`Store.Purge()` / `POST /api/admin/purge`), der alles abgelaufene ohne Markierungspflicht
|
||||
löscht. Der Cron darf **nur** Mails löschen, die zusätzlich vom Nutzer/Admin explizit als
|
||||
löschbar markiert wurden.
|
||||
|
||||
## User Stories
|
||||
- Als Admin möchte ich abgelaufene Mails in einer Review-Liste sehen und einzeln zur
|
||||
Löschung markieren, statt dass das System sie automatisch nach Fristablauf löscht.
|
||||
- Als Auditor möchte ich in jedem Löschvorgang nachvollziehen können, dass sowohl die
|
||||
Frist abgelaufen war als auch eine explizite menschliche Markierung vorlag.
|
||||
- Als Admin möchte ich, dass der nächtliche Cron nichts löscht, was nicht vorher markiert
|
||||
wurde — auch nicht bei einem Konfigurationsfehler oder Bug im Fristmodell.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [x] Cron-Job `archivmail purge` (nachts 03:40 via `/etc/cron.d/archivmail`) löscht
|
||||
ausschließlich Mails, die BEIDE Bedingungen erfüllen: `retain_until < NOW()` UND
|
||||
`marked_for_deletion = TRUE`.
|
||||
- [x] Reiner Fristablauf ohne Markierung löscht nichts automatisch — keine Ausnahme, kein
|
||||
Fallback.
|
||||
- [x] Admin-UI erlaubt pro Mail das Setzen/Löschen von `marked_for_deletion` (wer, wann).
|
||||
- [x] Review-Liste zeigt alle abgelaufenen Mails (markiert und unmarkiert), damit ein Mensch
|
||||
bewusst entscheiden kann.
|
||||
- [x] Jede Cron-Löschung erzeugt einen Audit-Log-Eintrag (`mail_purged`) inkl. Tenant-ID.
|
||||
- [x] Löschung entfernt Mail zusätzlich aus dem Suchindex (best effort — Index-Fehler
|
||||
blockieren die eigentliche Löschung nicht, GoBD-Löschpflicht hat Vorrang vor
|
||||
Index-Konsistenz).
|
||||
- [x] Fehlender Index-/Audit-Backend-Zugriff (z.B. Manticore down) blockiert die Löschung
|
||||
selbst nicht — nur die Zusatzschritte sind best effort.
|
||||
- [x] `--dry-run`-Flag listet Löschkandidaten ohne zu löschen (Betriebs-/Testzweck).
|
||||
- [x] Manueller Admin-Button (`POST /api/admin/purge`) bleibt unverändert bestehen und nutzt
|
||||
bewusst eine andere, permissivere Query (kein Markierungszwang) — getrennte Semantik
|
||||
für Cron vs. manuelle Aktion, keine Vermischung.
|
||||
|
||||
## Edge Cases
|
||||
- Mail wird nach Markierung, aber vor Fristablauf, wieder demarkiert → Cron lässt sie in Ruhe
|
||||
(beide Bedingungen müssen zum Ausführungszeitpunkt erfüllt sein, keine "einmal markiert,
|
||||
immer markiert"-Logik).
|
||||
- Manticore/Audit-DB beim Cron-Lauf nicht erreichbar → Löschung läuft trotzdem durch (Warn-Log),
|
||||
Index/Audit-Eintrag fehlt für diese Mail (kein Blocker, aber sichtbar im Log).
|
||||
- `--dry-run` gegen leere Kandidatenliste → sauberer No-Op-Log, kein Fehler.
|
||||
- Mail-Löschung schlägt fehl (z.B. Dateisystem-Fehler) → wird geloggt (`failed++`), Rest der
|
||||
Batch-Liste wird weiterverarbeitet, kein Abbruch der gesamten Cron-Ausführung.
|
||||
|
||||
## Technical Requirements
|
||||
- Tabelle `emails` erweitert um `marked_for_deletion BOOLEAN`, `marked_for_deletion_by TEXT`,
|
||||
`marked_for_deletion_at TIMESTAMPTZ`.
|
||||
- Separate Storage-Query `ListExpiredMarkedMailIDs` (Cron) vs. `ListExpiredMailIDs`/`Purge()`
|
||||
(manueller Button) — bewusst nicht zusammengeführt, um versehentliche Verschärfung/Lockerung
|
||||
einer der beiden Pfade durch spätere Refactorings zu vermeiden.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes (2026-06-22, nachträglich dokumentiert 2026-07-04)
|
||||
|
||||
### Neue/geänderte Dateien
|
||||
- `cmd/archivmail/cmd_purge.go` (NEU): CLI-Subcommand `archivmail purge [-config path] [-dry-run]`.
|
||||
Lädt Config, öffnet Storage, ruft `ListExpiredMarkedMailIDs`, löscht pro Mail
|
||||
(`mailStore.Delete(id)`), räumt Manticore-Index auf (`idxMgr.ForTenant(tenantID).Delete(id)`,
|
||||
best effort) und schreibt Audit-Eintrag `mail_purged` (best effort, Detail-Text nennt
|
||||
explizit "Aufbewahrungsfrist abgelaufen UND vom Nutzer zur Löschung markiert").
|
||||
- `internal/storage/mark_deletion.go` (NEU):
|
||||
- `SetMarkedForDeletion(ctx, id, marked, username)` — setzt/löscht die Markierung,
|
||||
inkl. `marked_for_deletion_by`/`_at`.
|
||||
- `GetMarkedForDeletion(ctx, id)` — liefert aktuellen Markierungs-Zustand.
|
||||
- `ListExpiredMails(ctx, tenantID)` — Review-Liste für die Admin-UI: ALLE abgelaufenen
|
||||
Mails (markiert und unmarkiert), metadata-only (kein Body-Zugriff nötig, SEC-29
|
||||
Aufgabentrennung), `LIMIT 500`.
|
||||
- `ListExpiredMarkedMailIDs(ctx)` — die vom Cron genutzte, restriktive Query:
|
||||
`retain_until IS NOT NULL AND retain_until < NOW() AND marked_for_deletion = TRUE`.
|
||||
- `/etc/cron.d/archivmail` (Testserver + Produktivserver, via `update.sh` bei jedem Deploy
|
||||
neu eingespielt): Zeile "Vollständigkeits-Reconciliation (PROJ-52)" referenziert im
|
||||
Kommentar auch PROJ-56c als verwandtes Cron-Muster; eigener Cron-Eintrag für
|
||||
`archivmail purge` läuft nachts 03:40 Uhr.
|
||||
|
||||
### Bewusste Trennung Cron vs. manueller Button
|
||||
`Store.Purge()` (manueller Admin-Button, `POST /api/admin/purge`) und
|
||||
`ListExpiredMarkedMailIDs` (Cron) sind absichtlich zwei getrennte Code-Pfade mit
|
||||
unterschiedlicher Semantik:
|
||||
- Manuell: superadmin-only, löscht alles mit `retain_until < NOW()`, kein Markierungszwang
|
||||
(der Admin klickt bewusst "Jetzt löschen" — das IST die menschliche Freigabe).
|
||||
- Cron: unbeaufsichtigt, darf nie auf Datum allein vertrauen — braucht zusätzlich die
|
||||
vorab von einem Menschen gesetzte Markierung.
|
||||
|
||||
### GoBD-Dokumentation
|
||||
Siehe `docs/GOBD_DSGVO_CHECKLIST.md`, Punkt 7 (Löschsperre) — als "Erfüllt" bewertet,
|
||||
inkl. Verweis auf diese Vier-Augen-Logik.
|
||||
|
||||
### Offene Punkte
|
||||
- Kein separater QA-Durchlauf für dieses Ticket dokumentiert (Feature war bereits vor der
|
||||
nachträglichen Spec-Erstellung produktiv im Einsatz). Empfehlung: bei nächster
|
||||
QA-Runde Edge Cases (Index-Backend down, Audit-DB down, Demarkierung vor Fristablauf)
|
||||
gezielt gegentesten.
|
||||
|
||||
## Tech Design (Solution Architect)
|
||||
Übersprungen — kleine, klar umrissene Ergänzung zu PROJ-34, kein architektonischer Schnitt
|
||||
(analog PROJ-55/PROJ-56).
|
||||
|
||||
## QA Test Results
|
||||
_Nachträglich zu ergänzen — siehe "Offene Punkte" oben._
|
||||
|
||||
## Deployment
|
||||
Bereits produktiv im Einsatz (Cron läuft nachts 03:40 Uhr auf 192.168.1.131), Datum der
|
||||
Erstauslieferung nicht mehr exakt rekonstruierbar (vor 2026-07-04). Diese Spec-Datei
|
||||
dokumentiert den Ist-Zustand nachträglich, kein neues Deployment ausgelöst.
|
||||
Reference in New Issue
Block a user