Accordez des droits avec Access Control
Nous avons des utilisateurs, et nous savons les authentifier.
C’est un bon début, mais rappelez-vous de la demande d’Amélie :
J’ai l’impression que pour l’instant tout le monde peut accéder aux formulaires et changer le contenu des données ! Pourriez-vous y remédier ?
Sa demande est en fait la suivante : restreindre l’accès des formulaires d’administration aux seuls utilisateurs connectés. Pour ce faire, nous allons donc devoir mettre en place la deuxième partie du mécanisme de sécurité présenté au début de cette partie : l’Autorisation.
Nous allons demander à vérifier des conditions pour que l’application réponde à des requêtes spécifiques. Ces conditions peuvent être multiples et très variées, mais nous y répondrons toujours par un mécanisme simple : les Voters.
Comment faire, je dois les appeler moi-même ?
Pas besoin. À chaque fois que vous voudrez demander une vérification, vous appellerez une méthode nommée isGranted
.
Utiliser la méthode isGranted
Vous allez appeler cette méthode là où vous souhaitez faire une vérification de sécurité. Vous lui dites ce que vous souhaitez vérifier, et éventuellement sur quel objet doit s'effectuer la vérification.
Dans l'ordre :
Vous posez une question, par exemple “Est-ce que l’utilisateur a le droit d’afficher cette liste ?”.
La méthode
isGranted
appelle tous les Voters enregistrés de l’application, et leur demande à chacun l'un après l'autre “Qu’est-ce que tu réponds à la question ?”.Chaque Voter pourra à son tour s’abstenir (“Je ne sais pas répondre à cette question”), donner l’accès ou le refuser.
Comprendre le fonctionnement du contrôle d'accès
Pour mieux comprendre, un exemple en situation.
Vous souhaitez que seul l’utilisateur qui a créé un livre (désigné par la variable $book
) puisse le modifier ou le supprimer. Vous allez donc demander :
<?php
if isGranted(‘book.is_creator’, $book)
then [modifier ou effacer l'objet $book]
Symfony va alors rassembler tous les Voters, et les appeler un par un pour leur demander ce qu’ils en pensent.
Demandez un contrôle d’accès
Comme nous l’avons vu, pour demander un contrôle d’accès, il existe une méthode universelle appelée isGranted
. Cependant, nous n’appellerons pas cette méthode de la même manière suivant l’endroit où nous nous trouvons dans notre code. Nous avons même des variantes, suivant que vous cherchiez à obtenir une information ou à refuser un accès.
Endroit de la demande | Demande de contrôle |
Dans une classe de controller |
|
Dans une autre classe | Injecter dans le
|
Dans un template | Utiliser la fonction |
Les vérifications les plus simples que vous pouvez demander par cette méthode sont de deux types différents :
Vous pouvez vérifier si un utilisateur est connecté.
Vous pouvez vérifier si un utilisateur a un rôle spécifique.
Pour vérifier si un utilisateur est connecté, vous pouvez passer à isGranted
cinq chaînes de caractères spécifiques :
Chaîne de caractères | Signification |
IS_AUTHENTICATED | Vérifie qu’un utilisateur est connecté, peu importe son origine |
IS_AUTHENTICATED_REMEBERED | Similaire à IS_AUTHENTICATED |
IS_AUTHENTICATED_FULLY | Ne laisse passer que les utilisateurs qui se sont connectés activement au cours de cette session |
IS_REMEMBERED | Ne laisse passer que les utilisateurs connectés depuis une session de cookie “remember me” |
PUBLIC_ACCESS | Aucune restriction |
Ainsi, si vous voulez restreindre un accès aux seuls utilisateurs connectés, quelle que soit leur origine, vous pouvez le faire de cette manière :
Donc on peut s’assurer qu’un utilisateur est connecté, mais c’est quoi ces rôles dont on parlait plus tôt, et à quoi ça sert ?
Pour vous répondre, une vidéo sera plus parlante :
Un rôle est simplement une chaîne de caractères en majuscules et qui commence par
ROLE_
.Les rôles sont stockés sur les utilisateurs dans une propriété
roles
, qui est un array de chaînes de caractères.Dans vos appels à
isGranted
, vous pouvez demander ce que vous voulez comme rôle. Si un utilisateur possède le rôle, il peut passer. Si aucun utilisateur n'a ce rôle dans sa propriétéroles
, personne ne pourra passer le contrôle d'accès.
Personnalisez les droits d’accès
La hiérarchie des rôles
Mais du coup, si je veux découper mes autorisations, et avoir par exemple un rôle ROLE_AJOUT_DE_LIVRE
, un rôle ROLE_EDITION_DE_LIVRE
et beaucoup d’autres rôles, mais que je veux que mes admins aient tous ces rôles-là, je suis obligé de les ajouter un par un sur mes utilisateurs ?
Non plus. Ouvrez le fichier config/packages/security.yaml
. Dedans, en dessous d’une section ( password_hashers
, par exemple), vous pouvez ajouter une nouvelle section comme suit :
:
# …
:
: ~
: ROLE_USER
: ROLE_USER
: ROLE_AJOUT_DE_LIVRE
: [ROLE_MODERATEUR, ROLE_EDITION_DE_LIVRE]
La section role_hierarchy
vous permet, comme son nom l’indique, de définir une hiérarchie entre vos rôles. Concrètement, ici, nous avons indiqué que le rôle user ne contient aucun autre rôle ( ROLE_USER: ~
), que le rôle de modérateur et celui permettant l’ajout de livre contiennent le rôle user automatiquement, le rôle d’édition de livre contient automatiquement le rôle d’ajout de livre (et donc le rôle user), et le role admin contient automatiquement le rôle d’édition de livre et le rôle modérateur (et donc le rôle d’ajout de livre, et donc le rôle user). Mis sous forme graphique, on obtient donc ceci :
Désormais, si par exemple dans un controller vous demandez :
<?php
if ($this->isGranted(‘ROLE_AJOUT_DE_LIVRE’) {
//…
}
les utilisateurs qui ont le rôle ROLE_ADMIN
pourront passer, même si vous ne leur avez pas explicitement ajouté le rôle ROLE_AJOUT_DE_LIVRE
en base de données.
Les Voters personnalisés
Parfois cependant, ce n’est pas suffisant. Prenons un exemple classique : imaginons que nous ayons ajouté dans notre entité Book
une propriété createdBy
qui contient l’objet User
qui représente l’utilisateur qui a enregistré le livre en base de données. Sa définition serait la suivante :
<?php
// src/Entity/Book.php
// …
#[ORM\Entity(repositoryClass: BookRepository::class)]
class Book
{
// …
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?User $createdBy = null;
// …
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
public function setCreatedBy(?User $createdBy): static
{
$this->createdBy = $createdBy;
return $this;
}
}
Si nous voulons restreindre la possibilité d’éditer un livre à la seule personne qui l’a créé, dans src/Controller/Admin/BookController.php
nous allons ajouter la condition suivante dans la méthode new
:
<?php
#[Route('/new', name: 'app_admin_book_new', methods: ['GET', 'POST'])]
#[Route('/{id}/edit', name: 'app_admin_book_edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
public function new(?Book $book, Request $request, EntityManagerInterface $manager): Response
{
// Si nous avons un objet book, nous sommes sur la page d'édition
if ($book) {
$this->denyAccessUnlessGranted('book.is_creator', $book);
}
// …
Ici nous ne demandons pas ni un rôle, ni une information de connexion, mais book.is_creator
. La question de sécurité pourrait se traduire par “Est-ce que l’utilisateur qui a créé $book
est celui qui est connecté ?”. Ce n’est pas quelque chose que Symfony reconnaît nativement. Tous les Voters vont être appelés pour essayer de répondre à la question, et ils vont tous s’abstenir. Dans ces cas-là, par défaut, Symfony refuse l’accès (ouf !). Mais comment faire pour que l’utilisateur qui a créé l’objet Book puisse passer ?
Nous allons créer un Voter. Et pour cela, il n’y a qu’une seule chose obligatoire : implémenter l’interface Symfony\Component\Security\Core\Authorization\Voter\VoterInterface
. Cependant, nous pouvons à la place choisir d’étendre la classe abstraite Symfony\Component\Security\Core\Authorization\Voter\Voter
, et ce sera généralement plus simple. Celle-ci implémente déjà l’interface et nous offre deux méthodes à implémenter obligatoirement : supports
et voteOnAttribute
:
La première a la signature suivante :
supports(string $attribute, mixed $subject): bool
. Elle reçoit l’attribut qui a été passé en premier argument àisGranted
ainsi qu’un éventuel second argument, et renvoietrue
si notre Voter peut prendre une décision,false
s'il s’abstient.La signature de la seconde est la suivante :
voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool)
. Elle reçoit aussi l’attribut, le sujet éventuel, et l’objetTokenInterface
contenant notre utilisateur. Elle renvoie ensuitetrue
pour autoriser l’accès,false
pour le refuser.
Ces deux méthodes sont abstraites, ce qui signifie que vous devez absolument les implémenter.
Créez des Voters personnalisés pour tous vos besoins d'autorisation spécifiques.
Organisez votre code en mettant vos classes dans des dossiers sémantiques (des dossiers dont le nom évoque la fonction des classes qu'ils contiennent).
Vérifiez systématiquement que l'utilisateur que vous récupérez n'est pas
null
.
Restreignez des schémas de route grâce à la configuration
En plus de ce système de Voters et de toutes les méthodes IsGranted
, Symfony dispose d’un mécanisme permettant de restreindre un grand nombre de routes très facilement et rapidement. Ouvrez à nouveau config/packages/security.yaml
et regardez en bas du fichier, en dessous des firewalls :
:
# …
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
Première chose importante, regardez le commentaire au-dessus de la section access_control
, et plus particulièrement la partie Note:
. Ici, on nous apprend que Symfony va essayer nos règles les unes après les autres, et s’arrêtera avec la première qui correspond. D’accord, mais qui correspond à quoi ?
Cette section est en fait un array, dans lequel chaque ligne est construite de la même manière : nous allons renseigner un chemin sous forme d’expression régulière, puis nous détaillons les règles qui s’appliquent sur les routes qui correspondent à ce chemin.
Par exemple, décommentez la première ligne d’exemple :
:
- { : ^/admin, : ROLE_ADMIN }
À partir de maintenant, pour pouvoir accéder à toutes les routes qui commencent par /admin
, il faudra être un utilisateur qui possède le rôle ROLE_ADMIN
. Parfait, non ?
À vous de jouer
Contexte
Nous savons maintenant comment restreindre certaines actions en fonction de critères variés. Mais Amélie vous a transmis des règles assez claires qui n'ont pas toutes été implémentées :
J'aimerais que certains utilisateurs puissent ajouter des auteurs, des éditeurs et des livres, mais que la modification de ces ressources soit limitée à un petit nombre d'entre eux. J'aimerais aussi avoir des comptes d'administrateurs, qui seraient les seuls à pouvoir ajouter de nouveaux utilisateurs, et qui pourraient aussi faire tout le reste. Ah et bien sûr, l'interface d'administration ne doit être accessible qu'aux utilisateurs connectés.
Consignes
La hiérarchie des rôles correspondante a été décrite plus haut dans le chapitre, il ne vous reste plus qu'à demander des contrôles d'accès aux bons endroits.
Utilisez pour cela les informations contenues dans l’encadré informatif ci-dessous.
Il faut bien penser à appeler isGranted
:
sur les méthodes
new
de nos controllersAdmin\AuthorController
,Admin/BookController
etAdmin\EditorController
en demandant le rôleROLE_AJOUT_DE_LIVRE
;dans ces mêmes méthodes, si la variable d'entité est non nulle, avec
ROLE_EDITION_DE_LIVRE
;sur
RegistrationController::register
en demandantROLE_ADMIN
.
Vous devez aussi, dans config/packages/security.yaml
, changer la règle de access_control
pour que les routes qui commencent par /admin
nécessitent IS_AUTHENTICATED
, à la place du rôle d'admin.
En résumé
On peut demander des contrôles d’accès grâce aux nombreuses versions de la méthode
isGranted
.Ensuite, Symfony appelle des Voters, qui peuvent s’abstenir, accorder l'accès ou le refuser.
Le comportement par défaut vérifie des rôles.
On peut créer nos propres Voters pour implémenter des restrictions spécifiques.
La section
access_control
du fichiersecurity.yaml
permet de restreindre des sections entières de notre application.
Nous connaissons le principal, il va maintenant être temps de revenir sur nos acquis et d’aller un peu plus loin avec Symfony !