# T23 — GESTION UTILISATEURS DIAGNOSTICS.WEB > Statut : **T23 VALIDÉ — AUTHENTIFICATION DIAGNOSTICS.WEB PRÊTE POUR PRODUCTION** > Aucun Git. Aucune Production. Identités de recette : `admin-test` / `support-test` (jetables, préprod). --- ## 1. Résumé exécutif T23 ajoute la gestion complète des **utilisateurs** de `Omika.Diagnostics.Web` (personnes qui ouvrent l'interface), distincte des **clients** Diagnostics (serveurs, `ClientCode`+`ApiKey`). Authentification hybride : utilisateurs PostgreSQL `diagnostics_users` en priorité, sinon opérateurs bootstrap `DiagnosticsAuth:Operators`. Hash PBKDF2-SHA256 réutilisé (`DiagnosticPasswordHasher`), jamais de mot de passe en clair, page `/Users` (Admin uniquement), page `/Account` (Mon compte), protections (dernier Admin, auto-*, brute-force, timing, cookie, CSRF). Recette live 33/33 PASS, migration 010 idempotente, permissions négatives confirmées, `Diagnostics.Tests` 536/536, `Omika.Tests` 3367/3370 (identique baseline), builds Release 0 erreur. --- ## 2. Authentification avant T23 - Cookie auth ASP.NET Core (`AddCookie`) ; opérateurs injectés via `DiagnosticsAuth:Operators` (nom + `PasswordHash` PBKDF2 + rôles), aucune identité DB. - `DiagnosticPasswordHasher` (PBKDF2-SHA256, 100 000 itérations, sel 32 o, clé 32 o, comparaison temps constant) et `DiagnosticOperatorAuthenticator` (échec générique). - Rôles `Support`/`Admin` + policies `DiagnosticsSupport` (Support OU Admin) / `DiagnosticsAdmin` (Admin seul). Conventions : dossier `/` → Support ; `/System`, `/Clients` → Admin. - Connexion dev sans mot de passe (Development uniquement) ; opérateur `admin-local` en Development. ## 3. Architecture finale authentification ``` Login (POST /Login) → LoginThrottle (5 échecs/5 min → blocage 10 min, clé username+IP) → DiagnosticUserAuthenticator.AuthenticateAsync 1. DB : GetByUsernameAsync (insensible casse) → Verify PBKDF2 + enabled (DB autoritaire, jamais de fallback si username DB existe) 2. sinon opérateurs DiagnosticsAuth:Operators (bootstrap) 3. sinon hash factice (anti-timing) → échec générique → succès DB : SignInAsync (claims Name + Role + user_source=db) + UpdateLastLoginAsync (best-effort) Cookie : HttpOnly / SameSite=Lax / Secure=Always (hors Dev) / 12 h glissante → OnValidatePrincipal : si user_source=db, re-vérifie enabled (~60 s de cache mémoire) ``` ## 4. Migration 010 `infra/postgresql/010_diagnostics_web_users.sql` — idempotente (CREATE TABLE IF NOT EXISTS + CREATE UNIQUE INDEX IF NOT EXISTS + GRANT), non destructive, appliquée **deux fois** : 0 erreur. ## 5. Table diagnostics_users `id BIGSERIAL PK`, `username TEXT NOT NULL`, `password_hash TEXT NOT NULL`, `role TEXT NOT NULL`, `enabled BOOLEAN DEFAULT TRUE`, `created_at_utc`, `updated_at_utc`, `password_changed_at_utc`, `last_login_at_utc` (TIMESTAMPTZ). Unicité insensible à la casse : index `LOWER(username)`. Aucune colonne `password`/`plain_password`/`clear_password`. ## 6. Ownership / GRANT PostgreSQL - Owner : `omika_diagnostics_owner` (table + séquence). - `REVOKE ALL FROM PUBLIC`. - `omika_diagnostics_web` : `SELECT`, `INSERT`, `UPDATE(password_hash, role, enabled, updated_at_utc, password_changed_at_utc, last_login_at_utc)`, `USAGE` séquence. - `omika_diagnostics_api` et `omika_diagnostics_worker` : **aucun** droit. ## 7. Permissions négatives (testé réellement sur `omika_diagnostics_preprod`) | Rôle | SELECT diagnostics_users | résultat | |---|---|---| | `omika_diagnostics_api` | refusé (`droit refusé`) | ✅ | | `omika_diagnostics_worker` | refusé (`droit refusé`) | ✅ | | `omika_diagnostics_web` | OK (SELECT/INSERT/UPDATE) | ✅ | | `omika_diagnostics_web` DELETE | refusé (pas de DELETE) | ✅ | ## 8. DiagnosticPasswordHasher Réutilisé tel quel (aucun nouvel algorithme). Ajout de `DummyHash` (hash factice, format valide) pour l'égalisation de temps lors d'un username inconnu. Les 4 tests existants restent verts. ## 9. Login hybride `DiagnosticUserAuthenticator.AuthenticateAsync` : DB prioritaire (§36), fallback opérateurs, échec générique « Identifiants invalides. » (jamais « inconnu »/« désactivé »/« mauvais mot de passe »). Testé : DB correct ✅, DB faux ✅, DB désactivé ✅, opérateur fallback ✅, DB autoritaire (DB désactivé + opérateur même nom → refus) ✅, inconnu ✅. ## 10. Bootstrap config `DiagnosticsAuth:Operators` conservé (fail-closed si vide). Outil hors-ligne `tools/Omika.DiagnosticHashTool` (`` ou `--stdin`) génère le hash PBKDF2. Aucun endpoint Web anonyme de génération de hash. ## 11. Login utilisateur DB `Verify(password, password_hash)` puis contrôle `enabled` ; `last_login_at_utc` mis à jour (best-effort) après succès. Vérifié en DB : `last_login_at_utc` renseigné après login. ## 12. Page Utilisateurs `/Users` (« Utilisateurs ») Admin uniquement, dans la zone Administration à côté de « Clients Diagnostics ». ## 13. Création Formulaire username / password / confirmation / rôle. Le Web hache automatiquement (`DiagnosticPasswordHasher.Hash`) puis stocke uniquement `password_hash`. L'Admin ne génère/copie jamais de hash. ## 14. Validation username Obligatoire, trim, unicité insensible à la casse, ≤ 64, `lettres/chiffres/./-/_`. ## 15. Validation password Non vide, ≥ 12 caractères, confirmation identique. Pas de politique de complexité superflue. ## 16. Support Dashboard, Diagnostics, Erreurs, Traces, Synchronisation, Telemetry, Parc, Versions, Référentiel, Mon compte. (Audité : inchangé, `IsSupport` = Support OU Admin.) ## 17. Admin Tout Support + Clients Diagnostics + Utilisateurs + Système. ## 18. Matrice autorisations (testée avec deux comptes distincts) | Page | Support | Admin | | ------------------- | :-----: | :---: | | Dashboard | ✅ | ✅ | | Diagnostics | ✅ | ✅ | | Erreurs | ✅ | ✅ | | Traces | ✅ | ✅ | | Synchronisation | ✅ | ✅ | | Telemetry | ✅ | ✅ | | Parc | ✅ | ✅ | | Versions | ✅ | ✅ | | Référentiel | ✅ | ✅ | | System | ❌ | ✅ | | Clients Diagnostics | ❌ | ✅ | | Utilisateurs | ❌ | ✅ | ## 19. System Audité : `/System` affiche l'état opérationnel (PostgreSQL, checkpoint d'import, info API). Décision : **Admin uniquement** (cohérent avec l'information opérationnelle affichée). Inchangé. ## 20. Activation/désactivation `enabled=true/false` (pas de DELETE en V1). Bouton « Désactiver »/« Réactiver ». ## 21. Changement rôle `Support → Admin` / `Admin → Support` (select + « Changer rôle »). ## 22. Dernier Admin Impossible de désactiver/rétrograder le dernier Admin actif (« Impossible de retirer le dernier administrateur actif. »). Testé réellement. ## 23. Auto-désactivation / rétrogradation Un Admin ne peut ni désactiver son propre compte, ni retirer son propre rôle Admin. Testé. ## 24. Reset mot de passe Action « Réinitialiser le mot de passe » (nouveau + confirmation) → `Hash` + UPDATE `password_hash` + `password_changed_at_utc` + `updated_at_utc`. ## 25. Mon compte Page `/Account`, accessible à tout utilisateur authentifié (Support + Admin). ## 26. Changement mot de passe `Mot de passe actuel` (vérifié réellement via `Verify`), `nouveau`, `confirmation` → `Hash(nouveau)` + UPDATE. Pour un opérateur bootstrap : non modifiable (notice). ## 27. Session utilisateur désactivé `OnValidatePrincipal` re-vérifie `enabled` pour les utilisateurs DB, à fréquence bornée (~60 s, cache mémoire mono-instance). Testé : cookie existant rejeté (302 → /Login) à la re-vérification. Documenté : au pire ~1 minute de grâce, jamais « plusieurs jours ». ## 28. Last login `last_login_at_utc` mis à jour après login DB réussi (best-effort). Vérifié en DB. ## 29. Mauvais login Message générique « Identifiants invalides. » pour username inconnu / mauvais password / désactivé. Aucune fuite « existe » / « désactivé ». Testé. ## 30. Brute force `LoginThrottle` (in-memory, mono-instance) : 5 échecs / 5 min → blocage 10 min (clé username+IP). Pas de Redis. Unit-testé. ## 31. Timing login Hash factice (`DiagnosticPasswordHasher.DummyHash`) quand username inconnu → même coût PBKDF2 que pour un vrai hash (même principe ApiKey). Résiduel documenté : entrées vides ne paient pas le hash. ## 32. Cookie `HttpOnly=true`, `SameSite=Lax`, `SecurePolicy=Always` hors Développement (Production HTTPS), `ExpireTimeSpan=12h`, `SlidingExpiration=true`. ## 33. CSRF Toutes les actions sensibles (Create/Disable/Enable/ChangeRole/ResetPassword/ChangeOwnPassword/ Login/Logout) utilisent l'antiforgery Razor Pages (FormTagHelper injecte le token ; validation automatique des handlers POST). ## 34. Sécurité HTML Scan du HTML rendu `/Users` : **0** `password_hash`, **0** hash PBKDF2 complet, **0** secret. Les `` ne sont jamais préremplis. Testé. ## 35. Sécurité PostgreSQL Scan `diagnostics_users` : colonnes attendues uniquement, **0** colonne `password`/`plain_password`/ `clear_password`, tous les `password_hash` commencent par `100000.` (PBKDF2). Testé. ## 36. Tests admin-test login OK ; Dashboard OK ; Diagnostics OK ; Clients OK ; Users OK. ## 37. Tests support-test login OK ; Dashboard OK ; Diagnostics OK ; Telemetry OK ; Parc OK ; Clients → refus (302 AccessDenied) ; Users → refus (302 AccessDenied). ## 38. Tests automatisés +73 tests (5 fichiers) : validateurs/guard, authentificateur hybride, throttle, PageModels (Users + Account), matrice autorisations, hash factice, FakeDiagnosticUserRepository. ## 39. Diagnostics.Tests **536/536** (baseline T22 : 463/463). Tous les anciens tests passent. ## 40. Omika.Tests **3367/3370** en Debug — **identique à la baseline T22** (« 3 échecs connus »). Aucune nouvelle régression. Les 3 échecs Debug sont : 2 Avalonia headless (`InterventionsViewConstructionTests`) + 1 HFSQL/OLE DB (`ResolveAsync_Batik_...`) nécessitant un serveur HFSQL réel, tous hors périmètre T23. (Note : en **Release**, 5 `DevelopmentLicenseTests` échouent en plus car le bypass dev est compilé hors `#if DEBUG` — comportement attendu, pas une régression.) ## 41. Builds `Omika.Diagnostics.Core`, `.Data`, `.Web`, `.Api`, `.Worker` + `Omika.Api` en **Release** : **0 erreur**. ## 42. Documentation `docs/DEPLOIEMENT_PRODUCTION_DIAGNOSTICS.md` : ajout §21 (gestion utilisateurs), §22 (bootstrap premier Admin + procédure 12 étapes), mise à jour §11 (rôles) et §12 (inventaire 001→010). ## 43. Procédure bootstrap Production 12 étapes documentées (§22.2) : déployer → opérateur bootstrap temporaire (hash seul) → démarrer → se connecter → `/Users` → créer Admin DB → se déconnecter → tester Admin DB → retirer bootstrap → redémarrer → confirmer bootstrap refusé → confirmer Admin DB fonctionnel. ## 44. Ordre migrations 001→010 `001 → 002 → 003 → 004 → 005 → 006 → 007 → 008 → 009 → 010`. Installation neuve : appliquer **toutes** dans l'ordre ; installation existante : uniquement les manquantes, dans l'ordre. ## 45. Nettoyage scratch `_v/` (outil scratch SQLite, non référencé par aucun projet/solution) supprimé proprement après vérification (0 référence). ## 46. Fichiers créés - `infra/postgresql/010_diagnostics_web_users.sql` - `src/Omika.Diagnostics.Core/Users/DiagnosticUserRecord.cs` - `src/Omika.Diagnostics.Core/Users/IDiagnosticUserRepository.cs` - `src/Omika.Diagnostics.Core/Security/DiagnosticUserPolicy.cs` - `src/Omika.Diagnostics.Core/Security/DiagnosticUserAuthenticator.cs` - `src/Omika.Diagnostics.Data/NpgsqlDiagnosticUserRepository.cs` - `src/Omika.Diagnostics.Web/Authorization/DiagnosticCookieValidator.cs` - `src/Omika.Diagnostics.Web/Authorization/LoginThrottle.cs` - `src/Omika.Diagnostics.Web/Pages/Users.cshtml` + `Users.cshtml.cs` - `src/Omika.Diagnostics.Web/Pages/Account.cshtml` + `Account.cshtml.cs` - `tools/Omika.DiagnosticHashTool/Omika.DiagnosticHashTool.csproj` + `Program.cs` - `tests/Omika.Diagnostics.Tests/Fakes/FakeDiagnosticUserRepository.cs` - `tests/Omika.Diagnostics.Tests/DiagnosticUserPolicyTests.cs` - `tests/Omika.Diagnostics.Tests/DiagnosticUserAuthenticatorTests.cs` - `tests/Omika.Diagnostics.Tests/LoginThrottleTests.cs` - `tests/Omika.Diagnostics.Tests/DiagnosticUserPageModelTests.cs` - `tests/Omika.Diagnostics.Tests/DiagnosticAuthorizationMatrixTests.cs` ## 47. Fichiers modifiés - `src/Omika.Diagnostics.Core/Security/DiagnosticPasswordHasher.cs` (ajout `DummyHash`) - `src/Omika.Diagnostics.Data/DiagnosticDataServiceCollectionExtensions.cs` (DI users) - `src/Omika.Diagnostics.Web/Program.cs` (throttle, cookie validator, `/Users` Admin) - `src/Omika.Diagnostics.Web/Pages/Login.cshtml.cs` (hybride + throttle + last login + message) - `src/Omika.Diagnostics.Web/Pages/Shared/_Layout.cshtml` (menu Utilisateurs + Mon compte) - `docs/DEPLOIEMENT_PRODUCTION_DIAGNOSTICS.md` - supprimé : `_v/` ## 48. Bugs trouvés - **BLOCKER** : aucun. - **MAJEUR** : aucun. - **MINEUR** : (1) PostgreSQL down → les utilisateurs DB retombent silencieusement sur les opérateurs config (fail-open cohérent avec l'outil, documenté) ; un opérateur bootstrap portant le même username qu'un utilisateur DB ne pourrait être distingué que DB indisponible. (2) Grâce ~60 s avant re-déconnexion d'un utilisateur désactivé (cache mémoire, documenté). - **COSMÉTIQUE** : lien « Mon compte » réutilise la classe `.nav-user` (style minimal). ## 49. Actions restantes avant Production - Créer la **vraie** identité Admin Production (jamais dans ce rapport) via la procédure §22. - Poser les vrais mots de passe PostgreSQL des rôles serveur (`\password`, jamais en script/log). - Configurer les URLs/ports HTTPS + reverse proxy Apache réels. - Publier et déployer les services (hors périmètre T23, à valider séparément). ## 50. Décision **T23 VALIDÉ — AUTHENTIFICATION DIAGNOSTICS.WEB PRÊTE POUR PRODUCTION**