# Compliance-Implementierung – Umsetzungsbericht **Erstellt:** 2026-05-09 **Zuletzt aktualisiert:** 2026-05-10 **Audit-Basis:** `docs/compliance-audit.md` **App-Version nach Umsetzung:** 0.8.68 --- ## Freigegebene Pakete und Umsetzungsstatus ### P-03 – Papierkorb-Retention-Job aktivieren ✅ **Status:** Umgesetzt **Betroffene Dateien:** - `docker-compose.yml` – neuer Service `retention-cron` **Technische Änderung:** Neuer Docker-Service `retention-cron` nutzt dasselbe Backend-Image und führt `scripts/media_retention_job.py` täglich um 03:00 Uhr aus. Der Service startet beim ersten Hochfahren sofort und schläft bis zum nächsten 3 AM (Python-basierter Loop ohne externe Cron-Abhängigkeit). Zugriff auf DB und Media-Volume identisch zur Backend-Konfiguration. **Tests:** Keine automatisierten Tests möglich (Runtime-Verhalten); operativ über Container-Logs (`docker logs shinkan-retention-cron`) überprüfbar. --- ### P-03b – Retention-Zeiten mit Löschkonzept abgleichen ✅ **Status:** Umgesetzt (2026-05-10) **Betroffene Dateien:** - `backend/media_lifecycle.py:24` – Default `HIDDEN_TO_PURGE_DAYS` - `docker-compose.yml` – explizite Env-Variable im `retention-cron`-Service **Befund des Re-Audits:** Das fachliche Löschkonzept (Audit §6.4) sieht 30 + 30 Tage vor: - Stufe 1 → Stufe 2 (Soft → Hidden): 30 Tage ✓ (war bereits korrekt) - Stufe 2 → Purge (Hidden → gelöscht): **weitere 30 Tage** → Code-Default war 90 Tage ❌ **Technische Änderung:** ```python # media_lifecycle.py Zeile 24 (vorher "90", jetzt "30"): HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS", "30"))) ``` `docker-compose.yml` enthält jetzt explizit `MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS: "${...:-30}"` mit Kommentar, der das 30+30-Konzept dokumentiert. Der Wert kann per Env-Variable in einzelnen Deployments überschrieben werden. --- ### P-04 – Copyright-Pflicht bei Archiv-Promotion vereinheitlichen ✅ **Status:** Umgesetzt; Tests nachgehärtet (2026-05-10) **Betroffene Dateien:** - `backend/routers/media_assets.py` – `patch_media_asset()` und `bulk_media_patch()` - `backend/tests/test_media_assets_copyright_promotion.py` **Technische Änderung:** Beide Endpoints prüfen, ob `copyright_notice` vorhanden ist, wenn `visibility` auf `club` oder `official` gewechselt wird. Priorität: Wert aus dem Request-Body > bestehender Wert in der DB. Ist beides leer, wird HTTP 400 zurückgegeben. Fehlermeldung: `"Für Vereins- oder offizielle Medien ist eine Urheberrechtsangabe (copyright_notice) Pflicht."` (Umlaut korrekt, 2026-05-10 korrigiert) **Tests:** 7 Tests, alle grün: - Promotion zu `club` ohne Copyright → **400** (exakt) - Promotion zu `official` ohne Copyright → **400** (exakt) - Promotion zu `club` mit Copyright im Body → **200** + Payload-Prüfung (gehärtet) - Promotion zu `club`, Copyright bereits auf Asset → **200** + Payload-Prüfung (gehärtet) - Kein Visibility-Wechsel → keine Copyright-Prüfung → **200** (exakt) - Bulk: ohne Copyright → in `failed`, `updated_count == 0` (exakt) - Bulk: mit Copyright → in `updated`, `updated_count == 1` (exakt) --- ### P-05 – Passwort-Mindestlänge angleichen ⚠️ (Teil 1 von 2 – Re-Audit-Auflage offen) **Status:** Teilweise umgesetzt **Betroffene Dateien (initialer Fix 2026-05-09):** - `backend/routers/auth.py:101` – `PUT /api/auth/pin`: `< 4` → `< 8` - `frontend/src/pages/LoginPage.jsx:175` – `minLength="6"` → `minLength="8"` - `frontend/src/pages/AccountSettingsPage.jsx:403` – `minLength={4}` → `minLength={8}` **Verbleibende Lücke identifiziert im Re-Audit 2026-05-09:** `POST /api/auth/reset-password` hatte kein Mindestlängen-Limit → siehe P-05b. --- ### P-05b – reset-password Mindestlänge 8 Zeichen ✅ **Status:** Umgesetzt (2026-05-10) **Betroffene Dateien:** - `backend/models.py:29` – `PasswordResetConfirm.new_password` - `backend/tests/test_auth_password_reset_minlength.py` – 7 neue Tests **Technische Änderung:** ```python # models.py (vorher): class PasswordResetConfirm(BaseModel): token: str new_password: str # models.py (nachher): class PasswordResetConfirm(BaseModel): token: str new_password: str = Field(min_length=8, max_length=128) ``` FastAPI lehnt Requests mit `new_password < 8 Zeichen` nun mit HTTP **422** (Pydantic Validation Error) ab, bevor der Endpoint-Handler ausgeführt wird. Kein DB-Zugriff erfolgt für unvalide Requests. **Tests:** 7 Tests, alle grün: - Leer-String → 422 - 1-Zeichen-Passwort → 422 - 7-Zeichen-Passwort (`"1234567"`) → 422 - Exakt 8 Zeichen (`"12345678"`) → **200** ✓ - Langes Passwort → **200** ✓ - Fehlendes `new_password`-Feld → 422 - Gültiges Passwort, ungültiger Token → 400 **P-05 vollständig geschlossen?** Ja — alle passwortverarbeitenden Backend-Endpoints erzwingen jetzt Mindestlänge 8: | Endpoint | Mindestlänge | Mechanismus | |---|---|---| | `POST /api/auth/register` | 8 | `if len(password) < 8` im Handler | | `PUT /api/auth/pin` | 8 | `if len(new_pin) < 8` im Handler | | `POST /api/auth/reset-password` | 8 | Pydantic `Field(min_length=8)` | | Management-Reset (profiles.py) | 8 | Pydantic `Field(min_length=8)` | | Frontend LoginPage | 8 | `minLength="8"` | | Frontend AccountSettingsPage | 8 | `minLength={8}` | --- ### P-07 – ALLOW_PUBLIC_MEDIA_STATIC Release-Test ✅ **Status:** Umgesetzt **Betroffene Dateien:** - `backend/tests/test_security_release.py` – 2 neue Tests **Tests:** Beide grün. --- ### P-12 – sessionStorage bei Logout bereinigen ✅ **Status:** Umgesetzt (2026-05-10, Version 0.8.68) **Betroffene Dateien:** - `frontend/src/context/AuthContext.jsx` – `logout()` - `tests/dev-smoke-test.spec.js` – neuer Playwright-Test **Befund (war offen):** `logout()` löschte nur `localStorage`-Einträge. `TrainingCoachPage` schrieb sessionStorage-Schlüssel mit Präfix `sj_coach_` (`sj_coach_step_{unitId}`, `sj_coach_deltas_{unitId}`, `sj_coach_debrief_{unitId}`), die nach Logout im Tab erhalten blieben. Bei Nutzerwechsel im selben Tab (geteilter Rechner) konnte der neue Nutzer Trainingsfortschritt des Vorgängers sehen. **Technische Änderung:** Gezielte Präfix-Löschung aller `sj_coach_*`-Schlüssel beim Logout (kein `sessionStorage.clear()`). Fremde sessionStorage-Schlüssel (Browser-Extensions o. ä.) bleiben erhalten. ```javascript // AuthContext.jsx logout() — Ergänzung: for (const key of Object.keys(sessionStorage)) { if (key.startsWith('sj_coach_')) { sessionStorage.removeItem(key) } } ``` **Begründung gezielte statt vollständiger Löschung:** Alle Shinkan-spezifischen sessionStorage-Schlüssel sind eindeutig über den Präfix `sj_coach_` identifizierbar (definiert in `TrainingCoachPage.jsx` Zeilen 15–25). Ein `sessionStorage.clear()` würde auch Schlüssel fremder Quellen im selben Tab löschen; die Präfix-Löschung ist spezifischer und sicherer. **Tests:** Playwright E2E-Test `tests/dev-smoke-test.spec.js` (Test „P-12: sessionStorage wird bei Logout bereinigt"): - Setzt drei `sj_coach_*`-Schlüssel und einen fremden Schlüssel - Klickt „Abmelden" - Prüft: `sj_coach_*` → null (entfernt), fremder Schlüssel → erhalten, `authToken` → null (localStorage weiterhin korrekt bereinigt) **Hinweis Tests:** Das Projekt verfügt über kein Frontend-Unit-Test-Framework (kein Vitest/Jest in package.json). Der Test ist als Playwright E2E-Test implementiert, der einen laufenden Dev-Server voraussetzt. Automatisierte Ausführung der Playwright-Tests erfordert `npx playwright test` mit gesetzter `PLAYWRIGHT_BASE_URL`. --- ### P-23 – LoginPage: minLength + Versionsstring ✅ **Status:** Umgesetzt **Betroffene Dateien:** - `frontend/src/pages/LoginPage.jsx` **Technische Änderung:** - `minLength="6"` → `minLength="8"` - Hardcodierter Versionsstring `v0.1.0 • Development` entfernt --- ### P-24 – CORS allow_methods und allow_headers einschränken ✅ **Status:** Umgesetzt **Betroffene Dateien:** - `backend/main.py:85-86` **Technische Änderung:** ```python allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "X-Auth-Token", "X-Active-Club-Id"], ``` --- ## Test-Zusammenfassung (Stand 0.8.67) ``` tests/test_auth_password_reset_minlength.py 7 passed (neu, P-05b) tests/test_media_assets_copyright_promotion.py 7 passed (gehärtet, P-04) tests/test_security_release.py 9 passed (inkl. 2 P-07-Tests) Weitere bestehende Tests: 81 passed, 6 skipped Gesamt: 104 passed, 6 skipped, 1 failed Fehlgeschlagener Test: test_list_media_assets_invalid_lifecycle_400 → Pre-existing: benötigt laufenden PostgreSQL-Container (Hostname "postgres") → Bestand bereits vor allen Compliance-Änderungen (verifiziert per git stash) ``` --- ## Nicht umgesetzte Pakete > Paket-IDs und -Titel gemäß kanonischem Register `docs/compliance-package-register.md`. > Abweichende Beschreibungen in der Ursprungsversion dieses Abschnitts wurden am 2026-05-10 korrigiert (P-06, P-08, P-09, P-10, P-11, P-18 — Details im Konsistenzbericht des Registers). | Paket | Kanonischer Titel | Status | Begründung | |-------|------------------|--------|------------| | P-01 | Rechtstexte | offen | Scope ausgeschlossen (juristischer Inhalt) | | P-02 | Self-Service-Kontolöschung + Datenexport | offen | Scope ausgeschlossen | | P-06 | Upload-Einwilligungsdialog (Recht am eigenen Bild) | offen | Scope ausgeschlossen | | P-08 | HSTS / externe Proxy-Sicherheit dokumentieren | offen | Scope ausgeschlossen (außerhalb Repo — Reverse-Proxy) | | P-09 | Admin-Audit-Log | offen | Scope ausgeschlossen | | P-10 | Mindestalter-Abfrage | offen | Scope ausgeschlossen | | P-11 | Legal-Hold Lifecycle-Status | offen | Scope ausgeschlossen | | P-12 | sessionStorage bei Logout bereinigen | ✅ umgesetzt | Version 0.8.68 — siehe §P-12 oben | | P-13 | Content-Melde-Backend | offen | Scope ausgeschlossen (erst juristisch klären) | | P-14 | Moderations-UI | offen | Scope ausgeschlossen | | P-15 | Uploader-Benachrichtigung bei Sperrung | offen | Scope ausgeschlossen | | P-16 | Beschwerdeverfahren | offen | Scope ausgeschlossen | | P-17 | MFA für Superadmins (TOTP) | offen | Scope ausgeschlossen | | P-18 | HttpOnly-Cookie als Auth-Alternative | offen | Scope ausgeschlossen | | P-19 | Anti-Virus-Scan (ClamAV) | offen | Scope ausgeschlossen | | P-20 | VVT erstellen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) | | P-21 | AV-Verträge abschließen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) | | P-22 | HTML-Sanitizer für Rich-Text-Felder | offen | Scope ausgeschlossen | --- ## Re-Audit-Empfehlung Operativ nach Deployment 0.8.67 prüfen: 1. **P-03/P-03b**: `docker logs shinkan-retention-cron` — Job läuft täglich 03:00 Uhr; Retention-Zeiten: 30 → 30 Tage 2. **P-04**: Manuell: PATCH privates Medium auf `official` ohne `copyright_notice` → muss 400 liefern 3. **P-05b**: Manuell: Reset-Link mit 7-Zeichen-Passwort → muss mit Fehler abgewiesen werden 4. **P-24**: Browser DevTools Preflight → `Access-Control-Allow-Headers: content-type, x-auth-token, x-active-club-id` Nächster vollständiger Re-Audit nach Umsetzung der kritischen Findings (P-01: Impressum/Datenschutz, P-02: DSGVO-Löschanfragen).