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 |
GitRust