Vos votants Symfony Security continuent de fonctionner

Toute application Symfony a des votants qui étendent la classe Voter de Security. Ils continuent d’être consultés, sans modification, et c’est sur cette seule pièce que repose toute la migration.

Un votant écrit pour Symfony\Component\Security\Core\Authorization\Voter\VoterInterface est enveloppé par un adaptateur et interrogé exactement comme avant. Le bundle les câble tous pour vous, et rien dans ces fichiers n’apprend que quoi que ce soit a changé.

Pourquoi l’adaptateur n’est pas facultatif

Sans lui, les votants applicatifs cesseraient simplement d’être consultés le jour où le gestionnaire de décision est pointé sur ce composant. Pas d’erreur, pas de dépréciation, pas de test en échec : juste des règles d’accès qui ne s’appliquent plus, en silence.

C’est la panne contre laquelle tout ce composant est bâti, et elle aurait été livrée comme une fonctionnalité. L’adaptateur existe donc, et CacheableVoterInterface est honoré avec lui : supportsAttribute() et supportsType() reçoivent les mêmes chaînes des deux piles, si bien qu’un votant qui était mis en cache avant l’est toujours.

En réécrire un, quand vous le voulez

// Before
final class PostVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, ['EDIT', 'DELETE'], true) && $subject instanceof Post;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        return $subject->author === $token->getUser();
    }
}
// After
final readonly class PostVoter implements VoterInterface
{
    public function supportsAttribute(mixed $attribute): bool
    {
        return in_array($attribute, ['EDIT', 'DELETE'], true);
    }

    public function supportsSubject(mixed $subject): bool
    {
        return $subject instanceof Post;
    }

    public function vote(AccessRequest $accessRequest): AccessOutcome
    {
        $requester = $accessRequest->requester;

        if (! $requester instanceof User) {
            return AccessOutcome::abstain('This voter only knows about users.');
        }

        return $accessRequest->subject->author === $requester
            ? AccessOutcome::grant('The requester is the author.')
            : AccessOutcome::deny('Only the author may edit this post.');
    }
}

Trois différences à nommer

  • supports() se scinde en deux, pour que les deux réponses soient mises en cache indépendamment.
  • Un booléen devient un résultat à trois valeurs. Renvoyer false depuis voteOnAttribute() signifiait refuser ; ici vous choisissez entre refuser et vous abstenir, et la différence est réelle.
  • Le demandeur n’est pas un jeton. Vérifiez le type dont vous avez besoin et abstenez-vous sinon, ce qui rend aussi le votant utilisable depuis une console ou un worker.

Et donnez une raison à chaque résultat. C’est la seule chose qui survive jusqu’au panneau, au journal et aux assertions.

La correspondance, d’un coup d’œil

  • affirmative, unanimous, consensus, priority deviennent permit_overrides, deny_overrides, majority, first_applicable.
  • security.access_control devient access_control.rules, avec une portée plus large.
  • security.role_hierarchy devient access_control.role_hierarchy, et reste où elle est tant que Security est installé.
  • Deux attributs #[IsGranted] deviennent un All, qui énonce la conjonction à voix haute.
  • allow_if_all_abstain passe de chaque stratégie au gestionnaire.

Déplacez, ne copiez pas : déclarer la même chose sur les deux clés lève à la compilation, et c’est le bon cas. Celui qui ne lève pas, c’est une règle d’URL déplacée alors que l’originale reste, où les deux s’appliquent alors et où vous obtenez l’intersection. La page de migration parcourt tout cela, pièce par pièce.