Les quatre algorithmes fournis couvrent les disputes habituelles entre votants. Une règle qu’ils ne savent pas exprimer tient en une classe à deux méthodes, et rien d’autre à déclarer.
interface StrategyInterface
{
public function getName(): string;
public function evaluate(AccessRequest $accessRequest, iterable $votes): AccessDecision;
}
getName() est le nom qu’une requête demande. evaluate() transforme les votes en décision. L’autoconfiguration étiquette le service access_control.strategy, si bien qu’une classe dans votre src/ est un algorithme de combinaison dès l’instant où elle existe. Elle peut ensuite être nommée partout où le sont ceux qui sont fournis : strategy: sur une politique d’accès, le second argument de decide(), ou default_strategy en configuration.
La règle des deux clés
L’application de démonstration en porte une qu’aucun des quatre n’exprime : une console de lancement, où une seule autorisation ne suffit pas et où deux clés indépendantes doivent être tournées ensemble.
Regardez pourquoi chacun des algorithmes fournis échoue à le dire. permit_overrides ouvre à la première autorisation. deny_overrides ne compte que les refus. majority pèse les deux camps l’un contre l’autre, donc deux autorisations battent un refus. first_applicable s’arrête à qui parle le premier.
if ($denials > 0) {
return AccessDecision::deny($accessRequest, $votes, 'A key was refused, and a refusal is not outvoted.');
}
if ($grants >= $this->required) {
return AccessDecision::grant($accessRequest, $votes, sprintf('%d keys were turned, %d being required.', $grants, $this->required));
}
Un refus l’emporte toujours d’emblée. Une règle de quorum qui laisserait deux autorisations battre un veto serait une règle de majorité sous un autre nom, et le veto est précisément la raison pour laquelle on demande une seconde clé.
Une clé de moins n’est pas du silence
La fin de cette méthode est la partie qui vaut d’être copiée. Personne ne s’est opposé et une clé a été tournée : c’est un refus, parce que le quorum n’est pas atteint. Personne n’a rien dit du tout : c’est une abstention, et le gestionnaire tranche selon allowIfAllAbstain.
if (0 === $grants) {
return AccessDecision::abstain($accessRequest, $votes, 'No voter had anything to say.');
}
return AccessDecision::deny($accessRequest, $votes, sprintf('Only %d of the %d required keys were turned.', $grants, $this->required));
Fondre ces deux cas en un seul refus marcherait, et coûterait au panneau son explication. Les chaînes de raison ne sont pas décoratives : c’est ce qu’un développeur lit six mois plus tard quand un lancement est refusé et que personne ne se souvient pourquoi deux clés étaient exigées.
Ce qu’une stratégie reçoit
On lui remet la requête et un itérable de CastVote, chacun appariant un votant avec son résultat. Une stratégie n’a besoin que du résultat ; garder la paire est ce qui permet au profileur de nommer qui a dit quoi, et à un test de s’appuyer sur le votant plutôt que sur le seul verdict.
Deux stratégies enregistrées sous le même nom lèvent à la construction du gestionnaire, et nommer un défaut que rien n’enregistre fait de même. Et pour tester la vôtre, AccessDecisionStrategyTestTrait lui fait traverser un tableau de résultats de votants, ce qui est la façon dont les quatre fournis sont vérifiés en parité avec Symfony Security.