Qui demande vraiment : usurpation et délégation

Deux situations partagent une même forme sous des noms opposés : un administrateur qui usurpe l’identité d’un utilisateur, lequel ne lui a rien accordé, et un agent qu’un utilisateur a autorisé à agir pour lui. Un seul mot couvre les deux.

C’est le mot qu’emploie la RFC 8693 : le demandeur est celui pour qui l’accès est demandé, l’acteur est celui qui demande vraiment.

Le dire dans votre propre application

use AccessControl\Requester\DelegatedRequesterInterface;

final readonly class ActingFor implements DelegatedRequesterInterface
{
    public function __construct(
        private User $onBehalfOf,
        private User $agent,
    ) {
    }

    public function getActor(): mixed
    {
        return $this->agent;
    }
}

Actor::of($requester) rend l’acteur, ou null quand personne d’autre n’agit, ce qui est le cas ordinaire. Actor::isActedFor($requester) répond à la même question sous forme de booléen, et c’est ce qui rend IS_IMPERSONATOR décidable là où il n’y a aucun jeton.

Un contrat sur le demandeur, pas une enveloppe autour de lui

La conception évidente serait un décorateur : un objet demandeur délégué qui contient le vrai. Elle a été écartée, et la raison vaut d’être empruntée.

Envelopper cache le demandeur à tout votant qui lit son type. Votre PostVoter teste instanceof User, l’enveloppe n’en est pas un, et le votant s’abstient : une règle de propriété cesse silencieusement de s’appliquer le jour où quelqu’un emploie l’usurpation d’identité. Sous forme d’interface sur le demandeur lui-même, le type reste ce qu’il était, et les votants que la délégation n’intéresse pas n’apprennent jamais le mot.

L’usurpation de Symfony, comprise gratuitement

Une application qui emploie le changement d’utilisateur de Security n’implémente rien. Actor::of() reconnaît un SwitchUserToken et rend son jeton d’origine, si bien qu’IS_IMPERSONATOR y trouve sa réponse et que toute expression nommant actor reçoit l’administrateur derrière l’usurpation.

La référence est souple, comme partout ici : l’instanceof répond false plutôt que de lever quand symfony/security-core est absent, donc le composant tient debout seul sans clause de garde.

Une seule classe connaît les deux façons de le dire, et c’est la seule. Demander à chaque votant de connaître les deux serait la même règle écrite deux fois, libre de diverger, ce qui est exactement la maladie contre laquelle ce composant a été écrit.

Ce que cela vous apporte dans une règle

« Un administrateur, même pendant qu’il usurpe l’identité de quelqu’un d’autre » est autrement inexprimable sans typer contre un jeton Security. Avec un acteur publié en variable d’expression seulement quand il y en a un, cela tient en une ligne :

#[AccessPolicy(new Expression('is_granted("ROLE_ADMIN") or actor.isAdministrator()'))]

Le sens de la délégation est la même forme lue dans l’autre sens : une règle qui refuse à un agent quelque chose que la personne pour qui il agit pourrait faire elle-même. Les deux sont le même champ, et la différence entre elles relève de la politique de votre application, pas de celle du composant.

Ce que le composant ne modélise pas, c’est la portée d’une délégation, une notion que Symfony ne modélise nulle part non plus, et que rien ici ne vous empêche d’ajouter sous forme d’attribut et de votant à vous.