feat(appview): Bearer-Auth für Timeline und Notifications
Die AppView hatte keinerlei Authentifizierung: jeder konnte /api/notifications?did=<beliebig> lesen und per /seen als gelesen markieren. Mit Phase 8 sind das die ersten privaten Daten im System. Das Access-JWT der PDS trug von Anfang an sub, scope "com.atproto.access" und aud "did:web:appview…" — es war für die AppView ausgestellt, nur hat sie es nie geprüft. Neu ist deshalb vor allem die Schlüsselbeschaffung: auth.rs holt das DID-Dokument der PDS (PDS_INTERNAL_URL, sonst PDS_PUBLIC_URL), cached den Schlüssel und lädt ihn bei einem Verifikationsfehler nach — höchstens einmal pro Minute, damit Müll-Tokens kein Werkzeug werden, die PDS zu fluten. Ein Schlüsselwechsel braucht damit keinen Neustart. Ist die PDS beim Start weg, warnt die AppView nur und startet trotzdem (sie indiziert den Firehose, der von der lokalen PDS unabhängig ist). Ist der Schlüssel beim Prüfen eines Tokens nicht zu beschaffen, gibt es 503 — fail closed. Geschützt: /api/timeline/home und die drei Notification-Endpoints, jeweils mit sub == did. Öffentlich bleiben Profile, Suche, Posts, Threads und die Follower-Listen; das sind in AT Proto öffentliche Records. 401 AuthMissing / 401 TokenInvalid / 403 Forbidden / 503 AuthUnavailable. TokenInvalid ist ein Vertrag mit dem Client: daran erkennt er, dass er sein Token erneuern und einmal wiederholen muss. Dazu CORS: statt Any für alles jetzt eine Allowlist über APPVIEW_CORS_ORIGINS (unset = altes Verhalten plus Warnung), und /internal/ingest-commit liegt außerhalb der CORS-Schicht — die Route wird server-zu-server aufgerufen, ein Allow-Origin darauf würde nur einer Webseite helfen, in den Index zu schreiben. APPVIEW_AUTH_REQUIRED=false stellt das alte Verhalten her (VPN-Instanz, fail-open-Tests) und warnt beim Start in Großbuchstaben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
co-authored by
Claude Opus 5
parent
786a892658
commit
a2a371b7d9
@@ -15,8 +15,31 @@
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! In production this endpoint would be protected with mTLS and a token
|
||||
//! minted by the PDS; for now it's open inside the cluster.
|
||||
//! ## Who may call this
|
||||
//!
|
||||
//! This is the AppView's only write path, and it is not a browser
|
||||
//! endpoint: it is excluded from the CORS layer in
|
||||
//! [`crate::routes::router`], because an `Access-Control-Allow-Origin`
|
||||
//! header here would only ever help a web page forge index entries.
|
||||
//!
|
||||
//! Authentication is the shared secret `APPVIEW_INGEST_SECRET`,
|
||||
//! compared in constant time against the caller's `X-Ingest-Secret`
|
||||
//! header:
|
||||
//!
|
||||
//! - **set** → enforced. A missing or wrong header is `401
|
||||
//! AuthenticationRequired`.
|
||||
//! - **unset** → anonymous writes are accepted, and the AppView shouts
|
||||
//! about it once at startup (see
|
||||
//! [`crate::auth::log_startup_posture`]). Refusing to start would
|
||||
//! break every existing single-machine dev setup for a service that,
|
||||
//! in that configuration, is bound to loopback anyway; accepting
|
||||
//! silently is how an internet-facing deployment ends up letting
|
||||
//! anyone forge posts, follows and notifications. So: keep working,
|
||||
//! but never quietly.
|
||||
//!
|
||||
//! A future hardening step is mTLS or a PDS-minted token, at which
|
||||
//! point the shared secret becomes the fallback rather than the only
|
||||
//! line.
|
||||
|
||||
use crate::indexer;
|
||||
use crate::state::AppState;
|
||||
@@ -51,8 +74,13 @@ pub struct IngestCommitReq {
|
||||
}
|
||||
|
||||
/// Authenticate internal ingest requests.
|
||||
/// - If `APPVIEW_INGEST_SECRET` env var is unset: dev mode, accept anything.
|
||||
/// - If set: require `X-Ingest-Secret: <value>` header to match.
|
||||
///
|
||||
/// - `APPVIEW_INGEST_SECRET` unset: accept anonymous writes (dev mode —
|
||||
/// the startup log warns, see the module docs for why this isn't a
|
||||
/// hard failure).
|
||||
/// - Set: require a matching `X-Ingest-Secret` header. The comparison
|
||||
/// is constant-time so a caller can't recover the secret byte by byte
|
||||
/// from response timings.
|
||||
pub fn check_ingest_secret(
|
||||
headers: &HeaderMap,
|
||||
configured: Option<&str>,
|
||||
|
||||
Reference in New Issue
Block a user