• 12 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 26/08/2024

Gérez les autorisations des utilisateurs

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 :

  1. Vous posez une question, par exemple “Est-ce que l’utilisateur a le droit d’afficher cette liste ?”. 

  2. 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 ?”. 

  3. 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

  • méthode  $this->isGranted([...])  (renvoie  true  ou  false  )

  • méthode  $this->denyAccessUnlessGranted([...])  (lance une exception si l’utilisateur n’a pas le droit)

  • Attribut  #[IsGranted([...])]  sur la méthode de controller (lance une exception si l’utilisateur n’a pas le droit)

Dans une autre classe

Injecter dans le   __construct  un objet : 

  • Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $checker  et appeler  $this->checker->isGranted([...])

  • Symfony\Bundle\SecurityBundle\Security $security  et appeler  $this->security->isGranted([...])  (cet objet permet aussi de récupérer l’utilisateur actuellement connecté grâce à  $security->getUser()  )

Dans un template

Utiliser la fonction  is_granted([...])  (renvoie true ou false)

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 :

security:
# …
    role_hierarchy:
        ROLE_USER: ~
        ROLE_MODERATEUR: ROLE_USER
        ROLE_AJOUT_DE_LIVRE: ROLE_USER
        ROLE_EDITION_DE_LIVRE: ROLE_AJOUT_DE_LIVRE
        ROLE_ADMIN: [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 :

Hiérarchie des rôles

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  :

  1. 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 renvoie  true  si notre Voter peut prendre une décision,  false  s'il s’abstient. 

  2. 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’objet  TokenInterface  contenant notre utilisateur. Elle renvoie ensuite  true  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.

Structures des voters
Structure des voters
  • 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 :

security:
# …
    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
    # - { 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 :

access_control:
    - { path: ^/admin, roles: 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 controllers  Admin\AuthorController  ,  Admin/BookController  et  Admin\EditorController  en demandant le rôle  ROLE_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 demandant  ROLE_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 fichier  security.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 !

Exemple de certificat de réussite
Exemple de certificat de réussite