Symfony 5 : Limiter la possibilité de « switch_user » en fonction du rôle de l’utilisateur cible.

Il existe dans Symfony une fonctionnalité permettant à certains utilisateurs de « se connecter en tant que » n’importe quel autre utilisateur (Documentation). Cette fonctionnalité est très utile pour que les administrateurs puissent se connecter sur le compte d’un client et vérifier les éventuels dysfonctionnements qui le concerne.

Cependant, dans son fonctionnement par défaut, dés que l’on attribut le rôle « ROLE_ALLOWED_TO_SWITCH » à un utilisateur il peux se connecter sur tout les autres utilisateurs existants. Cela peux s’avérer critique pour la sécurité de l’application, l’utilisateur pouvant alors gagner tous les privilèges. Concrètement si l’on prends un cas assez simple d’une application avec 3 rôles utilisateurs principaux : « ROLE_USER », « ROLE_ADMIN », « ROLE_SUPERADMIN » et que l’on définis la hiérarchie comme ceci :

security:
   # ...
    firewalls
:
        main
:
           # on active la fonctionnalité "switch_user"
            switch_user
: true  
    #
    role_hierarchy
:
        ROLE_ADMIN
:      [ROLE_USER, ROLE_ALLOWED_TO_SWITCH]
        ROLE_SUPER_ADMIN
: ROLE_ADMIN

Cela signifie que les administrateur et les superadmin pourront se connecter sur n’importe quel utilisateur. Mais cela implique que les simples administrateurs pourront devenir « superadmin » et donc avoir des droits auxquels in n’ont normalement pas accès. Nous allons voir comment limiter cela.

L’objectif est le suivant : les administrateurs « ROLE_ADMIN » peuvent se connecter sur tous les autres utilisateurs (user et administrateur) mais pas sur les superadmin. Pour les superadmin rien ne change, ils ont tous les accès.
La documentation de symfony explique la marche à suivre dans sa section limiting user switching. En suivant ce modèle on va donc modifier notre configuration sur 2 points : la propriété « switch_user » à qui on affecte un attribut « CAN_SWITCH_USER » et on supprime de notre hiérarchie le « ROLE_ALLOWED_TO_SWITCH » qui n’a plus d’utilité :

security:
   # ...
    firewalls
:
        main
:
            switch_user
: { role: CAN_SWITCH_USER }
    #
    role_hierarchy
:
        ROLE_ADMIN
:      ROLE_USER
        ROLE_SUPER_ADMIN
: ROLE_ADMIN

Pour la suite, on se base toujours sur la documentation pour créer le « Voter » correspondant, mais on se trouve confronté à un problème. On veux connaitre le rôle de l’utilisateur cible pour limiter l’accès si il est superadmin. Pour faire les choses proprement avec une hiérarchie des rôles réellement prise en charge on va utiliser notre service personnalisé « Securizer » permettant d’utiliser la méthode « isGranted » sur n’importe quel utilisateur. Des précisions sur l’utilité de ce service sont expliqué dans l’article précédent. Mais pour faire simple il suffit de créer le service suivant :

namespace App\Service;

use App\Entity\User;//l'entité user de notre aplication
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

class Securizer {

    private $accessDecisionManager;

    public function __construct(AccessDecisionManagerInterface $accessDecisionManager) {
        $this->accessDecisionManager = $accessDecisionManager;
    }

    public function isGranted(User $user, $attribute, $object = null) {
        $token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
        return ($this->accessDecisionManager->decide($token, [$attribute], $object));
    }

}

et de l’utiliser das notre Voter pour l’attribut « CAN_SWITCH_USER » qui est maintenant facile à mettre en place en adaptant l’exemple de la documentation, le code complet est le suivant :

<?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\Security;
use App\Service\Securizer;
use App\Entity\User;

class SwitchUserVoter extends Voter {

    private $security;
    private $securizer;

    public function __construct(Security $security, Securizer $securizer) {
        $this->security = $security;
        $this->securizer = $securizer;
    }

    protected function supports($attribute, $subject) {
        return in_array($attribute, ['CAN_SWITCH_USER']) && $subject instanceof User;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
        $user = $token->getUser();
        //l'utilisateur doit être connecté et la cible doit être un utilisateur
        if (!$user instanceof User || !$subject instanceof User) {
            return false;
        }
        //on ne peux pas se connecter en tant que sois même, ça n'a aucun sens
        if($user->getId() == $subject->getId()){
            return false;
        }
        //l'utilisateur doit avoir le ROLE_ADMIN
        if (!$this->security->isGranted('ROLE_ADMIN')) {
            return false;
        }
        //Impossible si je ne suis pas SUPERADMIN et que le sujet l'est
        if (!$this->security->isGranted('ROLE_SUPERADMIN') && $this->securizer->isGranted($subject, 'ROLE_SUPERADMIN')) {
            return false;
        }
        //sinon c'est ok.
        return true;
    }

}

Tout est maintenant prêt est fonctionnel. Pour résumer en appliquant la seconde configuration YAML indiqué dans cette article et en créant les fichiers correspondant aux deux portions de code suivants, la sécurité concernant le « switch_user » de notre application est en place, et facilement personnalisable.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.