feat(backend): Master-Passwort fuer die Dokumentenliste (Schritt 5)

GET /documents listet alle Dokumente und machte damit jede UUID auffindbar —
das Zugriffsmodell "unerratbarer Link" waere hinfaellig. Der Endpunkt
verlangt jetzt HTTP Basic; alles andere bleibt bewusst offen.

Ohne konfigurierten Hash ist die Liste ausdruecklich gesperrt (denyAll),
nicht offen: Ein vergessener Umgebungswert gaebe sonst jede UUID preis, und
niemandem fiele es auf, weil alles funktioniert. Bewusst als Regel und nicht
bloss als zufaelliges Passwort, das niemand kennt — der Unterschied ist
pruefbar: Mit dem Zufallspasswort blieb die Gegenprobe stumm, mit denyAll
faellt genau die danach benannte Zusicherung.

Die Sperre nach Fehlversuchen ist global statt je Adresse: Es gibt genau ein
Passwort, und hinter dem Reverse Proxy der Zielumgebung saehe der Server
ohnehin fuer alle dieselbe Adresse. Preis benannt (D76-Nachtrag 6).

135 Tests. Gegenproben: Schutz entfernt, Sperre entfernt, Voreinstellung
geoeffnet -> es faellt jeweils genau die danach benannte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-26 17:30:10 +02:00
co-authored by Claude Opus 5
parent 741b41ef11
commit bc8850a518
16 changed files with 544 additions and 18 deletions
+31 -5
View File
@@ -164,18 +164,44 @@ noch nicht steht. Voraussetzung ist eine **Einzelinstanz**; hinter einem Load
Balancer erführe ein Beobachter auf der zweiten Instanz nichts. Begründung:
D76-Nachtrag 5.
## Master-Passwort für die Dokumentenliste
`GET /api/v1/documents` listet **alle** Dokumente und machte damit jede UUID
auffindbar — das Zugriffsmodell „unerratbarer Link" wäre hinfällig. Dieser eine
Endpunkt verlangt deshalb HTTP Basic mit dem Benutzer `werkbaum`:
```bash
export WERKBAUM_MASTER_PASSWORD_HASH="{bcrypt}$(htpasswd -bnBC 12 "" geheim | tr -d ':\n')"
```
- **Ohne gesetzten Hash ist die Liste gesperrt** (401), nicht offen. Ein
vergessener Konfigurationsschritt darf nichts preisgeben; beim Start warnt
das Log.
- Der Hash trägt sein Verfahren als Präfix (`{bcrypt}…`) — so steht in der
Konfiguration, womit gehasht wurde.
- **Nach `max-attempts` Fehlversuchen** (5) ist der Endpunkt für `lockout`
(15 min) gesperrt: **429** mit `Retry-After`. Die Sperre ist **global**, nicht
je Adresse — es gibt genau ein Passwort, und hinter einem Reverse Proxy sähe
der Server ohnehin für alle dieselbe Adresse. Der Preis: Wer falsch rät,
sperrt die Liste für alle; die Dokumente selbst bleiben über ihre UUID
erreichbar.
- Die API ist zustandslos: keine Sitzung, kein CSRF-Token (der schützte hier
nichts und bräche jeden Client).
## Vorbereitete Erweiterungen
**Autorisierung**
- Geschützt ist bisher **genau ein** Endpunkt, siehe unten. Alles andere ist
über die unerratbare UUID erreichbar — das ist das Zugriffsmodell, nicht
eine Lücke.
- `bearerAuth` (JWT) ist in der OpenAPI-Spec als Security Scheme definiert,
aber noch auf keine Operation angewendet.
- Später: `spring-boot-starter-security` + `security: [bearerAuth]` in der
Spec; die Behavior-Tests erhalten dann einen Auth-Schritt
(„Angenommen ich bin als … angemeldet").
aber noch auf keine Operation angewendet. Später kommt echte
Authentifizierung als Schicht davor; am Protokoll ändert sich dadurch
nichts.
**Live-Editing** (Konzept: `docs/live-editing-proposal.md`, Entscheidung: D76)
- **Offen:** Umbenennen per `PATCH /title` (und damit das Ereignis
`RENAMED`), Master-Passwort für `GET /documents`, Client-Anpassung.
`RENAMED`) sowie die Client-Anpassung im Frontend.
- `DocumentUpdateRequest.expectedVersion` ist im Vertrag vorgesehen, wird aber
noch nicht ausgewertet.