docs : spec contexte forensique journal d'activité
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# Contexte forensique dans le journal d'activité
|
||||
|
||||
Date : 2026-06-24
|
||||
Branche : `feature/SIRH-41-ajouter-plus-d-info-dans-le-journal-d-activite`
|
||||
|
||||
## Problème
|
||||
|
||||
Le journal d'activité (`audit_logs`) ne stocke comme « qui » que le `username`. Or
|
||||
certains comptes sont **partagés** par plusieurs personnes (ex. compte « Usine »). Sous
|
||||
un compte partagé, toutes les actions apparaissent sous le même nom → impossible de
|
||||
distinguer les intervenants en cas de litige. Les utilisateurs se connectent aussi
|
||||
depuis des **smartphones**.
|
||||
|
||||
## Objectif
|
||||
|
||||
Ajouter du **contexte forensique automatique** à chaque entrée du journal, sans rien
|
||||
demander à l'utilisateur. But : disposer d'assez d'indices techniques pour enquêter
|
||||
(IP, type d'appareil/OS/navigateur, identifiant d'appareil stable, User-Agent brut) et
|
||||
distinguer les **appareils** derrière un compte partagé.
|
||||
|
||||
Non-objectif (volontairement exclu) : identification explicite de la personne physique
|
||||
(liste de noms, PIN…). Écarté par l'utilisateur — on reste sur du signal automatique.
|
||||
|
||||
## Périmètre
|
||||
|
||||
**Inclus :**
|
||||
- Capture automatique de 4 signaux à chaque écriture d'audit.
|
||||
- 4 nouvelles colonnes nullable sur `audit_logs` + migration (avec `down()`).
|
||||
- Service `UserAgentParser` (libellé appareil lisible, sans dépendance externe).
|
||||
- Front : identifiant d'appareil persistant (`localStorage`) envoyé en header sur toutes
|
||||
les requêtes API.
|
||||
- Exposition des 4 champs dans l'API de lecture du journal (`AuditLogResource` / provider)
|
||||
pour que la future refonte d'écran les ait à disposition.
|
||||
- Config `framework.trusted_proxies` documentée (conservatrice, à activer selon l'infra).
|
||||
- Docs (`doc/audit-logging.md`, `documentation-content.ts`, `CLAUDE.md`) + tests unitaires.
|
||||
|
||||
**Exclu (étape suivante) :**
|
||||
- Refonte de l'écran `frontend/pages/audit-logs.vue` (affichage des nouvelles colonnes,
|
||||
filtre par appareil). L'utilisateur prévoit de revoir cet écran séparément. On se
|
||||
contente d'exposer les données via l'API ; aucune modif du composant Vue dans ce lot.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Capture — un seul point d'entrée
|
||||
|
||||
Toutes les écritures d'audit passent par `AuditLogger::log()` (`src/Service/AuditLogger.php`).
|
||||
On y injecte `RequestStack`. À chaque `log()`, on lit la requête courante et on renseigne
|
||||
les 4 champs sur l'entité `AuditLog` avant persistance. **Aucun processor à modifier.**
|
||||
|
||||
Extraction depuis la requête :
|
||||
- `ip_address` ← `Request::getClientIp()`
|
||||
- `user_agent` ← header `User-Agent` (brut)
|
||||
- `device_label` ← `UserAgentParser::parse(userAgent)`
|
||||
- `device_id` ← header `X-Device-Id`
|
||||
|
||||
Si aucune requête courante (ex. commande CLI / cron), les 4 champs restent `null`
|
||||
(comportement « system » déjà existant pour le username).
|
||||
|
||||
### Modèle de données — `audit_logs`
|
||||
|
||||
Ajout de 4 colonnes **nullable** (pas de backfill, l'existant reste valide) :
|
||||
|
||||
| Colonne | Type | Contenu |
|
||||
|---|---|---|
|
||||
| `ip_address` | VARCHAR(45) | IP source. 45 = longueur max IPv6 (avec mapping IPv4). |
|
||||
| `user_agent` | TEXT | User-Agent brut, stocké tel quel. |
|
||||
| `device_label` | VARCHAR(255) | Libellé lisible, ex. `Mobile · Android · Chrome`. |
|
||||
| `device_id` | VARCHAR(64) | UUID persistant fourni par le front. |
|
||||
|
||||
Migration Doctrine avec `down()` supprimant les 4 colonnes. Pas de nouvel index dans ce
|
||||
lot (le filtre par appareil étant reporté à la refonte d'écran).
|
||||
|
||||
### Service `UserAgentParser`
|
||||
|
||||
Nouveau service `src/Service/UserAgentParser.php`, maison, sans dépendance.
|
||||
`parse(?string $userAgent): ?string` → libellé court composé de :
|
||||
- **Type** : Mobile / Tablette / Ordinateur (détecté sur tokens `Mobile`, `Tablet`, `iPad`…).
|
||||
- **OS** : Android / iOS / Windows / macOS / Linux / autre.
|
||||
- **Navigateur** : Chrome / Safari / Firefox / Edge / autre (ordre de test important :
|
||||
Edge avant Chrome, Chrome avant Safari, car les UA s'imbriquent).
|
||||
|
||||
Format : `Type · OS · Navigateur` (ex. `Ordinateur · Windows · Firefox`). Retourne `null`
|
||||
si User-Agent vide. Heuristique volontairement simple et lisible ; suffisant pour
|
||||
distinguer mobile/poste et familles d'OS. (Alternative écartée : librairie
|
||||
`matomo/device-detector` — plus précise mais lourde et non nécessaire ici.)
|
||||
|
||||
### Front — identifiant d'appareil persistant
|
||||
|
||||
- Composable `frontend/composables/useDeviceId.ts` : côté client uniquement, lit
|
||||
`localStorage['sirh-device-id']` ; si absent, génère `crypto.randomUUID()` et le persiste.
|
||||
Retourne l'ID (ou `null` côté serveur en SSR).
|
||||
- `frontend/composables/useApi.ts`, fonction `request()` (point unique de construction des
|
||||
headers) : `headers.set('X-Device-Id', deviceId)` quand l'ID est disponible. Appliqué à
|
||||
toutes les méthodes (GET/POST/PUT/PATCH/DELETE).
|
||||
- Note : l'auth est par cookie JWT (`credentials: 'include'`), donc le device ID n'est pas
|
||||
lié à l'auth — `localStorage` est ici un usage non sensible, acceptable.
|
||||
- **Limite assumée** : l'ID est par navigateur/appareil, pas par personne. Sur un poste
|
||||
partagé (même navigateur), l'ID est identique pour tous → distingue les appareils, pas
|
||||
les humains. Cohérent avec l'objectif forensique.
|
||||
|
||||
### API de lecture
|
||||
|
||||
Exposer `ipAddress`, `userAgent`, `deviceLabel`, `deviceId` dans la sortie de lecture du
|
||||
journal (`src/ApiResource/AuditLogResource.php` + sérialisation dans `AuditLogProvider`),
|
||||
ainsi que dans le DTO front `frontend/services/dto/audit-log.ts`. Aucune modif du
|
||||
composant `audit-logs.vue` (refonte ultérieure). Objectif : les données sont prêtes à
|
||||
être affichées par la future refonte.
|
||||
|
||||
### Trusted proxies (IP fiable)
|
||||
|
||||
`framework.trusted_proxies` n'est pas configuré aujourd'hui. Derrière un reverse proxy
|
||||
(nginx/traefik), `getClientIp()` renvoie l'IP du proxy. Architecture de déploiement non
|
||||
confirmée → on prévoit dans `config/packages/framework.yaml` une entrée **commentée et
|
||||
documentée** (avec exemple `trusted_proxies` réseau privé / loopback + `trusted_headers`),
|
||||
à activer selon l'infra. En attendant, l'IP est stockée telle que renvoyée par Symfony.
|
||||
|
||||
## Stratégie de test
|
||||
|
||||
- `tests/.../UserAgentParserTest` : table de User-Agents réels (Chrome desktop, Safari
|
||||
iPhone, Chrome Android, Firefox, Edge, UA vide/null) → libellés attendus.
|
||||
- `tests/.../AuditLoggerTest` : avec un `RequestStack` peuplé d'une `Request` factice
|
||||
(IP, headers User-Agent + X-Device-Id), vérifier que l'`AuditLog` persisté porte bien les
|
||||
4 champs ; et qu'avec une `RequestStack` vide (contexte CLI), les 4 champs sont `null`.
|
||||
|
||||
## Documentation à mettre à jour (règles obligatoires CLAUDE.md)
|
||||
|
||||
- `doc/audit-logging.md` : section « Données stockées par entrée » + nouveaux champs +
|
||||
note sur le device ID front et le caveat trusted proxies.
|
||||
- `frontend/data/documentation-content.ts` : doc in-app (niveau admin) du journal.
|
||||
- `CLAUDE.md` : section Audit Logging — mentionner les 4 nouveaux signaux et le point de
|
||||
capture unique (`AuditLogger` + `RequestStack`).
|
||||
|
||||
## Risques / limites
|
||||
|
||||
- Device ID = par appareil, pas par humain (cf. ci-dessus).
|
||||
- IP peu utile derrière proxy tant que `trusted_proxies` n'est pas activé.
|
||||
- Plusieurs personnes dans la même usine sortent souvent sur la même IP publique et
|
||||
peuvent avoir le même modèle de téléphone → les signaux se recoupent ; ce lot fournit
|
||||
des indices, pas une preuve d'identité.
|
||||
Reference in New Issue
Block a user