Guide de migration — Rustwarden
Ce document liste les changements qui impactent les projets consommateurs de rustwarden-core
et rustwarden-ui, avec les instructions de migration pour chacun.
Les entrees sont classees par date, de la plus recente a la plus ancienne.
rustwarden-cli update-project recopie ce fichier dans docs/ du projet consommateur
(avec SERVICES_REFERENCE.md).
2026-10-01 — RFC 0039 phase 2 : CSP par défaut sans unsafe-eval
Le défaut de SecurityHeadersConfig est maintenant CSP_WITHOUT_UNSAFE_EVAL
(script-src 'self' 'unsafe-inline'). 'unsafe-inline' reste : les scripts
de boot (thème, langue) et HTMX sont encore inline. HSTS, X-Frame-Options,
COOP et CORP ne changent pas.
L'UI de référence charge @alpinejs/csp (/static/js/alpine-csp.min.js) et
enregistre ses composants dans /static/js/alpine-components.js. Les
expressions Alpine qui appellent window, document ou une fonction globale
sont interdites par ce build : elles vivent dans Alpine.data(...).
Retour arrière si une app dépend encore du build Alpine standard (new Function) :
use std::borrow::Cow; use rustwarden_core::config::security_headers::{ DEFAULT_CONTENT_SECURITY_POLICY, SecurityHeadersConfig, }; let headers = SecurityHeadersConfig { content_security_policy: Cow::Borrowed(DEFAULT_CONTENT_SECURITY_POLICY), ..SecurityHeadersConfig::default() }; // .security_headers(headers) sur RustwardenBuilder
DEFAULT_CONTENT_SECURITY_POLICY est la politique 1.0.1, avec 'unsafe-eval'.
2026-09-18 — RFC 0036-0041 (1.1.0)
Minor de securite. Les RFC 0036 a 0040 sont Accepted (2026-09-18), la RFC
0041 Accepted (2026-09-19) ; toutes sont implementees. La RFC 0041
remplace la RFC 0038, passee Superseded avant toute publication : lisez
directement la section 0041 ci-dessous, il n'y a pas de transition v1 → v2 a
faire. Fenetre d'adoption 14 jours
(RUSTWARDEN_EVOLUTION_PROCESS.md §9).
Details : CHANGELOG [1.1.0], SERVICES_REFERENCE.md
bandeau 1.1.0 + §3, §4, §9, §10, §19.
Trois API ajoutees (types::Username,
config::security_headers::SecurityHeadersConfig,
services::email_change_service::EmailChangeService),
quatre changements de comportement sur des chemins d'auth existants,
deux migrations SeaORM a jouer, cinq routes ajoutees.
⚠️ Ordre imperatif : jouer la migration
m20260918_000010_totp_last_used_stepavant de demarrer le binaire 1.1.0. Le modele SeaORMuser_totpdeclarelast_used_step, donc SeaORM emet la liste de colonnes explicite : sur un schema pre-migration, toute requete suruser_totpechoue — y comprisTotpService::is_totp_enabled, appelee a chaque login. Un replica 1.1.0 sur un schema pre-migration fait donc echouer tous les logins en HTTP 500 (pas seulement ceux des comptes 2FA) tant que la colonneuser_totp.last_used_stepest absente.
Ordre d'operations
# 1. Sauvegarde avant tout pg_dump "$DATABASE_URL" > backup-avant-1.1.0.sql # 2. Recuperer le framework (vendor + docs framework-owned) cargo run -p rustwarden-cli -- update-project # 3. Aligner les pins de dependances de crates/rustwarden-core/Cargo.toml # sea-orm / sea-orm-migration = "2.0.0", sea-bae = "0.2.2", # validator_derive = "0.20.1" cargo update # 4. Migrations AVANT le deploiement du binaire # m20260918_000010_totp_last_used_step + m20260919_000011_pending_email_changes cargo run --bin migrate # (ou auto_migrate(true) si deploiement mono-instance) # 5. Rediriger les appels qui changeaient l'email (RFC 0041) grep -rn "update_profile" src/ # 6. Lint cargo clippy --workspace -- -D warnings # 7. Smoke tests auth : login, register, changement d'email, 2FA
cargo clippy --workspace --all-features est a eviter : la feature
redis-session de rustwarden-core ne compile pas.
RFC 0036 — newtype Username
docs/rfcs/0036-username-charset-validation.md
| Sujet | Avant (1.0.1) | Apres (1.1.0) |
|---|---|---|
| Charset username | Aucune contrainte : John_Doe, alice.bob, backticks et $ acceptes | [a-z0-9-] uniquement. Majuscule rejetee (pas de fold : Alice → erreur, pas alice) |
| Longueur | RegisterRequest : 3–64 | 3–39 (apres trim), RegisterRequest : max = 39 |
| Tirets | Libres | Ni en tete ni en queue, pas de -- consecutifs |
| Noms reserves | Aucun | types::team_slug::RESERVED + me + explore |
| Porte de validation | Aucune | UserService::create_user (unique gate) + POST /api/v1/auth/register (defense en profondeur) |
Rename (update_profile) | Unicite seulement | Username::parse seulement sur un vrai rename ; echo du username courant = no-op |
| Bootstrap admin | create_user | create_user_allow_reserved (pub(crate), reserve a ensure_default_admin) → ADMIN_USERNAME=admin continue de booter |
| OAuth auto-username | _ possible, longueur libre | OAuthService::generate_unique_username : minuscules [a-z0-9], base a 30 chars, suffixes -NNN / -user / -<hex8> |
| Schema DB | — | Aucun CHECK SQL, aucune migration, aucun rewrite |
| Login des comptes existants | — | Inchange — les handles hors charset se connectent toujours |
Actions consommateur
# Usernames generes par votre app : passent-ils Username::parse ? grep -rn "create_user(" src/ grep -rn "update_profile(" src/ # Newtype Username local a retirer (ex. gitrust) grep -rni "struct Username\|fn parse_username\|validate_username" src/
-
Si votre app genere des usernames (import LDAP, seed, provisioning SSO maison), verifier qu'ils sont en minuscules,
[a-z0-9-], 3–39 caracteres, sans--. Sinoncreate_userrenvoie desormaisAppError::Validation(HTTP 400). -
Retirer un newtype
Usernamelocal : utiliserrustwarden_core::types::Username(re-exporte au prelude). Pour valider un identifiant avant de le stocker ailleurs :use rustwarden_core::prelude::*; let username = Username::parse(&raw_input)?; // AppError::Validation si invalide UserService::create_user(&db, username.into_inner(), email, hash, false).await?;
-
Formulaires cote client : aligner le
maxlength/ le pattern HTML sur 39 et[a-z0-9-]pour ne pas renvoyer une erreur serveur a l'utilisateur.rustwarden-uile fait desormais pour ses propres ecrans (login.htmlinscription,profile.html,admin/user_form.htmlen modecreate) et ses six locales decrivent la bonne regle — si vous avez forke ces templates ou ces fichierslocales/*.yml, reportez le changement a la main :maxlength="39",pattern="[a-z0-9-]+", et le texte d'aide « minuscules, chiffres et tirets uniquement, 3 a 39 caracteres ».
Pieges
- Comptes existants hors charset : ils se connectent toujours, mais toute
tentative de rename vers une autre valeur passe par
Username::parse. Un utilisateurJohn_Doepeut renvoyerJohn_Doe(no-op), il ne peut pas renommer enJohn_Doe2. ADMIN_USERNAMEavec une majuscule ou un_: le chemin de creation de l'admin par defaut (ensure_default_admin→validate_new_admin_credentials→AdminConfig::validate) delegue aUsername::parse_allow_reserved, qui refuse toujours le mauvais charset.build()propage l'erreur : le boot echoue. A noter : ce controle ne s'applique que sur le chemin de creation (aucun admin en base +ADMIN_PASSWORDdefini). Un deploiement dont l'admin existe deja n'est pas impacte. Le wizardcargo run -p rustwarden-cli -- start-projectvalide maintenant cette saisie au moment de la poser, pour que l'erreur remonte dans le wizard et non au premierbuild().- Le nom reserve
adminreste accepte pour le bootstrap uniquement ; unPOST /api/v1/auth/registeravecadminest refuse.
RFC 0037 — anti-rejeu TOTP persistant (last_used_step)
docs/rfcs/0037-totp-last-used-step.md
| Sujet | Avant (1.0.1) | Apres (1.1.0) |
|---|---|---|
| Anti-rejeu | Garde in-process (HashMap OnceLock, TTL 90 s) | Garde persistee en base : user_totp.last_used_step BIGINT NULL |
| Multi-replicas | Un code valide rejouable sur un autre replica | Ferme : compare-and-set SQL partage |
| Mecanisme | — | UPDATE user_totp SET last_used_step = $step WHERE user_id = $id AND (last_used_step IS NULL OR last_used_step < $step), succes ssi rows_affected == 1 |
| Pas consomme | — | Le pas qui a matche (matching_totp_step, skew ±1), jamais now / 30 |
| Rejeu detecte | — | Ok(false) — meme reponse qu'un code invalide, pas d'oracle |
| Couverture | verify_code | verify_code et verify_and_enable (le code de setup ne peut plus servir de login immediat) |
| Erreur DB | — | Propagee en AppError — jamais fail-open |
| Plafonds 1.0.1 | 15 tentatives / 15 min (somme), 5 par challenge | Inchanges, la garde in-process est conservee en defense en profondeur |
Actions consommateur
# Patch local du rejeu TOTP a retirer grep -rn "last_used_step\|replay_guard\|totp_replay" src/
- Jouer la migration (
cargo run --bin migrateouauto_migrate(true)). - Verifier la colonne :
\d user_totpdoit listerlast_used_step | bigint | nullable. - Retirer tout patch local dupliquant l'anti-rejeu TOTP.
Pieges
- Ordre de deploiement : migration d'abord, binaire ensuite — le rayon de
souffle depasse le 2FA. Le modele SeaORM
user_totpdeclare desormaislast_used_step; SeaORM genere une liste de colonnes explicite, donc sur un schema pre-migration toute requete suruser_totpechoue (column user_totp.last_used_step does not exist), pas seulement le compare-and-set. OrTotpService::is_totp_enabledest appelee a chaque login parauth/handlers.rs: tous les logins renvoient 500, y compris ceux des comptes sans 2FA. Ce n'est donc pas une degradation partielle du 2FA, c'est une panne totale de l'authentification jusqu'a ce que la colonne existe. last_used_step IS NULL= « aucun pas consomme depuis la migration ». Le premier code presente apres migration est donc toujours accepte (grandfathering des lignes existantes) — c'est voulu.- La colonne est ajoutee sans defaut : l'
ALTER TABLE ... ADD COLUMN ... NULLne reecrit pas la table.
RFC 0041 — changement d'email en deux temps (remplace la RFC 0038)
docs/rfcs/0041-pending-email-changes.md
La RFC 0038 est Superseded : elle n'a jamais ete publiee, il n'y a donc aucune note de transition v1 → v2. Si vous aviez commence a l'implementer sur une pre-release, la colonne de droite ci-dessous est la seule qui compte.
| Sujet | Avant (1.0.1) | Apres (1.1.0) |
|---|---|---|
update_profile + nouvelle adresse | users.email ecrit immediatement, email_verified conserve a true | Refus : AppError::Validation qui renvoie vers EmailChangeService — l'adresse n'est jamais ecrite par ce chemin |
update_profile + echo de l'adresse courante | Ecriture silencieuse (valeur identique) | No-op complet, comparaison exacte apres trim — les formulaires qui resoumettent tous les champs continuent de marcher |
| Chemin utilisateur | — | EmailChangeService::request_change(&db, user_id, new_email, current_password) : mot de passe courant exige, ligne pending_email_changes, deux mails en file |
| Etat de la base tant que le lien n'est pas suivi | — | Rien ne bouge : users.email inchange, email_verified toujours true |
| Confirmation | — | POST /api/v1/auth/email-change/confirm/{token} : adresse ecrite deja verifiee, tous les refresh tokens revoques, demandes de changement de mot de passe invalidees, audit_logs email_changed |
| Mail vers la nouvelle adresse | Aucun | Lien de confirmation |
| Mail vers l'ancienne adresse | Aucun (ou duplique cote UI) | Avis + lien de signalement (alerte les admins, ne bloque pas) |
| Chemin admin | PUT /admin/users/:id ecrivait l'adresse | EmailChangeService::admin_force_change — role admin exige, sessions de la cible revoquees, audit_logs email_changed_by_admin (WARN). PUT /admin/users/{id} ne change plus l'adresse |
| Echec SMTP / file d'e-mails | Best-effort (l'update reussissait quand meme) | Fail-loud : request_change echoue, rien n'est ecrit — pas de demande dont le lien n'arriverait jamais |
| Limiteur | Budget IP seulement | 3 demandes / heure / utilisateur (email_change:{user_id}) + budget IP sur POST /api/v1/auth/email-change |
| Logs / audit / hooks | Adresse en clair possible | Adresses masquees (jo***@ex***), y compris dans le corps de reponse |
| Faute de frappe sur la nouvelle adresse | Lock-out definitif du compte | Sans effet : le lien part dans le vide, la demande expire, le compte n'a jamais bouge |
| Rename username seul | — | Aucun impact sur l'email |
Nouvelles routes (canal machine, merge_machine_routes / auth_api_routes())
Les liens envoyes par e-mail ouvrent une page interstitielle : le GET
affiche un formulaire, c'est le POST du bouton qui agit. Aucun GET n'a
d'effet de bord, donc aucune passerelle anti-phishing qui precharge les liens
ne peut confirmer un changement d'adresse a la place de l'utilisateur.
| Methode | Chemin | Auth | Effet |
|---|---|---|---|
| POST | /api/v1/auth/email-change | Bearer + mot de passe courant dans le corps | ouvre la demande |
| GET | /api/v1/auth/email-change/confirm/{token} | public (lien de mail) | rend la page, n'ecrit rien |
| POST | /api/v1/auth/email-change/confirm/{token} | public (bouton de la page) | applique le changement |
| GET | /api/v1/auth/email-change/report/{token} | public (lien de mail) | rend la page, n'ecrit rien |
| POST | /api/v1/auth/email-change/report/{token} | public (bouton de la page) | enregistre le signalement |
Les pages sont du HTML minimal autoportant produit par le core : ni Askama, ni
rustwarden-ui, ni JavaScript, ni ressource externe. Un consommateur qui veut
sa propre presentation monte sa propre route GET et poste vers la route
POST du core.
Actions consommateur
# 1. Appels qui passaient une nouvelle adresse grep -rn "update_profile" src/ # 2. Code applicatif devenu redondant grep -rn "email_verified" src/ grep -rn "account_modified" src/ grep -rn "invalidate_on_email_change" src/
-
Jouer la migration
m20260919_000011_pending_email_changes. Elle fait deux choses :- elle cree la table
pending_email_changes— nouvelle, aucune donnee existante n'est touchee,downla supprime ; - elle elargit la contrainte
chk_email_queue_email_typedeemail_queueaux quatre types de la RFC 0041 (email_change_validation,email_change_notice,email_change_reported,email_change_by_admin). Sans cet elargissement, le premierINSERTdu flux serait rejete par PostgreSQL (23514). Si vous avez redefini cette contrainte dans une migration a vous, rejouez-la apres celle-ci ou ajoutez-y les quatre valeurs : la migration part de la liste du schema initial et n'a aucun moyen de deviner vos ajouts. Ledownne retrecit la contrainte que si aucune ligne d'email_queuene porte encore un de ces types — un rollback ne detruit pas l'historique des mails envoyes.
- elle cree la table
-
Rediriger les appels a
update_profilequi passent une nouvelle adresse versEmailChangeService::request_change. Un echo de l'adresse courante reste un no-op : rien a changer pour les formulaires qui resoumettent tous les champs. -
Chemin administrateur : tout consommateur qui appelait
PUT /admin/users/{id}(ouupdate_profiledepuis un handler admin) pour corriger l'e-mail d'un compte doit basculer sur :EmailChangeService::new(email_config) .admin_force_change(&db, admin_id, user_id, "fixed@example.com", None) .await?;
C'est la meme capacite qu'avant, mais explicite et auditee au lieu d'etre un effet de bord de la mise a jour de profil. Le reste de
PUT /admin/users/{id}(username, roles, statut) est inchange. -
Retirer tout code applicatif qui, apres un changement d'adresse, remettait
email_verifieda la main, enqueuait un mail de validation / une alerteaccount_modified, ou appelaitPasswordChangeService::invalidate_on_email_change: le core emet ce dernier lui-meme dansconfirm_changeetadmin_force_change, c'est-a-dire aux deux seuls instants ou l'adresse change reellement. -
Verifier SMTP et le worker
email_queueavant le bump : sans file fonctionnelle,request_changeechoue (volontairement) au lieu d'ecrire une adresse non prouvee. -
Facultatif, purge des demandes expirees — le framework ne la demarre pas, comme pour les processeurs existants :
PendingEmailChangeCleanupProcessor::new(db.clone()).start();
Retention : 30 jours apres expiration, constante de code, aucune variable d'environnement a definir. Sans ce demarrage, une purge opportuniste limitee a l'utilisateur courant s'execute a chaque nouvelle demande.
-
Cote UI : rien d'obligatoire. Le champ e-mail du formulaire de profil ne produit plus d'erreur tant qu'il renvoie l'adresse courante. Les ecrans dedies (demande en cours, annulation, double saisie) sont un chantier UI separe.
-
Si vous surchargez les templates d'e-mail du framework, exposez les deux liens. Attention au libelle : le lien ouvre une page de confirmation, il ne confirme pas directement.
Pieges
- Duree de validite : le lien utilise
email_validation_expiration_hours, le reglage deja en place pour la validation d'adresse. Pas de nouvelle variable d'environnement. - Les sessions tombent a la confirmation, pas a la demande. L'utilisateur qui confirme depuis sa nouvelle boite est deconnecte partout et doit se reconnecter avec la nouvelle adresse : dites-le dans l'ecran qui ouvre la demande, sinon le support recoit des tickets « je suis deconnecte ».
- Le signalement n'annule rien. Le lien envoye a l'ancienne adresse alerte
les administrateurs et trace en
audit_logs(WARN) ; il ne bloque pas le changement. La boite qui recoit ce lien peut alerter, pas decider — c'est un choix explicite de la RFC (un attaquant qui controle l'ancienne boite ne doit pas pouvoir bloquer la reprise de controle legitime). - Course sur la meme adresse cible : si deux comptes demandent la meme
adresse, le second a confirmer recoit un
Validationneutre (« adresse plus disponible ») qui ne nomme pas le compte gagnant, et sa ligne n'est pas consommee — son lien refonctionne si l'adresse se libere avant expiration. - Recours perdu : un utilisateur qui a perdu a la fois son mot de passe
et l'acces a son adresse courante ne peut plus se debloquer seul ; c'est un
recours administrateur (
admin_force_change) apres verification d'identite hors bande. - Faute de frappe a l'inscription : hors perimetre.
registerecrit l'adresse saisie sans double verification ; un utilisateur qui se trompe au signup releve du meme recours administrateur. Si vous voulez fermer ce cas, ajoutez une double saisie dans votre formulaire d'inscription. - La locale des mails suit le
Localede la requete quand l'application monte l'I18nService; sans extension i18n, le service retombe sur les libelles anglais embarques.
RFC 0039 — SecurityHeadersConfig
docs/rfcs/0039-security-headers-config.md
| Sujet | Avant (1.0.1) | Apres (1.1.0) |
|---|---|---|
| CSP | Litteral hardcode dans security_headers_middleware | SecurityHeadersConfig.content_security_policy, configurable |
| COOP / CORP | Jamais emis | Opt-in : Option<Cow<'static, str>>, None par defaut = header non emis |
| Wiring | — | RustwardenApp::builder().security_headers(cfg) ; le middleware capture un Arc a la construction (pas d'Extension : le layer est plus externe que les Extension(...)) |
| Valeur par defaut | — | Bit-identique a 1.0.1 — 'unsafe-inline' et 'unsafe-eval' conserves (Alpine.js) |
| Autres headers | X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, HSTS | Inchanges et non configurables (non-goals de la RFC) |
| Valeur invalide | — | CSP invalide → fallback sur le defaut + warning ; COOP/CORP invalide → header saute + warning. Jamais de panique |
Ce que la RFC ne fait PAS : elle n'ameliore aucune politique par defaut.
Un consommateur qui n'appelle jamais .security_headers(..) obtient exactement
les memes en-tetes qu'en 1.0.1. La constante CSP_WITHOUT_UNSAFE_EVAL est
fournie mais non utilisee par Default — le durcissement du defaut est une
Phase 2 ulterieure, apres migration Alpine CSP
(plans/PLAN_CSP_HTMX.md).
Actions consommateur
# Overlay CSP local a retirer (ex. gitrust) grep -rn "SetResponseHeaderLayer" src/ grep -rn "CONTENT_SECURITY_POLICY\|Cross-Origin-Opener-Policy\|content-security-policy" src/
use rustwarden_core::prelude::*; // SecurityHeadersConfig // AVANT — overlay tower-http par-dessus le middleware framework let router = router.layer(SetResponseHeaderLayer::overriding( header::CONTENT_SECURITY_POLICY, HeaderValue::from_static("default-src 'self'; script-src 'self'"), )); // APRES — configuration native let app = RustwardenApp::builder() .from_env() .security_headers( SecurityHeadersConfig { content_security_policy: "default-src 'self'; script-src 'self'".into(), ..SecurityHeadersConfig::default() } .with_same_origin_isolation(), // COOP + CORP = same-origin ) .build() .await?;
Pieges
- Une CSP custom trop stricte casse l'UI
rustwarden-ui(HTMX + Alpine.js ont besoin de'unsafe-inline'et, pour la build Alpine standard,'unsafe-eval'). A reserver aux apps headless ou a UI maison. - COOP/CORP
same-origincassent les integrations cross-origin (iframes, CDN d'assets, widgets tiers). Tester avant d'activer.
RFC 0040 — authenticate_user honore validation_email_required
docs/rfcs/0040-authenticate-user-email-verified.md
| Sujet | Avant (1.0.1) | Apres (1.1.0) |
|---|---|---|
| Ou vit le controle | Handler HTTP POST /api/v1/auth/login | AuthService::authenticate_user (service) |
| Appel direct au service | Contourne la politique | Applique la politique |
| Reglage | validation_email_required / alias validation_email_requise | Identique |
| Defaut | true | true (fail-closed : erreur de lookup = verification requise) |
| Message d'erreur | Francais | Identique, meme AppError::Validation (HTTP 400) |
| Ordre des controles | — | Mot de passe d'abord, politique email ensuite → pas d'oracle « compte existant non verifie » |
verify_credentials | Mot de passe seul | Inchange — primitive mot de passe seul, pas de controle email (utilisee par le re-prompt TOTP) |
Hook auth.login_blocked | Emis par le handler | Plus emis (decision v1) — la ligne audit_logs est toujours ecrite |
Actions consommateur
grep -rn "authenticate_user\|verify_credentials" src/ grep -rn "login_blocked\|on_audit_event" src/
- Retirer tout patch local qui dupliquait le controle
email_verifiedapresauthenticate_user(double verification = double message d'erreur). - Si votre SSR appelait
authenticate_user: il applique desormais la politique. Verifier que votre ecran de login affiche le message d'erreur et propose « renvoyer l'email de validation ». - Si vous avez volontairement besoin d'un login sans controle email (outil
interne, migration), utilisez
verify_credentialsen connaissance de cause — c'est un contournement explicite de la politique.
Pieges
- Le hook
on_audit_event/auth.login_blockedn'est plus emis sur ce chemin. Une alerte SIEM branchee sur ce hook devient silencieuse : basculer la detection sur la tableaudit_logs(action = "login_blocked"). - Aucune interaction avec le changement d'email : la RFC 0041 ecrit la nouvelle adresse deja verifiee, donc un changement d'adresse ne bloque jamais le login suivant. Le couplage « changement d'adresse => prochain login refuse » qu'annoncait la RFC 0038 n'existe pas. Le compte reste soumis a cette politique sur le seul cas qu'elle vise : une inscription dont l'adresse n'a jamais ete validee.
- Le reglage reste global (
app_settings), pas par utilisateur.
Recapitulatif des workarounds consommateur a retirer
| Workaround | Remplace par |
|---|---|
Newtype Username / validate_username() local (gitrust) | rustwarden_core::types::Username (prelude) |
SetResponseHeaderLayer::overriding(CONTENT_SECURITY_POLICY, …) (gitrust) | builder().security_headers(SecurityHeadersConfig { .. }) |
| Headers COOP/CORP poses a la main | SecurityHeadersConfig::with_same_origin_isolation() |
Ecriture de users.email via update_profile / PUT /admin/users/{id} | EmailChangeService::request_change (utilisateur) ou admin_force_change (operateur) (RFC 0041) |
Reset manuel de email_verified apres un changement d'adresse | Sans objet : l'adresse est ecrite deja verifiee a la confirmation (RFC 0041) |
Enqueue applicatif du mail de validation / account_modified sur changement d'email | Fait par le core (RFC 0041) |
Appel applicatif a PasswordChangeService::invalidate_on_email_change | Fait par le core dans confirm_change / admin_force_change (RFC 0041) |
| Double saisie maison pour eviter le lock-out sur faute de frappe | Sans objet : l'adresse courante ne bouge pas tant que la nouvelle n'est pas confirmee (RFC 0041) |
Re-verification de email_verified apres authenticate_user | Fait par le service (RFC 0040) |
| Patch local de l'anti-rejeu TOTP | user_totp.last_used_step (RFC 0037) |
2026-09-18 — Security 1.0.1 (patch interne)
Patch de securite sans nouvelle API pub. Fenetre d'adoption 14 jours apres
diffusion de [1.0.1]. Details techniques : CHANGELOG [1.0.1],
SERVICES_REFERENCE.md bandeau 1.0.1 + §4, §6, §12, §15, §18, §19.
Mise a jour 2026-09-19 : les RFC 0036–0040 (charset username,
last_used_stepTOTP, re-validation email, headers de securite configurables,email_verifieddansauthenticate_user) sont depuis passees Accepted et livrees dans[1.1.0], la RFC 0038 ayant ete remplacee par la RFC 0041 (changement d'email en deux temps) avant publication. Voir l'entree 2026-09-18 — RFC 0036-0041 (1.1.0) ci-dessus.
Breaking operational (pas d'API)
| Sujet | Avant | Apres 1.0.1 |
|---|---|---|
| Serve HTTP | axum::serve(listener, router) sans ConnectInfo → toutes les cles auth:unknown | RustwardenApp::run() utilise into_make_service_with_connect_info::<SocketAddr>(). Si vous servez le routeur vous-memes, faire de meme. |
JWT_SECRET | Placeholders anglais uniquement | Egalite exacte aussi avec REMPLACER_PAR_UNE_CLE_SECRETE_LONGUE → refus de boot. |
ADMIN_PASSWORD | Placeholder / trop faible pouvait casser un redeploiement | AdminConfig::validate() seulement si creation d'admin. Sans variable, pas de bootstrap. |
| PostgreSQL compose | Mot de passe par defaut, bind 0.0.0.0 | Copier database/env.sample → database/.env, POSTGRES_PASSWORD obligatoire (:?), bind 127.0.0.1:5432. |
| Reset MDP faible | Token consomme puis 400 | Complexite verifiee avant hash / used_at. |
| Rate limiter plein | 429 sur une nouvelle cle | Eviction LRU a 100 000 entrees. |
| Auth middleware | Fail-open lookup user / blacklist / TOTP force | Fail-closed (503 sur erreur TOTP force). |
| TOTP login | Compteur de challenges, pas de rejeu in-process | 15 tentatives / 5 min ; rejeu in-process seulement. |
.env.example CLI | JWT_SECRET / ADMIN_PASSWORD placeholder | Secrets a definir ; ne plus copier un placeholder. |
Workarounds consommateur a retirer
Apres vendor / update-project + rebuild :
- garde JWT maison dupliquant
AuthConfig(placeholders FR) ; - rate-limit login base uniquement sur
HeaderMap/X-Forwarded-Forpour compenserauth:unknown— le layer JSON lit ConnectInfo ; un POST/loginSSR horsauth_api_routes()reste a limiter cote app ; RateLimitService::new()dans chaque handler de reset / verify email (les services framework partagent unOnceLock) ;- patch local du plafond TOTP ou du rejeu in-process.
check_auth_rate_limit(&HeaderMap) n'a pas change de signature. Il ignore toujours ConnectInfo.
Dependances
rustwarden-core pin sea-bae = "0.2.2" et validator_derive = "0.20.1" (drop
proc-macro-error2 / RUSTSEC-2026-0173). Apres sync des crates, cargo update
puis verifier l'absence de proc-macro-error2 dans cargo tree -i proc-macro-error2.
validator reste 0.20 (pas de 0.20.1 sur crates.io pour le crate principal).
2026-03-26 — UserService : nouvelles methodes + hook on_user_deleted
Changement
4 methodes ajoutees a UserService et 1 hook au trait RustwardenHooks :
| Methode | Description |
|---|---|
unassign_role_from_user(db, user_id, role_id) | Retire un role. Protege le dernier admin. |
delete_user(db, user_id, hooks) | Supprime un utilisateur avec nettoyage complet. |
admin_reset_password(db, user_id, new_password) | Reset admin sans confirmation email. Force must_change_password. |
change_password_direct(db, user_id, old, new) | Changement direct avec verification de l'ancien mot de passe. |
Hook ajoute : on_user_deleted(db, user_id, username) — appele avant la suppression
pour permettre le nettoyage des donnees metier (walls, repos, etc.).
Migration
Aucune migration requise. Toutes les methodes sont additives. Le hook a une implementation par defaut (no-op).
Remplacement des contournements
Si votre projet manipulait directement les entites SeaORM pour ces operations, vous pouvez maintenant utiliser les methodes du framework :
// AVANT — suppression manuelle user::Entity::delete_by_id(user_id).exec(&db).await?; // APRES — nettoyage complet (tokens, hook, cascade) UserService::delete_user(&db, user_id, Some(&my_hooks)).await?;
// AVANT — retrait de role manuel user_role::Entity::delete_many() .filter(user_role::Column::UserId.eq(user_id)) .filter(user_role::Column::RoleId.eq(role_id)) .exec(&db).await?; // APRES — avec protection du dernier admin UserService::unassign_role_from_user(&db, user_id, role_id).await?;
// AVANT — reset admin manuel let hash = PasswordService::hash_password(new_pwd).await?; UserService::update_password_hash(&db, user_id, &hash).await?; // APRES — hash + must_change + revocation sessions UserService::admin_reset_password(&db, user_id, new_pwd).await?;
2026-03-26 — PasswordService : methodes rendues async
Changement
PasswordService::hash_password et PasswordService::verify_password sont desormais async.
Bcrypt est CPU-intensif (~100-300ms a cost 12). Les methodes utilisent maintenant
tokio::task::spawn_blocking en interne pour ne pas bloquer le runtime Tokio.
Avant
let hash = PasswordService::hash_password("motdepasse")?; let valid = PasswordService::verify_password("motdepasse", &hash)?;
Apres
let hash = PasswordService::hash_password("motdepasse").await?; let valid = PasswordService::verify_password("motdepasse", &hash).await?;
Migration
La migration est mecanique : ajouter .await a chaque appel.
Rechercher dans votre projet :
grep -rn "PasswordService::hash_password\|PasswordService::verify_password" src/
Pour chaque occurrence, ajouter .await avant le ?. Les fonctions appelantes doivent
etre async (ce qui est deja le cas dans les handlers Axum et les fonctions #[tokio::main]).
Ce qui ne change pas
PasswordService::generate_secure_password()reste synchrone (pas de travail CPU lourd)PasswordService::generate_default_password()reste synchronePasswordService::validate_password_strength()reste synchrone- La signature de retour (
Result<String, AppError>/Result<bool, AppError>) est inchangee - Le comportement est identique (meme algorithme, meme cout bcrypt)
Workaround obsolete
Si votre projet wrappait manuellement les appels dans spawn_blocking, ce workaround
est maintenant inutile et peut etre supprime :
// AVANT — workaround dans le projet consommateur (a supprimer) let password = password.to_string(); let hash = tokio::task::spawn_blocking(move || { PasswordService::hash_password(&password) }).await??; // APRES — le spawn_blocking est integre dans le framework let hash = PasswordService::hash_password(&password).await?;
2026-03-26 — Getters sur RustwardenApp
Changement
RustwardenApp expose desormais 4 getters apres build() :
| Getter | Type retourne | Description |
|---|---|---|
database() | &DatabaseConnection | Connexion base de donnees du framework |
auth_config() | &AuthConfig | Configuration d'authentification |
i18n_service() | &Arc<I18nService> | Service de traduction |
address() | &str | Adresse de bind du serveur |
Migration
Aucune migration requise. Les getters sont purement additifs. Le code existant continue de fonctionner sans modification.
Nouveau pattern recommande
Les projets consommateurs peuvent simplifier leur main.rs :
// AVANT — gestion manuelle de la connexion DB let db = rustwarden_core::config::database::create_connection().await?; my_app::migrations::run_migrations(&db).await?; let app = RustwardenApp::builder() .from_env() .database(db.clone()) // clone obligatoire .auto_migrate(false) .build().await?; app.run().await // APRES — le framework gere la DB let app = RustwardenApp::builder() .from_env() .auto_migrate(true) // migrations framework automatiques .build().await?; // Migrations metier sur la meme connexion my_app::migrations::run_migrations(app.database()).await?; app.run().await
L'ancien pattern (create_connection() + db.clone()) reste fonctionnel.
2026-03-26 — rustwarden-cli start-project : documentation copiee
Changement
Le wizard start-project copie desormais les fichiers de documentation framework
dans le dossier docs/ du nouveau projet :
docs/SERVICES_REFERENCE.md— reference complete des services du frameworkdocs/MIGRATION_GUIDE.md— ce fichier
Migration
Aucune migration requise. Les projets existants peuvent copier ces fichiers manuellement depuis le depot Rustwarden.
2026-03-23 — Remember Me : session longue duree
Changement
Le flux d'authentification supporte un champ optionnel remember_me qui etend
la duree du refresh token (30 jours au lieu de 7 par defaut).
Cote serveur (rustwarden-core)
LoginRequest — nouveau champ :
pub struct LoginRequest { pub username: String, pub password: String, #[serde(default)] // retrocompatible : false si absent pub remember_me: bool, }
AuthConfig — nouvelle variable d'environnement :
| Variable | Defaut | Description |
|---|---|---|
REMEMBER_ME_EXPIRATION_DAYS | 30 | Duree du refresh token avec remember me |
LoginResponse et RefreshResponse — nouveau champ :
pub refresh_expires_in: i64, // duree du refresh token en secondes
refresh_token() handler — conserve la duree d'origine lors de la rotation :
un token remember me de 30 jours genere un nouveau token de 30 jours (pas de
raccourcissement a 7 jours). Le handler set aussi le cookie jwt_token dans la
reponse (coherence avec le flux login).
Cote client (rustwarden-ui)
login.html — checkbox "Se souvenir de moi" ajoutee au formulaire. Au login reussi,
le JS stocke dans localStorage :
refresh_token— le token de rafraichissementremember_me—"true"si cochejwt_expires_in— duree du JWT en secondes
base.html — script d'auto-refresh ajoute. Si remember_me === "true" en
localStorage, un setInterval appelle POST /api/v1/auth/refresh toutes les
(JWT_EXPIRATION - 2 min) pour renouveler le JWT avant expiration.
Migration
Aucune migration requise. Le champ remember_me est optionnel (#[serde(default)]).
Les clients existants qui n'envoient pas ce champ obtiennent le comportement standard
(refresh token de 7 jours).
Migration optionnelle pour en beneficier :
- Ajouter
REMEMBER_ME_EXPIRATION_DAYS=30au.env(ou garder le defaut) - Si votre projet a un formulaire de login custom (qui n'utilise pas
login.htmldu framework), ajouter une checkbox qui envoie"remember_me": truedans le body JSON duPOST /api/v1/auth/login - Si votre projet a un template de base custom (qui n'etend pas
base.htmldu framework), ajouter le script d'auto-refresh JWT (voircrates/rustwarden-ui/templates/base.htmllignes 174-221)
Aucune migration de base de donnees
La table refresh_token stocke deja expires_at par token. Le remember me utilise
cette colonne existante avec une duree plus longue.
2026-03-10 — OAuth / SSO (feature oauth)
Changement
Support OpenID Connect / OAuth2 pour Google, GitHub, Discord et Microsoft.
Protege derriere la feature oauth dans Cargo.toml.
Migration
Aucune migration requise si vous n'activez pas la feature.
Pour activer OAuth dans un projet existant :
-
Ajouter la feature dans
Cargo.toml:rustwarden-core = { path = "crates/rustwarden-core", features = ["oauth"] } -
Ajouter les variables d'environnement (voir
.env.example) :OAUTH_ENABLED=true OAUTH_REDIRECT_BASE_URL=https://votre-domaine.com OAUTH_ENCRYPTION_KEY=<64 chars hex> OAUTH_GOOGLE_ENABLED=true OAUTH_GOOGLE_CLIENT_ID=... OAUTH_GOOGLE_CLIENT_SECRET=...
-
La migration de base de donnees (
create_oauth_accounts_table) est incluse danscore_migrations()et sera executee automatiquement au prochain demarrage siauto_migrate(true).
Routes ajoutees
| Route | Description |
|---|---|
GET /api/v1/auth/oauth/{provider}/redirect | Initie le flux OAuth |
GET /api/v1/auth/oauth/{provider}/callback | Callback apres autorisation |
GET /api/v1/auth/oauth/providers | Liste les providers actifs (JSON) |
GET /api/v1/auth/oauth/accounts | Comptes OAuth lies de l'utilisateur |
DELETE /api/v1/auth/oauth/accounts/{id} | Delier un compte OAuth |
GitRust