TP n°8 - Sessions, gestion des utilisateurs et autorisations

Table des matières

1. Introduction

Cette séquence est consacrée à deux ensembles de fonctionnalités nécessaires dans les applications Web:

une gestion de sessions permettant à chaque utilisateur d’interagir avec l’application, dans un contexte qui lui est propre;

la mise en œuvre d’une gestion de comptes utilisateurs, avec des profils utilisateur, et des permissions pour contrôler l’accès aux différentes fonctions de l’application.

Une partie des indications données dans le présent support sont orientées vers une utilisation sous Linux. Pour Windows, il conviendra d’adapter les commandes.

2. Étape 1 : Ajout de « messages flash » aux formulaires

L’objectif de cette étape est d’expérimenter l’utilisation des messages flash de Symfony, pour comprendre le fonctionnement de la session.

Dans la séquence de travail précédente, vous avez étudié le fonctionnement du mécanisme de gestion de la soumission de données dans les formulaires.

À la fin des traitements qui se déroulent avec succès, la gestion faite dans les contrôleurs envoie un code de réponse HTTP qui entraîne une redirection effectuée par le navigateur, pour recharger une autre page.

Or cette redirection, qui entraîne une nouvelle requête, donc l’exécution d’une nouvelle instance de l’application, fait perdre le contexte de l’exécution précédente.

Grâce aux sessions, on peut cependant mettre en oeuvre une continuité entre le traîtement de la requête de soumission de données, avant la redirection, et la gestion de la requête après redirection, par exemple si l’on doit afficher un message à l’utilisateur.

2.1. Principe des « messages flash » Symfony

Dans la mise en œuvre des gestionnaires de soumissions des formulaires, on effectue des redirections, une fois les requêtes de soumission traitées. Or le rechargement qui est effectué dans le navigateur de la nouvelle page vers laquelle on est redirigé, masque les messages éventuellement affichés sur la page de résultat du traitement de la requête précédente.

On va utiliser les Flash Messages de la session Symfony, pour mettre en attente des messages qui seront affichés dès que possible, c’est-à-dire au prochain affichage d’un gabarit de page de l’application.

En pratique, le code suivant permet de stocker un message flash de type « notice », dans le « sac » (Bag ou multi-ensemble) des messages flash de la session :

//$request->getSession()->getFlashBag()->add('notice', 'blah blah');
$this->addFlash('notice', 'blah blah');

Pour le retrouver, dans les templates Twig, il suffit d’accéder à la liste des messages en attente, qui est en fait stocké dans la session courante (app.session) :

