main.rs
173 lignes · 9066 octets
//! Reference application for the Rustwarden framework. use rustwarden_core::prelude::*; #[tokio::main] async fn main() -> anyhow::Result<()> { let builder = RustwardenApp::builder() .from_env() // Browser SSR routes go through the CSRF-protected channel; the // `/api/v1/*` JSON API is mounted on the machine channel (no CSRF). .with_ui(rustwarden_ui::browser_routes) .merge_machine_routes(rustwarden_ui::api_routes()); // ActivityPub federation lives in the `rustwarden-fediverse` crate // (cf. docs/plans/2026-05-11-extract-rustwarden-fediverse-crate.md). // Enabling the `fediverse` feature links the crate and mounts its // routes onto the application router. The wiring opens a database // connection early so the federation router and the revocation // checker share the same `DatabaseConnection` as the rest of the // framework. #[cfg(feature = "fediverse")] let builder = { use std::sync::Arc; let db_url = std::env::var("DATABASE_URL") .map_err(|_| anyhow::anyhow!("DATABASE_URL must be set for the fediverse feature"))?; let db = sea_orm::Database::connect(&db_url).await?; let fediverse_config = rustwarden_fediverse::FediverseConfig::from_env() .map_err(|e| anyhow::anyhow!("invalid FediverseConfig: {e}"))?; // ANSSI F-NET-5 — load AuthConfig explicitly so misconfiguration // surfaces as a startup error rather than silently booting with // a placeholder JWT secret. let auth_config: Arc<rustwarden_core::config::auth::AuthConfig> = Arc::new( rustwarden_core::config::auth::AuthConfig::from_env() .map_err(|e| anyhow::anyhow!("invalid AuthConfig for fediverse: {e}"))?, ); // ANSSI F-CRYPTO-1/2 — production wires the strict // `HttpSigVerifier`, which enforces the Digest header and the // 5-min Date skew window. Dev / testbed mode (when // `FEDIVERSE_DEBUG_LOCAL_FEDERATION=true`) falls back to // `AlwaysAcceptVerifier::with_resolver` so the e2e suite can // exercise the signed-request happy path without the // testbed pre-seeding every actor's PEM into each peer's cache. let resolver: Arc<dyn rustwarden_fediverse::resolver::LocalUserResolver> = Arc::new(rustwarden_fediverse::resolver::SeaOrmLocalUserResolver::new(db.clone())); let debug_local = std::env::var("FEDIVERSE_DEBUG_LOCAL_FEDERATION") .map(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes")) .unwrap_or(false); let verifier: Arc< dyn rustwarden_fediverse::services::fediverse_signal_service::SignatureVerifier, > = if debug_local { tracing::warn!( "FEDIVERSE_DEBUG_LOCAL_FEDERATION=true — using AlwaysAcceptVerifier (DEV ONLY)" ); Arc::new( rustwarden_fediverse::services::fediverse_signal_service::AlwaysAcceptVerifier::with_resolver( db.clone(), ), ) } else { Arc::new( rustwarden_fediverse::http_signatures::HttpSigVerifier::with_resolver(db.clone()), ) }; let fediverse_router = rustwarden_fediverse::routes::router_with_auth( fediverse_config, resolver, verifier, auth_config, ); // RFC 0028 / Lot 7d — branch the guard observer on the // fediverse router when the `fediverse-guard` feature is active. // The observer extension layer is read by inbox / auth_routes / // signature verifier; absent layer ⇒ everything defaults to // `NoopFediverseObserver` (zero-cost no-op). // // Lot 2 CSRF — the guard observer stays bolted onto the machine // `fediverse_router` (it serves the inbox), but the guard admin UI // is now split out into its own `guard_ui_router` so it can ride the // CSRF-protected browser channel (see below). The store and config // are still shared between the runtime ban-manager and the UI so a // single source of truth backs both. #[cfg(feature = "fediverse-guard")] let (fediverse_router, guard_ui_router) = { use rustwarden_fediverse::observer::FediverseEventObserver; use rustwarden_fediverse_guard::{ BanManager, GuardConfig, GuardObserver, GuardStore, HybridStore, PostgresStore, default_detectors, }; let guard_config = GuardConfig::from_env().map_err(|e| anyhow::anyhow!("invalid GuardConfig: {e}"))?; let postgres_store: Arc<dyn GuardStore> = Arc::new(PostgresStore::new(db.clone())); let hybrid = HybridStore::new(postgres_store); // Repopulate the in-memory cache from durable storage before // serving requests — bans active at boot must be enforced // immediately, not only after the first detector fires. if let Err(e) = hybrid.rehydrate().await { tracing::warn!(error = %e, "fediverse guard rehydrate failed; continuing with empty cache"); } let hybrid_store: Arc<dyn GuardStore> = Arc::new(hybrid); let manager = BanManager::new(hybrid_store.clone(), guard_config.clone()); let detectors = default_detectors(&guard_config); let observer: Arc<dyn FediverseEventObserver> = Arc::new(GuardObserver::new(manager, detectors, None)); // Machine router: keep the inbox + observer here. The observer // extension is read on the inbox path. let fediverse_router = fediverse_router.layer(axum::Extension(observer)); // Browser router: the guard admin UI. Its single POST // (`/admin/fediverse-guard/manual_ban`) is a urlencoded form // carrying a hidden `csrf_token`; the handler still extracts // `Form { csrf_token }` and calls `validate_csrf_token` // (defense-in-depth, D3=a), while the global browser CSRF layer // validates the same token from the buffered body and rebuilds // it for the downstream extractor (cf. core test T4). let guard_ui_router = rustwarden_fediverse_guard::ui::default_routes() .layer(axum::Extension(hybrid_store)) .layer(axum::Extension(guard_config)); (fediverse_router, guard_ui_router) }; let checker: Arc< dyn rustwarden_core::middleware::revocation::RemoteIdentityRevocationChecker, > = Arc::new( rustwarden_fediverse::services::revocation::SeaOrmRemoteIdentityRevocationChecker::new( db.clone(), ), ); // The fediverse router carries `POST /inbox` (machine, authenticated // by HTTP signatures — no CSRF token possible), so it goes on the // machine channel. let builder = builder .database(db) .register_protected_prefix("/fediverse/consent") .register_protected_prefix("/fediverse/invite") .register_revocation_checker(checker) .merge_machine_routes(fediverse_router); // The guard admin UI is a browser family (SSR forms under // `/admin/fediverse-guard/*`): mount it on the CSRF-protected // browser channel and register its prefix so the auth middleware // gates it like every other `/admin/*` screen. #[cfg(feature = "fediverse-guard")] let builder = builder .register_protected_prefix("/admin/fediverse-guard") .merge_browser_routes(guard_ui_router) .add_dashboard_card(rustwarden_fediverse_guard::ui::dashboard_card()); builder }; // The reference UI for the federation lives in // `rustwarden-fediverse-ui` and is opt-in via the `fediverse-ui` // feature (which itself implies `fediverse`). When enabled, mount // the UI router and inject the federation card into the admin // dashboard via the builder hook. #[cfg(feature = "fediverse-ui")] let builder = builder // Famille browser : login fediverse, consent, invite et écrans // admin sont des formulaires SSR pilotés par cookie de session, on // les monte donc sur le canal CSRF-protégé. Chaque handler conserve // son `validate_csrf_token` en defense-in-depth (D3=a) ; le layer // CSRF global lit le token dans le body urlencoded bufferisé puis // reconstruit le body pour l'extracteur `Form<…>` aval (réf. test // core T4). L'inbox ActivityPub n'est PAS ici : elle reste sur le // canal machine via `fediverse_router`. .merge_browser_routes(rustwarden_fediverse_ui::default_routes()) .add_dashboard_card(rustwarden_fediverse_ui::dashboard_card()); builder.build().await?.run().await }
GitRust