Si vous souhaitez restreindre l’accès à certaines parties de votre application Symfony, vous avez plusieurs solutions.
La plus simple d’entre elles est l’utilisation des access control dans le fichier security.yml: il suffit de mettre le début de l’URL à contrôler, les rôles qui y ont accès et terminé, ça fonctionne.
1 2 |
access_control: - { path: ^/admin, role: ROLE_ADMIN } |
Résultat, si un utilisateur n’a pas l’accès à une page mais qu’il clique à un lien y menant, il se retrouvera face à une page d’erreur qui lui explique qu’il n’a pas l’autorisation de se trouver là.
Maintenant, le mieux c’est aussi de ne pas afficher les liens dans votre template Twig si l’utilisateur n’y a pas accès !
Pour ce faire, il suffit d’utiliser la fonction is_granted:
1 2 3 |
{% if is_granted('ROLE_ADMIN') %} <a href="{{ path('app_admin') }}>Admin</a> {% endif %} |
Cela fonctionne avec n’importe quel rôle.
C’est très simple et rapide à mettre en place. Oui mais voilà, si vous changez d’avis et que vous souhaitez changer le rôle qui a accès à une page (voire en rajouter un), il faudra faire la modification dans le fichier security.yml ET dans le template twig. Pas grave si vous avez une toute petite application, plus problématique quand il commence à y avoir pas mal de lien à contrôler…
Ce que je vous propose aujourd’hui, c’est de n’avoir à faire des modifications que dans le fichier security.yml sans devoir toucher le template à chaque fois.
Ça vous dit ? Alors suivez moi ! 🙂
Nous allons tout d’abord commencer par créer un service qu’on va appeler AccessControlService et qui sera placé dans le répertoire src/VotreBundle/Service :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<?php namespace AppBundle\Service; use AppBundle\Entity\User; use Symfony\Component\DependencyInjection\ContainerInterface as Container; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; use Symfony\Component\Security\Http\AccessMapInterface; /** * Class AccessControlService * @package AppBundle\Service */ class AccessControlService { protected $container; protected $accessMap; protected $authorizationChecker; /** * AccessControlService constructor. * @param Container $container * @param AccessMapInterface $access_map * @param AuthorizationChecker $authorizationChecker */ public function __construct(Container $container, AccessMapInterface $access_map, AuthorizationChecker $authorizationChecker) { $this->container = $container; $this->accessMap = $access_map; $this->authorizationChecker = $authorizationChecker; } /** * isGrantedPath * * Vérifie si un utilisateur peut accéder à une page via son path * * @param $path * @param User $user * @return bool */ public function isGrantedPath($path, User $user) { //Si l'utilisateur est admin, autorise l'accès if($user->getRole() == 'ROLE_ADMIN') return true; $authorization = false; //Créé une URL depuis le path $url = $this->container->get('router')->generate($path, array()); $url = str_replace('/app_dev.php', '', $url); //Créé un request depuis l'URL $request = Request::create($url); //Récupère les roles autorisés à afficher cette page list($roles, $channels) = $this->accessMap->getPatterns($request); //Si il n'y a pas de rôle défini, autorise l'accès if(count($roles) == 0) { $authorization = true; } else { //Parcours les rôles autorisés à afficher la page foreach($roles as $role) { //Si l'utilisateur a l'un de ces rôles, autorise l'accès if($this->authorizationChecker->isGranted($role)) { $authorization = true; break; } } } return $authorization; } } |
Ensuite, pour que celui-ci fonctionne, nous allons l’enregistrer dans la liste des services ( src/VotreBundle/Resources/config/services.yml ):
1 2 3 4 |
services: app.accesscontrol_service: class: AppBundle\Service\AccessControlService arguments: ["@service_container", "@security.access_map", "@security.authorization_checker"] |
Alors, qu’est-ce que fait ce service ?
Il va tout simplement vérifier que pour un chemin donné (via son path), un utilisateur a le droit d’y accéder. Pour cela, il récupère les différents access_control ainsi que les rôles qui ont des droits d’accès dessus grâce à l’accessMap, ensuite il va tout bêtement parcourir les différents rôles qui ont le droit d’accéder à ce path et comparer avec les rôles de l’utilisateur.
Ça, c’est fait. Maintenant comment dire à Symfony que je veux utiliser ce service à chaque fois que j’appelle la fonction is_granted dans mon template ?
Et bien en utilisant un Voter ! En gros, c’est une classe qui dit si oui ou non elle accepte de donner l’accès à une page.
Ce qu’on va faire, c’est créer un Voter qui utilise notre service précédemment créé:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
<?php namespace AppBundle\Security\Authorization\Voter; use Symfony\Component\DependencyInjection\ContainerInterface as Container; use AppBundle\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; /** * Class AccessControlVoter * @package AppBundle\Security\Authorization\Voter */ class AccessControlVoter implements VoterInterface { const ACCESS = 'access'; private $container; /** * AccessControlVoter constructor. * @param Container $container */ public function __construct(Container $container) { $this->container = $container; } /** * Vote * * Fonction qui autorise ou non l'accès à une page * * @param TokenInterface $token * @param mixed $subject * @param array $attributes * @return bool|int */ public function vote(TokenInterface $token, $subject, array $attributes) { $router = $this->container->get('router'); $user = $token->getUser(); //Vérifie si l'utilisateur est loggé if (!$user instanceof User) { return false; } //Vérifie que le sujet passé est bien une route if(!is_string($subject) OR $router->getRouteCollection()->get($subject) === null) return false; //Retourne l'accès à cette route if(!$this->canAccess($subject, $user)) return VoterInterface::ACCESS_DENIED; return VoterInterface::ACCESS_GRANTED; } /** * canAccess * * Vérifie si l'utilisateur peut accéder à une page via son path * * @param $path * @param User $user * @return bool */ private function canAccess($path, User $user) { $accessControlService = $this->container->get('app.accesscontrol_service'); return $accessControlService->isGrantedPath($path, $user); } } |
Et, tout comme pour l’AccessControlService pour que celui-ci fonctionne, nous allons l’enregistrer dans la liste des services, à la suite de l’autre ( src/VotreBundle/Resources/config/services.yml ):
1 2 3 4 5 6 7 8 9 10 11 |
services: app.accesscontrol_service: class: AppBundle\Service\AccessControlService arguments: ["@service_container", "@security.access_map", "@security.authorization_checker"] app.accesscontrol_voter: class: AppBundle\Security\Authorization\Voter\AccessControlVoter arguments: ["@service_container"] public: false tags: - { name: security.voter } |
Je n’explique pas tout dans le détail, les commentaires devraient vous aider à comprendre 🙂
Attention, je ne l’ai pas précisé mais il est indispensable que votre entité User ait la méthode getRoles() ! Sinon cela ne fonctionnera pas !
Maintenant, si la magie a opérée, dans votre template Twig il vous suffira de mettre:
1 2 3 |
{% if is_granted('access', 'app_admin') <a href="{{ path('app_admin') }}">Admin</a> {% endif %} |
Plus besoin de spécifier de rôle, c’est automatiquement calibré sur l’utilisateur en cours ! Il vous suffit juste de passer le path en paramètres et l’AccessControlService se chargera de vérifier si la route correspond à l’un des rôles de l’utilisateur (oui ça fonctionne aussi si l’utilisateur à plusieurs rôles).
Maintenant, si vous souhaitez changer le rôle qui a accès à ce path, il vous suffit de le modifier dans le security.yml, déconnecter votre utilisateur, le reconnecter et hop, les liens s’affichent/se cachent automatiquement ! C’est magique 🙂
Salut, super article. Je pensais être le seul a avoir besoin de ça.
En revanche, vous avez copié collé deux fois le fichier AccessControlService.
Il manque le Fichier AccessControlVoter.
Cependant je pense pouvoir compléter de mon coté, donc un grand merci.
Oups la boulette !
Merci de l’info, j’ai mis à jour l’article. Désolé pour cette bourde, en espérant que vous avez réussi à trouver la solution et que tout fonctionne !