- Adjusted retention policy to align with compliance requirements: - Changed HIDDEN_TO_PURGE_DAYS from 90 to 30 days. - Enhanced password reset functionality to enforce a minimum password length of 8 characters. - Updated tests to validate new password requirements and retention logic. - Corrected umlaut in copyright error messages for clarity.
9.3 KiB
Compliance-Implementierung – Umsetzungsbericht
Erstellt: 2026-05-09
Zuletzt aktualisiert: 2026-05-10
Audit-Basis: docs/compliance-audit.md
App-Version nach Umsetzung: 0.8.67
Freigegebene Pakete und Umsetzungsstatus
P-03 – Papierkorb-Retention-Job aktivieren ✅
Status: Umgesetzt
Betroffene Dateien:
docker-compose.yml– neuer Serviceretention-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– DefaultHIDDEN_TO_PURGE_DAYSdocker-compose.yml– explizite Env-Variable imretention-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:
# 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()undbulk_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
clubohne Copyright → 400 (exakt) - Promotion zu
officialohne Copyright → 400 (exakt) - Promotion zu
clubmit 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→< 8frontend/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_passwordbackend/tests/test_auth_password_reset_minlength.py– 7 neue Tests
Technische Änderung:
# 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: Nicht umgesetzt — weiterhin offen (MITT-05)
Befund:
logout() in frontend/src/context/AuthContext.jsx löscht nur localStorage-Einträge:
const logout = () => {
setUser(null)
localStorage.removeItem('authToken')
localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY)
// sessionStorage wird NICHT geleert
}
TrainingCoachPage schreibt folgende Schlüssel in sessionStorage:
storageStepKey(unitId)– aktueller Trainingsschritt (Zahl)storageDeltasKey(unitId)– Trainingsdeltas (JSON)storageDebriefKey(unitId)– Debrief-Status (Boolean)
Nach einem Logout bleiben diese Daten im sessionStorage des Tabs erhalten. Bei einem Nutzerwechsel im selben Tab (geteilter Rechner) kann der neue Nutzer Trainingsfortschritt des Vorgängers sehen, bis der Tab geschlossen wird.
Risikoeinstufung: MITT-05 (mittleres Risiko; sessionStorage ist tab-lokal und wird beim Tab-Schließen gelöscht; erfordert physischen Zugang zum selben offenen Tab)
Fix (nicht durchgeführt, ca. 15 Minuten Aufwand):
// AuthContext.jsx logout():
const logout = () => {
setUser(null)
localStorage.removeItem('authToken')
localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY)
sessionStorage.clear() // oder gezielt nach 'shinkan_coach_' prefix
}
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 • Developmententfernt
P-24 – CORS allow_methods und allow_headers einschränken ✅
Status: Umgesetzt
Betroffene Dateien:
backend/main.py:85-86
Technische Änderung:
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 | Status | Begründung |
|---|---|---|
| P-01 | offen | Rechtstexte — Scope ausgeschlossen |
| P-02 | offen | Self-Service-Löschworkflow — Scope ausgeschlossen |
| P-06 | offen | HSTS-Header — Scope ausgeschlossen |
| P-09 | offen | Kein Einwilligungsdialog Recht am eigenen Bild — Scope ausgeschlossen |
| P-10 | offen | DSA-Meldeverfahren — Scope ausgeschlossen |
| P-11 | offen | HttpOnly-Cookie-Migration — Scope ausgeschlossen |
| P-12 | offen | sessionStorage bei Logout — nicht freigegeben, Aufwand ca. 15 min |
| P-13–P-16 | offen | Scope ausgeschlossen |
Re-Audit-Empfehlung
Operativ nach Deployment 0.8.67 prüfen:
- P-03/P-03b:
docker logs shinkan-retention-cron— Job läuft täglich 03:00 Uhr; Retention-Zeiten: 30 → 30 Tage - P-04: Manuell: PATCH privates Medium auf
officialohnecopyright_notice→ muss 400 liefern - P-05b: Manuell: Reset-Link mit 7-Zeichen-Passwort → muss mit Fehler abgewiesen werden
- 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).