Un votant répond à une seule question : étant donné cette requête, accordez-vous, refusez-vous, ou n’avez-vous rien à dire ? L’interface a trois méthodes, et ce qui compte le plus est la chaîne que vous joignez à votre réponse.
interface VoterInterface
{
public function vote(AccessRequest $accessRequest): AccessOutcome;
public function supportsAttribute(mixed $attribute): bool;
public function supportsSubject(mixed $subject): bool;
}
Un votant complet, dans une application où Access Control est installé :
final readonly class PostVoter implements VoterInterface
{
public function vote(AccessRequest $accessRequest): AccessOutcome
{
$post = $accessRequest->subject;
$requester = $accessRequest->requester;
if (! $requester instanceof User) {
return AccessOutcome::abstain('This voter only knows about users.');
}
return $post->author === $requester
? AccessOutcome::grant('The requester is the author.')
: AccessOutcome::deny('Only the author may edit this post.');
}
public function supportsAttribute(mixed $attribute): bool
{
return in_array($attribute, ['EDIT', 'DELETE'], true);
}
public function supportsSubject(mixed $subject): bool
{
return $subject instanceof Post;
}
}
Dans une application Symfony, il n’y a rien de plus à faire. Implémenter l’interface suffit : l’autoconfiguration étiquette le service access_control.voter, et bin/console debug:container --tag=access_control.voter liste ceux qui ont répondu.
Les deux méthodes supports sont un filtre, pas une réponse
Elles existent pour que le gestionnaire puisse sauter un votant sans construire la question, et leurs réponses sont mises en cache par attribut et par type de sujet. Ce n’est pas là que va la décision.
Renvoyer true des deux et s’abstenir dans vote() est correct, et parfois la seule option : supportsSubject() reçoit le sujet, pas la requête, donc un votant dont l’applicabilité dépend du demandeur doit revendiquer le sujet ici et s’abstenir là-bas.
Séparer les deux, là où Symfony Security n’a qu’un seul supports(), est ce qui permet de mettre les deux réponses en cache indépendamment : un attribut est une chaîne, un type de sujet est un nom de classe, et ils ne changent pas au même rythme.
Donnez toujours une raison
La raison est la seule chose qui survive à la décision. C’est ce qu’affiche le panneau du profileur, ce sur quoi une assertion de test s’appuie, et ce qui dira à un développeur, six mois plus tard, pourquoi la porte était fermée.
Une raison n’est pas un message pour l’utilisateur final. Un refus qui atteint une réponse HTTP est un 403 nu : le diagnostic est délibérément retiré de la réponse et mis à disposition du profileur, du journal et des tests. Si vous voulez que le visiteur lise quelque chose, dites-le avec le message d’une politique d’accès, qui est un autre champ pour un autre public.
Écrivez-la comme une phrase sur cette requête, pas comme une étiquette. « The requester is the author » vaut d’être lu dans un panneau ; DENIED non.
Les votants fournis
- Le votant de rôles traite les attributs qui commencent par le préfixe de rôle,
ROLE_par défaut, face aux rôles que porte le demandeur, développés par la hiérarchie de rôles. - Le votant d’authentification traite les six états d’authentification :
IS_AUTHENTICATED_FULLY,IS_AUTHENTICATED_REMEMBERED,IS_AUTHENTICATED,IS_IMPERSONATOR,IS_REMEMBEREDetPUBLIC_ACCESS. - Le votant de fermeture prend un attribut donné sous forme de
Closureet l’appelle avec la requête. - Le votant d’expression prend un attribut donné sous forme d’
Expression, lorsquesymfony/expression-languageest installé.
Un nom de rôle et un état d’authentification sont deux vocabulaires distincts, lus par deux votants distincts. ROLE_PREVIOUS_ADMIN est un rôle, reconnu à son préfixe ; IS_IMPERSONATOR est un état, et le votant de rôles s’abstient dessus.
La page des votants donne le contrat complet. Et si vous avez déjà des votants écrits pour Symfony Security, ils continuent de fonctionner sans modification : c’est un article entier, plus loin dans cette série.