feat: Löschfristen je Datenklasse, Passwort-Zurücksetzen, DPF-Recherche

- Löschfristen (Migration 0020): pro Mandant einstellbar statt fest im
  Regelwerk, da die DSGVO selbst keine festen Fristen nennt (Art. 5
  Abs. 1 lit. e). Jede Firma wird mit risikogestaffelten Vorschlagswerten
  vorbelegt, loeschfrist_max_tage wird jetzt tatsächlich hart gegen
  Werkzeuge gefiltert (schließt rules/OPEN.md Punkt 4).
- Neues internal/mail-Paket (SMTP-Versand + Test-Doppel) und darauf
  aufbauend Passwort-Zurücksetzen (Migration 0019) - bisher nur als
  Absicht in der Rollentabelle genannt, nie gebaut.
- dpf_zertifiziert für alle 20 Katalogeinträge gegen das offizielle
  DPF-Register recherchiert und in der Produktions-DB aktualisiert
  (12 zertifiziert, 5 recherchiert-nicht-gefunden, 3 nicht anwendbar).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-09-01 07:38:12 +02:00
parent 690660b655
commit 8a8295dacd
30 changed files with 1120 additions and 63 deletions

134
CLAUDE.md
View File

@@ -377,12 +377,25 @@ Das öffnet jetzt auch DeepSeek (Verarbeitung in China) für einen
künftigen Katalogeintrag — die Sperre lag ausschließlich am alten
Wertebereich, nicht an einer fachlichen Entscheidung.
**Offen:** `dpf_zertifiziert` ist bei allen 19 Einträgen noch `false`
für keinen wurde bisher gezielt recherchiert, ob der jeweilige
US-Anbieter tatsächlich DPF-zertifiziert ist (das Feld wurde erst mit
dieser Migration eingeführt, `false` heißt hier "nicht recherchiert",
nicht "nachweislich nicht zertifiziert"). Nachrecherche pro Anbieter
noch zu tun.
**`dpf_zertifiziert` recherchiert (2026-09-01), Offener Punkt erledigt.**
Für alle 20 Einträge wurde gezielt gegen das offizielle Register
(dataprivacyframework.gov, per Participant-ID einzeln nachvollziehbar)
geprüft — nicht gegen die JS-Suchseite selbst (nicht scrapebar), sondern
gegen das dahinterliegende, von ihr selbst genutzte API. Ergebnis: 12
zertifiziert (Microsoft 365 Copilot, GitHub Copilot Business, Google
Gemini, Perplexity Enterprise Pro, Adobe Firefly, Otter.ai Business,
Notion AI, Grammarly Business, ElevenLabs, Zendesk AI, Canva Magic
Studio, Zapier), 5 recherchiert aber nicht im Register gefunden
(ChatGPT Enterprise, Claude for Work, Claude Code, Midjourney, Julius
AI — `false` heißt hier jetzt "geprüft, kein Eintrag gefunden", nicht
mehr "nicht recherchiert"), 3 nicht anwendbar (DeepL, Le Chat/Mistral,
Synthesia — reine EU-Verarbeitung ohne US-Rechtsträger im Register, DPF
regelt EU→USA-Transfers). Besonderheiten als `einschraenkungen`-Eintrag
je Werkzeug festgehalten: Perplexity hat nur EU-US/UK-, keine
Swiss-US-Zertifizierung; Notion AIs Zertifizierung ist als "Active" mit
bereits abgelaufenem Datum in Re-Zertifizierung; Grammarly ist im
Register unter dem fusionierten Rechtsträger "Superhuman Platform Inc."
gelistet, nicht mehr unter "Grammarly, Inc.".
`CurrentKatalogVersion` liefert eine reproduzierbare Kennung des
aktuellen Katalogzustands (Anzahl Einträge + letzte Änderung) — wird in
@@ -452,8 +465,11 @@ Genehmigungen erhalten ein Ablaufdatum (`gueltig_bis`: 12 Monate, bei
**Schritt 7 (Wiedervorlage, Katalog-Benachrichtigung) ist umgesetzt**
(`internal/web/wiedervorlage_handlers.go`, `GET /wiedervorlage`, Ebene
3). Es gibt keine E-Mail-Infrastruktur — "Benachrichtigung" ist bewusst
eine In-App-Liste, kein Versand. Eine aktive Genehmigung erscheint dort,
3). Zum Zeitpunkt dieser Umsetzung gab es noch keine E-Mail-Infrastruktur
(die kam erst 2026-09-01 mit `internal/mail`, siehe „Passwort-
Zurücksetzen und E-Mail-Versand" weiter unten, bisher nur für
Passwort-Zurücksetzen genutzt) — "Benachrichtigung" ist deshalb bewusst
weiterhin eine In-App-Liste, kein E-Mail-Versand. Eine aktive Genehmigung erscheint dort,
wenn mindestens einer zutrifft: (1) `gueltig_bis` ist erreicht oder
liegt innerhalb von 30 Tagen, (2) das zugesagte Werkzeug wurde aus dem
Katalog entfernt, (3) `werkzeugDiff` erkennt eine Abweichung zwischen
@@ -617,6 +633,83 @@ und live per curl gegen echten Server + Postgres verifiziert.
---
## Löschfristen je Datenklasse (2026-09-01, Migration 0020)
Schließt `rules/OPEN.md`, Punkt 4. Recherche ergab: die DSGVO nennt
**keine** festen Speicherfristen (Art. 5 Abs. 1 lit. e verlangt nur "so
lange wie für den Zweck nötig") — es gibt also keinen fest im Regelwerk
hinterlegbaren "richtigen" Zahlenwert. Lösung: **pro Mandant einstellbar**
(`loeschfrist_einstellung`, `GET/POST /verwaltung/loeschfristen`,
admin-only wie jede andere `/verwaltung/*`-Seite — bewusst NICHT an den
Namen der Genehmiger-Rolle "Datenschutzbeauftragter" gekoppelt, siehe
Freigabe-Workflow oben: Genehmiger-Rollen sind explizit ohne eigenes
Zugriffsrecht). Jede neue Firma wird mit risikogestaffelten
Vorschlagswerten vorbelegt (`seedStandardLoeschfristen`, aufgerufen wie
`seedStandardGenehmigerRollen` direkt nach `CreateAccount`):
`oeffentlich` 365 / `intern` 180 / `auftragsdaten` 90 / `personenbezogen`
90 / `berufsgeheimnis` 30 / `besondere_kategorie` 30 Tage — eigene
Einschätzung nach Sensibilität gestaffelt, **keine Rechtsvorgabe**, frei
editierbar (Formular-Hinweistext sagt das ausdrücklich).
**Harte Filterung jetzt aktiv** (vorher, siehe `rules/OPEN.md` Punkt 4
alt, wurde `loeschfrist_max_tage` gar nicht geprüft): `ErfuelltAnforderung`
und `FilterWerkzeuge` (`internal/rules/evaluate.go`) haben einen neuen
Parameter `loeschfristMaxTage *int`. `nil` (keine Frist konfiguriert)
verhält sich wie vorher (nicht gefiltert) — Rückwärtskompatibilität.
Ist eine Frist gesetzt, erfüllt ein Werkzeug ohne bezifferte
Aufbewahrungsdauer (`werkzeug.aufbewahrung_tage IS NULL`) sie NICHT
(fail closed, analog zur leeren Länderliste bei `eu_verarbeitung`) —
dafür wurde `rules.WerkzeugEigenschaften.AufbewahrungTage` von `int` auf
`*int` geändert, sonst wäre "unbekannt" nicht von "0 Tage" unterscheidbar
gewesen. `internal/web/antrag_handlers.go:bewerteAntrag` liest die
Einstellung für die aktuelle Datenklasse des Antrags aus
`ListLoeschfristEinstellungenForAccount` und reicht sie durch.
---
## Passwort-Zurücksetzen und E-Mail-Versand (2026-09-01)
Neues Paket `internal/mail`: `Mailer`-Interface (`Send(to, subject, body)
error`), `SMTPMailer` als echte Implementierung (stdlib `net/smtp`,
STARTTLS falls vom Server angeboten — deckt den Standard-Submission-Port
587 ab, implizites TLS auf Port 465 wird von `net/smtp` nicht
unterstützt) und `FakeMailer` als Test-Doppel (sammelt gesendete Mails
statt sie zu verschicken). Konfiguration über `SMTP_HOST`/`SMTP_PORT`/
`SMTP_USER`/`SMTP_PASSWORD`/`SMTP_FROM` (alle optional — ohne
`SMTP_HOST` startet der Dienst normal, nur der tatsächliche Versand
schlägt dann mit einem klaren Fehler fehl statt einer nie ankommenden
Mail vorzutäuschen; bewusst kein stiller No-op).
**Erste Anwendung: Passwort-Zurücksetzen** (Ebene 1, öffentlich, bisher
in der Rollentabelle nur als Absicht genannt, nie gebaut). `GET/POST
/passwort-vergessen` nimmt eine E-Mail-Adresse entgegen und zeigt
**immer** dieselbe Erfolgsmeldung ("Falls diese E-Mail-Adresse bei uns
registriert ist, wurde ein Link verschickt") — unabhängig davon, ob die
Adresse existiert, aktiv ist, oder der Versand tatsächlich geklappt hat.
Das ist derselbe User-Enumeration-Schutz wie beim Login (identische
Fehlermeldung bei falscher E-Mail/falschem Passwort); ein Versandfehler
wird serverseitig geloggt (`log.Printf`, nicht verschluckt), aber nicht
an den Client durchgereicht. `password_reset_token` (Migration 0019):
ein Token ist 1 Stunde gültig, genau einmal verwendbar (`used_at`),
`GetValidPasswordResetToken` liefert für "nicht gefunden", "abgelaufen"
und "schon verwendet" identisch `ErrNotFound` — auch hier kein
Unterschied, der einem Angreifer etwas verraten könnte. `GET/POST
/passwort-zuruecksetzen/{token}` prüft den Token, setzt bei Erfolg das
neue Passwort (`store.SetUserPassword`) und verbraucht den Token.
**Live end-to-end verifiziert** — nicht nur gegen `mail.FakeMailer` in
Tests, sondern gegen einen echten, minimalen SMTP-Server (selbst
geschriebener TCP-Listener, der die EHLO/MAIL FROM/RCPT TO/DATA-Sequenz
korrekt beantwortet): Registrierung → Anfrage → tatsächlich empfangene
Mail mit funktionierendem Link → Formular → neues Passwort → Login mit
altem Passwort abgelehnt, mit neuem Passwort erfolgreich. Dabei einen
echten Bug gefunden und behoben: der Versandfehler wurde zunächst mit
`_ = s.mailer.Send(...)` still verschluckt (Verstoß gegen die
"keine stillen Fallbacks"-Regel) — beim Debuggen selbst schmerzhaft
aufgefallen, jetzt geloggt statt verworfen.
---
## Onboarding
**Firma (Ebene 1, öffentlich, `POST /register`):** Registrierungsformular
@@ -1193,8 +1286,9 @@ journalctl -u deklarix -f
`kennzeichnungspflicht`/`dsfa_erforderlich` nur als Auflage vermerkt)
— weiterhin nicht ausdrücklich bestätigt, aber plausibel, keine
Rückmeldung dazu bisher eingeholt.
- **Löschfristen je Datenklasse** (konkrete Tageswerte für
`loeschfrist_max_tage`) — noch nicht fachlich festgelegt.
- ~~Löschfristen je Datenklasse~~ — **erledigt 2026-09-01**, siehe
„Löschfristen je Datenklasse" weiter oben: pro Mandant einstellbar
statt hartkodiert, da die DSGVO selbst keine festen Fristen nennt.
- **Mandantenisolation auf Datenbankebene (Postgres Row-Level Security)**
— die Frontend-Spezifikation fordert das explizit ("nicht in der
Anwendungslogik"), aktuell ist Isolation ausschließlich
@@ -1216,15 +1310,13 @@ journalctl -u deklarix -f
"sauberer" Lösung — falls künftig eine Firma admin und verantwortlicher
bewusst auf zwei verschiedene Personen verteilen will, funktioniert das
weiterhin unverändert (zwei separate Logins mit den jeweiligen Rollen).
- **Zentraler Werkzeugkatalog muss noch befüllt werden**die
Ebene-5/Betreiber-UI zur Katalogpflege ist jetzt gebaut (`GET
/betreiber/werkzeuge` Liste, `GET/POST /betreiber/werkzeuge/neu`
anlegen, `GET/POST /betreiber/werkzeuge/{id}` bearbeiten, `POST
.../loeschen` löschen — siehe `internal/web/betreiber_werkzeug_handlers.go`.
Bearbeitet ausschließlich `account_id IS NULL`-Einträge, ein
mandantenspezifischer Eintrag ist über diese Seiten nicht erreichbar
[404]). "Werkzeugkatalog aus der Plattform vorbelegt" beim Firma-
Onboarding bleibt trotzdem nicht automatisiert — der Katalog ist
inhaltlich weiterhin leer, bis der Betreiber tatsächlich Einträge
pflegt.
- ~~Zentraler Werkzeugkatalog muss noch befüllt werden~~**erledigt**,
20 recherchierte Einträge seit 2026-08-31 (siehe „Werkzeugkatalog"
weiter oben). ~~`dpf_zertifiziert` recherchieren~~ — **erledigt
2026-09-01** (siehe dort für Details je Anbieter). "Werkzeugkatalog aus
der Plattform vorbelegt" beim Firma-Onboarding bleibt weiterhin nicht
automatisiert — jede neue Firma sieht sofort den vollen zentralen
Katalog (kein Kopiervorgang nötig, `account_id IS NULL` ist für alle
Mandanten gleich sichtbar), es gibt nur keine mandantenspezifische
Vorauswahl/Kuratierung beim Onboarding.
- Markenanmeldung Deklarix, sobald erster Kunde zahlt.