fix(review) : applique fixes blockers review PR #9 (permission guard, sites LAZY, audit UI stale)
- 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>
This commit is contained in:
@@ -286,6 +286,14 @@ async function loadEntries(): Promise<void> {
|
|||||||
if (token !== requestToken) return
|
if (token !== requestToken) return
|
||||||
entries.value = data.member ?? []
|
entries.value = data.member ?? []
|
||||||
totalItems.value = data.totalItems ?? 0
|
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 {
|
} finally {
|
||||||
if (token === requestToken) {
|
if (token === requestToken) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ final class CoreModule
|
|||||||
['code' => 'core.users.manage', 'label' => 'Gerer les utilisateurs (creer, editer, supprimer)'],
|
['code' => 'core.users.manage', 'label' => 'Gerer les utilisateurs (creer, editer, supprimer)'],
|
||||||
['code' => 'core.roles.view', 'label' => 'Voir les roles RBAC'],
|
['code' => 'core.roles.view', 'label' => 'Voir les roles RBAC'],
|
||||||
['code' => 'core.roles.manage', 'label' => 'Gerer les roles et permissions'],
|
['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'],
|
['code' => 'core.audit_log.view', 'label' => 'Consulter le journal d\'audit'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
operations: [
|
operations: [
|
||||||
new GetCollection(
|
new GetCollection(
|
||||||
normalizationContext: ['groups' => ['permission:read']],
|
normalizationContext: ['groups' => ['permission:read']],
|
||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('core.permissions.view')",
|
||||||
),
|
),
|
||||||
new Get(
|
new Get(
|
||||||
normalizationContext: ['groups' => ['permission:read']],
|
normalizationContext: ['groups' => ['permission:read']],
|
||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('core.permissions.view')",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -260,6 +260,17 @@ final class UserRbacProcessor implements ProcessorInterface
|
|||||||
continue;
|
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.
|
// Snapshot = etat charge depuis la BDD avant denormalisation.
|
||||||
// On restaure en retirant les items actuels et en ajoutant les
|
// On restaure en retirant les items actuels et en ajoutant les
|
||||||
// originaux via l'adder/remover pour que les collections inverses
|
// originaux via l'adder/remover pour que les collections inverses
|
||||||
|
|||||||
@@ -166,16 +166,19 @@ final class PermissionApiTest extends AbstractApiTestCase
|
|||||||
self::assertResponseStatusCodeSame(401);
|
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 = $this->authenticatedClient('alice', 'alice');
|
||||||
$client->request('GET', '/api/permissions');
|
$client->request('GET', '/api/permissions');
|
||||||
|
|
||||||
self::assertResponseIsSuccessful();
|
self::assertResponseStatusCodeSame(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testStandardUserCanGetPermission(): void
|
public function testStandardUserWithoutPermissionIsForbiddenOnItem(): void
|
||||||
{
|
{
|
||||||
$permission = $this->getEm()->getRepository(Permission::class)
|
$permission = $this->getEm()->getRepository(Permission::class)
|
||||||
->findOneBy(['code' => 'test.core.users.view'])
|
->findOneBy(['code' => 'test.core.users.view'])
|
||||||
@@ -185,7 +188,7 @@ final class PermissionApiTest extends AbstractApiTestCase
|
|||||||
$client = $this->authenticatedClient('alice', 'alice');
|
$client = $this->authenticatedClient('alice', 'alice');
|
||||||
$client->request('GET', '/api/permissions/'.$permission->getId());
|
$client->request('GET', '/api/permissions/'.$permission->getId());
|
||||||
|
|
||||||
self::assertResponseIsSuccessful();
|
self::assertResponseStatusCodeSame(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cleanupTestPermissions(): void
|
private function cleanupTestPermissions(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user