Files
kaffeekasse-saas/docs/m8-haertung.md
T
clemensandClaude Sonnet 5 f3045dbab9 M8: Datenexport pro Mandant
Neue Seite datenexport.php (Owner/Admin) laedt einen vollstaendigen
JSON-Export des eigenen Mandanten herunter: Stammdaten, Einstellungen,
Teilnehmer, Mitglieder mit Rolle, alle Ledger-Buchungen, Hinweise,
CSV-Importe, Mail-Versandlog und Admin-Protokoll. Passwort- und
Token-Hashes werden bewusst nicht exportiert; der Export selbst wird im
Audit-Log protokolliert. Link von konto.php aus.

Live getestet: eigens angelegter Test-Mandant, Export heruntergeladen,
Header und JSON-Struktur geprueft, keine Passwoerter im Export gefunden.

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

166 lines
6.6 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.
## 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 27 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).
- Lösch-/Anonymisierungsprozess für Teilnehmer und Kunden.
- Backup-/Restore-Prozess und Monitoring/Fehlerlogging dokumentieren.