Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
## Résumé
Implémentation complète du système RBAC (Role-Based Access Control) pour Coltura.
### Backend
- Entités Permission et Role avec API Platform CRUD
- PermissionVoter : vérification des permissions effectives (rôles + directes), admin bypass
- Endpoints `PATCH /users/{id}/rbac` pour assigner rôles, permissions directes et isAdmin
- AdminHeadcountGuard : protection contre la suppression du dernier admin
- Commande `app:sync-permissions` pour synchroniser les permissions déclarées par les modules
- Filtrage sidebar par permission RBAC (`permission` key optionnelle dans sidebar.php)
- 115 tests PHPUnit (fonctionnels + unitaires)
### Frontend
- Composable `usePermissions()` avec `can()`, `canAny()`, `canAll()` et admin bypass
- Page `/admin/roles` : DataTable, création/édition via drawer, suppression avec confirmation
- Page `/admin/users` : DataTable, drawer RBAC avec rôles, permissions directes, résumé effectif
- PermissionGroup : checkboxes groupées par module avec "tout sélectionner"
- EffectivePermissions : résumé lecture seule avec badges source ("via Rôle X" / "Direct")
- Warning auto-édition, toggle isAdmin
- Tests Vitest pour usePermissions
### Permissions déclarées
- `core.users.view` — Voir les utilisateurs
- `core.users.manage` — Gérer les utilisateurs
- `core.roles.view` — Voir les rôles RBAC
- `core.roles.manage` — Gérer les rôles et permissions
- `GET /api/permissions` accessible à tout utilisateur authentifié (catalogue read-only)
## Tickets Lesstime
- ERP-23 (#343) — Entités Permission et Role
- ERP-24 (#344) — API CRUD Roles & Permissions
- ERP-25 (#345) — Voter Symfony + usePermissions
- ERP-26 (#346) — Interface Admin : Gestion des Rôles
- ERP-27 (#347) — Interface Admin : Permissions Utilisateur
## Test plan
- [ ] `make db-reset` puis vérifier les fixtures (admin/alice/bob, rôles système)
- [ ] Login admin : sidebar affiche Gestion des rôles + Utilisateurs
- [ ] Login alice : sidebar masque ces onglets (pas de permission)
- [ ] Page /admin/roles : CRUD rôles, permissions groupées, protection rôles système
- [ ] Page /admin/users : assignation rôles + permissions directes, résumé effectif
- [ ] Warning auto-édition quand admin modifie ses propres droits
- [ ] `make test` : 115 tests PHPUnit passent
- [ ] `cd frontend && npm run test` : tests Vitest passent
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Matthieu <mtholot19@gmail.com>
Co-authored-by: tristan <tristan@yuno.malio.fr>
Reviewed-on: #7
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
170 lines
6.2 KiB
PHP
170 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Core\Api;
|
|
|
|
use App\Module\Core\Domain\Entity\Permission;
|
|
use App\Module\Core\Domain\Entity\Role;
|
|
use App\Module\Core\Domain\Entity\User;
|
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
|
|
|
/**
|
|
* Tests fonctionnels de l'endpoint GET /api/me.
|
|
*
|
|
* Verifie que la reponse inclut `isAdmin` et `effectivePermissions`
|
|
* dans le groupe de serialisation `me:read`.
|
|
*
|
|
* Strategie de donnees :
|
|
* - Les tests 1-3 s'appuient exclusivement sur les fixtures (admin/alice).
|
|
* - Le test 4 cree un user jetable prefixe `test_me_` + role + permission,
|
|
* purges en tearDown.
|
|
*
|
|
* @internal
|
|
*/
|
|
final class MeApiTest extends AbstractApiTestCase
|
|
{
|
|
private const TEST_USER_PREFIX = 'test_me_';
|
|
private const TEST_ROLE_PREFIX = 'test_me_';
|
|
private const TEST_PERMISSION_PREFIX = 'test.me.';
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->cleanupTestData();
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* L'admin (isAdmin=true, role systeme sans permission explicite) doit
|
|
* obtenir un payload /me avec isAdmin=true et effectivePermissions=[].
|
|
*/
|
|
public function testMeEndpointReturnsIsAdminAndEffectivePermissionsForAdmin(): void
|
|
{
|
|
$client = $this->authenticatedClient('admin', 'admin');
|
|
$response = $client->request('GET', '/api/me', [
|
|
'headers' => ['Accept' => 'application/ld+json'],
|
|
]);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$data = $response->toArray();
|
|
|
|
self::assertSame('admin', $data['username'], 'Le champ username doit etre "admin".');
|
|
self::assertTrue($data['isAdmin'], 'isAdmin doit etre true pour l\'admin fixture.');
|
|
self::assertArrayHasKey('effectivePermissions', $data, 'effectivePermissions doit etre present dans le payload.');
|
|
self::assertIsArray($data['effectivePermissions'], 'effectivePermissions doit etre un tableau JSON.');
|
|
// Le role systeme admin n'a pas de permissions explicites : tableau vide attendu.
|
|
self::assertSame([], $data['effectivePermissions'], 'effectivePermissions doit etre [] pour l\'admin sans permissions explicites.');
|
|
}
|
|
|
|
/**
|
|
* Un utilisateur standard (isAdmin=false, role user sans permission) doit
|
|
* obtenir isAdmin=false et effectivePermissions=[].
|
|
*/
|
|
public function testMeEndpointReturnsEmptyPermissionsForStandardUser(): void
|
|
{
|
|
$client = $this->authenticatedClient('alice', 'alice');
|
|
$response = $client->request('GET', '/api/me', [
|
|
'headers' => ['Accept' => 'application/ld+json'],
|
|
]);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$data = $response->toArray();
|
|
|
|
self::assertFalse($data['isAdmin'], 'isAdmin doit etre false pour alice.');
|
|
self::assertArrayHasKey('effectivePermissions', $data, 'effectivePermissions doit etre present dans le payload.');
|
|
self::assertSame([], $data['effectivePermissions'], 'effectivePermissions doit etre [] pour un user sans role avec permission.');
|
|
}
|
|
|
|
/**
|
|
* Une requete non authentifiee sur /api/me doit retourner 401.
|
|
*/
|
|
public function testMeEndpointRequiresAuthentication(): void
|
|
{
|
|
$client = self::createClient();
|
|
$client->request('GET', '/api/me', [
|
|
'headers' => ['Accept' => 'application/ld+json'],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(401);
|
|
}
|
|
|
|
/**
|
|
* Un user rattache a un role portant la permission `core.users.view` doit
|
|
* retrouver cette permission dans effectivePermissions, triee alphabetiquement.
|
|
*/
|
|
public function testMeEndpointReturnsEffectivePermissionsForUserWithRolePermissions(): void
|
|
{
|
|
// --- Preparation des donnees de test ---
|
|
self::bootKernel();
|
|
$em = $this->getEm();
|
|
|
|
$this->cleanupTestData();
|
|
|
|
/** @var UserPasswordHasherInterface $hasher */
|
|
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
|
|
|
$permission = new Permission('test.me.core.users.view', 'View users (test me)', 'core');
|
|
$em->persist($permission);
|
|
|
|
$role = new Role('test_me_viewer', 'Viewer (test me)', false);
|
|
$role->addPermission($permission);
|
|
$em->persist($role);
|
|
|
|
$user = new User();
|
|
$user->setUsername('test_me_viewer_user');
|
|
$user->setIsAdmin(false);
|
|
$user->setPassword($hasher->hashPassword($user, 'secret'));
|
|
$user->addRbacRole($role);
|
|
$em->persist($user);
|
|
|
|
$em->flush();
|
|
$em->clear();
|
|
|
|
// --- Appel API ---
|
|
$client = $this->authenticatedClient('test_me_viewer_user', 'secret');
|
|
$response = $client->request('GET', '/api/me', [
|
|
'headers' => ['Accept' => 'application/ld+json'],
|
|
]);
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
$data = $response->toArray();
|
|
|
|
self::assertArrayHasKey('effectivePermissions', $data, 'effectivePermissions doit etre present dans le payload.');
|
|
self::assertContains(
|
|
'test.me.core.users.view',
|
|
$data['effectivePermissions'],
|
|
'effectivePermissions doit contenir le code de permission du role attribue.',
|
|
);
|
|
|
|
// Verifie le tri alphabetique (contrat spec section 9 ticket-343).
|
|
$sorted = $data['effectivePermissions'];
|
|
$copy = $sorted;
|
|
sort($copy);
|
|
self::assertSame($copy, $sorted, 'effectivePermissions doit etre trie alphabetiquement.');
|
|
}
|
|
|
|
/**
|
|
* Purge les entites de test creees par les methodes ci-dessus.
|
|
* Ordre : users d'abord (FK vers roles), puis roles, puis permissions.
|
|
*/
|
|
private function cleanupTestData(): void
|
|
{
|
|
$em = $this->getEm();
|
|
|
|
$em->createQuery(
|
|
'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix'
|
|
)->setParameter('prefix', self::TEST_USER_PREFIX.'%')->execute();
|
|
|
|
$em->createQuery(
|
|
'DELETE FROM '.Role::class.' r WHERE r.code LIKE :prefix'
|
|
)->setParameter('prefix', self::TEST_ROLE_PREFIX.'%')->execute();
|
|
|
|
$em->createQuery(
|
|
'DELETE FROM '.Permission::class.' p WHERE p.code LIKE :prefix'
|
|
)->setParameter('prefix', self::TEST_PERMISSION_PREFIX.'%')->execute();
|
|
}
|
|
}
|