voter = new PermissionVoter(); } // --------------------------------------------------------------- // Abstention : attributs non-RBAC // --------------------------------------------------------------- /** * Le voter s'abstient sur ROLE_ADMIN : commence par une majuscule, * ne correspond pas au pattern snake_case minuscule avec point. */ public function testAbstainsOnRoleAdminAttribute(): void { $user = $this->buildUser(username: 'alice', isAdmin: false); $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); $result = $this->voter->vote($token, null, ['ROLE_ADMIN']); $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $result); } /** * Le voter s'abstient sur IS_AUTHENTICATED_FULLY : contient des majuscules, * pas de point de separation conforme au pattern RBAC. */ public function testAbstainsOnIsAuthenticatedAttribute(): void { $user = $this->buildUser(username: 'alice', isAdmin: false); $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); $result = $this->voter->vote($token, null, ['IS_AUTHENTICATED_FULLY']); $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $result); } /** * Le voter s'abstient sur des attributs malformes : sans point ou avec * majuscules. */ #[DataProvider('malformedAttributeProvider')] public function testAbstainsOnMalformedAttribute(string $attribute): void { $user = $this->buildUser(username: 'alice', isAdmin: false); $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); $result = $this->voter->vote($token, null, [$attribute]); $this->assertSame( VoterInterface::ACCESS_ABSTAIN, $result, sprintf('Le voter aurait du s\'abstenir pour l\'attribut "%s".', $attribute), ); } /** * @return array */ public static function malformedAttributeProvider(): array { return [ 'sans point' => ['nodot'], 'majuscule milieu' => ['HAS.UPPERCASE'], 'commence chiffre' => ['1core.users.view'], 'chaine vide' => [''], ]; } // --------------------------------------------------------------- // Refus : utilisateur non reconnu // --------------------------------------------------------------- /** * Refuse l'acces quand le token ne porte pas une instance de User metier * (ex: InMemoryUser de Symfony). */ public function testDeniesWhenUserIsNotAUserEntity(): void { $inMemoryUser = new InMemoryUser('anonymous', null, ['ROLE_USER']); $token = new UsernamePasswordToken($inMemoryUser, 'main', $inMemoryUser->getRoles()); $result = $this->voter->vote($token, null, ['core.users.view']); $this->assertSame(VoterInterface::ACCESS_DENIED, $result); } // --------------------------------------------------------------- // Bypass admin // --------------------------------------------------------------- /** * Accorde l'acces systematiquement a un administrateur, meme sans aucune * permission explicite assignee. */ public function testGrantsForAdminBypass(): void { // Admin sans role ni permission directe : le bypass doit suffire. $user = $this->buildUser(username: 'admin', isAdmin: true); $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); $result = $this->voter->vote($token, null, ['core.users.view']); $this->assertSame(VoterInterface::ACCESS_GRANTED, $result); } // --------------------------------------------------------------- // Permissions effectives via role // --------------------------------------------------------------- /** * Accorde l'acces quand l'utilisateur possede la permission exacte via un role. */ public function testGrantsWhenUserHasExactPermission(): void { $permission = new Permission('core.users.view', 'Voir les utilisateurs', 'core'); $role = new Role('viewer', 'Viewer'); $role->addPermission($permission); $user = $this->buildUser(username: 'alice', isAdmin: false); $user->addRbacRole($role); $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); $result = $this->voter->vote($token, null, ['core.users.view']); $this->assertSame(VoterInterface::ACCESS_GRANTED, $result); } /** * Refuse l'acces quand l'utilisateur possede une permission differente de * celle demandee. */ public function testDeniesWhenUserLacksPermission(): void { $permission = new Permission('core.users.view', 'Voir les utilisateurs', 'core'); $role = new Role('viewer', 'Viewer'); $role->addPermission($permission); $user = $this->buildUser(username: 'alice', isAdmin: false); $user->addRbacRole($role); $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); // L'utilisateur a core.users.view mais pas core.roles.manage. $result = $this->voter->vote($token, null, ['core.roles.manage']); $this->assertSame(VoterInterface::ACCESS_DENIED, $result); } // --------------------------------------------------------------- // Permissions directes (hors roles) // --------------------------------------------------------------- /** * Accorde l'acces via une permission directe (assignee sans passer par un role). */ public function testGrantsForDirectPermission(): void { $permission = new Permission('core.users.view', 'Voir les utilisateurs', 'core'); $user = $this->buildUser(username: 'bob', isAdmin: false); $user->addDirectPermission($permission); $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); $result = $this->voter->vote($token, null, ['core.users.view']); $this->assertSame(VoterInterface::ACCESS_GRANTED, $result); } // --------------------------------------------------------------- // Helpers // --------------------------------------------------------------- /** * Construit un User metier minimal sans persistance. */ private function buildUser(string $username, bool $isAdmin): User { $user = new User(); $user->setUsername($username); $user->setIsAdmin($isAdmin); // Mot de passe factice pour satisfaire PasswordAuthenticatedUserInterface. $user->setPassword('hashed_placeholder'); return $user; } }