Dans ce chapitre nous allons revenir sur l'aspect sécurité et voir comment gérer des permissions plus fines qu'un simple système de rôle. Pour cela on va se reposer sur l'utilisation de Voters qui permettent de juger de l'accès de l'utilisateur à certaines opérations.
Exemple
Par exemple, on permet à tout le monde de lister ses propres recettes, mais il faut être l'auteur d'une recette pour l'éditer.
<?php
namespace App\Security\Voter;
use App\Entity\Recipe;
use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class RecipeVoter extends Voter
{
public const EDIT = 'RECIPE_EDIT';
public const DELETE = 'RECIPE_DELETE';
public const VIEW = 'RECIPE_VIEW';
public const CREATE = 'RECIPE_CREATE';
public const LIST = 'RECIPE_LIST';
public const LIST_ALL = 'RECIPE_ALL';
protected function supports(string $attribute, mixed $subject): bool
{
return
in_array($attribute, [self::CREATE, self::LIST, self::LIST_ALL]) ||
(
in_array($attribute, [self::EDIT, self::VIEW])
&& $subject instanceof \App\Entity\Recipe
);
}
/**
* @param Recipe|null $subject
*/
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof User) {
return false;
}
switch ($attribute) {
case self::EDIT:
case self::DELETE:
return $subject->getUser()->getId() === $user->getId();
break;
case self::LIST:
case self::CREATE:
case self::VIEW:
return true;
break;
}
return false;
}
}
Ensuite, dans mon Controller, je peux utiliser l'attribut IsGranted
pour valider le niveau de permission de l'utilisateur.
#[Route('/', name: 'index')]
#[IsGranted(RecipeVoter::LIST)]
public function index(Security $security): Response
{
$page = $request->query->getInt('page', 1);
$userId = $security->getUser()->getId();
$canListAll = $security->isGranted(RecipeVoter::LIST_ALL);
// On limite la liste des recettes à celle de l'utilisateur si il n'a pas les permissions de tout voir
$recipes = $repository->paginateRecipes($page, $canListAll ? null : $userId);
// ...
}
#[Route('/create', name: 'create')]
#[IsGranted(RecipeVoter::CREATE)]
public function create(Request $request): Response
{
}
#[Route('/{id}', name: 'edit', methods: ['GET', 'POST'], requirements: ['id' => Requirement::DIGITS])]
#[IsGranted(RecipeVoter::EDIT, subject: 'recipe')]
public function edit(Recipe $recipe, Request $request): Response
{
}
#[Route('/{id}', name: 'delete', methods: ['DELETE'], requirements: ['id' => Requirement::DIGITS])]
#[IsGranted(RecipeVoter::DELETE, subject: 'recipe')]
public function remove(Recipe $recipe)
{
}
Super admin
Par défaut le système est affirmative
, il suffit d'un seul voter qui vote "oui" pour donner l'accès à l'utilisateur à un système. Aussi, on peut créer un voter basé sur le rôle qui répondra "oui" à tout si l'utilisateur a le rôle administrateur.
<?php
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class AdminVoter extends Voter
{
protected function supports(string $attribute, mixed $subject): bool
{
return true;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
return in_array('ROLE_ADMIN', $user->getRoles());
}
}
Cela permet de ne pas polluer les autres voters tout en créant une règle qui outrepasse toutes les autres.