All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Successful in 26s
- 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.
248 lines
9.3 KiB
Markdown
248 lines
9.3 KiB
Markdown
# 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 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:** Nicht umgesetzt — weiterhin offen (MITT-05)
|
||
|
||
**Befund:**
|
||
`logout()` in `frontend/src/context/AuthContext.jsx` löscht nur `localStorage`-Einträge:
|
||
```javascript
|
||
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):**
|
||
```javascript
|
||
// 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 • 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 | 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:
|
||
|
||
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).
|