From f7b78fd5db2fae8ef3c0eb7e0e618dc46e2d1334 Mon Sep 17 00:00:00 2001 From: tomdebone Date: Wed, 9 Sep 2026 23:03:12 +0200 Subject: [PATCH] docs: Auth-Abschnitt, korrigierte Test-Anleitung, Phase 9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deployment.md bekommt einen eigenen Abschnitt zur Authentifizierung (Schlüsselweg, geschützte Endpoints, Fehlercodes, der Schalter für VPN-Instanzen) und eine CORS-Beschreibung, die die Allowlist statt des alten Wildcards erklärt — inklusive der Tauri-Origins, die sonst am Preflight scheitern. Im README steht jetzt der Hinweis, der diese Runde am meisten gekostet hat: ohne DATABASE_URL_APPVIEW in der Umgebung überspringen sich die DB-Tests selbst und `cargo test --workspace` meldet grün, ohne sie ausgeführt zu haben. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX --- README.md | 19 +++++---- docs/architecture.md | 10 ++++- docs/deployment.md | 97 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 94 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index fb93d7e..cfee0a9 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ cargo run -p appview | 6 Tauri-UI-Logik an Backend koppeln | ✅ done — LoginScreen, NavRail, PostCard, ComposeBox, Profile/Compose/Search/Settings-Views | | 7 Polish (Tray, Notifications, Auto-Update) | ✅ done — Tray-Icon custom (`tauri::include_image!`), Notification-Click navigiert via `app://notification`-Event + `openThread`-Helper zu Thread-Detail, Auto-Update in Dev inert (Production-Weg: [`docs/tauri-release.md`](docs/tauri-release.md)) | | 8 Social-Graph + Benachrichtigungen | ✅ done — `notifications`-Tabelle, Schreibpfad im Jetstream-Indexer (idempotent, keine Selbst-Notifications), `/api/notifications[/count|/seen]`, `/api/followers`, `/api/following`, eigene `/api/thread`-Route; im Client Notifications-View mit Unread-Badge und klickbare Follower-/Following-Listen im Profil | +| 9 Auth + Performance | ✅ done — AppView prüft Bearer-Tokens (ES256, Schlüssel aus dem neuen `/.well-known/did.json` der PDS, fail closed); Timeline und Notifications nur noch für die eigene DID; CORS-Allowlist statt `Any`; Indizes für Handle-Lookup und Cold-Start-Feed | ## Tests @@ -81,11 +82,13 @@ Stand zuletzt gegen den lokalen Dev-Stack (docker compose + laufender PDS + AppV Rust-Workspace grün (u.a. 27 MST, 24 PDS-Integration, 49 AppView-Lib, 14 AppView-Integration), Frontend grün. Zwei Vorbehalte: -* Die DB-gestützten Integrationstests sind *fail-open* — ohne erreichbare Postgres/PDS - überspringen sie sich selbst und melden das nur auf stderr. Ein grüner Lauf ohne - laufenden Stack sagt also weniger, als er aussieht. -* Einige Tests hängen am Zustand der Dev-Datenbank; auf einer frischen DB können - `handle_sync`-Tests abweichen. Wer sie ernst nimmt, prüft sie gegen eine definierte DB. +* Die DB-gestützten Tests sind *fail-open*: **ohne `DATABASE_URL_APPVIEW` in der + Umgebung überspringen sie sich selbst** und melden das nur auf stderr. `cargo test + --workspace` in einer nackten Shell meldet dann grün, ohne sie ausgeführt zu haben — + für einen aussagekräftigen Lauf `set -a; . ./.env; set +a` voranstellen und PDS + + AppView laufen lassen. +* Läuft die Auth (Default), holen sich die Integrationstests echte Tokens von der PDS + bzw. signieren sie aus `PDS_JWT_SECRET`; ohne erreichbare PDS überspringen sie. `crates/tauri-app/src-tauri` hat ein eigenes `[workspace]` und ist **nicht** Teil des Root-Workspace; `cargo test --workspace` von oben erfasst den IPC-Layer nicht. @@ -103,8 +106,8 @@ Root-Workspace; `cargo test --workspace` von oben erfasst den IPC-Layer nicht. * Die eigene PDS speist **keinen** Firehose (`com.atproto.sync.subscribeRepos` fehlt) — eigene Records erreichen die AppView nur über den Best-Effort-Push `POST /internal/ingest-commit`. -* Die AppView-Leseschnittstelle hat **keine Auth** und CORS `Any`; bei - `/api/notifications` sind das erstmals halbwegs private Daten. +* `aud` wird beim Token-Check nicht validiert (Signatur, Ablauf, `scope` und + `sub` schon). * Notifications werden nie gelöscht: Unlike/Unfollow lässt die Zeile stehen, und der Dedupe-Key macht sie „einmal pro (Empfänger, Autor, Art, Subject) für immer". * Auto-Update ist nur dokumentiert, nicht verdrahtet: niemand ruft `check()` auf, das @@ -112,8 +115,6 @@ Root-Workspace; `cargo test --workspace` von oben erfasst den IPC-Layer nicht. * Reply-Notifications gehen verloren, wenn die Antwort vor ihrem Parent indiziert wird (kein Nachlauf) — bei Jetstream möglich. * `at-blob` spricht MinIO ohne Signature V4 — echtes AWS S3 funktioniert damit nicht. -* Die PDS liefert kein `.well-known/did.json`; `describeServer` gibt die DID hart - als `did:web:pds.maarcadetweet.local` zurück. ## Design diff --git a/docs/architecture.md b/docs/architecture.md index aadcf10..27c9d46 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,11 +19,12 @@ auf welchem Weg kommt ein Post vom Client bis in die Timeline zurück. ┌──────────────────────────────────┐ ┌──────────────────────────────┐ │ pds-server (axum, :2583) │ │ appview (axum, :2584) │ │ │ │ │ - │ /xrpc/com.atproto.* │ │ GET /api/timeline/home │ + │ /xrpc/com.atproto.* │ │ GET /api/timeline/home 🔒 │ │ /xrpc/app.bsky.actor.profile.* │ │ GET /api/profile[/:handle] │ │ /blob/:cid │ │ GET /api/search │ │ /healthz │ │ GET /api/post|thread/*uri │ - │ │ │ GET /api/notifications… │ + │ /.well-known/did.json ──────────┼───┼─▶ Schlüssel für 🔒 │ + │ │ │ GET /api/notifications… 🔒 │ │ │ │ GET /api/followers|following│ │ │ │ GET /healthz │ │ at-lexicon Validierung (160) │ │ │ @@ -50,6 +51,11 @@ auf welchem Weg kommt ein Post vom Client bis in die Timeline zurück. └──────────────────────────────────────────┘ JetstreamConsumer ``` +🔒 = Bearer-Token nötig, und der `sub` des Tokens muss der angefragten DID +entsprechen. Die AppView verifiziert die ES256-Signatur mit dem öffentlichen +Schlüssel, den die PDS in ihrem DID-Dokument veröffentlicht — `PDS_JWT_SECRET` +verlässt die PDS nie. Details in [`deployment.md`](deployment.md), Abschnitt 6. + Zwei Wege führen in die AppView, und das ist Absicht: 1. **Direkter Push (schnell, lokal).** Jeder erfolgreiche Commit auf der PDS diff --git a/docs/deployment.md b/docs/deployment.md index 7ef7502..d2b22cf 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -287,7 +287,7 @@ Hinweise: * Es gibt **keinen** Signal-Handler für graceful Shutdown. `systemctl stop` beendet den Prozess hart; bei der AppView bedeutet das, dass der letzte Cursor-Flush nur passiert, wenn der Kanal regulär geschlossen wird — - praktisch also mit bis zu 100 Events Verlust (siehe Abschnitt 8). Das ist + praktisch also mit bis zu 100 Events Verlust (siehe Abschnitt 9). Das ist unkritisch, weil der Cursor beim Resume ohnehin leicht in die Vergangenheit zeigt und Events idempotent verarbeitet werden. * Eine Abhängigkeit `After=` auf Postgres/MinIO ist nur nötig, wenn diese auf @@ -296,7 +296,55 @@ Hinweise: `PgPoolOptions` mit `acquire_timeout(10s)`, bricht aber ebenfalls ab, wenn der erste Connect scheitert. `Restart=on-failure` fängt das ab. -## 6. Reverse-Proxy +## 6. Authentifizierung + +Die AppView prüft seit Phase 9 Bearer-Tokens. Wie das zusammenhängt: + +1. Die PDS stellt beim Login ein ES256-Access-JWT aus (`sub` = DID, + `scope` = `com.atproto.access`, eine Stunde gültig). +2. Die PDS veröffentlicht den *öffentlichen* Teil ihres P-256-Schlüssels unter + `GET /.well-known/did.json`. `PDS_JWT_SECRET` verlässt den PDS-Prozess nicht. +3. Die AppView holt dieses Dokument beim Start von `PDS_INTERNAL_URL` + (Fallback: `PDS_PUBLIC_URL`), cached den Schlüssel und lädt ihn bei einem + Verifikationsfehler einmal nach — höchstens einmal pro Minute, damit + Müll-Tokens kein Werkzeug werden, die PDS zu fluten. Ein Schlüsselwechsel + braucht also keinen Neustart der AppView. + +Ist die PDS beim Start nicht erreichbar, warnt die AppView nur und startet +trotzdem — sie indiziert den Firehose, was von der lokalen PDS unabhängig ist. +Der Schlüssel wird dann beim ersten authentifizierten Request geholt. Klappt +auch das nicht, antwortet sie `503 AuthUnavailable`: **fail closed**, nie +fail open. + +### Welche Endpoints + +| Endpoint | Zugriff | +|---|---| +| `/api/timeline/home`, `/api/notifications`, `/api/notifications/count`, `/api/notifications/seen` | Token nötig, `sub` muss dem `did`-Parameter entsprechen | +| `/api/profile*`, `/api/search`, `/api/post/*`, `/api/thread*`, `/api/followers`, `/api/following` | öffentlich (in AT Proto öffentliche Records) | +| `/internal/ingest-commit` | `APPVIEW_INGEST_SECRET`, server-zu-server | + +### Fehlercodes + +| Fall | Status | `error` | +|---|---|---| +| Header fehlt oder ist kein Bearer | 401 | `AuthMissing` | +| Signatur falsch, abgelaufen, falscher `scope` | 401 | `TokenInvalid` | +| Token gültig, aber `sub` ≠ `did` | 403 | `Forbidden` | +| Schlüssel der PDS nicht beschaffbar | 503 | `AuthUnavailable` | + +`TokenInvalid` ist ein Vertrag mit dem Desktop-Client: daran erkennt er, dass +er sein Access-JWT erneuern und den Request einmal wiederholen muss. Wer den +Code umbenennt, loggt jeden Nutzer eine Stunde nach dem Login aus. + +### `APPVIEW_AUTH_REQUIRED=false` + +Schaltet die Prüfung ab und stellt das alte Verhalten her — gedacht für eine +Instanz hinter VPN und für die fail-open-Integrationstests. Die AppView warnt +beim Start in Großbuchstaben. Öffentlich erreichbar heißt das: jeder kann die +Notifications jeder DID lesen und als gelesen markieren. + +## 7. Reverse-Proxy ### PDS @@ -357,25 +405,31 @@ im DID-Doc-`serviceEndpoint`, im JWT-`iss` und als Basis der Blob-URLs. kein `/.well-known/atproto-did` aus (im Router nicht vorhanden). `at-identity` kann solche Dokumente *auflösen*, aber wer `did:web`-Handles auf dieser PDS betreiben will, muss die Dateien vorerst statisch über den Proxy ausliefern. -Ebenfalls offen: `describeServer` gibt die DID hart als -`did:web:pds.maarcadetweet.local` zurück, unabhängig von `PDS_PUBLIC_URL`. +Die Service-DID wird inzwischen aus `PDS_PUBLIC_URL` abgeleitet +(`AppConfig::pds_did()`, did:web mit `%3A`-kodiertem Port) und von +`describeServer` **und** `/.well-known/did.json` identisch ausgeliefert. Das +heißt auch: ändert sich `PDS_PUBLIC_URL`, ändert sich die Service-DID. ### AppView -Die AppView setzt ihr CORS selbst — `crates/appview/src/routes.rs`: +Die AppView setzt ihr CORS selbst (`cors_layer()` in +`crates/appview/src/routes.rs`). Ohne `APPVIEW_CORS_ORIGINS` bleibt es beim +alten `Access-Control-Allow-Origin: *` — die AppView warnt dann beim Start. +Mit gesetzter Variable gilt eine Allowlist: -```rust -let cors = CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any); +``` +APPVIEW_CORS_ORIGINS=tauri://localhost,http://tauri.localhost,http://127.0.0.1:1430 ``` -Also `Access-Control-Allow-Origin: *` für alle Routen, inklusive -`/internal/ingest-commit`. Der Grund steht im Code: der Tauri-Webview ruft die -AppView von einem anderen Origin aus auf (`http://127.0.0.1:1430` im Dev, -`tauri://` / `asset://` im Bundle), und die Read-Endpoints tragen keine -Auth-Cookies. +Der Tauri-Webview ruft die AppView von einem anderen Origin aus auf — im Dev +die Vite-Adresse, im Bundle `tauri://localhost` (macOS/Linux) bzw. +`http://tauri.localhost` (Windows). Alle drei gehören in die Liste, sonst +scheitert der Preflight. + +`/internal/ingest-commit` liegt bewusst **außerhalb** der CORS-Schicht: die +Route wird von der PDS server-zu-server aufgerufen, nie von einem Browser. Ein +`Access-Control-Allow-Origin` darauf würde ausschließlich einer Webseite +helfen, in den Index zu schreiben. Für ein öffentliches Deployment heißt das: @@ -408,7 +462,7 @@ HTTP-Aufrufe an PDS/AppView laufen über den Rust-IPC-Layer (`src-tauri/src/pds_client.rs`, `appview_client.rs`), nicht aus dem Webview — die CSP muss also für neue Backend-URLs nicht angefasst werden. -## 7. Health-Checks und Logs +## 8. Health-Checks und Logs ### PDS @@ -472,7 +526,7 @@ Log-Zeilen, auf die es sich lohnt zu achten: | `s3 ping failed at startup` | MinIO beim PDS-Start nicht erreichbar | | `plc submit failed (dev ok)` | PLC-Directory nicht erreichbar; die DID bleibt lokal gültig, ist aber global nicht registriert | -## 8. Neustart-Verhalten +## 9. Neustart-Verhalten **PDS.** Zustandslos bis auf Postgres und MinIO. Der In-Memory-Blockstore (`MemoryBlockstore` in `state.rs`) wird beim Start neu aufgebaut; persistent @@ -515,14 +569,15 @@ nach — zuerst über die lokale PDS (`PdsHandleResolver`, 2 s Timeout), dann PL bzw. `did:web`. Nach einem Neustart holt der erste Durchlauf das nach; der Zustand ist reine Anzeigekosmetik. -## 9. Was noch offen ist +## 10. Was noch offen ist * Kein Compose-Service für `pds-server` / `appview` — das Compose-File deckt nur Postgres und MinIO ab. Es gibt kein Dockerfile im Repo. * `at-blob` spricht ausschließlich MinIO ohne Signature V4 (siehe Modul-Doku in `crates/at-blob/src/s3.rs`); echtes AWS S3 funktioniert damit nicht. * Kein Graceful-Shutdown, keine Readiness- (im Unterschied zur Liveness-)Probe. -* Keine konfigurierbare CORS-Allowlist in der AppView. -* Kein `.well-known`-Handling in der PDS, `describeServer` liefert eine - hartkodierte DID. +* `aud` wird beim Token-Check nicht validiert (`verify_jwt` setzt + `validate_aud = false`), obwohl die PDS `did:web:appview.maarcadetweet.local` + einsetzt. Signatur, Ablauf, `scope` und `sub` werden geprüft. +* Notifications werden nie gelöscht; ein Unlike/Unfollow lässt die Zeile stehen. * Kein Backfill-Werkzeug für Jetstream-Lücken.