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:
@@ -1,29 +1,25 @@
|
|||||||
# GoBD/DSGVO-Compliance-Checkliste – archivmail
|
# GoBD/DSGVO-Compliance-Checkliste – archivmail
|
||||||
|
|
||||||
Bewertung gegen den "Leitfaden zur E-Mail-Archivierung in Deutschland" (VOI-Grundsätze, GoBD, DSGVO).
|
Bewertung gegen den "Leitfaden zur E-Mail-Archivierung in Deutschland" (VOI-Grundsätze, GoBD, DSGVO).
|
||||||
Stand: 2026-06-13, basierend auf Code-Review (nicht nur Spec-Review).
|
Stand: 2026-07-04, basierend auf Code-Review (nicht nur Spec-Review).
|
||||||
|
|
||||||
## Zusammenfassung
|
## Zusammenfassung
|
||||||
|
|
||||||
**Gut abgedeckt:** Verschlüsselung at-rest (AES-256-GCM), Volltextsuche (Manticore), Audit-Log
|
**Gut abgedeckt:** Verschlüsselung at-rest (AES-256-GCM) mit Pflicht-Warnung (PROJ-49), Volltextsuche
|
||||||
in PostgreSQL mit Filterung/Export, Retention/Löschsperre (PROJ-34), Multi-Tenant-Rollenmodell,
|
(Manticore), unveränderliches Audit-Log per DB-Trigger + append-only JSON-Lines (PROJ-48),
|
||||||
|
Retention/Löschsperre inkl. Dokumentenart-Kategorien (PROJ-34, PROJ-51), DSGVO-Löschersuchen-Workflow
|
||||||
|
mit GoBD-Vorrang (PROJ-50), Vollständigkeits-Reconciliation (PROJ-52), Multi-Tenant-Rollenmodell,
|
||||||
Integritätsprüfung per SHA-256 (PROJ-18), Export in EML/MBOX/ZIP/CSV (PROJ-12, PROJ-15, PROJ-39, PROJ-47).
|
Integritätsprüfung per SHA-256 (PROJ-18), Export in EML/MBOX/ZIP/CSV (PROJ-12, PROJ-15, PROJ-39, PROJ-47).
|
||||||
|
|
||||||
**Top-Lücken (priorisiert):**
|
**Verbleibende Lücken (priorisiert):**
|
||||||
|
|
||||||
1. **Audit-Log ist NICHT unveränderbar** – kein DB-Trigger/Constraint gegen UPDATE/DELETE auf
|
1. **Migrationssicherheit teilweise** – kein dokumentierter Compliance-Erhalt-Check nach Schema-/
|
||||||
`audit_log`, und die in PROJ-11 spezifizierte append-only Flat-File (`/var/log/archivmail/audit.log`,
|
Index-Migrationen (Punkt 10).
|
||||||
JSON Lines) existiert im Code nicht. Das ist ein Kernpunkt von VOI-Grundsatz 8 (Protokollierung)
|
2. **Physische Tenant-Trennung fehlt** – Storage ist logisch (DB) getrennt, nicht auf Dateisystem-
|
||||||
und wird bei einer Prüfung als erstes angeschaut.
|
Ebene (Punkt 15, bekannt seit Tenant-Isolation-Review).
|
||||||
2. **Verschlüsselung ist optional/deaktivierbar** – `Keyfile` kann leer bleiben → kein "at rest"-Schutz
|
3. **Zeitstempel/Signaturerhalt (BSI TR 03125)** – nicht implementiert (Nice-to-have, niedrige
|
||||||
garantiert, kein erzwungenes Minimum.
|
Priorität, nur relevant bei signierten Mails im Kundenkreis).
|
||||||
3. **Keine Kennzeichnung/Klassifikation personenbezogener Daten** – kein `contains_personal_data`
|
4. **Informationspflicht der Mitarbeiter** – organisatorisch, nicht im Code lösbar (Punkt 13).
|
||||||
o.ä. Flag im Mail-Datenmodell; DSGVO-Löschkonflikt (Art. 17 vs. GoBD-Aufbewahrung) ist nirgends
|
|
||||||
im Code dokumentiert oder technisch abgebildet.
|
|
||||||
4. **Mindestaufbewahrung nicht erzwungen** – `retention_days` ist frei konfigurierbar inkl. `0`
|
|
||||||
(kein Lock); es gibt keine Differenzierung nach Dokumentenart (6 vs. 10 Jahre vs. dauerhaft) und
|
|
||||||
kein erzwungenes Minimum von 10 Jahren für Buchungsbelege.
|
|
||||||
5. **Zeitstempel/Signaturerhalt (BSI TR 03125)** – nicht implementiert (Nice-to-have, niedrige Priorität).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,29 +32,29 @@ Integritätsprüfung per SHA-256 (PROJ-18), Export in EML/MBOX/ZIP/CSV (PROJ-12,
|
|||||||
| 3 | **Unveränderbarkeit / Formaterhalt** | ✅ Erfüllt | `Save()` speichert Original-RFC2822-Bytes (ggf. gzip-komprimiert, PROJ-36) ohne inhaltliche Konvertierung; `Load()`/`maybeDecompress()` liefert Original zurück. Export liefert EML 1:1 (PROJ-12, `internal/api/export.go`). Kein Schreibzugriff/UPDATE auf gespeicherte Mail-Dateien im Code gefunden. | – |
|
| 3 | **Unveränderbarkeit / Formaterhalt** | ✅ Erfüllt | `Save()` speichert Original-RFC2822-Bytes (ggf. gzip-komprimiert, PROJ-36) ohne inhaltliche Konvertierung; `Load()`/`maybeDecompress()` liefert Original zurück. Export liefert EML 1:1 (PROJ-12, `internal/api/export.go`). Kein Schreibzugriff/UPDATE auf gespeicherte Mail-Dateien im Code gefunden. | – |
|
||||||
| 4 | **Zugriffsschutz** | ✅ Erfüllt | JWT-Auth (httpOnly Cookie), bcrypt Cost 12 (PROJ-1). Rollenmodell `user`/`auditor`/`admin` strikt getrennt; `admin` hat explizit KEINEN Mail-Zugriff. Tenant-Isolation via `tenantAccessAllowed()` (`internal/api/import_handlers.go:22`). | – |
|
| 4 | **Zugriffsschutz** | ✅ Erfüllt | JWT-Auth (httpOnly Cookie), bcrypt Cost 12 (PROJ-1). Rollenmodell `user`/`auditor`/`admin` strikt getrennt; `admin` hat explizit KEINEN Mail-Zugriff. Tenant-Isolation via `tenantAccessAllowed()` (`internal/api/import_handlers.go:22`). | – |
|
||||||
| 5 | **Wiederauffindbarkeit / Volltextsuche** | ✅ Erfüllt | Manticore-RT-Index (PROJ-30), Volltextsuche inkl. Anhänge via OCR (PROJ-35/44), Filter nach Datum/Absender/etc. (PROJ-6). | – |
|
| 5 | **Wiederauffindbarkeit / Volltextsuche** | ✅ Erfüllt | Manticore-RT-Index (PROJ-30), Volltextsuche inkl. Anhänge via OCR (PROJ-35/44), Filter nach Datum/Absender/etc. (PROJ-6). | – |
|
||||||
| 6 | **Aufbewahrungsfristen (konfigurierbar je Dokumentenart, i.d.R. 6–10 Jahre)** | ⚠️ Teilweise | `RetentionDays` global (`config/config.go:74`) und pro Tenant (`config/config.go:122`, Spalte `tenants.retention_days`) konfigurierbar — aber als EINZELNER Wert für alle Mails, keine Differenzierung nach Dokumentenart (Geschäftsbrief 6J / Rechnung 10J / Vertrag dauerhaft). `0` = kein Lock ist als Default zulässig (kein erzwungenes Minimum). | Spec-Erweiterung: Mindestwert (z.B. Warnung/Block unter 2190 Tagen) und/oder Kategorisierung nach Mail-Typ (PROJ-43 Archivierungsregeln könnte hierfür genutzt werden). |
|
| 6 | **Aufbewahrungsfristen (konfigurierbar je Dokumentenart, i.d.R. 6–10 Jahre)** | ✅ Erfüllt | PROJ-51 (deployt 2026-06-13): Tabelle `archiving_rules` (`internal/storage/retention_rules.go`) — Muster-Regeln (Absender/Betreff/etc.) mit individueller `retention_days` pro Dokumentenart, Priorität bei mehreren Treffern, `retain_until_source` dokumentiert welche Regel griff. `Store.minRetentionDays`/`SetMinRetentionDays` erzwingt konfigurierbares Minimum. Admin-UI unter `/api/admin/archiving-rules`. | – |
|
||||||
| 7 | **Löschsperre** | ✅ Erfüllt | PROJ-34 implementiert: `emails.retain_until`, `ErrRetentionLock` (`internal/storage/storage.go:27,659-667`), `Delete()` verweigert Löschung vor Fristablauf. Zusätzlich Cron-Purge (PROJ-56c, `cmd/archivmail/cmd_purge.go`, nachts 03:40 via `/etc/cron.d/archivmail`): löscht NUR Mails die BEIDE Bedingungen erfüllen — `retain_until < NOW()` UND vom Nutzer explizit `marked_for_deletion=TRUE` gesetzt (`ListExpiredMarkedMailIDs`, `internal/storage/mark_deletion.go:127-148`). Reiner Fristablauf ohne manuelle Markierung löscht nichts automatisch (Vier-Augen-Prinzip: Frist + Mensch). Der manuelle Admin-Button (`Store.Purge()`, `POST /api/admin/purge`) löscht dagegen alles abgelaufene ohne Markierungspflicht — bewusst getrennte, absichtlich unterschiedliche Semantik für Cron vs. manuelle Aktion. | – |
|
| 7 | **Löschsperre** | ✅ Erfüllt | PROJ-34 implementiert: `emails.retain_until`, `ErrRetentionLock` (`internal/storage/storage.go:27,659-667`), `Delete()` verweigert Löschung vor Fristablauf. Zusätzlich Cron-Purge (PROJ-56c, `cmd/archivmail/cmd_purge.go`, nachts 03:40 via `/etc/cron.d/archivmail`): löscht NUR Mails die BEIDE Bedingungen erfüllen — `retain_until < NOW()` UND vom Nutzer explizit `marked_for_deletion=TRUE` gesetzt (`ListExpiredMarkedMailIDs`, `internal/storage/mark_deletion.go:127-148`). Reiner Fristablauf ohne manuelle Markierung löscht nichts automatisch (Vier-Augen-Prinzip: Frist + Mensch). Der manuelle Admin-Button (`Store.Purge()`, `POST /api/admin/purge`) löscht dagegen alles abgelaufene ohne Markierungspflicht — bewusst getrennte, absichtlich unterschiedliche Semantik für Cron vs. manuelle Aktion. | – |
|
||||||
| 8 | **Protokollierung/Audit-Log unveränderlich** | ❌ Fehlt | `internal/audit/audit.go:64-75` erstellt `audit_log`-Tabelle OHNE Trigger/Rule gegen `UPDATE`/`DELETE` (PROJ-11 AC: "kein UPDATE/DELETE durch Admin oder Anwendung möglich" — NICHT umgesetzt). Die geforderte append-only JSON-Lines-Datei (`/var/log/archivmail/audit.log`) existiert nicht im Code — nur `New(dsn, logDir, logger)` mit Kommentar "logDir is reserved for future". Audit-Events (Login, Suche, Export, Import) werden korrekt geschrieben, aber Unveränderbarkeit ist nicht garantiert. | PROJ-11 nachbessern: (a) PostgreSQL-Trigger/REVOKE UPDATE/DELETE auf `audit_log`, (b) append-only Flat-File-Logger implementieren. Hohe Priorität für GoBD-Prüfungen. |
|
| 8 | **Protokollierung/Audit-Log unveränderlich** | ✅ Erfüllt | PROJ-48 (deployt 2026-06-13): `internal/audit/audit.go` installiert Funktion `audit_log_no_mutation()` + Trigger `audit_log_immutable` (blockt UPDATE/DELETE auf DB-Ebene, idempotent via `CREATE OR REPLACE`/`DROP TRIGGER IF EXISTS`). Zusätzlich append-only JSON-Lines-Datei (`ResolvedLogPath()`, Default `/var/log/archivmail/audit.log`), Schreibfehler blockieren den DB-Pfad nicht (best effort, DB bleibt Quelle der Wahrheit). | – |
|
||||||
| 9 | **Nachprüfbarkeit durch Dritte / GDPdU-Export** | ✅ Erfüllt | Export-Funktionen: Einzel-EML/PDF, ZIP-Massenexport mit `manifest.csv` (PROJ-12, `internal/api/export.go`), eDiscovery-ZIP mit `metadata.csv` + README (PROJ-39, `internal/api/ediscovery.go`), CLI-Export inkl. Tenant-Voll-Export (PROJ-15, PROJ-47, `cmd/archivmail/cmd_export.go`), Audit-Log-CSV-Export für Auditoren. | Optional: PROJ-47 (In Review) fertigstellen/QA. |
|
| 9 | **Nachprüfbarkeit durch Dritte / GDPdU-Export** | ✅ Erfüllt | Export-Funktionen: Einzel-EML/PDF, ZIP-Massenexport mit `manifest.csv` (PROJ-12, `internal/api/export.go`), eDiscovery-ZIP mit `metadata.csv` + README (PROJ-39, `internal/api/ediscovery.go`), CLI-Export inkl. Tenant-Voll-Export (PROJ-15, PROJ-47, `cmd/archivmail/cmd_export.go`), Audit-Log-CSV-Export für Auditoren. | Optional: PROJ-47 (In Review) fertigstellen/QA. |
|
||||||
| 10 | **Migrationssicherheit** | ⚠️ Teilweise | Migrationstools vorhanden (PROJ-19 Mailpiler-Import, PROJ-30 Xapian→Manticore), `initSchema`/idempotente `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` Pattern durchgängig genutzt (z.B. `storage.go:99-100`). Kein dokumentierter "Compliance-Erhalt-Check" nach Migration (z.B. automatischer Verify-Lauf nach Schema-Änderung). | Migrations-Runbook + Post-Migration-Integritätscheck (kann PROJ-18-Job nach Migration triggern) dokumentieren. |
|
| 10 | **Migrationssicherheit** | ⚠️ Teilweise | Migrationstools vorhanden (PROJ-19 Mailpiler-Import, PROJ-30 Xapian→Manticore), `initSchema`/idempotente `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` Pattern durchgängig genutzt (z.B. `storage.go:99-100`). Kein dokumentierter "Compliance-Erhalt-Check" nach Migration (z.B. automatischer Verify-Lauf nach Schema-Änderung). | Migrations-Runbook + Post-Migration-Integritätscheck (kann PROJ-18-Job nach Migration triggern) dokumentieren. |
|
||||||
| 11 | **Maschinelle Auswertbarkeit (Standardformate)** | ✅ Erfüllt | Export als EML (Original-MIME, PROJ-12/15), MBOX (`cmd_export.go`), CSV-Metadaten (PROJ-39). | – |
|
| 11 | **Maschinelle Auswertbarkeit (Standardformate)** | ✅ Erfüllt | Export als EML (Original-MIME, PROJ-12/15), MBOX (`cmd_export.go`), CSV-Metadaten (PROJ-39). | – |
|
||||||
| 12 | **Zeitstempel/Signaturerhalt (BSI TR 03125)** | ❌ Fehlt | Keine S/MIME- oder PGP-Signaturprüfung, kein qualifizierter Zeitstempel-Dienst im Code gefunden. Für die meisten KMU nicht zwingend erforderlich. | Als Backlog-Item vermerken, nur bei Bedarf (signierte Mails im Kundenkreis) als neue Spec aufnehmen. |
|
| 12 | **Zeitstempel/Signaturerhalt (BSI TR 03125)** | ❌ Fehlt | Keine S/MIME- oder PGP-Signaturprüfung, kein qualifizierter Zeitstempel-Dienst im Code gefunden. Für die meisten KMU nicht zwingend erforderlich. | Als Backlog-Item vermerken, nur bei Bedarf (signierte Mails im Kundenkreis) als neue Spec aufnehmen. |
|
||||||
| 13 | **Informationspflicht der Mitarbeiter** | ❌ Fehlt (organisatorisch) | Nicht im Code prüfbar/lösbar. | Organisatorische Maßnahme: Betriebsvereinbarung / Datenschutzhinweis außerhalb des Systems. |
|
| 13 | **Informationspflicht der Mitarbeiter** | ❌ Fehlt (organisatorisch) | Nicht im Code prüfbar/lösbar. | Organisatorische Maßnahme: Betriebsvereinbarung / Datenschutzhinweis außerhalb des Systems. |
|
||||||
| 14 | **Trennung/Kennzeichnung personenbezogener Daten, Löschkonflikt Art. 5/17/32** | ❌ Fehlt | Kein Feld wie `contains_personal_data` o.ä. im Mail-Datenmodell (`internal/storage/storage.go` Tabelle `emails`); PROJ-20 (Nutzer-Löschung) behandelt nur Anonymisierung von Nutzerkonten/Audit-Einträgen, nicht von Mail-Inhalten. Konflikt GoBD-Aufbewahrung vs. DSGVO-Löschanspruch ist nirgends dokumentiert oder im Code abgebildet (z.B. `Purge()` prüft nur `retain_until`, keine "Lösch-trotz-Anfrage-abgelehnt"-Logik mit Begründung). | Neue Feature-Spec: DSGVO-Auskunfts-/Löschersuchen-Workflow, der bei aktivem `retain_until` automatisch auf "Aufbewahrungspflicht hat Vorrang" verweist und dies dokumentiert/protokolliert. |
|
| 14 | **Trennung/Kennzeichnung personenbezogener Daten, Löschkonflikt Art. 5/17/32** | ✅ Erfüllt | PROJ-50 (deployt 2026-06-13): `internal/storage/dsgvo_requests.go` — Tabelle `dsgvo_requests`, Workflow sucht betroffene Mails (From/To/CC, Volltext via Manticore `AnyAddress`), löscht NUR Mails ohne aktive Löschsperre (`store.Delete` respektiert `retain_until`), lehnt den Rest mit Begründung "Aufbewahrungspflicht hat Vorrang" ab und protokolliert (`EventDSGVORequest`). Bekannte Deviation: `bcc_addr` ist im Schema vorhanden, aber `mailparser` befüllt BCC nicht (kein BCC-Header in archivierten Mails) — Such-Vollständigkeitszusage entsprechend einschränken. | Deviation (BCC) in Kunden-Doku erwähnen, sonst keine Aktion nötig. |
|
||||||
| 15 | **Mandantentrennung/Zugriffskontrolle nach Rolle (Multi-Tenant)** | ✅ Erfüllt | PROJ-21 Multi-Tenancy, `tenantAccessAllowed()` als Standardmuster (`internal/api/import_handlers.go:22`), Tenant-Quotas (PROJ-29), Pro-Tenant-Retention (`tenants.retention_days`). Laut Memory: keine physische Storage-Trennung pro Tenant (logische Trennung über `email_refs`/DB). | Bekannte Lücke (siehe Memory `project_tenant_isolation_review.md`): physische Storage-Trennung als optionales Härtungsfeature evaluieren, falls von Kunden gefordert. |
|
| 15 | **Mandantentrennung/Zugriffskontrolle nach Rolle (Multi-Tenant)** | ✅ Erfüllt | PROJ-21 Multi-Tenancy, `tenantAccessAllowed()` als Standardmuster (`internal/api/import_handlers.go:22`), Tenant-Quotas (PROJ-29), Pro-Tenant-Retention (`tenants.retention_days`). Laut Memory: keine physische Storage-Trennung pro Tenant (logische Trennung über `email_refs`/DB). | Bekannte Lücke (siehe Memory `project_tenant_isolation_review.md`): physische Storage-Trennung als optionales Härtungsfeature evaluieren, falls von Kunden gefordert. |
|
||||||
| 16 | **Verschlüsselung at-rest** | ⚠️ Teilweise | AES-256-GCM implementiert (`internal/storage/storage.go:154-...`, `encrypt()`), inkl. gzip vor Verschlüsselung (PROJ-36). ABER: `Keyfile` ist optional — `loadKey()` gibt bei leerem Pfad `nil` zurück → "no encryption configured" (`storage.go:126`). Es gibt keine Pflicht/Warnung im Setup, dass ein Keyfile gesetzt sein MUSS. | Setup-/Healthcheck-Warnung (evtl. in `archivmail status`, siehe letzter Commit) ergänzen, falls `Keyfile` leer ist — und/oder Pflichtfeld in Produktions-Config-Validierung. |
|
| 16 | **Verschlüsselung at-rest** | ✅ Erfüllt | PROJ-49 (deployt 2026-06-13): AES-256-GCM (`internal/storage/storage.go`, `encrypt()`), inkl. gzip vor Verschlüsselung (PROJ-36). `Store.EncryptionEnabled()` + `warnEncryptionStatus()` gibt beim Start eine WARN-Zeile aus wenn Keyfile fehlt/unlesbar/falsche Größe; `archivmail status` prüft `checkEncryption`; Dashboard (`handleSystemStats`) zeigt `encryption.enabled` systemweit. `Keyfile` bleibt technisch optional (Abwärtskompatibilität bestehender Installationen), aber Zustand ist jetzt sichtbar statt stillschweigend. | – |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nächste Schritte (Vorschlag, Priorität absteigend)
|
## Nächste Schritte (Vorschlag, Priorität absteigend)
|
||||||
|
|
||||||
1. **PROJ-11 nachbessern** (Audit-Log-Unveränderbarkeit): DB-Trigger gegen UPDATE/DELETE +
|
1. **Migrations-Runbook** (Punkt 10): Compliance-Erhalt-Check nach Schema-/Index-Migrationen
|
||||||
append-only JSON-Lines-Datei wie ursprünglich spezifiziert.
|
dokumentieren (kann PROJ-18-Integritätsjob nach Migration triggern).
|
||||||
2. **Encryption-Pflicht**: Healthcheck/Setup-Validierung, dass `storage.keyfile` konfiguriert ist.
|
2. **Physische Tenant-Trennung** (Punkt 15) evaluieren, falls von Kunden gefordert
|
||||||
3. **Neue Spec**: DSGVO-Löschersuchen-Workflow mit GoBD-Vorrang-Dokumentation (Punkt 14).
|
(siehe Memory `project_tenant_isolation_review.md`).
|
||||||
4. **PROJ-34 erweitern**: Mindestaufbewahrung (kein `retention_days=0` als stiller Default in
|
3. Nice-to-have: BSI TR 03125 Zeitstempel/Signaturerhalt (Punkt 12), nur bei konkretem Bedarf.
|
||||||
produktiven Setups) + Differenzierung nach Dokumentenart (ggf. via PROJ-43 Regeln).
|
4. Organisatorisch: Mitarbeiter-Informationspflicht (Punkt 13) außerhalb des Codes klären.
|
||||||
5. **Vollständigkeits-Reconciliation** (Punkt 1) als neues Feature evaluieren.
|
|
||||||
6. Nice-to-have: BSI TR 03125 Zeitstempel/Signaturerhalt (Punkt 12), nur bei konkretem Bedarf.
|
|
||||||
|
|
||||||
PROJ-47 (Tenant-Voll-Export, Status "In Review") sollte für Punkt 9 final abgeschlossen werden.
|
**Erledigt seit Stand 2026-06-13:** PROJ-48 (Audit-Log-Unveränderbarkeit), PROJ-49
|
||||||
|
(Verschlüsselungspflicht/Warnung), PROJ-50 (DSGVO-Löschersuchen), PROJ-51 (Retention-Kategorien),
|
||||||
|
PROJ-52 (Vollständigkeits-Reconciliation), PROJ-47 (Tenant-Voll-Export, jetzt Deployed).
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
| PROJ-54 | Fix Listenansicht/Pagination für Rolle "user" (Nachbesserung PROJ-6/PROJ-21) | Deployed | [PROJ-54](PROJ-54-fix-listenansicht-total.md) | 2026-06-14 |
|
| PROJ-54 | Fix Listenansicht/Pagination für Rolle "user" (Nachbesserung PROJ-6/PROJ-21) | Deployed | [PROJ-54](PROJ-54-fix-listenansicht-total.md) | 2026-06-14 |
|
||||||
| PROJ-55 | Fix Tenant-Isolation für Rolle "auditor" + Audit-Log (Sicherheitsbug, DSGVO-relevant) | Deployed | [PROJ-55](PROJ-55-fix-auditor-tenant-isolation.md) | 2026-06-21 |
|
| PROJ-55 | Fix Tenant-Isolation für Rolle "auditor" + Audit-Log (Sicherheitsbug, DSGVO-relevant) | Deployed | [PROJ-55](PROJ-55-fix-auditor-tenant-isolation.md) | 2026-06-21 |
|
||||||
| PROJ-56 | Last-Entzerrung für Hintergrundjobs (OCR-Zeitfenster, IMAP-Sync-Jitter) | Deployed | [PROJ-56](PROJ-56-last-entzerrung-hintergrundjobs.md) | 2026-06-22 |
|
| PROJ-56 | Last-Entzerrung für Hintergrundjobs (OCR-Zeitfenster, IMAP-Sync-Jitter) | Deployed | [PROJ-56](PROJ-56-last-entzerrung-hintergrundjobs.md) | 2026-06-22 |
|
||||||
|
| PROJ-56c | Cron-gesteuerte Löschung abgelaufener + markierter Mails (GoBD-Retention-Purge) | Deployed | [PROJ-56c](PROJ-56c-cron-purge-markierte-mails.md) | 2026-07-04 |
|
||||||
| PROJ-57 | UTF-8-Encoding-Fix für Mails mit Nicht-UTF-8-Charset | Deployed | [PROJ-57](PROJ-57-utf8-encoding-fix.md) | 2026-06-24 |
|
| PROJ-57 | UTF-8-Encoding-Fix für Mails mit Nicht-UTF-8-Charset | Deployed | [PROJ-57](PROJ-57-utf8-encoding-fix.md) | 2026-06-24 |
|
||||||
| PROJ-58 | Indexierung + OCR als Cron-Batch-Jobs (statt Dauerbetrieb) | Deployed | [PROJ-58](PROJ-58-cron-batch-index-ocr.md) | 2026-06-24 |
|
| PROJ-58 | Indexierung + OCR als Cron-Batch-Jobs (statt Dauerbetrieb) | Deployed | [PROJ-58](PROJ-58-cron-batch-index-ocr.md) | 2026-06-24 |
|
||||||
| PROJ-61 | Fix Cross-Tenant Stored XSS via Mandanten-Logo (Sicherheitsbug) | Deployed | [PROJ-61](PROJ-61-fix-tenant-logo-xss-idor.md) | 2026-06-25 |
|
| PROJ-61 | Fix Cross-Tenant Stored XSS via Mandanten-Logo (Sicherheitsbug) | Deployed | [PROJ-61](PROJ-61-fix-tenant-logo-xss-idor.md) | 2026-06-25 |
|
||||||
|
|||||||
@@ -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