Skip to content
Retour au journal3 min de lecture

Publié le 3 septembre 2026

Du rôle global au voter : quand la hiérarchie des rôles ne suffit plus

Un signalement venu du terrain, aucune erreur applicative, et pourtant un modèle de droits faux. Retour technique sur le passage d'un contrôle d'accès par rôles à une autorisation contextuelle avec les voters Symfony.

SymfonySécuritéVotersContrôle d'accèsMigration

Pendant la migration d'un logiciel de gestion interne vers Symfony 7, le problème le plus intéressant n'est pas remonté d'un test. Il est remonté d'un responsable de site, qui a signalé que des informations relevant de son périmètre étaient visibles par des équipes rattachées à d'autres sites.

Aucune exception, aucun log d'erreur, aucune alerte. L'application faisait exactement ce qu'on lui avait demandé. C'est précisément ce qui rend ce type de défaut coûteux : il n'y a rien à corriger dans le code, il y a un modèle à revoir.

Ce qu'une hiérarchie de rôles sait faire

Un contrôle d'accès par rôles répond à une seule question : qui es-tu ?

# config/packages/security.yaml
role_hierarchy:
  ROLE_MANAGER: [ROLE_USER]
  ROLE_ADMIN: [ROLE_MANAGER]
#[IsGranted('ROLE_MANAGER')]
public function collectes(): Response
{
    // Tous les responsables voient cet écran.
}

C'est lisible, c'est rapide, et ça marche tant que l'organisation est plate. Le jour où la même fonction existe sur plusieurs sites, la question posée par le code n'est plus la bonne. ROLE_MANAGER dit que l'utilisateur est responsable. Il ne dit pas de quoi il est responsable.

La question à poser à la place

La bonne question n'est pas « quel est ton rôle ? » mais « as-tu le droit sur cette ressource-là ? ». Elle dépend de deux choses à la fois : l'utilisateur et l'objet consulté. Dans Symfony, c'est exactement le contrat d'un voter.

final class CollecteVoter extends Voter
{
    public const VIEW = 'COLLECTE_VIEW';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return $attribute === self::VIEW && $subject instanceof Collecte;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token,
    ): bool {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        // La décision dépend de la ressource, pas seulement du rôle porté.
        return $user->getSite() === $subject->getSite();
    }
}

Le contrôleur ne change presque pas — mais il ne pose plus la même question :

#[IsGranted(CollecteVoter::VIEW, subject: 'collecte')]
public function show(Collecte $collecte): Response
{
    // Ne s'ouvre que pour les collectes du site de l'utilisateur.
}

Le gain n'est pas syntaxique. La règle d'autorisation quitte les contrôleurs et les templates, où elle était dispersée et recopiée, pour vivre dans une classe qu'on peut lire, tester et faire évoluer seule.

Ne pas poser de rustine sur une version qui va disparaître

La tentation, face à un signalement de ce type, est de filtrer la requête sur l'écran concerné et de passer à autre chose. Nous ne l'avons pas fait : la version existante a été figée, ses évolutions limitées, et la refonte du modèle de droits portée par la migration elle-même.

Un filtre ajouté à la main sur un écran ne corrige qu'un symptôme, et il se recopie mal. Le même défaut réapparaît sur le prochain écran qui interroge la même donnée.

Ce que ça change

Une hiérarchie de rôles décrit une organisation à un instant donné. Elle vieillit avec elle. Une autorisation contextuelle décrit une règle : on ne voit que ce qui relève de son périmètre — et cette règle-là survit aux réorganisations.

Le déclencheur, ici, n'était ni un test, ni une revue de code, ni un outil d'analyse statique : c'était quelqu'un qui utilise le logiciel tous les jours et qui a trouvé que ce qu'il voyait à l'écran ne correspondait pas à son travail. Sur un logiciel métier, c'est souvent la meilleure source de bugs.

Le récit non technique de ce chantier est publié sur le site de l'ÉSTIAM.

/ Contact

Un logiciel à concevoir, déployer, opérer ?

Écrivez-moi. Je réponds sous 48 h.