Archives mensuelles : janvier 2020

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.

Symfony 5 : Utiliser la fonction « isGranted » sur n’importe quel objet utilisateur

Cet article est écrit pour la version 5 de Symfony mais à priori il devrait aussi fonctionner au moins pour les version 3 et 4 du framework

Sous Symfony la fonction permettant de savoir si l’utilisateur en train de parcourir l’application possède un certain rôle est « isGranted » elle est directement disponible à l’utilisation dans les templates twig, via les annotations, depuis le services « Symfony\Component\Security\Core\Security » ou directement dans les controllers de la façon suivante :

if ($this->isGranted('ROLE_ADMIN')) {
   //l'utilisateur à les droits admin
}

Par contre il n’existe pas de façon directe d’utiliser cette fonction sur un objet utilisateur différent de l’utilisateur courant.
Étant donnée le fonctionnement des rôles sous symfony, qui peuvent hériter des droits d’un autre rôle il ne faut pas utiliser un simple

if(in_array('ROLE_ADMIN', $user->getRoles())){
   //mauvaise pratique
}

En effet dans ce cas, si un utilisateur à le rôle ‘ROLE_SUPERADMIN’ qui hérite de ‘ROLE_ADMIN’ le contrôle ci dessus ne lui accordera pas les droits.
On imagine ici la hiérarchie des rôle suivante :

role_hierarchy:
    ROLE_ADMIN
: ROLE_USER
    ROLE_SUPERADMIN
: ROLE_ADMIN

Pour résoudre ce problème, nous allons créer la fonction « isGranted » prenant en paramètre un utilisateur et le rôle à vérifier dans un service « Securizer », je vous le livre directement prêt à utiliser :

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));
    }

}

Grâce à la configuration par défaut de Symfony on peux sans plus attendre utiliser ce service dans notre application et vérifier correctement les droits de n’importe quel utilisateur en prenant bien en compte la hiérarchie des rôles configuré dans le fichier « security.yaml ». Par exemple dans un controller on peux désormais faire :

public function monController(App\Service\SecurizerSecurizer $securizer) {
   //récupération d'un utilisateur $user de n'importe quelle manière, par exemple via une requête en base de données.
   //verification des droits
   if($securizer->isGranted($user, 'ROLE_ADMIN')){
       //l'utilisateur à les droits admin
   }
}

Il y’a sans doute de nombreux cas ou ce besoin de contrôle des rôles sur un utilisateur autre que celui connecté se fait sentir (d’où la mise en place de cette fonctionnalité sous la forme d’un service), particulièrement lors de l’utilisation de Voters pour sécuriser la gestion des droits de l’application. Dans mon cas j’ai d’abord eu besoin de cette fonctionnalité pour contrôler finement la possibilité de se « connecter en tant que » (switch_user) pour mes administrateurs.
Voir les détails dans le prochain article : Limiter la possibilité de « switch_user » en fonction du rôle de l’utilisateur cible.

Source