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 :
# ...
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é :
# ...
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 :
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 :
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.