Permissions avancées avec les Voter — Formation Symfony 7

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("https://grafikart.fr/", 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.