Files
kaffeekasse-saas/docs/m8-haertung.md
T
clemensandClaude Sonnet 5 f60c0bb85e M8: Loesch-/Anonymisierungsprozess fuer Teilnehmer und Mandanten
Zwei getrennte Flows:

- Teilnehmer anonymisieren statt loeschen (ledger_anonymize_participant):
  Name/E-Mail/PayPal-Name werden durch einen Platzhalter ersetzt, das
  Mitglied deaktiviert und vom Login-Konto getrennt. Buchungshistorie
  bleibt fuer die Kassenfuehrung erhalten, konsistent mit dem
  Storno-statt-Delete-Prinzip. Default-Mandant spiegelt die
  Anonymisierung in kl_Mitarbeiter (Email dort NOT NULL UNIQUE, bekommt
  Platzhalter statt NULL). Inhaber kann nicht anonymisiert werden.
- Mandant vollstaendig loeschen (mandant-loeschen.php, nur Inhaber):
  erfordert exakte Eingabe des Kundenkuerzels, loescht die tenants-Zeile;
  alle tenant-scoped Tabellen kaskadieren per Fremdschluessel. users
  bleiben bestehen (koennen zu mehreren Mandanten gehoeren). Der
  migrierte Default-Mandant ist ausgenommen, da seine kl_Mitarbeiter-
  Historie sonst verwaisen wuerde.

Live getestet: Mitglied mit Buchungshistorie anonymisiert (Historie
blieb erhalten), Mandantenloeschung mit falscher/richtiger Bestaetigung
geprueft, vollstaendiger Cascade-Delete ueber alle tenant-scoped Tabellen
verifiziert, globale users-Zeile bleibt korrekt erhalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:17:57 +02:00