{# would work: app.session.flashBag.get('notice') #}
{% for message in app.flashes('notice') %}
    {{ message }}
{% endfor %}

Comme ils sont stockés dans la session, ces messages peuvent survivre à la redirection qui est provoquée en fin de traitement des requêtes POST.

Si les messages sont sauvés dans la session avant la redirection, on les y retrouve à l’exécution suivante pour traiter la requête GET post-redirection.

2.2. Étude du fonctionnement des messages flash

Vous allez ajouter une gestion de messages de confirmation, sur le résultat du traitement des données soumises via les formulaires CRUD : sauvegarde effectuée, confirmation d’ajout, etc.

Ces messages seront mis en oeuvre via des « messages flash » Symfony.

2.2.1. TODO Ajout du code PHP et Twig

Procédez aux étapes suivantes :

  1. Dans le code d’un des gestionnaires de soumissions de formulaire (pour la création des Pastes, par exemple), ajoutez un « message flash », juste avant la redirection :

    Par exemple :

    public function new(Request $request, EntityManagerInterface $entityManager): Response
    {
         // ...
    
         if ($form->isSubmitted() && $form->isValid()) {
                 $entityManager->persist($todo);
                 $entityManager->flush();
    
                 // Make sure message will be displayed after redirect
                 $this->addFlash('message', 'bien ajouté');
                 // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add()
    
                 return $this->redirectToRoute('app_todo_index', [], Response::HTTP_SEE_OTHER);
         }
    
         // ...
    }
    
  2. Ajoutez le code ci-dessous dans le template Twig de base, juste au-dessus du bloc body, pour ajouter l’affichage des messages flashs :

    <body>
    ...
    
       {%  block alerts %}
          {% for type, messages in app.flashes %}
            {% for message in messages %}
               {%if type == 'error'%} {% set type = 'danger' %} {%endif%}
               {%if type == 'message'%} {% set type = 'info' %} {%endif%}
               <div class="alert alert-{{ type }} alert-dismissible" role="alert">
                  <div>{{ message|raw }}</div>
                  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
               </div>
            {% endfor %} {# messages #}
         {% endfor %} {# type, messages #}
       {% endblock %} {# alerts #}
    
       {% block body %}{% endblock %}
    </body>
    ...
    

    Ce block alerts accède à tous les messages en attente dans le flashbag, et affiche des boutons Bootstrap correspondant au type de message (erreur, message).

2.2.2. TODO Test de fonctionnement des messages

Maintenant, testez l’application ainsi modifiée en créant une nouvelle Paste dans la base de données :

  1. Ouvrez une « fenêtre de navigation privée » dans le navigateur, et connectez-vous à l’application. Ouvrez l’inspecteur réseau pour observer les contenus des requêtes et réponses.

    La navigation privée permet de simuler une première connexion à l’application, sans conserver des cookies précédemment enregistrés par le navigateur, donc de faire session de navigation complètement nouvelle.

  2. Examinez la génération du cookie transmis par le serveur PHP lors de la première soumission d’un formulaire, de création par exemple, qui active le code qui fait appel à un flashbag.

    Ce cookie est généré par l’en-tête Set-Cookie présent dans la réponse à la requête POST.

  3. Vérifiez dans la barre d’outils Symfony : la session Symfony contient bien les messages flash en attente (onglet Flashes de l’outil « Request/Response »)

3. Étape 2 : Mise en œuvre des « marque-pages » de tâches prioritaires, grâce à la session

L’objectif de cette séquence est de mettre en œuvre dans l’application une gestion des « marque-pages », en s’appuyant sur le mécanismes des sessions.

Imaginons qu’on souhaite pouvoir utiliser des « marque-pages » qui permettent de naviguer dans la liste de tâches et de marquer des tâches comme étant prioritaires, au fur et à mesure de la navigation.

Cette fonction ressemble aux « paniers » des sites d’e-commerce qui permettent de préparer des achats, en mémorisant des articles dans un panier, même sans s’être encore connecté au site.

3.1. Principe de sauvegarde dans la session

Si on déploie notre application de gestion de tâches sur un serveur, afin que les membres d’une équipe puissent partager une liste de tâches, la base de données sert à enregistrer la liste de toutes les tâches.

Pour l’instant, on ne gère pas de comptes, d’authentification, donc tout le monde a accès à l’ensemble des tâches communes de la base de données.

Pourtant, on va tirer parti du mécanisme de sessions Symfony pour reconnaître le visiteur (ou plutôt son client HTTP, son navigateur) et afficher ainsi des données propres à ce visiteur, qui seront connues de notre application, qui dépendront des requêtes précédentes quand ce même utilisateur a visité des pages sur le site.

On s’appuiera sur les concepts de cookies et de sessions Symfony qui ont été abordés en cours.

Les visiteurs devront donc pouvoir « marquer » les tâches qu’ils jugent urgentes/prioritaires, dans leur session, au fur et à mesure de leur visite sur le site, pour les retrouver ultérieurement. Mais cette information sera inconnue des autres visiteurs, car chaque session est propre à un client HTTP qui a interagi avec le serveur.

On peut stocker dans la session Symfony (qui est déjà utilisée pour les « messages flash ») les références de ces tâches, en stockant dans la session d’une liste d’identifiants des tâches urgentes, pour un utilisateur donné.

Exemple de récupération, en mémoire, d’une liste d’identifiants de tâches urgentes précédemment stockée dans la session via la clé urgents :

$urgents = $request->getSession()->get('urgents');
if( ! is_array($urgents) ) {
   ...
}

Exemple de sauvegarde de la même liste (vide) dans la session, pour la clé urgents :

$urgents = array();
$request->getSession()->set('urgents', $urgents);

3.2. TODO Ajout des méthodes au contrôleur pour la gestion des tâches urgentes

Vous allez ajouter de nouvelles routes permettant de mettre en œuvre une gestion des marque-pages / tâches urgentes dans un contrôleur de l’application.

Procédez aux opérations suivantes :

  1. Ajoutez dans le code PHP, une nouvelle méthode au contrôleur des tâches, gérant l’invocation d’une nouvelle route. Vous pouvez partir du modèle suivant, qui ressemble pour l’instant à la consultation d’une tâche:

    /**
     * Mark a task as current priority in the user's session
     * 
     */
    #[Route('/mark/{id}', name: 'todo_mark', requirements: ['id' => '\d+'], methods: ['GET'])]
    public function markAction(Request $request, Todo $todo): Response
    {
         dump($todo);
         return $this->redirectToRoute('todo_show', 
              ['id' => $todo->getId()]);
    }
    
  2. Modifiez ce code. Au lieu d’afficher les détails, le but est de mémoriser cette tâche dans les tâches « urgentes » de la session, et rediriger ensuite vers l’affichage de la tâche.

    Dans le corps de la méthode, gérez la mémorisation de l’identifiant de la tâche dans un tableau qui sera stocké dans la session. Le stockage du tableau urgents dans la session obéit par exemple à ce type d’algorithme :

    public function markAction(Request $request, Todo $todo): Response
    {
         // ...
    
         // Récupération du tableau d'id urgents dans la session
         $urgents = $request->getSession()->get('urgents');
         dump($urgents);
         if( ! is_array($urgents) ) {
                 $urgents = array();
         }
    
         // ...
    
         dump($urgents);
         // Sauvegarde du tableau d'id urgents dans la session
         $request->getSession()->set('urgents', $urgents);
    
         // ...
    }
    
  3. Terminez le traitement en générant une redirection;
  4. Testez le fonctionnement, et consultez le contenu du tableau présent dans la session, avec les outils du profiler/barre d’outils Symfony (Le contenu de la session est visible dans les détails des requêtes/réponses) ;
  5. Modifiez le code pour que le marque-page fonctionne comme un interrupteur booléen : si on accède une deuxième fois à la même route, on annule le marque-page : on supprime l’identifiant du tableau des tâches urgentes.

    Exemple de code de changement de l’urgence associée à une tâche d’identifiant donné (utilisant la fonction PHP array_diff()):

    $id = $todo->getId();
    // si l'identifiant n'est pas présent dans le tableau des urgents, l'ajouter
    if (! in_array($id, $urgents) ) 
    {
        $urgents[] = $id;
    }
    else
    // sinon, le retirer du tableau
    {
        // substract two arrays
        $urgents = array_diff($urgents, array($id));
    }
    

Vous vous aiderez des outils intégrés à la barre d’outils de Symfony, de dump( ), pour examiner l’impact sur la session, et les requêtes.

Comprenez-vous le fonctionnement des sessions PHP et des sessions Symfony qui sont mises en œuvre aussi bien pour les messages Flash que pour ces marque-pages/ ?

Notez que la base de données n’est pas impactée par ce mécanisme de marque-pages. Impossible pour le serveur de déterminer si une tâche n’est jugée prioritaire par personne, avec cette implémentation. Cela ne concerne que le navigateur qui stocke les cookies et les mécanismes internes au serveur d’exécution de PHP, qui gère les sessions, mais cela se fait indépendamment de Doctrine, donc de la base de données.

3.3. Consultation des fichiers de stockage de la session côté serveur HTTP/PHP

  1. Lancez l’application, et utilisez le marque-page pour marquer une tâche urgente
  2. Allez dans les outils du Profiler Symfony. Dans l’écran « Configuration », puis dans la section « PHP Configuration », cliquez sur « View full PHP configuration ».
  3. Cherchez la valeur de session.save_path. Cela vous donne l’emplacement des fichiers de sauvegarde des sessions du serveur PHP, par exemple /var/lib/php/sessions (sur Linux).
  4. Lancez un autre terminal en parallèle de l’exécution du serveur Web, et listez le contenu du répertoire de stockage des sessions :

    $ ls -alrt /var/lib/php/sessions
    ls: impossible d'ouvrir le répertoire '/var/lib/php/sessions': Permission non accordée
    
    

    le répertoire n’est pas accessible, par défaut, à n’importe qui, pour éviter du vol de données sensibles des applications Web s’exécutant sur le serveur.

    Si vous le pouvez, utilisez sudo pour forcer la lecture (sur machines personnelles) :

    $ sudo ls -alrt /var/lib/php/sessions
    [sudo] Mot de passe de olivier : 
    total 12
    drwxr-xr-x 4 root    root    4096 30 avril  2018 ..
    drwx-wx-wt 2 root    root    4096  9 oct.  13:07 .
    -rw------- 1 olivier olivier  266  9 oct.  14:00 sess_dosclgq3dt6v7uaetnjq1n6g60
    
  5. Consultez le fichier de session (puisque vous en avez les droits… oui, vous ne savez pas quel nom il a, sans l’aide de sudo, mais ensuite vous pouvez le consulter… enfin, comme le processus php-cli que vous faites tourner).

    Vous verrez qu’il contient des sérialisations (dans un format proche du JSON) des données de la session courante, dont le tableau d’identifiants stocké dans urgents :

    $ cat /var/lib/php/sessions/sess_dosclgq3dt6v7uaetnjq1n6g60
    _sf2_attributes|a:2:{s:13:"_csrf/delete4";s:43:"6U_2ctZWxuSkaIWPnWUxjb00G6V5HlJEd1eBcHbndjs";s:7:"urgents";a:1:{i:0;i:4;}}_sf2_meta|a:3:{s:1:"u";i:1602763751;s:1:"c";i:1602763749;s:1:"l";s:1:"0";}
    

Vous constatez que la session peut donc être gérée dans un fichier stocké sur le serveur, sous le contrôle du serveur d’exécution des programmes PHP (en production).

Ce stockage est indépendant de la base de données, et n’est pas partagé avec d’autres applications système, contrairement à un SGBD qui peut être accédé par différentes applications en parallèle.

En production, le stockage de la session peut aussi se faire de bien d’autres façons, par exemple dans un système de mémoire partagée, quand l’exécution des applications PHP s’effectue sur un cluster, avec chaque requête gérée sur une machine différente. Le stokage en système de fichier n’est qu’une option parmi d’autres.

3.4. TODO Amélioration de l’interface pour gérer les tâches prioritaires

Une fois que vous aurez ajouté ce mécanisme de « marque-page », vous pouvez ajouter :

  • une page de consultation des tâches prioritaires, s’il y en a dans la session
  • un indicateur, dans la page de consultation d’une tâche, qui permet d’afficher si elle est prioritaire, et de changer le statut en redirigeant vers la route associée

    {% set urgents = app.session.get('urgents') %}
    {% dump(urgents) %}
    <a href="{{ path('todo_mark' , {'id': todo.id}) }}">
    {% if todo.id in urgents %}
    URGENT
    {% else %}
    NOT URGENT
    {% endif %}
    </a>
    

Vous pourrez utiliser la navigation privée du navigateur, pour constater que cette liste est propre à chaque « session » de navigation, qui garde une trace du cookie de session PHP. Comme il est réinitialisé en navigation priée, on perd l’accès à la session précédente.

4. Étape 3 : Ajout du contrôle d’accès dans l’application

Ajouter une fonction de gestion de comptes d’utilisateurs stockés dans la base de données, sur l’application « fil-rouge » ToDo.
Puis intégrer les fonctions nécessaires à l’authentification et au contrôle d’accès.

Vous allez procéder aux étapes suivantes, pour ajouter la gestion des utilisateurs dans l’application de gestion des tâches.

Cela permettra que l’application soit utilisée par différents utilisateurs simultanément, mais que chaque utilisateur ne puisse pas nécessairement polluer les tâches qui correspondent à un autre utilisateur, s’il fait des modifications.

4.1. TODO Étape 3-a : Ajout d’une entité User

Ajouter l’entité gérant les comptes utilisateurs dans le modèle de données du projet.

Utilisez l’assistant, qui va procéder à l’ajout d’une entité au modèle, dans le répertoire de l’application Symfony :

symfony console make:user
The name of the security user class (e.g. User) [User]:
> User

Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes

Enter a property name that will be the unique "display" name for the user (e.g.
email, username, uuid [email]
> email

Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).

Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes

created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml

  Success! 


 Next Steps:
   - Review your new App\Entity\User class.
   - Use make:entity to add more fields to your User entity and then run make:migration.
   - Create a way to authenticate! See https://symfony.com/doc/current/security.html

Il est alors nécessaire, comme d’habitude, de mettre à jour le schéma de la base de données.

Plutôt que d’appliquer les migrations comme suggéré par l’outil, nous vous conseillons de regénérer complètement la base de données :

  1. suppression de la base de données : symfony console doctrine:database:drop --force
  2. recréation de la base de données : symfony console doctrine:database:create
  3. recréation du schéma de la base de données : symfony console doctrine:schema:create

4.2. TODO Étape 3-b : Ajout d’utilisateurs de tests

La documentation de Symfony explique comment intégrer dans la génération des données de test de l’application l’insertion d’utilisateurs dans la base de données, au sein de la section Accessing Services from the Fixtures.

Pour que cela soit plus simple à faire, ajoutez un nouveau fichier aux Data Fixtures de votre projet, en recopiant directement dans le fichier src/DataFixtures/UserFixtures.php l’exemple suivant :

<?php

namespace App\DataFixtures;

use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use App\Entity\User;

class UserFixtures extends Fixture
{
    private UserPasswordHasherInterface $hasher;

    public function __construct(UserPasswordHasherInterface $hasher)
    {
        $this->hasher = $hasher;
    }

    public function load(ObjectManager $manager)
    {
        $this->loadUsers($manager);
    }

    private function loadUsers(ObjectManager $manager)
    {
        foreach ($this->getUserData() as [$email,$plainPassword,$role]) {
            $user = new User();
            $password = $this->hasher->hashPassword($user, $plainPassword);
            $user->setEmail($email);
            $user->setPassword($password);

            $roles = array();
            $roles[] = $role;
            $user->setRoles($roles);

            $manager->persist($user);
        }
        $manager->flush();
    }
        private function getUserData()
        {
                yield [
                        'chris@localhost',
                        'chris',
                        'ROLE_USER'
                ];
                yield [
                        'anna@localhost',
                        'anna',
                        'ROLE_ADMIN'
                ];
        }
}

Lorsque l’application sera déployée en production, les comptes des utilisateurs seront créés depuis l’interface de l’application, mais pour les besoins des tests, nous avons chargé des utilisateurs dans la base de données.

Consultez dans votre IDE :

  • la nouvelle entité User ajoutée au modèle de données
  • le code de chargement des Data Fixtures qui a servi à initialiser les données de tests présentes dans la base, qui donne les identifiants et les mots-de-passe des utilisateurs de tests

Rechargez les données de test pour ré-encoder les mots-de-passe par rapport à l’environnement local :

symfony console doctrine:fixtures:load

4.3. TODO Étape 3-c : Mise en œuvre de l’authentification

Dans ce qui suit, on va résumer différentes étapes de modification nécessaires, qui sont documentées dans la documentation Authenticating Users / Form Login de Symfony.

  1. créons un contrôleur Symfony standard qui servira au login

    symfony console make:controller Login
    
  2. Modifions le fichier de configuration config/packages/security.yaml pour y placer les directives suivantes :

    security:
         # ...
    
         firewalls:
                main:
                    # ...
                    form_login:
                        # "app_login" is the name of the route created previously
                        login_path: app_login
                        check_path: app_login
                        enable_csrf: true
                        default_target_path: todo_list
    
                    logout:
                        path: app_logout
                        # where to redirect after logout
                        target: home
    
         role_hierarchy:
                ROLE_ADMIN:       ROLE_USER
                ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    
         access_control:
                - { path: ^/admin, roles: ROLE_ADMIN }
                # - { path: ^/profile, roles: ROLE_USER }
    

    Explications :

    • on définit une route pour invoquer le login via un formulaire « classique » Symfony (qui contiendra une option de sécurité : CSRF)
    • on définit une route pour la redirection par défaut une fois le login réussi (’default_target_path’, ici sur todo_list)
    • on définit une route pour le logout, qui, une fois réussi, renvoit sur une route de l’application (ici home, à adapter à votre code)
    • on définit une hiérarchie de rôles pour les utilisateurs
    • enfin, on déclare que le backoffice (derrière l’URL /admin) sera réservé aux utilisateurs administrateurs.
  3. on modifie le code du contrôleur LoginController pour ajouter la construction de la page de login via un gabarit login/index.html.twig :

    <?php
    
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
    
    class LoginController extends AbstractController
    {
     #[Route('/login', name: 'app_login')]
         public function index(AuthenticationUtils $authenticationUtils): Response
         {
                 // get the login error if there is one
                 $error = $authenticationUtils->getLastAuthenticationError();
    
                 // last username entered by the user
                 $lastUsername = $authenticationUtils->getLastUsername();
    
                 return $this->render('login/index.html.twig', [
                         'last_username' => $lastUsername,
                         'error'         => $error,
                 ]);
         }
    }
    
    
  4. on ajoute enfin le gabarit Twig correspondant, dans templates/login/index.html.twig :

    {% extends 'base.html.twig' %}
    
    {# ... #}
    
    {% block body %}
        {% if error %}
            <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
        {% endif %}
    
        <form action="{{ path('app_login') }}" method="post">
        <div class="form-outline mb-4">
            <label for="username">Email:</label>
            <input type="text" id="username" name="_username" value="{{ last_username }}"/>
         </div>
         <div class="form-outline mb-4">
            <label for="password">Password:</label>
            <input type="password" id="password" name="_password"/>
         </div>
            {# If you want to control the URL the user is redirected to on success
            <input type="hidden" name="_target_path" value="/account"/> #}
    
         <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
    
            <button type="submit" class="btn btn-primary btn-block mb-4">login</button>
        </form>
    {% endblock %}
    

    Ce gabarit affiche un simple formulaire login (email) + mot-de-passe. Il contient quelques directives pour Bootstrap pour améliorer un peu l’affichage

  5. Testez que l’authentification fonctionne quand on navigue sur /login (avec les utilisateurs définis précédemment dans les fixtures).

    Si le login est réussi, vous devez être redirigé vers todo_list.

    Vous devez voir l’utilisateur courant affiché dans les icônes de la barre d’outils Symfony (profiler).

  6. Ajoutez enfin une route dans le contrôleur LoginController pour permettre le logout :

    #[Route('/logout', name: 'app_logout', methods: ['GET', 'POST'])]
    public function logout()
    {
        // controller can be blank: it will never be called!
        dump("logout");
        // throw new \Exception('Don\'t forget to activate logout in security.yaml');
        return new Response();
    }
    
  7. Testez que la déconnexion (logout) fonctionne bien, et qu’une fois déconnecté, il n’y a plus de spécification d’utilisateur courant dans la barre d’outils Symfony
  8. Naviguez vers le back-offiche /admin/ et vérifiez que l’authentification est bien demandée. Connectez-vous alternativement avec un utilisateur ADMIN ou un utilisateur normal, et vérifiez si l’accès est autorisé ou non.

Vous pourriez ensuite générer un formulaire d’inscription avec l’assistant symfony console make:registration-form, mais ce n’est pas forcément utile pour notre TP.

4.4. TODO Étape 3-d : Adaptation des menus

Cette étape suppose que votre application utilise déjà les menus Bootstrap pour afficher les menus de haut de page, si vous avez effectué cette étape de la mise en œuvre (cf. TP 6).

Vous allez alors ajouter un menu offrant des fonctions de connexion / déconnexion.

Le principe consiste à prévoir, dans le bloc menu du gabarit de base des pages, une section optionnelle dépendant de si l’utilisateur est connecté (ou de son rôle), du style :

{% if app.user %}
...
    {# Affichage d'un menu 'account' permettant de se déconnecter #}
...
{% else %}
...
    {# Affichage d'un menu 'anonymousaccount' permettant de se connecter #}
...
{% endif %}

La détection de l’utilisateur connecté est faite en vérifiant s’il y a un utilisateur courant, auquel cas, app.user est définie.

On prépare ainsi deux menus différents : celui pour les utilisateurs déjà connectés, et l’autre qui permettra justement, de se connecter.

  1. ajoutez dans le gabarit, dans la définition des menus le code suivant :

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
         <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
              {{ render_bootstrap_menu('main') }}
              {% if app.user %}
                   {{ render_bootstrap_menu('account') }}
              {% else %}
                   {{ render_bootstrap_menu('anonymousaccount') }}
              {% endif %} {# app.user #}
         </ul>
    </div>
    
  2. dans la configuration des menus, dans config/packages/bootstrap_menu.yaml ajoutez les définitions des deux menus :

    bootstrap_menu:
      menus:
        main:
    ...
        anonymousaccount:
          items:
             login:
                label: 'Login'
                route: 'app_login'
    #        register:
    #           label: 'Register'
    #           route: 'app_register'
        account:
          items:
             logout:
                label: 'Logout'
                route: 'app_logout'
                roles: [ 'ROLE_USER' ]
    
    

Testez le fonctionnement. Le menu doit afficher des informations différentes si l’utilisateur est authentifié ou non.

4.5. TODO Étape 3-e : Adaptations de l’interface en fonction de l’utilisateur

On souhaite que l’accès en consultation aux tâches présentes dans la base de donnée de l’application soit accessible à tout le monde (on a par exemple déployé l’application en intranet : pas besoin de se connecter pour cela).

Mais on souhaite que les fonctions de modifications soient réservées à des utilisateurs authentifiés :

  • modification possible par tout utilisateur
  • suppression possible uniquement par l’administrateur.

On va commencer par modifier les gabarits de l’application pour masquer les liens vers les fonctions qui ne doivent pas être accessibles :

  1. Modifiez le template d’affichage de la liste des tâches (templates/todo/index.html.twig), afin de n’afficher le lien vers les fonctions d’édition que si l’utilisateur connecté possède le rôle ROLE_USER :

    {% if is_granted('ROLE_USER') %}
        <a href="{{ path('todo_edit', {'id': todo.id}) }}">edit</a>
    {% endif %}
    
  2. Procédez de même pour la suppression, dans le gabarit d’affichage templates/todo/edit.html.twig, pour que l’affichage du bouton « Delete » ne se fasse que pour le rôle ROLE_ADMIN

Lancez l’application et vérifiez que l’application a donc un fonctionnement qui dépend du profil de l’utilisateur connecté.

4.6. TODO Étape 3-f : Vérification du contrôle d’accès effectif

Cette étape vise à vérifier que le contrôle d’accès fonctionne effectivement, pour protéger l’accès aux fonctions de l’application, en fonction de l’authentification ou non de l’utilisateur.

Qu’advient-il si un utilisateur devine que le lien /todo/{id}/edit permet d’accéder aux fonctions de modification d’une tâche ?

Le lien masqué vers « edit » suffit-il à protéger l’accès aux fonctions de l’application ?

De même, le bouton de suppression existe encore dans le formulaire de modification… risqué…

Modifiez le code du contrôleur pour sécuriser l’accès aux routes, comme indiqué dans la documentation Securing Controllers and other Code :

  1. Modifiez les annotations au-dessus de la méthode TodoController::edit qui gère les modifications, pour ajouter l’annotation IsGranted("ROLE_USER")

    use Symfony\Component\Security\Http\Attribute\IsGranted;
    ...
        #[Route('/{id}/edit', name: 'todo_edit', methods: ['GET', 'POST'])]
        #[IsGranted('ROLE_USER')]
        public function edit(Request $request, Todo $todo, EntityManagerInterface $entityManager): Response
        {
    
  2. Modifiez de façon identique la route des suppressions, pour réserver l’accès aux utilisateurs possédant le rôle ROLE_ADMIN
  3. Vérifiez que vous aboutissez bien à la page de connexion lorsque vous essayez de rentrer manuellement les URL ’edit’.

On sait donc bien masquer les lien ET contrôler effectivement l’accès aux fonctions CRUD, pour prévenir des tentatives d’utilisation malveillantes.

4.7. TODO Étape 3-g : Gestion des droits d’accès plus contextuelle (optionnelle)

On souhaite affiner un peu plus le fonctionnement de l’application de façon à ce que les contrôles sur les droits d’accès à la fonction de suppression soient moins restrictifs, de façon à ce que :

  • les utilisateurs authentifiés et administrateurs peuvent supprimer toutes les tâches
  • les utilisateurs authentifiés non-administrateurs peuvent supprimer uniquement les tâches déjà terminées.

Modifiez la gestion de la suppression comme suit :

  1. modifiez le code du gabarit d’édition des tâches de façon à afficher le bouton delete, soit :
    • pour les utilisateurs administrateurs
    • soit pour les utilisateurs authentifiés « ordinaires », mais dans ce cas, uniquement quand la tâche est terminée
  2. vérifiez que le bouton s’affiche bien dans ces conditions
  3. testez la suppression avec un utilisateur de rôle ROLE_USER

    Normalement, la suppression est toujours refusée, car même si le bouton est affiché, on garde la vérification « inconditionnelle » sur le rôle ROLE_ADMIN dans l’annotation de la route todo_delete.

  4. modifiez donc le code de la méthode delete pour effectuer un traitement conditionnel :
    1. remplacez l’attribut IsGranted("ROLE_ADMIN") par IsGranted("ROLE_USER") sur la méthode

      Cette fois-ci, tout utilisateur peut tenter sa chance même s’il n’est pas administrateur…

    2. dans le corps du code, vérifiez la règle de privilège administrateur, ou de tâche terminée :

      if ($todo->isCompleted() == false) {
          $this->denyAccessUnlessGranted('ROLE_ADMIN');
      }
      

      Essayez de passer par le formulaire d’affichage ou le lien Delete reste encore quelque soit le contexte, pour déclencher la suppression.

      Une page « 403 » Access denied est affichée en réponse à la requête POST du « delete », lorsque vous essayez de supprimer sans être administrateur :

      La barre d’outils Symfony indique pourquoi :

      • HTTP status 403 Forbidden
      • Controller TodoController:: delete
      • Route name todo_delete

Vous comprenez maintenant la façon dont on peut gérer les permissions dans l’application, de façon large (via les attributs IsGranted() en fonction des rôles), ou contextuelle, selon les utilisateurs ou les données à traîter (dans un algorithme, grâce à denyAccessUnlessGranted()).

5. Évaluation

Vous comprenez les mécanismes suivants des applications Web :

  • le stockage d’informations dans la session, qui est déterminée grâce aux cookies échangées entre le client HTTP et le serveur Web. Ces informations sont donc propres à chaque client, chaque utilisateur, et non partagées entre tous les utilisateurs comme dans le cas des données présentes dans la base de données
  • les principes de la gestion des permissions avec le modèle RBAC tel qu’il est mis en œuvre dans Symfony. Elle se décompose en deux grands axes :
    • personnaliser l’affichage en fonction du profil de l’utilisateur, ce qui est fait essentiellement dans les gabarits
    • le contrôle effectif des permissions effectué dans le routage ou dans le code qu’on place dans les contrôleurs, pour une gestion plus fine.

Auteur: Olivier Berger (TSP)

Created: 2023-10-24 Tue 19:24

Validate