Le demandeur est celui qui demande, et il est typé mixed. Ce seul choix est ce qui permet à ce composant de décider sans jeton, sans utilisateur et sans pare-feu.
$accessControlManager->decide(new AccessRequest($token, 'EDIT', $post));
$accessControlManager->decide(new AccessRequest($apiKey, 'EDIT', $post));
$accessControlManager->decide(new AccessRequest($serviceAccount, 'EDIT', $post));
$accessControlManager->decide(new AccessRequest(null, 'PUBLIC_ACCESS'));
Un votant qui ne comprend pas un demandeur s’abstient. Rien ne lève, rien ne refuse par accident, et une requête venue d’un worker de fond traverse les mêmes votants qu’une requête venue d’un navigateur.
Les cas que cela vise
- Une commande de console, où il n’y a pas de jeton parce qu’il n’y a pas de session, et où l’identité est un compte de service que l’application nomme.
- Un appel de machine à machine authentifié par une clé d’API, où inventer un objet utilisateur pour satisfaire un type serait une fiction.
- Un consommateur de messages, qui agit pour l’utilisateur ayant mis le message en file des heures plus tôt.
- Une application sans Security du tout, qui est ici une forme prise en charge et non une expérience de pensée.
Des rôles sans Symfony Security
Le votant de rôles lit les rôles d’un jeton Symfony quand il en reçoit un. Sinon, il les lit sur tout demandeur qui implémente le contrat propre à ce composant :
use AccessControl\Voter\RBAC\UserWithRoleInterface;
final readonly class ServiceAccount implements UserWithRoleInterface
{
public function getRoles(): array
{
return ['ROLE_IMPORTER'];
}
}
Un objet qui porte simplement une méthode getRoles() convient aussi, sans rien implémenter, ce qui suffit à faire fonctionner telles quelles la plupart des classes utilisateur existantes.
Ce contrat appartient délibérément au composant. La notion de rôle est censée quitter Security, donc rien ici ne type contre l’interface de Security, et une application peut porter des rôles sans porter de pile de sécurité.
La hiérarchie s’applique toujours
Les rôles sont développés par la hiérarchie de rôles avant d’être comparés, si bien que porter ROLE_ADMIN répond à une question sur ROLE_USER quand la hiérarchie le dit. Déclarez-la sous access_control.role_hierarchy quand l’application n’a pas Security :
access_control:
role_hierarchy:
ROLE_ADMIN: [ROLE_USER]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
Avec SecurityBundle installé, gardez-la dans security.yaml et ne déclarez rien ici. La passerelle pointe le service de ce composant sur la hiérarchie de Security, de sorte qu’un seul arbre sert les deux piles, et une application qui a remplacé ce service est honorée ici aussi. La déclarer sur les deux clés lève à la compilation plutôt que d’en choisir une.
L’adaptation va dans ce sens exprès : placer l’implémentation de ce composant derrière security.role_hierarchy casserait toute application typée contre l’interface de Security, que ce composant n’implémente pas et ne doit pas implémenter.
Deux vocabulaires, deux votants
Un nom de rôle et un état d’authentification ne sont pas la même sorte de chose, et ils sont lus par des votants différents. 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.
D’où la question à laquelle répond l’article suivant : comment décider quelque chose comme IS_IMPERSONATOR quand il n’y a pas de jeton où le lire ?