211 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M8 Härtung, Datenschutz und Betrieb
Stand: 2026-07-15
M8 macht die SaaS-App für den mehrmandantenfähigen Betrieb sicherer und
nachvollziehbarer: Security-Headers, Rate-Limits, ein zentrales Audit-Log,
sowie (folgend) Mandanten-Isolation, Rollenmatrix, Datenexport und
Löschung/Anonymisierung.
## Security-Headers
Umgesetzte Dateien:
```text
app/bootstrap.php
landing.php
```
- `app_send_security_headers()` setzt `X-Content-Type-Options: nosniff`,
`X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`,
`Permissions-Policy` (Geolocation/Mikrofon/Kamera aus) sowie `Strict-
Transport-Security` bei HTTPS. Der Aufruf steht am Ende von
`app/bootstrap.php`, das transitiv von jeder dynamischen Seite geladen
wird, und läuft damit automatisch einmal pro Request.
- Bewusst **kein** Content-Security-Policy-Header: Die bestehenden
Templates nutzen durchgängig `style=""`-Inline-Attribute (Banner,
Tabellenzellen, Status-Einfärbung). Ein CSP-Lockdown würde diese
brechen und bräuchte einen eigenen Template-Durchgang als offener
Punkt vermerkt, nicht in diesem Schritt umgesetzt.
- `landing.php` war die einzige Seite ganz ohne PHP (reines HTML). Ein
minimaler `require_once app/bootstrap.php`-Aufruf am Dateianfang sorgt
dafür, dass auch sie die Header bekommt, ohne das Markup zu verändern.
## Rate-Limits
Umgesetzte Dateien:
```text
database/migrations/0010_saas_rate_limits.sql
app/rate-limit.php
login.php
register.php
passwort-vergessen.php
```
- Neue Tabelle `rate_limit_attempts` (Bucket + Zeitstempel) mit
`app_rate_limit_check()`: schreibt einen Versuch, räumt alte Einträge
für denselben Bucket auf und meldet, ob das Limit noch eingehalten ist.
- Login: 10 Versuche / 15 Minuten je E-Mail-Adresse **und** 20 Versuche /
15 Minuten je IP (beide müssen greifen, damit ein einzelnes Konto nicht
gezielt von verteilten IPs aus brute-forced werden kann und eine IP
nicht viele Konten gleichzeitig durchprobieren kann).
- Registrierung: 5 Versuche / Stunde je IP (gegen Spam-Registrierungen).
- Passwort-Reset-Anfrage: 5 / Stunde je E-Mail, 10 / Stunde je IP. Bei
überschrittenem Limit erscheint bewusst dieselbe generische
Erfolgsmeldung wie im echten Erfolgsfall, damit ein ausgereiztes Limit
nicht zusätzlich verrät, ob ein Konto existiert.
- Live getestet: 11 aufeinanderfolgende Fehlversuche gegen `login.php`
die ersten 10 zeigen „ungültig“, der 11. wird korrekt mit „Zu viele
Anmeldeversuche“ abgewiesen.
## Audit-Log
Umgesetzte Dateien:
```text
database/migrations/0011_saas_audit_log.sql
app/audit.php
mitarbeiterverwalten.php
letzteneintraege.php
mandant-einstellungen.php
hinweise.php
csvupload.php
jahresauswertung.php
mailversenden.php
```
- Neue Tabelle `audit_log` (Mandant, ausführender Nutzer, Aktion,
betroffener Datensatz, Metadaten als JSON, IP, Zeitpunkt) gemäß
Kernschema aus dem Umstrukturierungsplan.
- `app_audit_log()` schreibt einen Eintrag, `app_fetch_audit_log()` liest
die letzten N Einträge eines Mandanten.
- Protokollierte Aktionen: Mitglied anlegen/bearbeiten/(de)aktivieren,
Zugang gewähren/entziehen, Storno von Einzahlung/Strich, Mandant-
Einstellungen ändern, Hinweis anlegen/löschen, CSV-Import bestätigen,
Jahresbonus-Verteilung, Live-Mailversand.
- Sichtbar für Owner/Admin auf `mandant-einstellungen.php` unter
„Protokoll" (letzte 50 Einträge, mit Namen des ausführenden Nutzers).
- Live getestet: Hinweis anlegen erzeugt einen Log-Eintrag mit korrekten
Metadaten; über einen eigens angelegten Test-Mandanten geprüft, dass
eine Einstellungsänderung im UI-Protokoll mit korrektem Nutzernamen
erscheint. Testdaten anschließend entfernt.
## Mandanten-Isolation (automatisiert)
Umgesetzte Datei: `scripts/check-m8-tenant-isolation.php`
Legt zwei frische, isolierte Test-Mandanten mit je einem Teilnehmer, einer
Ledger-Buchung, einem Hinweis und einem Audit-Log-Eintrag an und prüft
10 Fälle: Teilnehmerlisten, Einzelabruf, letzte Buchungen, aktive Hinweise,
Schreibversuche (Buchung, Zugangsvergabe, Storno) und Audit-Log sind
strikt auf den jeweils richtigen Mandanten beschränkt; ein Zugriff über den
falschen Mandanten liefert nichts beziehungsweise schlägt sauber fehl statt
fremde Daten zurückzugeben. Räumt die Testdaten am Ende selbst auf.
Ergebnis: grün mit 10 Assertions.
## Rollenmatrix (automatisiert)
Umgesetzte Datei: `scripts/check-m8-role-matrix.php`
Legt einen Test-Mandanten mit je einem Nutzer pro Rolle (`owner`, `admin`,
`treasurer`, `member`, `viewer`) an, loggt sich für jede Rolle per echtem
HTTP-Request ein (manueller Cookie-Jar über `file_get_contents`, da diese
PHP-Installation keine curl-Extension hat) und ruft alle rollen-geschützten
Seiten auf: `kaffeeliste.php`, `mitarbeiterverwalten.php`, `hinweise.php`,
`stricheintragen.php`, `einzahlung.php`, `letzteneintraege.php`,
`csvupload.php`, `exportKaffeeliste.php`, `mailversenden.php`,
`jahresauswertung.php`, `mandant-einstellungen.php`. Für jede
Rolle-Seite-Kombination wird geprüft, ob der tatsächliche Zugriff (anhand
der „Kein Zugriff"/„keine Berechtigung"-Marker in der Antwort) mit der
erwarteten Rollenliste übereinstimmt.
Ergebnis: grün mit 55 Assertions (5 Rollen × 11 Seiten). Räumt die
Testdaten am Ende selbst auf.
## Datenexport pro Mandant
Umgesetzte Dateien:
```text
app/data-export.php
datenexport.php
konto.php
scripts/http-smoke.php
```
- Neue Seite `datenexport.php` (Owner/Admin, echte SaaS-Session nötig)
lädt einen vollständigen JSON-Export des eigenen Mandanten herunter:
Tenant-Stammdaten, Einstellungen, Teilnehmer, Mitglieder mit Rolle,
alle Ledger-Buchungen, Hinweise, CSV-Importe (Batches und Zeilen),
Mail-Versandlog und Admin-Protokoll.
- Passwort-Hashes und Token-Hashes werden bewusst nicht exportiert.
- Der Export selbst wird im Audit-Log protokolliert
(`tenant_data.exported`).
- Link von `konto.php` aus neben „Mandant-Einstellungen".
- Live getestet: eigens angelegter Test-Mandant, Export heruntergeladen,
Header (`Content-Type`, `Content-Disposition`) und JSON-Struktur
geprüft, verifiziert dass keine Passwörter enthalten sind. Testdaten
anschließend entfernt.
## Lösch-/Anonymisierungsprozess
Umgesetzte Dateien:
```text
app/ledger.php (ledger_anonymize_participant)
mitarbeiterverwalten.php
mandant-loeschen.php
konto.php
scripts/http-smoke.php
```
Zwei getrennte Flows, je nachdem was gelöscht werden soll:
- **Teilnehmer anonymisieren** statt löschen: Name, E-Mail und PayPal-Name
werden durch einen nicht-identifizierenden Platzhalter ersetzt, das
Mitglied wird deaktiviert und vom Login-Konto getrennt
(`ledger_anonymize_participant()`). Die Ledger-Historie (Buchungen)
bleibt für die Kassenführung erhalten konsistent mit dem
„keine harten Deletes"-Prinzip für Buchungen. Für den Default-Mandanten
wird die verknüpfte `kl_Mitarbeiter`-Zeile mitanonymisiert (`Email` ist
dort NOT NULL UNIQUE, bekommt also einen Platzhalter statt NULL). Der
Inhaber kann nicht anonymisiert werden. Button „Anonymisieren" mit
JS-Bestätigungsdialog in `mitarbeiterverwalten.php`.
- **Mandant vollständig löschen**: neue Seite `mandant-loeschen.php`
(nur Inhaber). Erfordert die exakte Eingabe des Kundenkürzels zur
Bestätigung. Löscht die `tenants`-Zeile; alle tenant-scoped Tabellen
(`participants`, `ledger_entries`, `notices`, `tenant_memberships`,
`audit_log`, `outbound_emails`, `payment_import_batches`/`_rows`,
`tenant_settings`) sind per Fremdschlüssel `ON DELETE CASCADE`
verknüpft und verschwinden automatisch mit. `users` bleiben bestehen,
da ein Login-Konto zu mehreren Mandanten gehören kann. Der migrierte
Default-Mandant ist von dieser Selbstbedienungs-Löschung ausgenommen
(seine `kl_Mitarbeiter`-Historie hat keinen Fremdschlüssel zu `tenants`
und würde verwaisen); dafür verweist die Seite an den Betreiber.
- Beide Aktionen werden auditiert (`participant.anonymized`; die
Mandantenlöschung selbst nicht, da mit ihr auch das Audit-Log dieses
Mandanten verschwindet das ist beabsichtigt, echte Löschung soll
keine Spuren hinterlassen).
Live getestet: Mitglied mit Buchungshistorie angelegt, anonymisiert
Name/E-Mail weg, Jahresstriche unverändert erhalten. Mandantenlöschung mit
falscher Bestätigung abgewiesen, mit korrekter Bestätigung vollständig
durchgeführt; anschließend geprüft, dass wirklich alle tenant-scoped
Tabellen leer sind und die globale `users`-Zeile erhalten bleibt.
## Prüfstatus
- Golden Master: grün mit 104 Assertions.
- M4 Ledger-Migration: grün mit 73 Assertions.
- M3 Settings-Flow: grün mit 13 Assertions.
- HTTP-Smoke: grün mit 28 geprüften Seiten.
- M8 Mandanten-Isolation: grün mit 10 Assertions.
- M8 Rollenmatrix: grün mit 55 Assertions.
## Noch offen in M8
- Content-Security-Policy (braucht Template-Bereinigung der Inline-Styles).
- Backup-/Restore-Prozess und Monitoring/Fehlerlogging dokumentieren.