327 lines
14 KiB
Markdown
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**
|