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>
166 lines
6.6 KiB
Markdown
166 lines
6.6 KiB
Markdown
# 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.
|