Files
Omika/docs/T23-RAPPORT-FINAL.md
2026-10-23 18:46:50 +02:00

327 lines
14 KiB
Markdown

# 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` (`<password>` 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 `<input type=password>` 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**