From b75288339261db238cfb23d9bb237267dd750314 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 7 May 2026 10:52:14 +0200 Subject: [PATCH] feat: enhance media access and security for exercises - Updated PostgreSQL binding in docker-compose to restrict access to localhost only. - Implemented a new API endpoint for secure media file delivery, requiring authentication via token. - Enhanced governance checks for exercise media access, ensuring only authorized users can retrieve files. - Updated frontend components to utilize the new media file access method, improving user experience while maintaining security. - Documented changes in production readiness audit and access layer endpoint audit for clarity on security enhancements. --- .../working/ACCESS_LAYER_ENDPOINT_AUDIT.md | 2 + .../PRODUCTION_READINESS_AUDIT_2026-05.md | 27 ++++--- backend/main.py | 7 +- backend/routers/clubs.py | 10 ++- backend/routers/exercises.py | 66 +++++++++++++++- backend/routers/maturity_models.py | 4 +- backend/tenant_context.py | 30 +++++++- backend/tests/test_exercise_media_download.py | 75 +++++++++++++++++++ docker-compose.yml | 3 +- frontend/nginx.conf | 2 + .../src/components/ExerciseFullContent.jsx | 16 +--- frontend/src/pages/ExerciseDetailPage.jsx | 16 +--- frontend/src/utils/exerciseMediaUrl.js | 19 +++++ 13 files changed, 235 insertions(+), 42 deletions(-) create mode 100644 backend/tests/test_exercise_media_download.py create mode 100644 frontend/src/utils/exerciseMediaUrl.js diff --git a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md index 1806218..0f8026d 100644 --- a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md +++ b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md @@ -11,6 +11,7 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C. | club_memberships | `/clubs/{id}/members*` | ja | `get_tenant_context` | ja | | | club_join_requests | `/me/club-join-requests`, `/clubs/{id}/join-requests*` | ja | `get_tenant_context` | ja | | | exercises | `PATCH /api/exercises/bulk-metadata` | ja | `get_tenant_context` | ja | Liste: UI-Mehrfachwahl; bis 500 IDs; nur Ersteller oder Plattform-Admin | +| exercises | `GET .../media/{mid}/file` | ja | `get_tenant_context_flexible` | ja (wie Übung lesen) | Datei oder `?ssetoken`; kein anonymes `/media/` ohne ALLOW_PUBLIC_MEDIA_STATIC | | exercises | übrige geschützte `/api/exercises*` | ja | `get_tenant_context` | ja | | | exercise_progression_graphs | `/api/exercise-progression-graphs*` | ja | `get_tenant_context` | Liste wie Bibliothek; Schreiben Ersteller/Plattform-Admin | Kanten: Lesen wenn Graph lesbar | | training_planning | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Vorlagen-Liste wie Übungen; POST Vorlage Default club_id | @@ -32,6 +33,7 @@ Letzte Änderung: 2026-05-06 — MULTI_TENANCY §3 Gap-Analyse aktualisiert; Aud ### Changelog (Fortführung) - **2026-05-07:** Legacy `GET/PUT /api/profile` auf Session-Profil gehärtet; OpenAPI/Health-Ready Produktionsdefaults; Security-Release-Tests + CI-Schritt `security_release_checks.py` — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`. +- **2026-05-07 (Phase 2):** Geschützte Übungs-Mediendatei-API; `/media`-Static optional; Mitgliederverzeichnis E-Mail eingeschränkt; GET maturity-by-id nur Admin; Postgres `127.0.0.1` im Prod-Compose. --- diff --git a/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md b/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md index 24b1c40..79f4491 100644 --- a/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md +++ b/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md @@ -14,9 +14,8 @@ | SEC-01 | `GET /api/profile` (Legacy): ohne Header wurde das **erste Profil der DB** geliefert → IDOR / Datenleck | **Behoben:** Profil immer aus Session | | SEC-02 | OpenAPI `/docs`, `/redoc`, `/openapi.json` in Produktion exponiert | **Behoben:** bei `ENVIRONMENT=production` aus; Override `PUBLIC_OPENAPI=1` | | SEC-03 | `/api/health/ready` mit Tabellendetails / Migrationszähler öffentlich | **Behoben:** in Prod nur kompakte Antwort; Detail via `HEALTH_READY_PUBLIC_DETAIL=1` | -| SEC-04 | Statische Auslieferung `/media` ohne Auth (Link-Leak) | **Geplant** (Phase 2): geschützter Download oder signierte URLs | -| OPS-01 | PostgreSQL-Hostport in `docker-compose.yml` | **Dokumentiert:** nur intern binden / Firewall | -| OPS-02 | Fehlende Security-Header (CSP, X-Frame-Options, …) | **Teilweise:** nginx-Basis-Header ergänzt | +| SEC-04 | Statische Auslieferung `/media` ohne Auth (Link-Leak) | **Behoben:** `GET /api/exercises/.../media/.../file` mit Governance + `ssetoken`; `/media` nur mit `ALLOW_PUBLIC_MEDIA_STATIC=1` | +| OPS-01 | PostgreSQL-Hostport in `docker-compose.yml` | **Behoben:** Bind `127.0.0.1:5434` (nur Host-Local) | ### A.2 Mittel / Konsistenz @@ -24,12 +23,15 @@ |----|--------|--------| | CON-01 | Frontend: direktes `fetch('/api/profiles')` ohne zentralen Client | **Behoben** (AdminCatalogsPage → `api.listProfiles`) | | CON-02 | `ProfileContext.jsx` (unused): falsches Token-Feld / `session` | **Behoben** (`user` + `getCurrentProfile` / `listProfiles`) | -| CON-03 | Sparten (`division`) vs. Zielbild ACCESS_LAYER | **Offen** (Roadmap Stufe D) | +| MEM-01 | Vereinsmitgliederverzeichnis zeigt E-Mail an alle Mitglieder | **Behoben:** E-Mail nur Plattform-Admin / `can_manage_club_org` | +| MAT-01 | `GET /maturity-models/{id}` für jeden Auth-Nutzer | **Behoben:** nur Portal-Admin (UI nutzt ohnehin nur Admin-Panel) | -### A.3 Niedrig +### A.3 Niedrig / Offen | ID | Befund | Status | |----|--------|--------| +| CON-03 | Sparten (`division`) vs. Zielbild ACCESS_LAYER | **Offen** (Roadmap Stufe D) | +| OPS-02 | CSP / restliche Browser-Härtung | **Teilweise** (Basis-Header nginx; CSP offen) | | MISC-01 | `auth.py` Debug-`print` beim Import | **Behoben** (entfernt) | | MISC-02 | `delete_profile`: DELETE auf Mitai-Tabellen schlägt fehl, wenn Tabelle fehlt | **Behoben:** nur löschen, wenn Tabelle existiert | @@ -46,12 +48,12 @@ 5. Profil-Löschung: optionale Tabellen nur bei Existenz. 6. Frontend: Admin-Kataloge Profilliste über `api.js`. -### Phase 2 — Kurzfristig (vor breitem Go-Live) +### Phase 2 — Kurzfristig (vor breitem Go-Live) ✅ Hauptpunkte umgesetzt (2026-05-07) -1. **Medien:** Zugriff an Übungs-/Governance-Rechte koppeln (kein anonymes `/media/...` für sensible Objekte). -2. **Mitgliederverzeichnis:** E-Mail-Sichtbarkeit rollenbasierend oder Richtlinie/DSE. -3. **DB-Port:** Prod-Compose ohne öffentliche DB-Port-Publikation oder strikte Bindung. -4. `GET /api/maturity-models/{id}`: Leserechte mit Admin/Matrix-Politik abstimmen. +1. **Medien:** `GET /api/exercises/{id}/media/{mid}/file` — Auth (`X-Auth-Token` oder `?ssetoken=`), Zugriff wie GET Übung; statisches `/media` standardmäßig aus. +2. **Mitgliederverzeichnis:** E-Mail nur wenn Plattform-Admin oder `can_manage_club_org`. +3. **DB-Port:** Prod-Compose Postgres an `127.0.0.1` gebunden. +4. **`GET /api/maturity-models/{id}`:** nur Portal-Admin (Liste/resolve unverändert für Trainer). ### Phase 3 — Mittelfristig @@ -76,7 +78,12 @@ | `ENVIRONMENT` | `production` / `prod` → OpenAPI aus, Health-Ready kompakt | | `PUBLIC_OPENAPI` | `1` / `true` → OpenAPI trotz Prod einschalten (nur Debugging) | | `HEALTH_READY_PUBLIC_DETAIL` | `1` / `true` → volle Ready-JSON inkl. Tabellenliste in Prod | +| `ALLOW_PUBLIC_MEDIA_STATIC` | `1` / `true` → öffentliches Mount von `/media/` (Notfall/Legacy; Standard: aus) | --- **Pflege:** Nach jeder relevanten Änderung **Status-Spalte** und ggf. **Phase** aktualisieren; bei neuem Audit Datum im Titel anpassen oder Abschnitt „Changelog“ ergänzen. + +### Changelog + +- **2026-05-07:** Phase 2 Medien, Mitgliederverzeichnis E-Mail, maturity GET admin-only, Postgres localhost bind, Tests `test_exercise_media_download.py`. diff --git a/backend/main.py b/backend/main.py index 6ed81ab..64e3652 100644 --- a/backend/main.py +++ b/backend/main.py @@ -205,10 +205,13 @@ app.include_router(matrix_stack_bundle.router) app.include_router(import_wiki.router) app.include_router(import_wiki_admin.router) -# Lokale Medien (Übungen-Uploads) unter MEDIA_ROOT, ausliefern unter /media/... +# Lokale Übungs-Medien: standardmäßig nur über geschützten API-Pfad +# GET /api/exercises/{id}/media/{mid}/file (?ssetoken für /