actualise service reference et documentation avec les csrf handlers cablés

EBO <eric.bouhana@softalys.com> committé le 2026-06-04 21:03

40b62f99e048dc95639c94d136c58c0f41964a71

1 parent(s)

2 fichiers modifiés +90 -20
M docs/SERVICES_REFERENCE.md
+65 -13
@@ -5,6 +5,13 @@
5 5 > Ce document decrit les services exposes par le framework, leurs methodes publiques,
6 6 > les patterns d'utilisation et les invariants de securite qu'ils garantissent.
7 7
8 +> ⚠️ **Breaking (CSRF structurel — 2026-06) — action requise lors de la mise à jour.**
9 +> Le framework **câble désormais lui-même la protection CSRF** sur un routeur « browser » dédié, et l'API du builder sépare deux familles de routes :
10 +> - `merge_browser_routes(..)` / `with_ui(..)` → **routeur browser, CSRF appliqué** (cookie de session) ;
11 +> - `merge_machine_routes(..)` → **routeur machine, jamais de CSRF** (API JSON Bearer, webhooks, inbox ActivityPub).
12 +>
13 +> **`merge_routes(..)` vaut maintenant `merge_browser_routes` (CSRF par défaut, fail-safe).** Tout consommateur qui montait une **API JSON / un webhook** via `merge_routes` doit le **migrer vers `merge_machine_routes`**, sinon ces routes renverront `403` faute de token. Le routeur UI est partitionné : `rustwarden_ui::default_routes()` est **déprécié** au profit de `browser_routes()` (SSR) + `api_routes()` (`/api/v1/*`). Détails et table de migration en **[§18](#18-middleware-csrf-auth-rate-limit)**.
14 +
8 15 ---
9 16
10 17 ## Table des matieres
@@ -959,16 +966,47 @@ Installation : `RustwardenApp::builder().hooks(Arc::new(MyHooks)).build()?`.
959 966
960 967 ### CSRF
961 968
962 -**Module** : `rustwarden_core::middleware::csrf`
969 +> **Depuis 2026-06, le framework câble la protection CSRF automatiquement.** Vous n'avez plus à poser `csrf_protection*` à la main pour le cas standard : il suffit de monter vos routes sur le **bon canal**. Le middleware reste exposé pour les montages personnalisés.
970 +
971 +#### Câblage par canal (browser / machine) — le modèle à adopter
972 +
973 +`build()` assemble **deux sous-routeurs** puis les fusionne :
974 +
975 +| Canal | Méthode builder | CSRF | Pour quoi |
976 +|---|---|---|---|
977 +| **browser** | `with_ui(fn)`, `merge_browser_routes(router)`, `merge_routes(router)` *(= browser par défaut)* | **Oui** (layer appliqué par le framework) | Pages SSR pilotées par cookie de session : formulaires, écrans admin, actions mutantes rendues en HTML. |
978 +| **machine** | `merge_machine_routes(router)` | **Non** | API JSON (auth `Bearer`), webhooks, callbacks, inbox ActivityPub — tout ce qui ne peut/ne doit pas porter de token CSRF. |
979 +
980 +Le `session_layer` reste global et s'exécute **avant** le CSRF (la Session est donc disponible au moment du contrôle). L'exemption machine est **topologique** (le routeur machine ne porte structurellement pas le layer) — pas de flag « CSRF off ».
981 +
982 +```rust
983 +RustwardenApp::builder()
984 + .from_env()
985 + .with_ui(rustwarden_ui::browser_routes) // SSR → CSRF
986 + .merge_machine_routes(rustwarden_ui::api_routes()) // /api/v1/* → pas de CSRF
987 + .merge_browser_routes(my_app::ui_routes()) // vos pages mutantes → CSRF
988 + .merge_machine_routes(my_app::api_routes()) // votre API JSON / webhooks → pas de CSRF
989 + .build().await?;
990 +```
991 +
992 +**Secure-by-default (B1).** Une route mutante ajoutée sur le canal browser est protégée **sans code supplémentaire** dans le handler. Le mode d'échec par défaut est *fail-safe* (403 visible), jamais *fail-open*.
993 +
994 +#### Délivrer le token côté client (routes browser)
995 +
996 +Deux canaux acceptés, au choix :
997 +- **Header** `X-CSRF-Token: <token>` (SPA / HTMX / `fetch`). Pensez à l'ajouter dans `allow_headers` CORS (déjà fait côté framework).
998 +- **Champ de formulaire** `csrf_token=<token>` en `application/x-www-form-urlencoded` (SSR sans JS). Le middleware bufferise le body, valide, puis **reconstruit** le body pour l'extracteur `Form<…>` aval.
999 +
1000 +Le token est obtenu via la session (`get_or_create_csrf_token`) et injecté dans le template — voir [§20 Layouts](#20-patterns-communs) (`LayoutContext::from_session` peuple `csrf_token` automatiquement).
1001 +
1002 +#### API du module `rustwarden_core::middleware::csrf`
963 1003
964 -- `csrf_protection` — middleware par defaut, verifie le header `X-CSRF-Token`.
965 -- `csrf_protection_with_config` — lit aussi le body `application/x-www-form-urlencoded` (pour apps SSR sans JS).
966 -- `csrf_protection_impl` — variante qui prend `&DatabaseConnection` au lieu de `State<_>`.
967 -- `get_or_create_csrf_token(&Session)` — acces au token pour injection dans les templates.
968 -- `validate_csrf_token(&Session, &str)` — validation manuelle depuis un handler.
969 -- `extract_csrf_from_form(&[u8]) -> Option<String>` — helper public pour les apps qui bufferisent le body elles-memes.
1004 +- `get_or_create_csrf_token(&Session) -> Result<String>` — token de session, à injecter dans les templates.
1005 +- `validate_csrf_token(&Session, &str)` — validation manuelle depuis un handler (defense-in-depth ; le layer browser valide déjà).
1006 +- `extract_csrf_from_form(&[u8]) -> Option<String>` — parseur public pour les apps qui bufferisent le body elles-mêmes.
1007 +- `csrf_protection`, `csrf_protection_with_config`, `csrf_protection_impl` — **middlewares bas niveau**, uniquement pour un montage manuel sur un routeur que vous gérez vous-même (cas avancé ; le câblage par canal ci-dessus est préféré).
970 1008
971 -**`CsrfProtectionConfig`** :
1009 +**`CsrfProtectionConfig`** (utilisé en interne par le canal browser avec `accept_form_body: true`) :
972 1010 ```rust
973 1011 pub struct CsrfProtectionConfig {
974 1012 pub accept_form_body: bool, // default: false
@@ -976,7 +1014,7 @@ pub struct CsrfProtectionConfig {
976 1014 }
977 1015 ```
978 1016
979 -Wire dans le router :
1017 +Montage manuel (routeur custom, hors canaux du builder) :
980 1018 ```rust
981 1019 use std::sync::Arc;
982 1020 use axum::middleware;
@@ -990,10 +1028,22 @@ let router = Router::new()
990 1028
991 1029 **Securite** :
992 1030 - Comparaison constant-time (SEC-H4, `subtle::ConstantTimeEq`).
993 -- Skip uniquement pour requetes `Authorization: Bearer ...` (API clients non CSRF-vulnerables).
994 -- Multipart jamais bufferise (skip + 403).
1031 +- Skip pour méthodes sûres (`GET`/`HEAD`/`OPTIONS`) et pour `Authorization: Bearer ...` (clients API non CSRF-vulnérables).
1032 +- Multipart jamais bufferisé (skip + 403).
995 1033 - Body > cap → 413 `PAYLOAD_TOO_LARGE`.
996 1034
1035 +#### Migration depuis l'ancienne API (avant 2026-06)
1036 +
1037 +| Avant | Maintenant |
1038 +|---|---|
1039 +| `with_ui(rustwarden_ui::default_routes)` | `with_ui(rustwarden_ui::browser_routes)` **+** `merge_machine_routes(rustwarden_ui::api_routes())` |
1040 +| `merge_routes(api_router)` *(votre API JSON / webhooks)* | `merge_machine_routes(api_router)` — **sinon 403** |
1041 +| `merge_routes(ssr_router)` *(vos pages HTML mutantes)* | inchangé (= browser/CSRF) ou explicite via `merge_browser_routes(ssr_router)` |
1042 +| Vous posiez `csrf_protection*` à la main sur le routeur global | Retirez-le : le canal browser le pose pour vous (gardez-le uniquement sur un routeur custom non monté via les canaux) |
1043 +| `rustwarden_ui::default_routes()` | **déprécié** → `browser_routes()` + `api_routes()` |
1044 +
1045 +> **Check-list** : (1) les pages SSR mutantes envoient le token (`X-CSRF-Token` header ou `csrf_token` form) ; (2) toute API JSON / webhook par cookie est sur `merge_machine_routes` ; (3) les clients API utilisent `Authorization: Bearer` (bypass CSRF) ; (4) `cargo test` + un POST forgé cross-site → 403.
1046 +
997 1047 ### Rate limiting helpers
998 1048
999 1049 **Module** : `rustwarden_core::middleware::rate_limit`
@@ -1412,14 +1462,16 @@ Abstraction utilisée par les services qui acceptent indifféremment un user loc
1412 1462
1413 1463 **Module** : `rustwarden_fediverse::routes`
1414 1464
1415 -Deux constructeurs de routeur. Tous deux retournent `Router<DatabaseConnection>` mergeable via `builder.merge_routes(...)`.
1465 +Deux constructeurs de routeur. Tous deux retournent `Router<DatabaseConnection>`.
1416 1466
1417 1467 | Fonction | Signature | Routes montées |
1418 1468 |---|---|---|
1419 1469 | `router` | `(config: FediverseConfig, db: DatabaseConnection) → Router<DatabaseConnection>` | **Minimum** : `/.well-known/webfinger`, `/.well-known/nodeinfo`, `/.well-known/host-meta`, `/nodeinfo/2.0`, `/users/{username}`, `/inbox`. Pas d'auth, pas d'invites. |
1420 1470 | `router_with_auth` | `(config, resolver: Arc<dyn LocalUserResolver>, verifier: Arc<dyn SignatureVerifier>, auth_config: Arc<AuthConfig>) → Router<DatabaseConnection>` | **Complet** : `router(..)` + 5 endpoints `/api/v1/auth/login/fediverse/*` + `/fediverse/auth/inbox` + `/api/v1/fediverse/auth/consent` + 6 endpoints invites + `/fediverse/consent` + `/fediverse/invite`. |
1421 1471
1422 -Côté `rustwarden-fediverse-ui` : `default_routes()` retourne le routeur UI complet (admin trust/blocks/outbound + écrans user consent/login), et `dashboard_card()` la `DashboardCardDTO` injectable via `builder.add_dashboard_card(..)`.
1472 +> **Canal CSRF (cf. [§18](#18-middleware-csrf-auth-rate-limit)).** Ces routeurs portent `POST /inbox` (authentifié par **signature HTTP**, aucun token CSRF possible) → ils se montent sur **`builder.merge_machine_routes(..)`**, jamais sur le canal browser.
1473 +
1474 +Côté `rustwarden-fediverse-ui` : `default_routes()` retourne le routeur UI (admin trust/blocks/outbound + écrans user consent/login). Ce sont des **formulaires SSR pilotés par cookie** → famille **browser** : montez-le sur **`builder.merge_browser_routes(rustwarden_fediverse_ui::default_routes())`** (le layer CSRF du framework les protège ; chaque handler conserve son `validate_csrf_token` en defense-in-depth). `dashboard_card()` reste injectable via `builder.add_dashboard_card(..)`. Idem pour `rustwarden_fediverse_guard::ui::default_routes()` (écrans `/admin/fediverse-guard/*`) → **`merge_browser_routes`**, tandis que l'observer guard reste branché sur le routeur fédération machine.
1423 1475
1424 1476 ### 21.15 `RemoteResourceShareService`
1425 1477
M docs/fediverse-consumer-guide.md
+25 -7
@@ -110,7 +110,10 @@ use rustwarden_core::prelude::*;
110 110
111 111 let builder = RustwardenApp::builder()
112 112 .from_env()
113 - .with_ui(rustwarden_ui::default_routes);
113 + // UI partitionnée (CSRF, cf. SERVICES_REFERENCE §18) : SSR sur le canal
114 + // browser (CSRF), API JSON `/api/v1/*` sur le canal machine (pas de CSRF).
115 + .with_ui(rustwarden_ui::browser_routes)
116 + .merge_machine_routes(rustwarden_ui::api_routes());
114 117
115 118 #[cfg(feature = "fediverse")]
116 119 let builder = {
@@ -166,12 +169,17 @@ let builder = {
166 169 .register_protected_prefix("/fediverse/consent")
167 170 .register_protected_prefix("/fediverse/invite")
168 171 .register_revocation_checker(checker)
169 - .merge_routes(fediverse_router)
172 + // L'inbox (`POST /inbox`) est authentifiée par signature HTTP : aucun
173 + // token CSRF possible → canal machine (jamais de CSRF). Cf. §18.
174 + .merge_machine_routes(fediverse_router)
170 175 };
171 176
172 177 #[cfg(feature = "fediverse-ui")]
173 178 let builder = builder
174 - .merge_routes(rustwarden_fediverse_ui::default_routes())
179 + // Écrans SSR (consent / invite / login fediverse / admin) pilotés par
180 + // cookie de session → canal browser (CSRF). La validation par handler
181 + // `validate_csrf_token` est conservée en defense-in-depth.
182 + .merge_browser_routes(rustwarden_fediverse_ui::default_routes())
175 183 .add_dashboard_card(rustwarden_fediverse_ui::dashboard_card());
176 184
177 185 builder.build().await?.run().await
@@ -314,13 +322,23 @@ observation interne 799 du 2026-05-13). `rustwarden-fediverse-ui`
314 322 fournit une carte par défaut ; si vous voulez la remplacer ou ajouter
315 323 la vôtre, appelez `add_dashboard_card` après le merge des routes.
316 324
317 -### 8.5 `merge_routes` — handlers consommateur
325 +### 8.5 Monter vos routes — canal browser / machine
318 326
319 327 Si votre app veut sa propre page `/profile/fediverse` ou un endpoint
320 328 d'admin spécifique (ex. import/export de allowlist au format YAML),
321 -écrivez un `Router<AppState>` et fusionnez-le via
322 -`builder.merge_routes(my_router)`. **Ne** dupliquez **pas** un service
323 -existant : étendez-le via wrapper / trait (cf. processus §11.4).
329 +écrivez un `Router<DatabaseConnection>` et fusionnez-le sur le **bon
330 +canal** (CSRF, cf. SERVICES_REFERENCE §18) :
331 +
332 +- page SSR mutante pilotée par cookie → `builder.merge_browser_routes(my_router)`
333 + (CSRF appliqué ; le formulaire doit porter `X-CSRF-Token` ou `csrf_token`) ;
334 +- API JSON / webhook / endpoint signé → `builder.merge_machine_routes(my_router)`
335 + (pas de CSRF).
336 +
337 +`merge_routes(..)` reste disponible et **équivaut à `merge_browser_routes`**
338 +(CSRF par défaut, fail-safe) : une API JSON montée par ce biais renverrait
339 +`403` faute de token → utilisez `merge_machine_routes` pour ces routes.
340 +**Ne** dupliquez **pas** un service existant : étendez-le via wrapper /
341 +trait (cf. processus §11.4).
324 342
325 343 ---
326 344