feat(core) : RBAC #345 - expose effectivePermissions via /api/me
- Ajoute #[Groups(['me:read'])] sur getEffectivePermissions() dans User.php
- Fixe la serialisation de isAdmin : le prefixe "is" etait strip par Symfony,
expose desormais via le getter avec #[SerializedName('isAdmin')] + groups lecture,
la propriete conserve uniquement le groupe d'ecriture user:rbac:write
- Cree MeApiTest avec 4 tests fonctionnels (isAdmin admin, permissions vides user,
401 sans auth, effectivePermissions avec role portant une permission)
This commit is contained in:
@@ -68,7 +68,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
private ?string $username = null;
|
||||
|
||||
#[ORM\Column(name: 'is_admin', options: ['default' => false])]
|
||||
#[Groups(['me:read', 'user:list', 'user:rbac:write', 'user:rbac:read'])]
|
||||
// Groupe d'ecriture uniquement sur la propriete pour la denormalisation PATCH /rbac.
|
||||
// Les groupes de lecture sont declares sur le getter isAdmin() afin d'exposer
|
||||
// la cle JSON "isAdmin" (Symfony strip le prefixe "is" sur les methodes sans SerializedName).
|
||||
#[Groups(['user:rbac:write'])]
|
||||
private bool $isAdmin = false;
|
||||
|
||||
/**
|
||||
@@ -169,6 +172,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $roles;
|
||||
}
|
||||
|
||||
// Groupes de lecture + nom serialise explicite pour eviter que Symfony
|
||||
// ne strip le prefixe "is" et expose la cle "admin" au lieu de "isAdmin".
|
||||
#[Groups(['me:read', 'user:list', 'user:rbac:read'])]
|
||||
#[SerializedName('isAdmin')]
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->isAdmin;
|
||||
@@ -245,6 +252,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
#[Groups(['me:read'])]
|
||||
public function getEffectivePermissions(): array
|
||||
{
|
||||
$codes = [];
|
||||
|
||||
169
tests/Module/Core/Api/MeApiTest.php
Normal file
169
tests/Module/Core/Api/MeApiTest.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user