- testItemEndpointWithoutPermissionGets403 : symetrique de
testAuthenticatedUserWithoutPermissionGets403 sur /api/audit-logs/{id},
prouve que la security expression `is_granted('core.audit_log.view')`
est appliquee aussi sur l'operation Get item (couvre M-4 du backlog).
- testPageZeroDoesNotProduceServerError : verrouille l'invariant
fonctionnel "?page=0 ne produit jamais 500 PG", quel que soit le
mecanisme protecteur (clamp provider ou validation API Platform amont).
Couvre M-7 du backlog.
restoreAbsentCollections utilisait `array_key_exists('sites', $payload)`
qui retourne true pour une valeur null, sautant la restauration.
API Platform rejette deja `sites: null` au denormalize (400 type mismatch),
donc le bypass n'est pas reellement exploitable aujourd'hui via l'API HTTP.
Mais le test `&& is_array(...)` reste une defense-in-depth si la config
denormalizer change un jour, et rend l'intention explicite.
Test de regression : PATCH {sites: null} -> 400 + collection sites intacte
en DB (aucune trace de l'echec).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trois corrections issues du code review multi-agent sur la PR audit-log :
- AuditListener : reset defensif de pendingLogs en debut de onFlush. Si
un flush precedent a leve une exception avant postFlush (qui n'est
jamais appele sur un flush rate), le state listener gardait des
changements jamais committes, ecrits a tort par le prochain postFlush
reussi — audit_log pouvait donc contenir des lignes decrivant des
evenements qui n'ont pas eu lieu en DB. Test de regression via
Reflection pour injecter un log orphelin et verifier qu'il n'arrive
pas dans audit_log.
- AuditLogProvider : validation explicite des filtres performed_at[after]
et performed_at[before] (strtotime) + whitelist stricte sur `action`
(create|update|delete). Avant, un input malforme remontait jusqu'a
Postgres et faisait un 500 (SQLSTATE[22007]). Desormais 400 explicite,
pas de log pollue.
- doc/audit-log.md : ajoute une section "Contrat" qui explicite ce que
audit_log garantit (journal des intentions appliquees par l'ORM) et ne
garantit PAS (reflet exact du commit outermost — une ligne audit peut
persister si une transaction outermost rollback apres un flush inner
reussi, parce que l'audit ecrit sur une connexion DBAL dediee).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend :
- AuditLogWriter::stripSensitive rendu reellement recursif (matche doc).
- Tests GET /api/permissions/{id} non-admin pour chaque branche OR (gap Codex).
- Gardes non-regression UserRbacProcessor : PATCH /rbac sans clef sites ne
doit ni auto-selectionner currentSite ni exiger sites.manage.
Frontend :
- useAuditLog : renomme export trompeur fetchLogs -> fetchLogsCached, le
nom reflete desormais le comportement (cache pollue sinon).
- RoleDrawer / UserRbacDrawer : catch explicite + message d'erreur +
bouton save disabled si le chargement des referentiels a echoue (evite
un ecrasement silencieux des droits).
- AuditTimeline / AuditLogDetail : `oui`/`non` passent par common.yes/no.
- AuditTimeline : Intl.RelativeTimeFormat et toLocaleString suivent la
locale i18n courante (plus de hardcode 'fr').
E2E :
- sidebar-visibility.spec : remplace waitForLoadState('networkidle')
fragile par attente semantique sur accountDashboardLink (stable en CI).
Tests : 237/237 green, eslint clean, php-cs-fixer clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issues remontees par la seconde passe de review de la PR #9 :
- Regression `GET /api/permissions` 403 silencieux sur les drawers RBAC
(UserRbacDrawer, RoleDrawer) apres le fix precedent qui imposait
`core.permissions.view`. Les users porteurs de `core.users.manage` /
`core.roles.manage` ne voyaient plus le catalogue pour hydrater leurs
checkboxes. Elargit la security expression sur Permission en OR avec
ces deux codes : les gestionnaires ont par nature besoin du catalogue
(codes/libelles seuls, pas de secret expose).
- Race condition dans UserRbacProcessor : `restoreAbsentCollections()`
lisait le snapshot Doctrine hors transaction, puis `wrapInTransaction()`
flushait plus tard. Fenetre courte mais reelle ou une modification
concurrente aurait pu etre annulee par une restauration depuis un
snapshot stale. Deplace l'appel a l'interieur de la transaction.
- Stale-data sur les pages admin users / roles / sites : meme pattern
try/finally sans catch que sur audit-log (deja corrige). Aligne les
trois pages avec un catch qui reset la liste locale.
- Tests manquants : garde de non-regression sur PATCH /rbac sans `sites`
(assure que la collection elle-meme est preservee, pas seulement le
currentSite). Couverture positive sur GET /api/permissions pour les
trois branches OR de la security expression (permissions.view,
users.manage, roles.manage) via des users non-admin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Permission entity : remplace le guard `ROLE_USER` par `core.permissions.view`
sur GetCollection/Get. Le catalogue complet des permissions RBAC etait
accessible a tout utilisateur authentifie. Ajoute la permission manquante
dans CoreModule::permissions() et inverse les tests standardUser*
(attendent maintenant un 403 pour un user sans la permission).
- UserRbacProcessor::restoreAbsentCollections() : force
PersistentCollection::initialize() avant de lire le snapshot. Pour une
association fetch=LAZY (ex: User::$sites), le snapshot est vide tant que
la collection n'est pas materialisee, ce qui faisait vider silencieusement
tous les sites d'un user sur un PATCH ne contenant pas la cle `sites`.
- admin/audit-log.vue : ajoute un catch sur loadEntries() qui reset
entries/totalItems pour ne pas afficher de donnees stale si le fetch echoue
(reseau coupe, 403 inopinee...). Le toast d'erreur reste gere par useApi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TIMESTAMP(6) WITH TIME ZONE + tie-breaker id DESC sur l'ORDER BY pour
garantir un tri deterministe quand plusieurs lignes partagent la meme
timestamp (batch fixture, bulk flush < 1µs).
- Suppression de la clause ESCAPE '\\' redondante (`\` est deja
l'echappement LIKE par defaut en PostgreSQL) et fragile sur
standard_conforming_strings. Le str_replace des wildcards reste.
- paginationMaximumItemsPerPage : 100 -> 50. Reduit le pire cas de
reponse lourde sur un endpoint admin (changes JSONB volumineux).
Resout les 5 findings de la review automatique + couverture ManyToMany
annoncee dans CLAUDE.md :
- AuditListener : resolution de la classe via ClassMetadata plutot que
`$entity::class` direct (defense proxy Doctrine : sous ORM 2 les lazies
sont des `Proxies\__CG__\...`). Test de regression via getReference().
- AuditListener : capture des modifications de collections to-many
(OneToMany / ManyToMany) via getScheduledCollectionUpdates /
getScheduledCollectionDeletions. Les diffs sont mergees dans le
changeset existant ou creent une entree "update" dediee.
- AuditLogResource + Provider : filtre multi-valeurs
`entity_type[]=X&entity_type[]=Y` (IN clause DBAL via
ArrayParameterType::STRING), endpoint `/audit-log-entity-types` pour
alimenter le MalioSelectCheckbox cote front.
- audit-log.vue : refonte complete. Passage a `MalioDataTable`,
composants `Malio*` (MalioInputText, MalioSelectCheckbox, MalioButton),
suppression complete de la persistance URL (`readQuery` / `syncQuery`
/ `route.query`). `datetime-local` conserve avec TODO pointant
l'exception CLAUDE.md.
- AuditTimeline : fix du saut d'items 11-30. `PAGE_SIZE = 10` aligne
avec un `itemsPerPage=10` passe au backend. Token anti-race pour
ignorer les reponses tardives quand l'entite affichee change.
- AuditLogDetail : affichage des diffs de collections to-many (+ / -)
dans le tableau field/old/new existant.
- logout.vue : ajout du `resetAuditLog()` au logout pour eviter qu'un
user suivant (meme onglet) voie l'etat audit de l'ancien.
- Permission / Role / Site : marquage `#[Auditable]`.
- Version bump 0.1.32 → 0.1.34.
Tests : 228 / 228 (221 assertions → 851, dont regressions proxy + M2M).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blocker
- Frontend attendait `hydra:member` / `hydra:totalItems` / `hydra:view` mais
API Platform 4 sert `member` / `totalItems` / `view` (sans prefixe) sous
ld+json, et un tableau plat sous json. Consequence : tableau admin et
timeline silencieusement vides.
Fix : `useAuditLog` force `Accept: application/ld+json` (necessaire pour
obtenir l'objet Hydra avec pagination), types `HydraCollection`/`HydraView`
renommes, composants accedent aux proprietes sans prefixe. Nouveau test
fonctionnel verrouille le format.
Should-fix
- `AuditLogWriter` : ajout de `'id' => Types::GUID` pour expliciter le type
natif PG `uuid` (fonctionnait par cast implicite mais l'intention etait
floue).
- `AuditListener` docblock : documente que le DQL bulk DELETE/UPDATE et
`Connection::executeStatement()` bypassent le listener (onFlush non
appele). Piege pour les futures commandes de purge.
- `AuditLogResource` : ajout d'une regex UUID dans `requirements` de
l'operation Get — un `GET /api/audit-logs/not-a-uuid` produisait un 500
(cast PG rejete) au lieu d'un 404.
- `audit-log.vue` : le watcher des filtres faisait `filters.page = 1` ce
qui declenchait le watcher de `page`, causant deux `loadEntries()` en
parallele. Fusionne : la navigation page appelle `loadEntries()`
directement depuis `goPrevious`/`goNext`, plus de watcher dedie.
- `useAuditLog.fetchEntityLogs` : bypass du cache `lastCollection` pour ne
pas polluer la reference page-level quand la timeline est ouverte.
- `AuditTimeline.vue` : remplacement du `<div v-if="!canView"/>` vide par
un `v-if` sur le wrapper — aucun DOM quand l'utilisateur n'a pas le droit.
- `AuditListenerTest` tag : retire le `_` (wildcard LIKE SQL) du prefix
pour eviter un faux negatif de match cross-test.
- `AuditLogApiTest` : proprietes `auditConnection` / `runTag` nullable et
tearDown guarde, sinon un echec setUp provoquait un fatal typed-property
au lieu de propager l'exception d'origine.
Stabilite suite de tests
- `doctrine.yaml when@test` : `idle_connection_ttl: 1` sur les deux
connexions pour eviter l'accumulation de connexions orphelines.
- tearDown des tests audit : `close()` explicite sur la connexion audit
apres chaque test.
- `docker-compose.yml` : `max_connections=300` sur la DB dev (defaut PG=100
insuffisant pour 220+ tests * 2 connexions/test).
Implemente le journal d'audit append-only sur toutes les mutations Doctrine
des entites portant #[Auditable]. Couvre les 5 tickets de doc/audit-log.md :
1. Table PG audit_log (uuid PK, jsonb changes, index entity/time/performer)
+ AuditLogWriter (DBAL connexion dediee audit, blacklist defense-in-depth
sur password/plainPassword/token/secret) + RequestIdProvider (UUID v4 par
requete HTTP principale).
2. Attributs Auditable / AuditIgnore dans Shared/Domain/Attribute/
+ AuditListener (onFlush capture + postFlush ecriture hors transaction ORM,
pattern swap-and-clear, erreurs loguees jamais propagees). User annote.
3. API Platform read-only /api/audit-logs (permission core.audit_log.view)
avec filtres entity_type / entity_id / action / performed_by / plage
performed_at + DbalPaginator implementant PaginatorInterface (hydra:view
genere automatiquement).
4. Page admin /admin/audit-log : tableau pagine, filtres persistes en query
params, row expandable (diff + timeline de l'entite), entree sidebar avec
permission. Composable useAuditLog avec resetAuditLog() auto-enregistre
sur onAuthSessionCleared.
5. Composant AuditTimeline reutilisable : garde permission, lazy loading,
dates relatives FR, skeleton loader.
Fix connexe : phpunit.dist.xml forcait APP_ENV=dev via <env> ce qui cablait
framework.test=false et rendait test.service_container indisponible ; le
JWT_PASSPHRASE ne matchait pas non plus les cles dev. Corrige en meme temps
pour debloquer la suite de tests.