Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Rien pour l'instant.
[1.2.0] - 2026-10-02 — Breaking (RFC 0042) + Security (RFC 0039 phase 2)
Minor assumé breaking sur un comportement par défaut (décision D1 du
2026-10-02, docs/rfcs/0042-auth-middleware-resolved-auth-config.md) :
les consommateurs sont adaptés manuellement au déploiement. Les sept
manifestes Cargo.toml du workspace passent à 1.2.0 (ils étaient restés à
1.0.0 malgré les trains 1.0.1 et 1.1.0).
⚠️ BREAKING — RFC 0042 : les overrides du builder atteignent enfin les middlewares de préfixe
AuthService (préfixes enregistrés via register_protected_prefix) et
AdminService (/admin*, /api/v1/admin*) étaient posés avant le layer
Extension(Arc<AuthConfig>) dans RustwardenBuilder::build() ; ils retombaient
donc à chaque requête sur AuthConfig::from_env() et ignoraient
.login_path(..), .auth_failure_mode(..) et la bascule Json401 de
.headless(). Ils reçoivent désormais la configuration résolue à la
construction.
Ce qui change pour vous :
| Votre câblage | Avant 1.2.0 (bug) | Depuis 1.2.0 |
|---|---|---|
with_ui(..) | 303 → /login | inchangé |
.headless() + .auth_failure_mode(Redirect) (ex. BotPlace) | 303 (par l'env uniquement) | 303 (par le builder, comme documenté) |
.headless() + AUTH_FAILURE_MODE=redirect | 303 | 303 (l'env prime désormais sur la bascule headless) |
.headless() sans setter ni env | 303 → /login vers une page inexistante | 401 {"error":"unauthorized"} sur préfixes protégés et /api/v1/admin* |
.login_path("/x") | ignoré par les middlewares | honoré partout |
Précédence (nouvelle, explicite) : .auth_failure_mode(..) > AUTH_FAILURE_MODE >
bascule .headless() > Redirect.
Intervention consommateur au déploiement : si votre app .headless() sert une
UI SSR et comptait sur le 303, ajoutez .auth_failure_mode(AuthFailureMode::Redirect)
au builder (ou AUTH_FAILURE_MODE=redirect dans l'environnement — rollback sans
redéploiement de code). Si vous composez votre pile tower à la main, passez à
AuthService::with_auth_config / AdminService::with_auth_config.
Fixed
- RFC 0042.
AuthServiceetAdminServicehonorentRustwardenBuilder::login_path,::auth_failure_modeet la basculeJson401de::headless(). Préfixes protégés,/admin*et extracteursAuthUser/auth_requiredpartagent la même configuration : les middlewares la posent eux-mêmes dans les extensions de la requête, ce qui corrige aussi les piles tower composées à la main. rustwarden-ui::root_redirect(GET /) lit la configuration injectée avant de retomber sur l'environnement, et redirige verslogin_pathau lieu de/logincodé en dur.
Changed
AuthConfig::from_env()n'est plus exécutée à chaque requête par les middlewares de préfixe ni parroot_redirect; le logAuth configuration loadedn'apparaît plus qu'au démarrage dans le câblage canonique.- Variables
AUTH_FAILURE_MODEetLOGIN_PATHdocumentées dans.env.sampleettemplate/.env.sample. RustwardenBuilder::build()résout l'AuthConfigavant la connexion à la base, les migrations et la création de l'admin par défaut : unJWT_SECRETabsent ou placeholder fait échouerbuild()immédiatement (fail-fast), là où ces trois étapes s'exécutaient auparavant d'abord. L'ordre des logs de démarrage change en conséquence.- Une valeur
AUTH_FAILURE_MODEvide ou invalide est ignorée (avertissement au démarrage) et ne supprime pas la basculeJson401de.headless(): seule une valeur valide (redirect/json401) compte comme override opérateur.
Deprecated
AuthService::new(inner, db, prefixes)→AuthService::with_auth_config(inner, db, prefixes, auth_config).AdminService::new(inner, db)→AdminService::with_auth_config(inner, db, auth_config). Le chemin déprécié émet untracing::warn!une fois par processus. Suppression au plus tôt en 1.5.0, effective dans la prochaine major.
Security
- RFC 0039 phase 2.
SecurityHeadersConfig::defaultn'émet plus'unsafe-eval'. L'UI charge le build Alpine CSP.'unsafe-inline'est conservé. La politique 1.0.1 reste disponible viaDEFAULT_CONTENT_SECURITY_POLICYetRustwardenBuilder::security_headers.
Added
-
Mesure d'audience générique (
config::analytics). Support d'un script de suivi tiers modélisé comme une URL plus des attributsdata-*: couvre Rybbit, Plausible, Umami et Matomo-tag sans code spécifique par fournisseur. Ajout additif — par défaut le suivi est désactivé, rien n'est rendu et la CSP reste bit-identique.AnalyticsConfig(prelude) :from_env,validate,script_origin,render_snippet, setters chaînables.AnalyticsState/AnalyticsContext/AnalyticsHandle(prelude) : snippet pré-rendu au démarrage et rechargeable à chaud, plus deux extracteurs Axum infaillibles pour les templates et l'écran d'administration.RustwardenBuilder::analytics(cfg).SecurityHeadersConfig::allowing_script_origin(origin): ajoute une origine àscript-srcetconnect-src. Appliqué parbuild(), donc l'ordre des appels.analytics(..)/.security_headers(..)est indifférent.rustwarden-ui: section « Mesure d'audience » dans/admin/settings, composant d'oppositioncomponents/analytics_opt_out.html(clélocalStorageconfigurable), traductionsanalytics.opt_out.*etadmin.settings.analytics_*dans les six locales.- Trois clés
app_settings:analytics_enabled,analytics_site_id,analytics_attributes.
Aucune migration SeaORM. Aucun changement de comportement pour les consommateurs existants.
⚠️ L'URL du script n'est pas modifiable depuis l'administration : elle alimente la CSP, capturée une fois au
build()(contrainte RFC 0039), et reste donc une variable d'environnement (ANALYTICS_SCRIPT_URL). C'est aussi ce qui empêche un compte d'administration compromis d'élargir la CSP vers un domaine arbitraire. Détail :docs/SERVICES_REFERENCE.md§23.
[1.1.0] - 2026-09-18 — Security
Minor de sécurité livrant les RFC 0036 à 0040, toutes passées Accepted le
2026-09-18 et implémentées dans rustwarden-core. Deux ajouts d'API publique
(types::Username, config::security_headers::SecurityHeadersConfig) et quatre
durcissements de comportement sur des chemins d'authentification existants.
Fenêtre d'adoption 14 jours pour les consommateurs
(docs/RUSTWARDEN_EVOLUTION_PROCESS.md §9).
⚠️ Une migration SeaORM est à jouer :
m20260918_000010_totp_last_used_step(colonneuser_totp.last_used_step). Avecauto_migrate(true)elle part au boot ; sinoncargo run --bin migrate. Jouer la migration avant de déployer le nouveau binaire. Le modèle SeaORMuser_totpdéclarelast_used_step, donc SeaORM émet la liste de colonnes explicite : sur un schéma pré-migration, toute requête suruser_totpéchoue — y comprisTotpService::is_totp_enabled, appelée à chaque login. Conséquence : un réplica 1.1.0 sur un schéma pré-migration fait échouer tous les logins en HTTP 500, pas seulement ceux des comptes 2FA, tant que la colonneuser_totp.last_used_stepest absente. Procédure complète :docs/MIGRATION_GUIDE.md§ 2026-09-18 — RFC 0036-0040 (1.1.0).
Le versioning minor (et non patch) reflète les deux API ajoutées ; la section
Security ci-dessous liste les changements de comportement, qui sont techniquement
non-breaking à la compilation mais peuvent refuser des entrées auparavant
acceptées (usernames hors charset, login sans e-mail vérifié via appel service).
Added
- Newtype
Username(docs/rfcs/0036-username-charset-validation.md) —rustwarden_core::types::Username, re-exporté au prelude.Username::parse(&str) -> Result<Username, AppError>applique des règles GitHub-like : 3 à 39 caractères après trim, charset[a-z0-9-], pas de--, pas de-en tête ni en queue, majuscules rejetées (aucun fold silencieux :Aliceéchoue, il ne devient pasalice), et noms réservés refusés (types::team_slug::RESERVED+EXTRA_RESERVED=me,explore). Accesseursas_str(),into_inner(),Display,Serialize/Deserializetransparents. Calqué surTeamSlug. SecurityHeadersConfig(docs/rfcs/0039-security-headers-config.md) —rustwarden_core::config::security_headers::SecurityHeadersConfig, re-exporté au prelude, avec la méthode builderRustwardenApp::builder().security_headers(cfg). Champs :content_security_policy: Cow<'static, str>,cross_origin_opener_policy: Option<Cow<'static, str>>,cross_origin_resource_policy: Option<Cow<'static, str>>, plus le helperwith_same_origin_isolation()(COOP + CORP àsame-origin). ConstantesDEFAULT_CONTENT_SECURITY_POLICYetCSP_WITHOUT_UNSAFE_EVAL. Le défaut est bit-identique à 1.0.1 : même CSP ('unsafe-inline'et'unsafe-eval'conservés pour Alpine.js), COOP/CORP non émis. Un consommateur qui n'appelle jamais.security_headers(..)ne voit aucun changement d'en-tête.CSP_WITHOUT_UNSAFE_EVALest fournie mais non utilisée parDefault: le durcissement du défaut est une Phase 2 ultérieure, après la migration Alpine CSP (docs/plans/PLAN_CSP_HTMX.md).EmailChangeServiceet la tablepending_email_changes(docs/rfcs/0041-pending-email-changes.md) —rustwarden_core::services::email_change_service::EmailChangeService, le changement d'adresse en deux temps. Méthodes publiquesnew,with_i18n,request_change,confirm_change,report_suspicious,admin_force_change,cleanup_expired, plus les lectures sans effet de bordinspect_confirmation_token/inspect_report_token. Nouvelle migrationm20260919_000011_pending_email_changes(tablepending_email_changes:token_hashetreport_token_hashuniques en SHA-256,expires_at,used_at,reported_at, FKON DELETE CASCADE). La même migration élargit la contraintechk_email_queue_email_typeaux quatre nouveaux types (email_change_validation,email_change_notice,email_change_reported,email_change_by_admin) : sans cet élargissement le premierINSERTdu flux échouerait. La contrainte reste fermée — unemail_typeinconnu est toujours rejeté — et ledown()ne la resserre que si plus aucune ligne n'utilise ces types, pour ne jamais détruire d'historique d'envoi. Nouveau modèlemodels::pending_email_change, nouveau processeur de purgePendingEmailChangeCleanupProcessor(prelude) — comme les processeurs existants,app.rsne le démarre pas : appelez.start()vous-même. Durée de validité du lien :email_validation_expiration_hours, le réglage déjà utilisé par la validation d'adresse.- Cinq routes de changement d'adresse (canal machine,
auth_api_routes()) —POST /api/v1/auth/email-change(Bearer + mot de passe courant), puisGET/POSTsur/api/v1/auth/email-change/confirm/{token}et/api/v1/auth/email-change/report/{token}. Les liens envoyés par e-mail ouvrent une page interstitielle : leGETn'a aucun effet de bord et rend un formulaire, c'est lePOSTdu bouton qui agit — une passerelle anti-phishing qui précharge les liens ne peut donc pas confirmer un changement d'adresse à la place de l'utilisateur. Ces pages sont du HTML minimal autoportant produit par le core (ni Askama, nirustwarden-ui, ni JavaScript, ni ressource externe), donc montables en headless. Quatre nouveaux templates d'e-mail et deux nouvelles sections de locales (email_change,email_change_page) dans les six langues.
Changed
RegisterRequest.username(POST /api/v1/auth/register) passe de#[validate(length(min = 3, max = 64))]àmax = 39, et le handler appelleUsername::parseaprèsvalidate()(défense en profondeur — la porte de validation unique resteUserService::create_user).UserService::create_uservalide désormais son argumentusernameviaUsername::parse. Un chemin bootstrappub(crate) create_user_allow_reserved(charset + longueur, mais noms réservés autorisés) est réservé àensure_default_admin, pour queADMIN_USERNAME=admincontinue de booter.AdminConfig::validatedélègue au même parse permissif.OAuthService::generate_unique_usernameproduit des candidats conformes au nouveau charset : minuscules,[a-z0-9]uniquement, base tronquée à 30 caractères (marge pour les suffixes-NNN/-<hex8>), suffixe-usersi la base tombe sur un nom réservé. Les séparateurs sont des-(plus de_).rustwarden-ui—handlers::user_handler::CreateUserRequest.usernamepasse à#[validate(length(min = 3, max = 39))].rustwarden-ui— les formulaires livrés appliquent enfin la RFC 0036 côté client :templates/profile.htmlpasse demaxlength="50" pattern="[a-zA-Z0-9_-]+"àmaxlength="39" pattern="[a-z0-9-]+", le champ username du formulaire d'inscription detemplates/login.html(maxlength="50", aucun pattern) reçoit les mêmes contraintes, ettemplates/admin/user_form.html(modecreate), qui n'avait ni pattern nimaxlength, les reçoit également. Les six locales (fr,en,es,it,de,pt) sont corrigées :profile.username_patternetprofile.username_helpdécrivaient une règle devenue fausse (« lettres, chiffres, tirets et underscores », « 3-50 caractères ») et annoncent désormais « minuscules, chiffres et tirets uniquement, 3 à 39 caractères » ; deux clés sont ajoutées (login.username_pattern_register,admin.user_form.username_pattern) et les placeholders d'exemple contenant un_sont passés au-. Sans cela l'UI de référence laissait saisir des valeurs que le serveur refuse en 400.rustwarden-cli— le wizardstart-projectvalide désormais le username admin saisi (ask_admin_config) contre les règles RFC 0036 : minuscules, chiffres et tirets uniquement, 3 à 39 caractères, pas de--, pas de tiret en tête ni en queue. Les noms réservés (admin) restent acceptés, le bootstrap passant parparse_allow_reserved. Auparavant une saisie commeAdmin,admin_useroujean.dupontgénérait un projet dont le premierbuild()échouait surusername_formatsans message actionnable.rustwarden-ui—PUT /settings/profilene traite plus du tout l'e-mail : l'alerteaccount_modifiedet l'appel applicatif àPasswordChangeService::invalidate_on_email_changeont été retirés, le core s'en charge au moment de la confirmation (RFC 0041). L'alerte « username modifié » n'est envoyée que sur un vrai rename (le formulaire de profil renvoyant tous les champs, un echo ne déclenche plus rien). Le champ e-mail du formulaire continue de passer tant qu'il renvoie l'adresse courante ; une vraie modification remonte désormais l'erreurValidationdu core qui renvoie versPOST /api/v1/auth/email-change. Les écrans dédiés (demande en cours, annulation) sont un chantier UI séparé.
Security
- Anti-rejeu TOTP persistant
(
docs/rfcs/0037-totp-last-used-step.md) — nouvelle migrationm20260918_000010_totp_last_used_stepajoutantuser_totp.last_used_step BIGINT NULL.TotpService::verify_codeetverify_and_enableconsomment le pas qui a réellement matché (matching_totp_step, skew ±1) via un compare-and-set SeaORM (UPDATE user_totp SET last_used_step = $step WHERE user_id = $id AND (last_used_step IS NULL OR last_used_step < $step), succès ssirows_affected == 1). Un rejeu renvoieOk(false)— sémantique identique à un code invalide, donc pas d'oracle de rejeu. Ferme le rejeu inter-réplicas que la garde in-process de 1.0.1 ne couvrait pas (une map par processus). La garde in-process est conservée en défense en profondeur, après le CAS. Les erreurs DB remontent enAppError: le garde ne fail jamais open.NULLsignifie « aucun pas consommé depuis la migration » : le premier code présenté après migration est toujours accepté (grandfathering des lignes existantes). - Changement d'adresse e-mail en deux temps (BREAKING pour les appelants de
update_profile— voir § Migration) (docs/rfcs/0041-pending-email-changes.md) —UserService::update_profilen'écrit plus jamaisusers.email. Un vrai changement renvoie uneAppError::Validationexplicite qui renvoie versEmailChangeService::request_change; un echo de l'adresse courante reste un no-op complet (les formulaires qui resoumettent tous les champs continuent de fonctionner). Le nouveau chemin :request_changere-authentifie l'utilisateur avec son mot de passe courant, vérifie l'unicité et le format, consomme toute demande pendante du même compte, écrit une lignepending_email_changeset met deux mails en file : le lien de confirmation vers la nouvelle boîte, et un avis vers l'ancienne portant un lien de signalement.users.emailetemail_verifiedne bougent pas.- La confirmation écrit la nouvelle adresse déjà vérifiée
(
email_verified = true), révoque tous les refresh tokens du compte (les sessions en cours tombent) et invalide les demandes de changement de mot de passe pendantes. Traceaudit_logsemail_changed. - Le signalement trace en
audit_logs(niveau WARN) et alerte tous les administrateurs ; il ne bloque pas le changement — la boîte qui reçoit ce lien peut alerter, pas décider. Les envois ne sont pas best-effort : si la file d'e-mails est indisponible,request_changeéchoue au lieu de laisser une demande dont le lien n'arrivera jamais. Rien n'est écrit sur le compte dans ce cas. Limiteur dédié : 3 demandes par heure et par utilisateur (email_change:{user_id}, budget distinct deemail_verifyetpwd_reset), plus le budget IP existant surPOST /api/v1/auth/email-change. Les adresses sont masquées dans les logs, l'audit et les hooks (jo***@ex***), y compris dans le corps de réponse de la demande. Ce que cela ferme par rapport à la v1 annoncée (RFC 0038, jamais publiée, désormais Superseded) : plus de lock-out sur faute de frappe — une adresse mal saisie ne reçoit simplement jamais de clic, la demande expire et le compte n'a jamais bougé ; plus de fenêtre « adresse changée mais session toujours valide » ; plus de couplage avec la politiquevalidation_email_requiredde la RFC 0040. Recours opérateur :admin_force_changeremplace le changement d'adresse parPUT /admin/users/{id}. Elle exige un acteur portant le rôleadmin(sinonAppError::Forbidden), écrit l'adresse sans confirmation, révoque les sessions de la cible, notifie les deux boîtes et trace enaudit_logs(email_changed_by_admin, WARN) +on_audit_event. Le hookon_email_verifiedn'est pas émis : il n'y a eu aucune preuve de possession. Événementson_audit_eventémis — les trois nomment leur cible, un consommateur peut donc router surtargetsans distinguer les chemins :user.email_changed(actor_idettarget= le compte, chemin libre-service),user.email_change_reported(actor_id = None— le signalement vient d'un lien de mail, pas d'une session —target= le compte),user.email_changed_by_admin(actor_id= l'opérateur,target= la cible). Adresses masquées dans les troismetadata. Limite connue : un utilisateur qui a perdu à la fois son mot de passe et l'accès à son adresse courante ne se débloque plus seul — c'est un recours administrateur après vérification d'identité hors bande. La faute de frappe à l'inscription relève du même recours :registerécrit l'adresse saisie sans double vérification, ce flux n'est pas traité par cette RFC. Détail :docs/developer-guide/07-security-model.md§ Cycle de vie de l'e-mail vérifié.
- Charset
usernameappliqué aux frontières d'écriture (docs/rfcs/0036-username-charset-validation.md) —UserService::create_user,UserService::update_profileetPOST /api/v1/auth/registerrefusent les identifiants hors charset (backticks,_,., majuscules, noms réservés). Réduit la surface d'injection dans les chemins d'URL, les templates et les logs. AucunCHECKSQL, aucune migration, aucun rewrite : les comptes existants hors charset restent utilisables et continuent de se connecter.update_profilene valide que sur un vrai rename — un client qui renvoie le username courant (y compris un handle grandfathered) est traité en no-op. AuthService::authenticate_userhonorevalidation_email_required(docs/rfcs/0040-authenticate-user-email-verified.md) — le contrôle « e-mail vérifié » a été déplacé du handler HTTPloginvers le service. Mêmes clés de réglage (validation_email_required, alias legacyvalidation_email_requise), même défauttrue, même comportement fail-closed (une erreur de lookup vaut « vérification requise »), même message d'erreur français et mêmeAppError::Validation(HTTP 400). Le contrôle intervient après la vérification du mot de passe, donc pas d'oracle « le compte existe mais n'est pas vérifié ». Conséquence : une app consommatrice qui appelait le service (SSR maison, handler custom) ne peut plus contourner la politique.AuthService::verify_credentialsreste la primitive « mot de passe seul » (utilisée par le re-prompt TOTP) et n'applique pas ce contrôle. Effet de bord documenté (décision v1) : le blocage n'émet plus le hookon_audit_event/auth.login_blocked— la ligneaudit_logsest toujours écrite parAuthLoggingService::log_blocked_login, mais le paramètrehooksvautNonedepuis le service.
Migration
- Sauvegarder :
pg_dumpavant toute opération. cargo run -p rustwarden-cli -- update-project(ou vendor manuel) pour récupérerrustwarden-core/rustwarden-uiet les docs framework-owned (docs/SERVICES_REFERENCE.md,docs/MIGRATION_GUIDE.md).- Aligner les pins de dépendances de
crates/rustwarden-core/Cargo.toml(sea-orm/sea-orm-migration2.0.0,sea-bae = "0.2.2",validator_derive = "0.20.1"), puiscargo update. - Jouer la migration avant de déployer le binaire :
cargo run --bin migrate(ouauto_migrate(true)sur un déploiement mono-instance). - Rediriger les appels qui changeaient l'e-mail (RFC 0041) :
grep -rn "update_profile" src/— tout appel passant une nouvelle adresse doit basculer surEmailChangeService::request_change(chemin utilisateur) ouEmailChangeService::admin_force_change(chemin opérateur, en remplacement dePUT /admin/users/{id}pour l'adresse). Un echo de l'adresse courante reste un no-op : rien à changer pour les formulaires qui resoumettent tous les champs. Vérifier avant le bump que SMTP et le workeremail_queuetournent : sans file fonctionnelle,request_changeéchoue volontairement au lieu d'écrire une adresse non prouvée. cargo clippy --workspace -- -D warningspuis les smoke tests auth (login, register, changement d'e-mail, 2FA).- Retirer les workarounds locaux devenus redondants : newtype
Usernamemaison, overlaySetResponseHeaderLayer::overriding(CONTENT_SECURITY_POLICY, …), tout patch re-vérifiantemail_verifiedaprèsauthenticate_user, tout code applicatif qui, après un changement d'adresse, remettaitemail_verifiedà la main, enqueuait un mail de validation / une alerteaccount_modified, ou appelaitPasswordChangeService::invalidate_on_email_change. - Facultatif : démarrer la purge des demandes expirées — le framework ne le
fait pas.
PendingEmailChangeCleanupProcessor::new(db.clone()).start();(rétention 30 jours après expiration, constante de code, aucune variable d'environnement). Sans ce démarrage, une purge opportuniste limitée à l'utilisateur courant s'exécute à chaque nouvelle demande.
Table Avant / Après par RFC, greps de repérage et pièges connus :
docs/MIGRATION_GUIDE.md
§ 2026-09-18 — RFC 0036-0040 (1.1.0).
[1.0.1] - 2026-09-18 — Security
Patch de sécurité (pas de nouvelle API publique). Fenêtre d'adoption 14 jours
pour les consommateurs (docs/RUSTWARDEN_EVOLUTION_PROCESS.md §9).
Security
- E-3 / IP rate-limit —
run()sert viainto_make_service_with_connect_info; layerauth_ip_rate_limitsur login, register, refresh, forgot-password, 2FA verify et resend-verification. Le budget n'est plus globalauth:unknown. - E-3 / TOTP — plafond somme des tentatives par utilisateur (15 / 15 min)
au lieu de compter les challenges saturés ; anti-rejeu in-process du même
pas TOTP (TTL 90 s). Pas de migration
last_used_step(RFC 0037). - C-7 —
JWT_SECRETrejette le placeholder CLIREMPLACER_PAR_UNE_CLE_SECRETE_LONGUE(égalité exacte, pas decontains).AdminConfig::validaterefusepasswordetREMPLACER_PAR_UN_MOT_DE_PASSE_FORT..env.exampledu wizard n'embarque plus ces littéraux. - M-2 —
PasswordResetService::reset_passwordappliquevalidate_password_complexityavant le hash et avant de consommer le token. - M-3 — limiteurs reset mot de passe / validation email partagés (
OnceLock) au lieu d'une instance par requête ; éviction LRU au cap au lieu d'un 429 sur une clé neuve. - M-8 — vérification des backup codes TOTP dans
spawn_blocking. - Force-2FA / lookup / blacklist — fail-closed : erreur DB TOTP ou lookup
utilisateur → 503 ; user absent → non authentifié ; blacklist JWT en erreur →
refus. Plus de
unwrap_or(false)/is_ok_andfail-open sur ces chemins. - Admin bootstrap —
AdminConfig::validate+PasswordRulesuniquement sur le chemin de création (unADMIN_PASSWORDfaible résiduel ne casse plus le boot si un admin existe déjà). - sanitize_redirect — normalisation
\→/pour bloquer/\evil.example. - Dépendances —
bcrypt0.19.3,rustls0.23.45,h20.4.19 (lockfile). - PostgreSQL dev — bind
127.0.0.1:5432;POSTGRES_PASSWORDobligatoire (:?, plus de fallbackpass3ord).
[Unreleased]
Fixed
- Middleware
AuthService— branche optional auth alignée sur la branche protected (cf.docs/rfcs/0031-auth-service-optional-fallback.md, plandocs/plans/2026-05-16-auth-service-optional-fallback.md). La branche optional applique désormais le fallbackAuthConfig::from_env()quand l'extensionExtension(Arc<AuthConfig>)n'est pas encore posée par les layers downstream — symétrise le comportement avec la branche protected patchée précédemment pard4d6828. Conséquence : tout consommateur qui mappait des routes auth-required horsprotected_prefixes(typiquement les intégrations fédérées qui consommentOption<Extension<Claims>>) voit enfin les claims injectés au lieu de recevoir silencieusementNonepuis d'être redirigé vers/login. Lesregister_protected_prefix()ajoutés uniquement pour contourner ce bug peuvent être retirés (garder ceux qui sont là pour rejeter les non-authentifiés). Aucun changement d'API publique. Test de non-régression ajouté danscrates/rustwarden-core/tests/middleware_authconfig_fallback.rs.
Changed (BREAKING)
- Extraction du crate
rustwarden-fediverse(cf.docs/plans/2026-05-11-extract-rustwarden-fediverse-crate.md). La fédération ActivityPub vit désormais dans un crate dédié (crates/rustwarden-fediverse/, ~9 000 lignes) plutôt que dans le modulerustwarden_core::fediverse. Les consommateurs externes doivent migrer leurs imports :rustwarden_core::fediverse::*→rustwarden_fediverse::*rustwarden_core::services::fediverse_*_service→rustwarden_fediverse::services::fediverse_*_servicerustwarden_core::services::actor_key_service→rustwarden_fediverse::services::actor_key_servicerustwarden_core::services::remote_identity_service→rustwarden_fediverse::services::remote_identity_servicerustwarden_core::models::{remote_identity, fediverse_*}→rustwarden_fediverse::models::*rustwarden_core::config::FediverseConfig→rustwarden_fediverse::FediverseConfigLa feature Cargoexperimental-fediverse(côtérustwarden-core) est supprimée ; côtérustwarden-uietappla nouvelle feature s'appellefediverseet tire la dépendance optionnellerustwarden-fediverse. La méthodeRustwardenBuilder::with_fediverse()est remplacée par un mount explicite : ouvrir la connexion DB tôt, construirerustwarden_fediverse::routes::router(config, db), puis enchaîner.register_protected_prefix("/fediverse/consent").register_protected_prefix("/fediverse/invite").register_revocation_checker(checker).merge_routes(router)sur le builder (voirapp/src/main.rs). Schéma DB inchangé — toutes les migrations restent dansrustwarden-core/migrations/(code SQL DSL pur, aucune dépendance sur les modèles Rust).
AuthService::newchange de signature : prend désormaisprotected_prefixes: Arc<Vec<String>>en troisième argument au lieu d'une liste hardcodée. Les consommateurs enregistrent leurs préfixes viaRustwardenBuilder::register_protected_prefix(prefix). Les valeurs par défaut installées par le builder (/home,/profile,/test-base,/settings,/admin) sont préservées ;rustwarden-fediverseajoute/fediverse/consentet/fediverse/invitelors du mount.- F10 (révocation remote JWT) via trait : le check
remote_identities.disabled_ateffectué parAuthServiceau passage d'une route protégée est désormais délégué à un traitRemoteIdentityRevocationCheckerinjecté enExtension.rustwarden-fediversefournit l'implémentation SeaORM (SeaOrmRemoteIdentityRevocationChecker) ; sans le crate de fédération, le check est sauté — comportement correct car aucun remote JWT ne peut être émis. - Module
identity(RFC 0008) reste dansrustwarden-coresans gate de feature. Les traitsIdentityetIdentityResolversont des primitives d'auth/RBAC indépendantes de la couche fédération. - Race
FEDIVERSE_DOMAINcorrigée dans les tests : les helperstest_fediverse_config()des servicesfediverse_auth_serviceetfediverse_invite_servicen'utilisent plusunsafe { std::env::set_var }; ils appellent directementFediverseConfig::for_tests("test.local"), ce qui supprime un risque de flakiness inter-tests. - i18n complétée : les 5 namespaces fediverse (
fediverse,fediverse_nodeinfo,fediverse_invites,fediverse_connected_apps,fediverse_invite_landing) sont désormais présents dans les 6 locales (fr/en/de/es/it/pt) — auparavant seuls fr et en étaient traduits. - Tests E2E doivent tourner en série :
cargo test -p rustwarden-fediverse --test e2e_fediverse -- --test-threads=1. Les 32 scénarios mutent l'état partagé du testbed Docker (trust list,kill_domain,remote_identities.disabled_at,fediverse_trust_mode) et se cassent en parallèle. La règle est documentée danstests/fediverse-testbed/README.md. Le test S3 (s3_untrusted_domain_is_rejected_with_403) flippe désormais lui-mêmefediverse_trust_modeenallowlist(testbed boot defaultopen_with_blocklist) et restore avant l'assertion. - Validation finale Phase 5 :
cargo build --workspace,cargo clippy --workspace --features rustwarden-app/fediverse -- -D warningsetcargo test --workspacepassent ; les 32 tests E2E + les tests interop wiremock contre le testbed Docker tri-instance passent en--test-threads=1. Le Dockerfile du testbed a été mis à jour pour utiliser la nouvelle featurefediverseau lieu de l'ancienexperimental-fediverse.
Added
- RFC 0029 —
RemoteResourceShareService(rustwarden_fediverse::services::remote_resource_share_service) : partage de ressources métier (repository,wall, …) avec une identité ActivityPub remote, sans passer par une team. 5 méthodes publiques (share,unshare,effective_permission_for_remote_identity,list_shares,list_for_remote_identity) — upsert par(resource_id, remote_identity_id), hiérarchieread < write < admin, FK cascade surresources/remote_identities/users. Migrationm20260516_000002_remote_resource_shares. 11 tests unitairesMockDatabase. Origine : RFC 0001 gitrust. Doc : SERVICES_REFERENCE §21.15. - RFC 0030 —
RemotePersonalAccessTokenService<S>(rustwarden_fediverse::services::remote_personal_access_token_service) : PAT pour identités ActivityPub remote, generic sur le traitTokenScopeframework. 5 méthodes publiques (create,verify,revoke,list_for_remote_identity,revoke_all_for_remote_identity) — SHA-256 + constant-time compare viasubtle, soft-revoke, refus du token si l'identité remote estdisabled_at. Préfixe distinctrwfed_pat_<64 hex>pour discrimination côté consommateur. Migrationm20260516_000003_remote_pat(table + partial index sur token_hash WHERE revoked_at IS NULL). 20 tests unitairesMockDatabase. Origine : RFC 0002 gitrust. Doc : SERVICES_REFERENCE §21.16. - Documentation opérateur : nouveau guide
docs/fediverse-operations.md(10 sections, Diátaxis how-to + reference) : activation, env vars,app_settingsclés, trust/block list, incident playbook, observabilité, GC, critères promotion stable, URLs exposées, limites connues. - Observabilité fédiverse — toggle DB default-off : nouveau réglage
app_settings.fediverse_observability_enabled(defaultfalse) et helper publicservices::fediverse_outbound_service::is_fediverse_observability_enabled. Lestracing::info!/warn!/error!structurés (RFC 0019) émis parFediverseOutboundService::drain_onesont désormais conditionnés à ce flag. Sans agent (Vector/Promtail) qui consomme, laisserfalsepour éviter le bruit logs ; activer quand un pipeline d'observabilité est en place. - Audit privacy passé : revue manuelle confirme que les payloads AP exposés
(Person, WebFinger, NodeInfo) sont minimaux — aucune fuite email / password hash /
rôles internes / IPs.
UserSummaryest une DTO curatée. Aucuntracing::*ne log password / JWT / clé privée / encryption key brut. Finding F12 latent identifié (summary_htmlreçu via Update(Person) stocké brut, mais jamais rendu UI — non exploitable ; ajouterammonia::clean()à l'INSERT si jamais utilisé en rendu). - Suite E2E à 32 tests : ajout des scénarios S5 (modes de panne — 4 tests) et S6 (lifecycle Update/Delete Person — 2 tests). Les 6 scénarios du plan parent (S1-S6) sont désormais automatisés en HTTP réel avec crypto réelle.
- S5 valide la replay protection (UNIQUE constraint sur
fediverse_auth_challenges.nonce), le WebFinger 404, la validation handle malformé, et documente F11 (clock skew non-enforced avecAlwaysAcceptVerifier— non-bloquant, by-design dev mode). - S6 valide que
Update(Person)etDelete(Person)signés sur/inboxpropagent correctement : refreshdisplay_name, désactivationremote_identities.disabled_at, et invalidation instantanée des JWT remote via F10.
- S5 valide la replay protection (UNIQUE constraint sur
- F10 — Révocation immédiate des JWT remote lors de kill_domain :
ajout d'un check
disabled_atsurremote_identitiesdans le middlewareAuthService(feature-gatedexperimental-fediverse). Un JWT remote dont l'identité backing a été désactivée est immédiatement rejeté (redirect 303 vers/login) au prochain request, sans nécessiter d'entrée dansjwt_blacklist. Cheap : un SELECT par PK. - F9 — Auto-kill sur add_blocked_domain : le handler admin
add_trust_entry(modeOpenWithBlocklist) effectue désormais atomiquement l'INSERT dansfediverse_blocked_domainsET un UPDATEdisabled_atsur tous lesremote_identitiesdu domaine. Un seul clic admin pour « couper l'instance hostile » — combiné avec F10 les sessions actives sont rejetées au prochain request. - Suite E2E étendue à 26 tests : +4 tests S4 (
s4_blocked_domain_payload_is_rejected_with_403,s4_kill_domain_marks_remote_identities_disabled,s4_add_blocked_domain_auto_kills_remote_identities,s4_kill_domain_invalidates_active_remote_jwt). Le dernier valide bout-en-bout via vrai HTTP (POST signé → JWT remote minté → GET protégé OK → kill_domain → GET protégé redirige/login). - F8 — Trust enforcement au niveau protocole : la trust list était
décorative —
FediverseTrustService::is_domain_allowedexistait uniquement appelé depuis l'UI. Un domaineevil.tldsigné valide pouvait POST des activités à n'importe quelle instance Rustwarden, indépendamment de la trust list.- Nouvelle méthode
FediverseTrustService::assert_domain_allowedqui retourneAppError::Validationquand un domaine n'est pas autorisé par le mode courant. - Nouvelle variante
FediverseError::Forbidden(String)→ 403. - Appel
assert_domain_allowedau début deauth_inboxet/inbox, avant la vérification de signature (rejette les domaines hostiles sans même fetcher leur actor). - 2 tests E2E S3 dédiés :
s3_untrusted_domain_is_rejected_with_403confirme l'enforcement,s3_trusted_domain_still_passes_protocol_inboxvérifie l'absence de régression.
- Nouvelle méthode
- Suite E2E étendue : passe de 14 à 22 tests (8 nouveaux pour S2 +
S3) en 7.83s. Nouvelle couverture :
- 4 tests S2 (admin invite → team_member binding, reject, revoke, listing IdP-side).
- 4 tests S3 (non-transitivité du trust, F8 enforcement positif et négatif).
- RFC 0025 — Fediverse back-channel
Accept(AuthRequest)(RFC 0006 Phase 3) : comble le trou identifié par l'évaluation E2E (iter 1c) où A approuvait un challenge mais B ne le savait jamais. Le SSO bilatéral A ↔ B est désormais opérationnel bout en bout sans DB-direct.FediverseAuthService::record_inbound_decision— applique unAcceptouRejectreçu sur l'inbox SP. Sécurité : domaine de l'approver vérifié contrechallenge.domain(anti-spoofing). Idempotent sur replay.auth_inboxdispatche désormais sur le champtypeAP (AuthRequest / Accept / Reject / inconnu → silent ignore per AP §7).- Helper public
build_auth_decision_payload(...)qui sérialise le payload JSON-LD au format AP standard avecobjecten forme inline-object pour porter le nonce. consent_submit(UI) enqueue leAccept/Rejectsortant viaFediverseOutboundService::enqueueaprèsrecord_decision.- Recherche externe : on utilise les types AP standards
Accept/Reject(alignés sur le pattern MastodonAccept(Follow)) plutôt qu'inventerAuthApproval/AuthDenial.
- F2 —
AlwaysAcceptVerifierpopulateremote_identities: le verifier test/dev (with_resolver(db)) fetche désormais l'actor URL et upsert sa ligneremote_identitiesau lieu de laisser le cache vide. Restaure le contrat documenté depersist_inbound_request.- Helper public réutilisable
services::fediverse_signal_service::fetch_and_cache_remote_actoravec hardening : timeout 10s, body cap 1 MiB (Content-Length pre-flight- bytes() post-read), SSRF guard rejetant loopback / private IPv4 /
link-local / ULA IPv6 (
allow_private_ips=trueréservé au testbed).
- bytes() post-read), SSRF guard rejetant loopback / private IPv4 /
link-local / ULA IPv6 (
- 9 nouveaux tests unitaires couvrant happy path INSERT, refetch UPDATE, 404, JSON malformé, publicKey manquant, inbox manquant, URL invalide, body trop gros, SSRF guard loopback.
- Helper public réutilisable
- F1 — Normalisation
FEDIVERSE_DOMAIN:auth_inboxaccepte désormais la formehttp://example.orgaussi bien queexample.org. Strip scheme / trailing slash avant comparaison contre letarget.domain. - Testbed
tests/fediverse-testbed/: topologie 3 instances locales 100 % automatisable (Docker Compose), 4 scripts cycle de vie (setup.sh,start.sh,stop.sh,teardown.sh). Image multi-stagerustc 1.91-slimembarquant les binairesrustwarden,migrate,seed. Réseau Docker avecnetwork_aliasesalpha.local/beta.local/gamma.local; pas de TLS, pas de/etc/hostsrequis grâce àFEDIVERSE_DEBUG_LOCAL_FEDERATION. - Suite E2E
crates/rustwarden-core/tests/e2e_fediverse.rs— 14 tests HTTP réels contre le testbed, dont 6 du flux S1 SSO complet avec signatures RSA-SHA256 réelles (iter 1a / 1b / 1c / 1d) :- 7 smoke tests (WebFinger, NodeInfo, Actor fetch, inbox 401)
- 1 test signed AuthRequest B → A (iter 1b)
- 1 test consume B → JWT remote (iter 1c)
- 5 tests F3 round-trip Accept/Reject (iter 1d) : happy path, reject, unknown nonce, expired challenge, replay idempotence
- Workspace : edition Rust 2024 +
rust-version = "1.85"au niveau[workspace.package]. Toutes les crates héritent (edition.workspace = true). - RFC 0006 Phase 2 — inbox enrichment côté IdP :
auth_inboxpersiste désormais le challenge entrant + résout la remote_identities du requester.- Nouvelle méthode publique
FediverseAuthService::persist_inbound_request(db, body, idp_domain)qui :- Parse les champs
actor/target/nonce/expiresAtdu payload APAuthRequest. - Valide que
target_domaincorrespond bien à l'IdP (refus si requête mal-routée). - Résout
remote_identitiesparap_id == actor_url(la row est créée upstream par le verifier de signature, RFC 0017). - Insère un challenge
Pendingavecrequested_remote_idpeuplé.
- Parse les champs
- Câblage dans
crates/rustwarden-core/src/fediverse/auth_routes.rs:auth_inboxaprès vérification de signature : sur erreur de Validation, 400 BadRequest renvoyé proprement (au lieu du précédent 202 silencieux). - Débloque :
- Slice B Approve E2E (RFC 0023 iter. 4) :
consent_submitpeut désormais lirerequested_remote_idrempli et passer àrecord_decision::Approvesans exception Validation. - Slice C
/profile/connected_apps(RFC 0024 iter. 5) : la table d'apps connectées sera désormais peuplée dès qu'un user remote authentifie via cet IdP.
- Slice B Approve E2E (RFC 0023 iter. 4) :
- 4 nouveaux tests unitaires sur
persist_inbound_request: happy path, mismatched IdP domain, missing actor, unknown requester. - Test existant
auth_inbox_returns_202_for_signed_authrequestrenommé enauth_inbox_returns_400_when_requester_actor_unknownet adapté pour valider la nouvelle sémantique 400. - 5 quality gates verts : build, tests 413 pass / 0 fail / 19
ignored (+4 vs avant), clippy
-D warnings, doc clean, pas de migration.
- Nouvelle méthode publique
- RFC 0024 (Slice C iter. 6 — close-out) — Fediverse UI: page
d'acceptation invitation team cross-instance
/fediverse/invite/{id}.- 2 nouveaux handlers dans
crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs:invite_landing_form(GET) charge l'invite + le team, affiche le statut, indique si l'utilisateur courant peut accepter (=Claims::is_remote() && remote_identity_id().is_some()) ;invite_landing_submit(POST, CSRF) appelleFediverseInviteService::accept(invite_id, remote_id)sur Approve, oureject(invite_id)sur tout autre. - Nouveau template standalone
crates/rustwarden-ui/templates/fediverse/invite_landing.htmlavec card DaisyUI affichant team / rôle / statut, badges colorés par status, alert warning quand l'utilisateur courant ne peut pas accepter (local user — explication explicite de la mécanique AP-native), bouton Accept conditionnel{% if can_accept %}. - 2 nouvelles routes UI authentifiées (gated feature, mode dégradé
404). Extension
AuthService::call:crates/rustwarden-core/src/middleware/auth_service.rsajoute/fediverse/inviteàprotected_routespour injecterClaimsdans la requête. - i18n : namespace
fediverse_invite_landing:(top-level dédié) — 11 nouvelles clés FR + EN avec note explicite UX sur la contrainte « accept = remote identity uniquement ». - Slice C clôturé : 6 itérations livrées (RFC 0024 + queue + sessions_remote + nodeinfo + team_invites + connected_apps + invite_landing). 11 handlers, 6 templates HTML, 14 routes, ~110 clés i18n FR+EN.
- 5 quality gates verts : build, tests 409 pass / 0 fail / 19
ignored, clippy
-D warnings, doc clean, pas de migration.
- 2 nouveaux handlers dans
- RFC 0024 (Slice C iter. 5) — Fediverse UI: section profil
utilisateur « Apps connectées ».
- Nouveau handler
connected_appsdanscrates/rustwarden-ui/src/handlers/fediverse_user_auth.rsqui interrogefediverse_auth_challengespour le handle de l'utilisateur courant (@username@FEDIVERSE_DOMAIN) avecstatus IN ('approved', 'consumed'), regroupe par domaine, calcule le compte de sessions + dernière activité. - Nouveau template standalone
crates/rustwarden-ui/templates/profile/fediverse_connected_apps.htmlavec table zebra (Instance / Sessions / Dernière activité), alert info expliquant que la révocation manuelle arrivera avec RFC 0006 Phase 2, empty state, lien retour profil. - Route
GET /profile/connected_apps(gated feature, mode dégradé 404). IntercepteClaimsvia le préfixe/profiledéjà dansAuthService::protected_routes. - i18n : namespace
fediverse_connected_apps:(top-level dédié) — 11 nouvelles clés FR + EN avec note explicite sur la limitation de révocation actuelle. - Iter. 5 lecture-seule : la révocation explicite côté utilisateur
est différée à RFC 0006 Phase 2 (méthode
FediverseAuthService::revoke_remote_session(challenge_id)avec blacklist JWT) ; en attendant, l'admin peut révoquer via/admin/fediverse/sessions_remote(iter. 2) ou attendre l'expiration naturelle du JWT. - 5 quality gates verts : build, tests 409 pass / 0 fail / 19
ignored, clippy
-D warnings, doc, pas de migration.
- Nouveau handler
- RFC 0024 (Slice C iter. 4) — Fediverse UI: invitations team
cross-instance
/admin/fediverse/invites/{team_id}.- 3 nouveaux handlers dans
crates/rustwarden-ui/src/handlers/admin_fediverse.rs:get_team_invites(GET) liste les 100 dernières invitations pour un team_id, tous statuts confondus.create_team_invite(POST add, CSRF) parse handle + role, appelleFediverseInviteService::invite_remoteavec TTL 7j.revoke_team_invite(POST revoke, CSRF) appelleFediverseInviteService::revoke(autorisé sur Pending + Accepted).
- Nouveau template
crates/rustwarden-ui/templates/admin/fediverse/team_invites.htmlavec form ajout (handle pattern + select rôle), table zebra avec badges colorés par status, bouton révoquer conditionnel (Pending/Accepted seulement), modal confirmation Alpine. - 3 nouvelles routes admin (gated feature, mode dégradé 404, vérifie l'existence du team avant rendu).
- i18n : namespace
fediverse_invites:(top-level dédié) — 25 nouvelles clés FR + EN. - 5 quality gates verts : build, tests 409 pass / 0 fail / 19
ignored, clippy
-D warnings, doc, pas de migration.
- 3 nouveaux handlers dans
- RFC 0024 (Slice C iter. 3) — Fediverse UI: page NodeInfo
humanisée
/admin/fediverse/nodeinfo.- Nouveau handler
get_nodeinfodanscrates/rustwarden-ui/src/handlers/admin_fediverse.rsqui calcule en live :- software : nom (
rustwarden), version (CARGO_PKG_VERSION), domaine (FEDIVERSE_DOMAINenv) ; - runtime :
trust_mode,interop_scope,authorized_fetch(lus depuisapp_settings) ; - users : total, actifs 30 j, actifs 180 j (sur
last_login_at) ; - remote identities : actives + nb domaines distincts.
- software : nom (
- Nouveau template
crates/rustwarden-ui/templates/admin/fediverse/nodeinfo.htmlavec 4 cards (grid-cols-1 md:grid-cols-2) : Logiciel / Configuration runtime / Utilisateurs locaux / Identités distantes. Stats DaisyUI colorées. - Route
GET /admin/fediverse/nodeinfo(gated feature, mode dégradé 404). - i18n : namespace dédié
fediverse_nodeinfo:+ 2 cléscommon.yes/common.no(raison : éviter de re-ouvrirfediverse:qui aurait écrasé sous yaml-rust2 toutes les sous-clés settings/trust/auth/queue/sessions_remote). 22 nouvelles clésfediverse_nodeinfo.*FR + EN. - 5 quality gates verts : build, tests 409 pass / 0 fail / 19
ignored, clippy
-D warnings, doc, pas de migration.
- Nouveau handler
- RFC 0024 (Slice C iter. 2) — Fediverse UI: page sessions distantes
- kill switch domaine.
- 3 nouveaux handlers dans
crates/rustwarden-ui/src/handlers/admin_fediverse.rs:get_sessions_remote(GET) liste les rowsremote_identitiesactives (disabled_at IS NULL, ordonnéeslast_fetched_at desc, limit 200) ;revoke_remote_session(POST, CSRF) marque une row spécifiquedisabled_at = now;kill_domain(POST, CSRF) bulk-update toutes les rows actives d'un domaine. - Nouveau template
crates/rustwarden-ui/templates/admin/fediverse/sessions_remote.htmlavec card kill-switch (bordererror+ select des domaines distincts)- table zebra des sessions + 2 modals confirmation Alpine
(
role="alertdialog"Plan UI §3 P2) + empty state.
- table zebra des sessions + 2 modals confirmation Alpine
(
- 3 nouvelles routes admin (gated feature, mode dégradé 404).
- i18n FR + EN : 21 nouvelles clés
fediverse.sessions_remote.*. - 5 quality gates verts : build, tests 409 pass / 0 fail / 19
ignored, clippy
-D warnings, doc, pas de migration.
- RFC 0024 (Slice C iter. 1) — Fediverse UI: dashboard file
outbound
/admin/fediverse/queue.- Nouvelle RFC docs/rfcs/0024-fediverse-ui-ops-slice-c.md — cadre les 6 écrans Slice C (queue, sessions_remote, nodeinfo, invites team, connected_apps, invite_landing).
- Nouveau handler
get_queuedanscrates/rustwarden-ui/src/handlers/admin_fediverse.rsqui :- compte les rows par status (
pending/delivered/failed/dead) viaPaginatorTrait::count; - charge les 100 dernières rows ordonnées
created_at desc, tronque les URLs (60 chars) et erreurs (80 chars) pour la lisibilité ; - rend le template avec stats grid + table zebra + empty state explicite.
- compte les rows par status (
- Nouveau template
crates/rustwarden-ui/templates/admin/fediverse/queue.htmlavec 4 stat cards colorées par sévérité + table avec badges conditionnels (warning/success/error/neutral selon status) + empty state. - Route
GET /admin/fediverse/queue(gated feature, mode dégradé 404 sifediverse_enabled = false). - i18n FR + EN : 18 nouvelles clés
fediverse.queue.*. - 5 quality gates verts : build, tests 409 pass / 0 fail / 19
ignored, clippy
-D warnings, doc, pas de migration.
- RFC 0023 (Slice B iter. 5 — close-out) — Procédure E2E manuelle
documentée.
- Nouveau guide docs/fediverse-e2e-manual.md
décrivant la procédure de test bout-en-bout deux instances locales
(sans Docker, deux
cargo runsur des ports différents avec bases distinctes) et qualifiant 5 chemins négatifs testables aujourd'hui (Reject, untrusted, malformed handle, timeout, mode dégradé 404). - Slice B clôturé : 4 itérations livrées (RFC 0023 +
error_unauthorized,login_form,login_waiting+SSE,consent_screen). 8 routes UI, 38 clés i18n FR + EN, 6 nouveaux tests unitaires. Le « happy path Approve » E2E reste bloqué par la dette RFC 0006 Phase 2 (enrichment du challenge avecrequested_remote_idcôté inbox), documenté §7 du guide.
- Nouveau guide docs/fediverse-e2e-manual.md
décrivant la procédure de test bout-en-bout deux instances locales
(sans Docker, deux
- RFC 0023 (Slice B iter. 4) — Fediverse UI: écran de consentement
IdP-side.
- 2 nouveaux handlers dans
crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs:consent_form(GET, requiert local user loggué) renvoie l'écran de validation,consent_submit(POST, CSRF) appelleFediverseAuthService::record_decision. - Sur Reject : décision enregistrée, redirect
/home. - Sur Approve : exige que la row challenge porte déjà un
requested_remote_id(mis par l'inbox handler côté IdP — voir TODO dansauth_routes.rs:188). Sans ce binding, le handler refuse l'approbation avec une erreur Validation explicite (évite qu'un attaquant injecte unremote_identity_idarbitraire via le form). Le wiring complet landera en RFC 0006 Phase 2. - Nouveau template standalone
crates/rustwarden-ui/templates/fediverse/consent_screen.htmlavec card DaisyUI affichantdomain+handle, deux boutonssubmit name="decision", et bascule vers un alertaria-livequand la challenge est déjà décidée. - 2 nouvelles routes UI authentifiées (gated feature, mode dégradé
404) :
GET+POST/fediverse/consent/{challenge_id}. - Extension
AuthService::call:crates/rustwarden-core/src/middleware/auth_service.rsajoute/fediverse/consentà la listeprotected_routespour queClaimssoit injecté dans la request avant le handler. Aucun impact sur les routes existantes. - i18n FR + EN : 8 nouvelles clés
fediverse.auth.consent.*. - 5 quality gates verts : build, tests 409 pass / 0 fail / 19
ignored, clippy
-D warnings, doc, pas de migration.
- 2 nouveaux handlers dans
- RFC 0023 (Slice B iter. 3) — Fediverse UI: page d'attente +
Server-Sent Events.
- 2 nouveaux handlers dans
crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs:waiting(GET) renvoie la page spinner,sse_status(GET) émet un événement SSEevent: statustoutes les 2 s avec{"state":"pending|approved|rejected|expired|consumed"}. - Stream construit via
futures::stream::unfold(nouvelle dev-dep optionnellefutures = "0.3"activée parexperimental-fediverse). Cap à 150 polls (5 min ↔ TTL challenge RFC 0006) puis fermeture propre.KeepAlive::default()envoie un:pingtoutes les 15 s. - Nouveau template standalone
crates/rustwarden-ui/templates/fediverse/login_waiting.htmlavec spinner DaisyUI (role="status" aria-live="polite") et JSEventSourcenatif (≤ 50 LoC, pas de framework lourd Plan UI §3 P10) qui :- sur
approved→POST /api/v1/auth/login/fediverse/consume/{id}pour minter le JWT remote (RFC 0007), redirige/home; - sur
rejected→error?reason=rejected; - sur
expired→error?reason=timeout; - sur
consumed(déjà échangé) →/home; - sur
onerror(connexion perdue persistante) → fallbackerror?reason=timeoutaprès 30 s.
- sur
- 2 nouvelles routes publiques (gated feature, mode dégradé 404) :
GET /login/fediverse/waiting/{id},GET /login/fediverse/sse/{id}. - i18n FR + EN : 5 nouvelles clés
fediverse.auth.waiting.*. - 5 quality gates verts : build, tests 409 pass / 0 fail,
clippy
-D warnings, doc, pas de migration.
- 2 nouveaux handlers dans
- RFC 0023 (Slice B iter. 2) — Fediverse UI: formulaire login
ActivityPub.
- 2 nouveaux handlers dans
crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs:login_form(GET) renvoie le formulaire HTML,login_initiate(POST) valide CSRF, parse@user@domain, vérifieFediverseTrustService::is_domain_allowed, appelleFediverseAuthService::initiateavec TTL 5 min, redirige vers/login/fediverse/waiting/{challenge_id}(page livrée iter. 3). - Sur mauvais handle → redirect
error?reason=unknown. - Sur domaine refusé → redirect
error?reason=untrusted. - Nouveau template standalone
crates/rustwarden-ui/templates/fediverse/login_form.htmlavec inputpattern="^@?[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$", labelfor=, help-text viaaria-describedby. - 2 routes publiques :
GET+POST/login/fediverse(gated feature, mode dégradé 404). - i18n FR + EN : 8 nouvelles clés
fediverse.auth.login.*. - 2 nouveaux tests unitaires sur
extract_handle_domain(canonical- malformed). Workspace : 409 pass / 0 fail / 19 ignored.
- 5 quality gates verts : build, tests, clippy
-D warnings, doc, pas de migration.
- 2 nouveaux handlers dans
- RFC 0023 (Slice B iter. 1) — Fediverse UI: page d'erreur
d'authentification fédérée
(docs/rfcs/0023-fediverse-ui-user-auth.md).
- Nouveau handler
crates/rustwarden-ui/src/handlers/fediverse_user_auth.rsavecErrorReasonenum (5 variantes :timeout/rejected/untrusted/invalid_signature/unknownfallback). - Nouveau template standalone (sans extends, comme
login.html)crates/rustwarden-ui/templates/fediverse/error_unauthorized.htmlavec icône SVG conditionnelle (clock/lock/alert) selon la variante, H1 + body i18n, CTA « Retour à la connexion ». - Route
GET /login/fediverse/error?reason=…(publique, gated par feature). Mode dégradé silencieux Plan UI §3 P9 → 404 quandfediverse_enabled = false. - i18n FR + EN : 13 nouvelles clés
fediverse.auth.error.*(page title + subtitle + back_to_login + 5 × {title, body}). - 4 tests unitaires sur
ErrorReason(parsing, fallback, trim/lowercase, round-trip). Tests workspace : 407 pass / 0 fail / 19 ignored. - 5 quality gates verts : build, tests, clippy
-D warnings, doc, pas de migration.
- Nouveau handler
- RFC 0022 — Smoke tests anti-régression
(docs/rfcs/0022-smoke-tests-regression.md) :
deux fichiers de tests qui ferment les boucles des deux régressions
observées le 2026-05-10.
crates/rustwarden-core/tests/middleware_authconfig_fallback.rs— 3 tests vérifient queAuthConfig::from_env()reste fiable (succès avecJWT_SECRET, échec sans, idempotence). C'est le pré-requis du fallback ajouté àAuthService/AdminService/admin_helper.crates/rustwarden-ui/tests/admin_handler_pagination.rs— 5 tests vérifient les formes additives utilisées dansusers_list.htmlaprès le fix (p + 2 >= page,p + 3 == page) sur tout(page, p) ∈ [0..10]². Empêche le retour àp >= page - 2qui underflow suru64.- Quality gate :
cargo test --workspacepasse maintenant à 403 tests (395 + 3 RFC 0022 middleware + 5 RFC 0022 pagination).
- Fix
admin_handler::list_userspanicattempt to subtract with overflow(pré-existant) :- Handler :
((page - 1) * per_page) as usizepaniquait sur?page=0. Corrigé parparams.page.unwrap_or(1).max(1)params.per_page.unwrap_or(50).max(1).
- Template
templates/admin/users_list.htmllignes 211 / 214 :p >= page - 2etp == page - 3underflowaient suru64quandpage < 3. Toute première visite/admin/users?page=1(par défaut) crashait le worker tokio sans log d'erreur. Remplacé par les formes additives équivalentesp + 2 >= page && p <= page + 2etp + 3 == page || p == page + 3. Couvert par RFC 0022.
- Handler :
rustwarden-app: featureexperimental-fediverseexposée dansapp/Cargo.toml— délègue àrustwarden-core/experimental-fediverseETrustwarden-ui/experimental-fediverse. Permetcargo run -p rustwarden-app --features experimental-fediversepour monter/admin/fediverse/settings(RFC 0021) et le backend ActivityPub (RFC 0001+).- Fix régression
AuthService/AdminService/admin_helper(commitd4d6828) — quandExtension(Arc<AuthConfig>)n'est pas appliqué avant le layerAuthServicedans leServiceBuilderdu consommateur, toute requête sur une route protégée renvoyait500 Internal Server Errorsilencieusement (sans log ERROR).AuthService::callretombe désormais surAuthConfig::from_env()en fallback, comportement identique à l'avantd4d6828. Untracing::error!est levé seulement sifrom_envéchoue aussi. - RFC 0021 (Slice A iter. 2) — Fediverse: page admin
/admin/fediverse/trust_list. Deuxième écran de la couche UI fédération, fermant Slice A.- 3 nouveaux handlers dans
crates/rustwarden-ui/src/handlers/admin_fediverse.rs:get_trust_list(GET),add_trust_entry(POST add),delete_trust_entry(POST delete). Dispatchent allow vs blocklist selonapp_settings.fediverse_trust_mode. - Nouveau template
crates/rustwarden-ui/templates/admin/fediverse/trust_list.htmlavec table zebra, badge mode actif, modal de confirmation Alpine (role="alertdialog"), formulaire d'ajout (champseverityconditionnel blocklist), état vide explicite (Plan UI §3 P4). - Mode dégradé silencieux (Plan UI §3 P9) :
404retourné quandfediverse_enabled = falsesur les 3 routes trust_list, settings reste accessible pour activer. - i18n FR + EN : 28 nouvelles clés
fediverse.trust.*. - 5 quality gates verts : build, tests 403 pass / 0 fail / 19
ignored, clippy
-D warnings, doc clean, pas de migration. - Différé iter. 3 : tests handler trust_list (5 états P4), partials
Askama réutilisables (le partage cross-template via
{% include %}nécessite une refacto en macros Askama qui dépasse Slice A).
- 3 nouveaux handlers dans
- RFC 0021 (Slice A iter. 1) — Fediverse: page admin
/admin/fediverse/settings. Premier écran HTML de la couche UI fédération.- Nouveau handler
crates/rustwarden-ui/src/handlers/admin_fediverse.rs(GET + POST), gated parexperimental-fediverse. - Nouveau template
crates/rustwarden-ui/templates/admin/fediverse/settings.htmlavec 4 cards (Activation / Confiance / Interopérabilité / Sécurité avancée), CSRF, breadcrumb, toast Alpine?saved=1. - Persistence SeaORM via
AppSettingsService::set_settingsur 4 clés :fediverse_enabled,fediverse_trust_mode,fediverse_interop_scope,fediverse_authorized_fetch. Validation des sélecteurs côté serveur (whitelist). - i18n complète FR + EN (24 clés
fediverse.settings.*) ; les autres locales (de/es/it/pt) tombent automatiquement en fallback FR viaI18nService(cf.service.rs:159). - Conformité Plan UI HTML §3 : P1 (CTA primaire unique), P2
(confirmation modale — pour révocations dans Slice C), P3 (toast
succès auto-dismiss 4 s), P7 (
<label for>,aria-current,aria-live="polite"), P10 (HTMX + Alpine seuls). - 5 quality gates verts : build clean, 395 tests pass / 0 fail /
19 ignored, clippy
-D warningszéro, doc clean (2 warnings préexistantsadmin_dto.rs:134hors scope), pas de migration. - Reste pour Slice A (iter. 2) : page
/admin/fediverse/trust_list, partials_modal_confirm.html+_toast.html+_empty_state.html, tests handlers, couverture des 5 états (empty / 5xx / 403 / loading / happy).
- Nouveau handler
- Phase 5 — quality gate
cargo audit: nouvelle config.cargo/audit.tomlqui figue les advisories transitives sans fix upstream avec justification par entrée (RUSTSEC-2023-0071rsa Marvin Attack ;RUSTSEC-2023-0086lexical-core via imap-proto ;RUSTSEC-2026-0097rand custom-logger unsoundness).cargo auditpasse désormais clean (0 vulnerability) en local et en CI. Toute nouvelle ignore exige une ligne traçant le pourquoi versdocs/SECURITY_AUDIT_2026_05.md§H3. - RFC 0020 — Fediverse: tests d'interopérabilité wiremock
(signed delivery round-trip).
- Nouvelle dev-dependency
wiremock = "0.6"surrustwarden-core(test only, non livrée aux apps consommatrices). - Nouveau binaire de tests d'intégration
crates/rustwarden-core/tests/fediverse_interop.rssous flagexperimental-fediverse:signed_request_round_trip_through_wiremock— POST signé capturé par wiremock puis re-vérifié viaparse_and_verify_cavagecontre la même clé publique.outbound_delivers_when_inbox_returns_202—drain_oneflippe la row àDeliveredquand l'inbox répond202.outbound_defers_when_inbox_returns_503—drain_onelaisse la rowPendingavecnext_retry_atcalé sur le ladder RFC 0010 (5 min pour la 1ʳᵉ tentative) quand l'inbox répond503.
- Pas d'API publique modifiée, pas de migration. Les tests
n'utilisent ni Docker ni vrai Mastodon : ils exercent la chaîne
rsa_sha256_sign→reqwest::Client→wiremock→parse_and_verify_cavagepour détecter tout mismatch silencieux de format de signature avant la prod.
- Nouvelle dev-dependency
- RFC 0019 — Fediverse: observabilité du worker outbound (tracing
counters). Quatre points d'instrumentation
tracingsous le target dédiérustwarden::fediverse::outbound:event = "delivered"(levelinfo) — succès, avecattempts,target_inbox,target_domain,activity_id.event = "deferred"(levelwarn) — échec retryable, avecnext_retry_at,last_error,attempts,target_domain.event = "dead"(levelerror) —attempts >= max_attempts; la row reste persistée pour audit.event = "drain_failed"(levelerror) — erreur DB en bouclant le worker, ne casse pas la boucle.- Aucune nouvelle dépendance, aucun schéma DB. Les apps consommatrices
branchent l'exporter de leur choix au niveau
tracing-subscriber(Prometheus, Loki, OTEL). - Aucun nouveau test ajouté — la sémantique des transitions est déjà couverte par les tests RFC 0010 / 0011 (les events sont best-effort et ne changent pas les valeurs de retour).
- RFC 0018 — Fediverse: clôture de la dette technique (PK relax
team_members+ JWT blacklist par remote identity).- Nouvelle migration SeaORM
m20260510_000016_relax_team_members_pk: drop de la PK composite(team_id, user_id),user_iddevient nullable, ajout deid UUID PRIMARY KEY(surrogate), CHECK XORchk_team_members_principal, et index unique partielteam_members_local_uniquesur(team_id, user_id) WHERE user_id IS NOT NULLqui préserve l'unicité des rows locales. - Breaking sur
team_member::Model:- Nouveau champ
id: Uuid(PK SeaORM). user_idpasse deUuidàOption<Uuid>.- Les call-sites
Entity::find_by_id((team_id, user_id))ont été migrés versEntity::find().filter(...)(deux call-sites internes mis à jour).
- Nouveau champ
team_service::add_member_identity(Identity::Remote(_))insère désormais avecuser_id = NULL(au lieu du sentinelUuid::nil()).FediverseInviteService::acceptégalement mis à jour.- Levée de l'
#[ignore]sur le testrfc0009_add_member_identity_remote_writes_remote_identity_id_columnqui passe désormais en CI. - Nouveaux items publics gated
experimental-fediverse:JwtBlacklistService::blacklist_by_remote_identity_id(db, id, iter).- Trait
ActiveTokenIterator+ implNoopActiveTokenIterator. FediverseSignalService::apply_delete_actor_with_iterator(db, ap_id, iter).
apply_delete_actorlegacy continue de retournerjwts_blacklisted = 0(rétrocompatible) ; les apps qui veulent l'invalidation effective utilisent la variante_with_iterator.- 2 tests RFC 0018 ajoutés
(
apply_delete_actor_with_noop_iterator_keeps_jwts_blacklisted_zero,noop_active_token_iterator_returns_empty_list).
- Nouvelle migration SeaORM
- RFC 0017 — Fediverse:
HttpSigVerifierauto-résout le PEM public viaRemoteIdentityService(RFC 0003) quandctx.public_key_pemest vide. Élimine le contrat caller-side laissé ouvert par RFC 0016.- Nouveau constructeur
HttpSigVerifier::with_resolver(db).HttpSigVerifier::new()reste disponible pour les apps qui veulent gérer la résolution elles-mêmes. - Lookup direct
remote_identities.ap_id == ctx.actor_ap_id, sans nouveau cache (le service de RFC 0003 a déjà son TTL). - 2 tests unitaires :
http_sig_verifier_resolves_pem_from_remote_identities_when_empty(round-trip RSA via cache),http_sig_verifier_rejects_when_actor_not_in_cache.
- Nouveau constructeur
- RFC 0016 — Fediverse: élargissement du trait
SignatureVerifier+HttpSignedDeliverycomplet. Branche cryptographique end-to-end de rustwarden ↔ Mastodon.- Nouvelle struct publique
VerifyCtx<'a>portant la full request context (method, path,HeaderMap, body, public_key_pem). - Breaking sur
SignatureVerifier::verify(gatedexperimental-fediverse) : ancienne signatureverify(actor, sig, body, pem)remplacée parverify(&VerifyCtx<'_>). Les apps consommatrices qui implémentaient leur propre verifier doivent migrer. - Nouveaux items publics :
rustwarden_core::fediverse::http_signatures::HttpSigVerifier,rustwarden_core::fediverse::http_signatures::HttpSignedDelivery,rustwarden_core::services::fediverse_signal_service::VerifyCtx. inbox::handleetauth_routes::auth_inboxreconstituent désormais leVerifyCtxcomplet à partir duRequestAxum, ce qui permet àHttpSigVerifierd'effectuer la vérification réelle draft-cavage sans plus aucun TODO côté handler.HttpSignedDelivery:- Charge la paire RSA via
ActorKeyService::ensure_keys. - Construit
(request-target) host date, signe RSA-SHA256. - POST l'activité avec
reqwest::Client+ headersDate/Host/Signature/Content-Type. - Maps les statuts HTTP :
2xx → Ok(()), le reste →Err(Internal), ce qui réinjecte la row dans le ladder retry de RFC 0010.
- Charge la paire RSA via
reqwestajouté àexperimental-fediversedeps (réutilise la version 0.12 déjà tirée paroauth).- 4 tests RFC 0016 :
http_sig_verifier_rejects_when_signature_header_missing,http_sig_verifier_rejects_when_public_key_pem_empty,http_sig_verifier_accepts_valid_signature(round-trip RSA),http_signed_delivery_rejects_when_actor_user_id_none. - Limitations : la résolution automatique du PEM public via
RemoteIdentityServicereste à câbler (le verifier exige pour l'instant que le caller passepublic_key_pem). Tests d'intégration HTTP réel viawiremockoumockitodifférés à un follow-uptests/interop/.
- Nouvelle struct publique
- RFC 0015 — Fediverse: signatures HTTP draft-cavage (RSA-SHA256).
Premier déblocage cryptographique réel pour interop Mastodon.
- Nouveau module public gated
experimental-fediverse:rustwarden_core::fediverse::http_signatures::{ParsedSignature, parse_signature_header, build_signature_input, rsa_sha256_sign, rsa_sha256_verify, parse_and_verify_cavage}. - Aucune nouvelle dépendance externe : réutilise
rsa(RFC 0005) +sha2+base64déjà tirés. - Helpers de bas niveau (parse + canonical input + sign/verify
primitives) + helper haut niveau
parse_and_verify_cavagepour les inbox handlers qui ont accès aux full headers. - 11 tests unit (parsing + canonical + crypto round-trip + tampered rejection + algorithm/base64 validation).
- Limitations : couverture draft-cavage-12 uniquement (Mastodon-
compat) ; pas de RFC 9421 final ; pas de support
hs2019; intégration dans leSignatureVerifiertrait (qui reçoit seulement signature_header + body) reste à étendre dans un follow-up qui veut accepter une structInboundRequestcomplète. La fonctionparse_and_verify_cavageest déjà la cible.
- Nouveau module public gated
- RFC 0014 — Fediverse: 6 routes HTTP des invitations cross-instance
(slice 3/N des routes). Couverture HTTP complète de RFC 0009 :
- Côté
b.tld(admin) :POST /api/v1/teams/{team_id}/members/invite-fediverse(201).GET /api/v1/teams/{team_id}/invites(200).POST /api/v1/teams/{team_id}/invites/{invite_id}/revoke(200).
- Côté
a.tld(IdP) :GET /api/v1/fediverse/invites?handle=…(200).POST /api/v1/fediverse/invites/{invite_id}/accept(200).POST /api/v1/fediverse/invites/{invite_id}/reject(200).
- Nouveau module public gated
experimental-fediverse:rustwarden_core::fediverse::invite_routes::{router, InviteRoutesState, InviteBody, InviteResponse, InviteRow, IdpListQuery}. - Nouvelle méthode
FediverseInviteService::list_pending_for_handle(utilisée par le handler IdP-side ; retourne les invitations pending pour un@user@domaindonné). - 12 tests unit
axum::Router+tower::ServiceExt::oneshot(5 inviter-side + 5 IdP-side + 2 default-TTL/empty-list). - Limitations : pas de UI HTML, pas de pagination, et le mode IdP
suppose actuellement une DB partagée (
debug_local_federation) — un follow-up RFC ajoutera le mirror IdP-side via inbox dispatch.
- Côté
- RFC 0013 — Fediverse: 5 routes HTTP du flow d'auth AP-native
(slice 2/N des routes). Couverture HTTP complète de RFC 0006 :
POST /api/v1/auth/login/fediverse/initiate(201, bodyInitiateBody, réponseInitiateResponse).GET /api/v1/auth/login/fediverse/status/{challenge_id}(200, réponseStatusResponse).POST /api/v1/auth/login/fediverse/consume/{challenge_id}(200, réponseConsumeResponse— JWTIdentityKind::Remote).POST /fediverse/auth/inbox(202, vérifié parSignatureVerifier).POST /api/v1/fediverse/auth/consent(200, bodyConsentBody).- Nouveau module public gated
experimental-fediverse:rustwarden_core::fediverse::auth_routes::{router, AuthFlowState, InitiateBody, InitiateResponse, StatusResponse, ConsumeResponse, ConsentBody}. - Nouvelle fonction
routes::router_with_auth(...)permettant de plugger unAuthConfigexplicite quand les env vars ne sont pas disponibles au moment du build. - 10 tests unit
axum::Router+tower::ServiceExt::oneshot. - Limitations : pas de UI HTML / SSE polling, pas de rate-limit spécifique, pas de CSRF middleware (les apps consommatrices le branchent à leur niveau).
- RFC 0012 — Fediverse: route HTTP
POST /inbox(slice 1/N des routes). Première route HTTP qui rend la couche service livrée par RFC 0010 réellement utilisable end-to-end.- Nouveau module public gated par
experimental-fediverse:rustwarden_core::fediverse::inboxexposanthandleetInboxState. - Dispatch table :
Update→apply_update_actor,Delete→apply_delete_actor,Revoke→apply_revoke_membership. - Réponses :
202 Accepted,400 Bad Request,401 Unauthorized(signature manquante / verifier rejette),501 Not Implemented(activité inconnue). - Nouvelle fonction
routes::router_with(config, resolver, verifier)permettant de plugger uneArc<dyn SignatureVerifier>custom. L'ancienneroutes::routercontinue de monterAlwaysAcceptVerifierpar défaut ;routes::router_with_resolverest marquée#[deprecated]. - Nouvelles variantes
FediverseError::{Unauthorized, BadRequest, Unsupported}. - 8 tests unit
axum::Router+tower::ServiceExt::oneshot(3 success-paths + 5 error-paths). - Limitations : impl HTTP-Sig RFC 9421 réelle restera reportée à
une RFC crypto dédiée ; les routes auth-flow et team-invite
arrivent dans
0013/0014.
- Nouveau module public gated par
- RFC 0010 — Fediverse: signaux lifecycle + retry/backoff outbound.
Pièce maîtresse de la Phase 4.
- Nouveau service public gated par
experimental-fediverse:rustwarden_core::services::fediverse_signal_service::{FediverseSignalService, UpdatedActor, DeleteOutcome, SignatureVerifier, AlwaysAcceptVerifier}.apply_update_actorupsert idempotent dansremote_identities.apply_delete_actorflipdisabled_at = NOW()(compteurjwts_blacklisted = 0en attendant une méthode dédiéeJwtBlacklistService::blacklist_by_remote_identity_id).apply_revoke_membershipdrop ciblé duteam_membersrow distant.
- Trait pluggable
SignatureVerifier+ impl test-onlyAlwaysAcceptVerifier. L'implémentation HTTP-Sig RFC 9421 réelle sera livrée par un follow-up RFC crypto dédié. - Retry/backoff ladder pour
FediverseOutboundService::drain_one: sur échec, la row restependingavecnext_retry_atcalculé selon le ladder (5min → 30min → 2h → 6h → 24h → 48h). Transition terminalepending → deadquandattempts >= max_attempts. Nouvelle méthodeOutboundStatus::next_retry_offset(attempts). - 10 tests unit signal (1 pure + 9 DB-mocked) + 2 nouveaux tests
outbound (
drain_one_marks_failed_pending_with_next_retry_at_when_below_max_attempts,drain_one_marks_dead_when_attempts_reach_max,next_retry_offset_grows_then_caps) en remplacement de l'anciendrain_one_marks_failed_on_error(la sémantique de Failed a évolué vers transitoire). - Limitations : routes HTTP
/inbox+ UI HTML "apps connectées" / sessions remote admin reportées à un follow-up RFC routes ;JwtBlacklistServicene ferme pas encore les sessions associées à une identité remote supprimée.
- Nouveau service public gated par
- RFC 0009 — Fediverse: équipes et partages cross-instance + invitations.
- Nouvelle migration SeaORM
m20260510_000015_fediverse_pending_invites(table + 2 indexes dont le UNIQUE partielteam_members_remote_uniquequi interdit les doublons remote pour la même paire(team, identity)). - Nouveaux items publics gated par
experimental-fediverse:rustwarden_core::services::fediverse_invite_service::{FediverseInviteService, InviteStatus, build_invite_payload, build_accept_invite_payload, build_reject_invite_payload, build_revoke_membership_payload},rustwarden_core::models::fediverse_pending_invite::Model,rustwarden_core::services::team_service::TeamService::{add_member_identity, teams_of_identity}. - 11 tests unitaires (3 pure + 8 DB-mocked) + 2 tests d'intégration
team_service(gatedexperimental-fediverse, vrai DB). - Limitation : routes HTTP admin/IdP (5 endpoints) reportées à un
follow-up RFC ;
resource_service::share_with_remote_identityégalement différé (la table support existe).
- Nouvelle migration SeaORM
- RFC 0008 — Fediverse: abstraction
Identity+ colonnes sibling FK. Fondation de la Phase 3 : enum publicIdentity { Local(Uuid), Remote(Uuid) }+ traitIdentityResolver, plus colonnes nullablesremote_identity_idajoutées àteam_members,resource_shares.shared_with_remote_identity_id, etaudit_logs.{actor,target}_remote_identity_id.- Nouvelle migration SeaORM
m20260510_000014_identity_sibling_fks(4 colonnes nullable + 2 indexes partiels). - Nouveau module public gated par
experimental-fediverse:rustwarden_core::identity::{Identity, IdentityResolver}. - Helpers
Identity::is_local(),is_remote(),id(),as_local(),as_remote(),from_claims(&Claims)(préserve le guard confused-deputy de RFC 0007). - Sérialisation JSON snake_case
{"kind":"local","id":"<uuid>"}. - 8 tests unitaires (helpers + serde round-trip + conversion claims).
- Limitations : aucune modification des services existants
(
team_service,resource_service,audit_service) ni de la PK compositeteam_members(team_id, user_id)— RFC 0009 consomme ces colonnes via un index unique partiel sans relax de la PK.
- Nouvelle migration SeaORM
- RFC 0006 — Fediverse: flow d'authentification AP-native (couche
service). State machine
Pending → Approved | Rejected → Consumedavec one-shot guard et nonce cryptographique URL-safe base64 256 bits. Les routes HTTP/api/v1/auth/login/fediverse/*+ UI consent sont volontairement reportées à un follow-up — la couche service livrée ici est suffisante pour brancher des handlers consommateurs.- Nouvelle migration SeaORM
m20260510_000013_fediverse_auth_challenges(table + index partielpending). - Nouveaux items publics gated par
experimental-fediverse:rustwarden_core::services::fediverse_auth_service::{FediverseAuthService, ChallengeStatus, InitiateRequest, InitiatedChallenge, ChallengeDecision, ConsumedSession, build_auth_request_payload},rustwarden_core::models::fediverse_auth_challenge::Model. - 12 tests unitaires (3 pure + 9 DB-mocked).
- Nouvelle migration SeaORM
- RFC 0007 — Fediverse: extension du JWT
Claimspour distinguer les sujets locaux (users.id) des sujets distants (remote_identities.id).- Nouvel enum public
IdentityKind(Local/Remote), sérialisé snake_case sous la clékind.#[serde(default)]garantit la rétrocompatibilité avec les JWT pré-RFC 0007. - Nouveau champ
Claims.remote_id: Option<Uuid>(omis du JSON quandNone). - Helpers
Claims::is_remote(),is_local(),remote_identity_id()(guard confused-deputy : retourneNonesikind = Localmême si unremote_idparasite est présent). - Nouvelle méthode
JwtService::generate_remote_token(config, remote_id, handle)(sub = remote_id, kind = Remote, roles/permissions vides). - 8 tests unitaires (helpers, serde round-trip, legacy compat).
- Nouvel enum public
- RFC 0011 — Fediverse: file outbound + worker (slice Phase 2
minimal). Persistance des activités sortantes + worker polling +
trait pluggable
OutboundDelivery(implNoopDelivery; la signature HTTP réelle vient avec RFC 0010).- Nouvelle migration SeaORM
m20260510_000012_fediverse_outbound_queue. - Nouveaux items publics gated par
experimental-fediverse:rustwarden_core::services::fediverse_outbound_service::{FediverseOutboundService, OutboundStatus, EnqueueRequest, OutboundDelivery, NoopDelivery, OutboundWorker},rustwarden_core::models::fediverse_outbound_queue::Model. - 10 tests unitaires (2 pure + 8 DB-mocked + worker smoke test).
- Limitation : transitions retry/backoff + transport HTTP-Sig réel déléguées à RFC 0010.
- Nouvelle migration SeaORM
- RFC 0005 — Fediverse: stockage des clés d'acteur RSA chiffrées.
Génération paresseuse d'une paire RSA-2048 (PKCS#8 / SPKI) lors de la
première résolution AP du user, persistance avec clé privée chiffrée
via
CryptoService(AES-256-GCM), et exposition automatique de la clé publique dansPersonJson::public_key.- Nouvelle migration SeaORM
m20260510_000011_extend_users_with_actor_keys: ajouteusers.ap_public_key_pem TEXT NULL,users.ap_private_key_pem TEXT NULL(chiffré at-rest),users.ap_discoverable BOOLEAN NOT NULL DEFAULT TRUE. - Nouveaux items publics gated par
experimental-fediverse:rustwarden_core::services::actor_key_service::{ActorKeyService, ActorKeyMaterial}. - Wiring :
SeaOrmLocalUserResolver::find_local_userappelleActorKeyService::ensure_keyslazy quandCryptoService::is_available(), sinon retourne un acteur sans clé (mieux qu'une 500). Une erreur de génération est tracée mais ne casse pas la résolution. - 13 tests unitaires (4 pure RSA + 8 DB-mocked + 1 DB-mocked NotFound).
- Limitations : pas de rotation automatique de clé, pas de backfill de masse au démarrage (commande CLI dédiée prévue dans RFC 0011), pas de support Ed25519. Les routes signature HTTP sortantes/entrantes qui consomment ces clés viendront avec RFC 0010 / 0011.
- Nouvelle dépendance :
rsa = { version = "0.9", features = ["pem", "getrandom"], optional = true }, tirée uniquement avecexperimental-fediverse.
- Nouvelle migration SeaORM
- RFC 0004 — Fediverse: modèle de confiance (allowlist / blocklist + scope
d'interop). Permet à l'admin de choisir entre
allowlist(zero-trust par défaut) etopen_with_blocklist(modèle Mastodon), et expose le check centralis_domain_allowed(domain)que les RFCs suivantes appliqueront avant toute opération avec un domaine distant.- Nouvelle migration SeaORM
m20260510_000010_fediverse_trust_and_blocks: tablesfediverse_trusted_domainsetfediverse_blocked_domains(PK uuid,domainUNIQUE,severitypour blocklist, audit trailadded_by_user_id), plus 6 entréesapp_settings:fediverse_enabled(false),fediverse_trust_mode(allowlist),fediverse_interop_scope(rustwarden_only),fediverse_authorized_fetch(false),fediverse_node_name(Rustwarden),fediverse_node_description(vide). - Nouveaux items publics gated par
experimental-fediverse:rustwarden_core::models::fediverse_trusted_domain::Model,rustwarden_core::models::fediverse_blocked_domain::Model,rustwarden_core::services::fediverse_trust_service::{FediverseTrustService, TrustMode, InteropScope, BlockSeverity}. - Normalisation domaine systématique (lowercase + strip
www.) avant comparaison. - 12 tests unitaires (6 pure logic + 6 DB-mocked via
sea-orm/mock).
- Nouvelle migration SeaORM
- RFC 0003 — Fediverse: stockage des identités distantes + résolution
@user@domain. Cache local persistant des acteurs ActivityPub résolus, avec TTL configurable.- Nouvelle migration SeaORM
m20260510_000009_remote_identities: tableremote_identities(PK uuid,ap_idUNIQUE, contrainte UNIQUE sur(username, domain),disabled_atpour soft-delete,raw_jsonpour audit / re-processing). - Nouveaux items publics gated par
experimental-fediverse:rustwarden_core::models::remote_identity::Model,rustwarden_core::services::remote_identity_service::RemoteIdentityService,rustwarden_core::fediverse::{RemoteFetcher, UnconfiguredFetcher}. - 11 tests unitaires (5 pure logic + 6 DB-mocked via
sea-orm/mock). - Limitations : la livraison
RemoteFetcherréelle (HTTP signé) arrive avec RFC 0010. Pour l'instantUnconfiguredFetchersert de placeholder et retourneAppError::Configà toute tentative d'appel.
- Nouvelle migration SeaORM
Tooling
-
Ajout de la feature
mockde SeaORM en[dev-dependencies]pour permettre les tests unitaires sans connexion DB réelle. -
RFC 0002 — Fediverse: WebFinger + Acteur + NodeInfo (couche découverte). Câbles automatiquement par
with_fediverse()quand la featureexperimental-fediverseest activée et que la méthode est appelée.- Nouvelles routes HTTP :
GET /.well-known/webfinger?resource=acct:user@domain(CORS*, content-typeapplication/jrd+json)GET /.well-known/nodeinfoGET /nodeinfo/2.1GET /users/{username}(négociationAccept: AP JSON ou 406 si seulementtext/htmlet UI absente)
- Nouveaux items publics gated par
experimental-fediverse:- Module
rustwarden_core::fediverse(sous-moduleswebfinger,actor,nodeinfo,routes,state,types,resolver,error). - Trait
LocalUserResolveravec impl par défautSeaOrmLocalUserResolver. - Types JSON sérialisés :
PersonJson,PublicKeyJson,EndpointsJson,WebFingerResponse,WebFingerLink,NodeInfoDiscovery,NodeInfo,NodeInfoSoftware,NodeInfoServices,NodeInfoUsage,NodeInfoUsageUsers,NodeInfoLink. - Erreur
FediverseError(404 / 400 / 406, conversionFrom<AppError>). - Helper
actor_context()pour@contextMastodon-compat.
- Module
- 15 tests unitaires (5 webfinger, 6 actor, 3 nodeinfo, 1 helper).
- Limitation connue :
PersonJson::public_keyestNonetant que la RFC 0005 (actor-key-storage) n'est pas livrée — l'acteur est discoverable mais Mastodon refusera de fédérer (signatures HTTP requises).
- Nouvelles routes HTTP :
-
RFC 0001 — Fediverse:
FediverseConfig+with_fediverse()builder. Nouvelle Cargo feature optionnelleexperimental-fediversesurrustwarden-coreetrustwarden-ui, off par défaut. Charge la configuration de fédération ActivityPub depuis l'environnement et la stocke sur le builder. Les routes HTTP, le middleware de fédération et le worker outbound arrivent dans les RFCs 0002+.- Nouvelles dépendances optionnelles (gated
experimental-fediverse) :activitypub_federation = "=0.7.0-beta.11"(features["axum"], no default),ammonia = "4",url = "2"(avecserde). - Nouveaux items publics gated par
experimental-fediverse:rustwarden_core::config::FediverseConfig,rustwarden_core::config::FediverseConfigError,RustwardenBuilder::with_fediverse().
- Nouvelles dépendances optionnelles (gated
Fixed
- Race env var
FEDIVERSE_DOMAINdans la suite tests unit (révélée par l'élargissement de la fenêtre de mutation après ajout de F8).auth_inbox_returns_400_when_requester_actor_unknownmutaitFEDIVERSE_DOMAINpuisremove_var, ce qui faisait sporadiquement rater les 4 testsbuild_*_payloadqui lisentFediverseConfig::from_env()sur thread parallèle. Corrigé en supprimant la mutation env du test (l'assertionBadRequestreste vraie quelle que soit la valeur de l'env grâce au mismatch target.domain). Dette restante : la mutation env globale danstest_fediverse_configest un anti-pattern à traiter dans une RFC dette technique future.
Changed
- 4 occurrences de
std::env::set_varenveloppées dansunsafe { ... }avec commentaire// SAFETY:(1 prod dansapp.rs, 3 tests danscrypto_service.rsettotp_service.rs). Requis par edition 2024 — comportement runtime inchangé. - 9 lints clippy résolus (7×
explicit_auto_derefsur&*auth_configdans les middlewares, 2×let_and_returndanserror_handler.rsetservices/oauth/service.rs). oauth_callbackreçoit#[allow(clippy::too_many_arguments)]local avec commentaire — handler Axum à 8 extractors, refacto sans valeur.
Migration steps
Pour les apps consommatrices qui ne veulent pas la fédération : aucune action requise. La feature est off par défaut et n'a aucun coût (compile-time ou runtime).
Pour activer la fédération :
- Dans
Cargo.toml:rustwarden-core = { version = "X.Y.Z", features = ["experimental-fediverse"] } rustwarden-ui = { version = "X.Y.Z", features = ["experimental-fediverse"] }
- Dans
.env, au minimum :FEDIVERSE_DOMAIN=your.tld - Dans
main.rs:RustwardenApp::builder() .from_env() .with_fediverse()? .build().await? .run().await
À ce stade aucune route HTTP n'est exposée — ces dernières arrivent avec la
RFC 0002 (webfinger-actor-nodeinfo).
GitRust