L’autorisation sans framework

La bibliothèque tient en un objet : il interroge les votants, remet leurs résultats à un algorithme de combinaison, et rend une décision. Pas de conteneur, pas de framework, pas de pile d’authentification.

use AccessControl\AccessControlManager;
use AccessControl\AccessRequest;
use AccessControl\Strategy\DenyOverridesStrategy;
use AccessControl\Strategy\PermitOverridesStrategy;

$accessControlManager = new AccessControlManager(
    strategies: [new PermitOverridesStrategy(), new DenyOverridesStrategy()],
    voters: [new PostVoter(), new RoleVoter()],
    defaultStrategy: 'permit_overrides',
);

$decision = $accessControlManager->decide(new AccessRequest($user, 'EDIT', $post));

if (! $decision->isGranted()) {
    throw new AccessDeniedException($decision->reason);
}

Voilà une couche d’autorisation complète, dans un fichier qui demande composer require spomky-labs/access-control-lib et rien d’autre. Dans une application Symfony, le bundle construit le même objet pour vous et vous injectez AccessControlManagerInterface.

Les cinq arguments du constructeur

  • strategies : les algorithmes de combinaison à rendre disponibles, par nom. Laissé vide, permit_overrides est enregistré tout seul, de sorte que le gestionnaire n’en manque jamais.
  • voters : les votants à consulter, dans l’ordre. L’ordre ne porte de sens que sous first_applicable.
  • defaultStrategy : l’algorithme appliqué quand une requête n’en nomme aucun. En nommer un qui n’est pas enregistré lève.
  • dispatcher : facultatif. Quand on lui en donne un, le gestionnaire annonce chaque question, chaque vote et chaque décision.
  • allowIfAllAbstain : quoi répondre quand aucun votant n’avait quelque chose à dire. Vaut false par défaut.

Deux stratégies qui partagent un nom lèvent une InvalidStrategyException plutôt que de laisser l’une gagner en silence. C’est la forme de la plupart des décisions de ce composant : une contradiction arrête le programme là où elle est encore peu coûteuse à corriger.

Poser une question

public function decide(AccessRequest $accessRequest, ?string $strategy = null): AccessDecision;

Le second argument passe outre l’algorithme par défaut pour cette seule question, ce qui permet à un point de terminaison d’exiger l’unanimité dans une application par ailleurs permissive.

Ce que sont les dépendances, et ce qu’elles ne sont pas

La bibliothèque demande PHP 8.4 et symfony/event-dispatcher-contracts. C’est toute la liste. Tout le reste est facultatif, et l’absence d’un paquet facultatif n’est jamais une erreur :

  • symfony/security-core pour réemployer les votants, les jetons et la hiérarchie de rôles de Symfony Security.
  • symfony/expression-language pour les attributs et les conditions donnés sous forme d’expression.
  • twig/twig pour les fonctions de gabarit.
  • symfony/workflow pour garder des transitions, et symfony/console pour garder des commandes.

Il n’y a pas de HTTP dans cette liste. La bibliothèque n’exige pas HttpFoundation, et c’est pourquoi elle se pose dans un worker, un outil en ligne de commande, une application ancienne qui gère ses sessions à sa façon, ou un framework qui n’est pas Symfony du tout.

Regarder ce qui se passe

Donnez un répartiteur d’événements au gestionnaire et il annonce la question posée, le votant qui répond, la décision atteinte, et chaque nœud d’une politique d’accès évalué. Une décision prise pendant qu’une autre l’était porte sa parente, si bien que ce qui en sort est un arbre plutôt qu’une liste de réponses sans rapport.

C’est ce qui alimente le panneau du profileur dans une application Symfony, et c’est tout autant ce qui permet à une application PHP nue de collecter le même journal et de l’affirmer dans ses tests. La page du gestionnaire de la documentation parcourt l’objet entier.