feat : audit log (table + writer + listener + API + admin UI + timeline) #9

Merged
matthieu merged 38 commits from feat/audit-log into develop 2026-05-13 08:29:31 +00:00
5 changed files with 30 additions and 7 deletions
Showing only changes of commit bb6a4c387b - Show all commits

View File

@@ -286,6 +286,14 @@ async function loadEntries(): Promise<void> {
if (token !== requestToken) return
entries.value = data.member ?? []
totalItems.value = data.totalItems ?? 0
} catch {
// En cas d'echec (reseau, 403, 500...), on reset l'etat pour ne pas
// laisser l'utilisateur croire que les donnees affichees sont a jour.
// Le toast d'erreur est deja emis par `useApi()` via useAuditLog.
if (token === requestToken) {
entries.value = []
totalItems.value = 0
}
} finally {
if (token === requestToken) {
loading.value = false

View File

@@ -34,6 +34,7 @@ final class CoreModule
['code' => 'core.users.manage', 'label' => 'Gerer les utilisateurs (creer, editer, supprimer)'],
['code' => 'core.roles.view', 'label' => 'Voir les roles RBAC'],
['code' => 'core.roles.manage', 'label' => 'Gerer les roles et permissions'],
['code' => 'core.permissions.view', 'label' => 'Consulter le catalogue des permissions RBAC'],
['code' => 'core.audit_log.view', 'label' => 'Consulter le journal d\'audit'],
];
}

View File

@@ -20,11 +20,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
operations: [
new GetCollection(
normalizationContext: ['groups' => ['permission:read']],
security: "is_granted('ROLE_USER')",
security: "is_granted('core.permissions.view')",
),
new Get(
normalizationContext: ['groups' => ['permission:read']],
security: "is_granted('ROLE_USER')",
security: "is_granted('core.permissions.view')",
),
],
)]

View File

@@ -260,6 +260,17 @@ final class UserRbacProcessor implements ProcessorInterface
continue;
}
// Force l'initialisation LAZY avant de lire le snapshot : pour une
// association fetch=LAZY (ex: User::$sites), la PersistentCollection
// existe mais son snapshot est vide tant que la collection n'a pas
// ete materialisee. Sans cet init, `getSnapshot()` renvoie `[]` et
// la boucle de restauration ci-dessous appelle `remover()` sur
// chaque item charge par `toArray()` → **vide silencieusement la
// collection** au lieu de la preserver. Idempotent si deja initialisee.
if (!$currentCollection->isInitialized()) {
$currentCollection->initialize();
}
// Snapshot = etat charge depuis la BDD avant denormalisation.
// On restaure en retirant les items actuels et en ajoutant les
// originaux via l'adder/remover pour que les collections inverses

View File

@@ -166,16 +166,19 @@ final class PermissionApiTest extends AbstractApiTestCase
self::assertResponseStatusCodeSame(401);
}
public function testStandardUserCanListPermissions(): void
public function testStandardUserWithoutPermissionIsForbiddenOnCollection(): void
{
// Le catalogue de permissions est accessible a tout utilisateur authentifie.
// Le catalogue de permissions est protege par `core.permissions.view` :
// un user authentifie sans cette permission (ni flag admin) doit
// recevoir un 403. Alice n'a que le role systeme "user" (zero
// permission attachee) — elle est le cobaye ideal pour ce test.
$client = $this->authenticatedClient('alice', 'alice');
$client->request('GET', '/api/permissions');
self::assertResponseIsSuccessful();
self::assertResponseStatusCodeSame(403);
}
public function testStandardUserCanGetPermission(): void
public function testStandardUserWithoutPermissionIsForbiddenOnItem(): void
{
$permission = $this->getEm()->getRepository(Permission::class)
->findOneBy(['code' => 'test.core.users.view'])
@@ -185,7 +188,7 @@ final class PermissionApiTest extends AbstractApiTestCase
$client = $this->authenticatedClient('alice', 'alice');
$client->request('GET', '/api/permissions/'.$permission->getId());
self::assertResponseIsSuccessful();
self::assertResponseStatusCodeSame(403);
}
private function cleanupTestPermissions(): void