ActiveSupport::CurrentAttributes et ne correspondent pas à l’authentification générée par Rails 8.
Il s’avère que la construire soi-même ne prend qu’une trentaine de lignes, car le code généré est assez court pour être lu et structuré pour être étendu.
Ce que couvre ce tutoriel
- Surcharger
Current.userpour que l’impersonation traverse le code existant. - Garder
true_userdisponible pour l’autorisation et l’affichage. - La contrainte d’ordre dans
resume_sessionqui, sinon, vous bloquerait l’accès.
Prérequis
Comment fonctionne le code généré
Deux fichiers comptent.app/models/current.rb conserve l’état par requête. Il stocke la session et délègue user à celle-ci :
app/controllers/concerns/authentication.rb assigne cette session. require_authentication appelle resume_session, qui retrouve l’enregistrement de session à partir d’un cookie signé. Quand elle ne retourne rien, la requête est envoyée vers la page de connexion.
Le cookie est signé plutôt que chiffré : un utilisateur peut lire l’ID de sa session, mais ne peut pas le modifier.
Surcharger Current.user
Tout, dans l’application, demande déjà àCurrent.user qui est connecté. Plutôt que d’apprendre l’impersonation à tout ce code, interceptez-la à un seul endroit.
Remplacez la délégation par une méthode explicite :
Current.user retourne désormais l’utilisateur impersonné s’il y en a un, et l’utilisateur connecté sinon. Sans les deux, il retourne nil et la requête est considérée comme non authentifiée, ce qui est exactement ce que le reste du concern attend.
true_user n’est pas optionnel. Vous en avez besoin pour la bannière qui indique à un administrateur qui il est en train d’impersonner, et vous en avez besoin pour l’autorisation.
Ajouter les méthodes d’impersonation
Dansapp/controllers/concerns/authentication.rb :
Current est réinitialisé après chaque requête, donc l’utilisateur impersonné doit être retrouvé à chaque fois. La session du cookie transporte l’ID d’une requête à l’autre. La guard clause dans find_impersonated_user évite une requête pour un utilisateur qui ne peut pas exister.
Remarquez ce qui manque délibérément : aucun enregistrement Session n’est créé. L’impersonation n’est pas une connexion. Si vous en créiez un, le client verrait une seconde session active sur son compte, ce qui serait inquiétant et inexact.
L’intégrer dans resume_session
stop_impersonating à terminate_session. Sans cela, se déconnecter puis se reconnecter reprend l’impersonation de la personne impersonnée précédemment, car l’ID reste dans la session du cookie.
Ajouter le contrôleur
L’afficher dans le layout
Attribuer l’activité impersonnée
L’impersonation crée un trou dans votre télémétrie : chaque trace enregistre l’utilisateur impersonné, si bien que l’activité du support est indissociable de celle du client lui-même. Comblez-le en taguant les requêtes avec l’acteur réel :Transcription
Transcrit depuis la vidéo et légèrement édité : les sous-titres automatiques ont mal compris plusieurs noms de produits et d’API, qui ont été corrigés.Lire la transcription
Lire la transcription
0:02 Salut à tous, dans cet épisode, on va ajouter l’impersonation au générateur d’authentification Rails 8. C’est en fait beaucoup plus simple qu’on ne pourrait le penser. Alors, allons-y. Je vais exécuter
rails generate authentication pour créer les modèles user et session, ainsi que tous les contrôleurs et le module d’authentification qui fait l’essentiel du travail. Et on va faire rails db:migrate pour créer ces tables en base de données.0:31 Ensuite, je lance la console Rails, et on va créer deux utilisateurs. On se connectera comme l’un, on impersonnera l’autre. Donc User.create!, avec pour adresse e-mail chris@gorails.com et le mot de passe “password”. Puis on refait la même chose, mais avec un autre utilisateur ayant une adresse e-mail différente.0:51 Je lui mets un mot de passe quelconque, et ça devrait suffire. Alors jetons un œil rapide à l’authentification elle-même. Allons dans application.html.erb et écrivons : si authenticated?, alors on peut afficher current_user.email_address. C’est ce qu’on verra pour l’utilisateur actuellement connecté, et on veut que ça change quand on impersonne un autre utilisateur. On aura aussi un bouton de déconnexion qui ira vers le chemin de session avec la méthode delete pour nous déconnecter.1:34 Bon, c’est bien. On devrait pouvoir lancer notre serveur Rails et ouvrir ça dans le navigateur. Voilà mon précédent test que j’avais mis en place. Et donc ici, sessions/new est l’endroit où on va être emmenés. Quand on va sur l’URL racine, on va être redirigés ici.1:52 J’ai déjà créé une URL racine principale pour nous. Elle nous redirige donc vers l’authentification, ce qui se produit par défaut. Chaque page est authentifiée, et il faut explicitement lui dire qu’une page n’a pas besoin d’authentification. Ici, on peut se connecter en tant que chris@gorails.com, et voilà. chris@gorails.com, je peux me déconnecter et me reconnecter.2:16 On va refaire ça une fois de plus pour être sûr que tout fonctionne. Alors, comment fait-on l’authentification avec l’impersonation ? Regardons rapidement la classe Current. C’est elle qui gère le stockage de la session et de l’utilisateur courants pour la requête. Elle est réinitialisée une fois la requête terminée, et on assigne simplement la session, et on récupère l’utilisateur via cette session.2:44 Le module d’authentification est celui qui assigne cette session. Donc avec require_authentication, il va appeler cette méthode qui va reprendre la session actuelle ou vous demander de vous authentifier. Reprendre la session, c’est aussi simple que de la retrouver via le cookie, ce qui va chercher l’enregistrement de session en base de données à partir de l’ID défini dans vos cookies. Ils sont signés pour qu’on ne puisse pas les altérer, mais ils restent lisibles par les utilisateurs, parce que ça n’a pas d’importance, puisqu’ils ne peuvent rien en faire pour en abuser. Pour l’impersonation, on doit donc essentiellement ajouter des éléments pour intercepter tout ça.3:29 Et quand on récupère l’utilisateur courant, on récupère en fait l’utilisateur qu’on impersonne, pas celui avec lequel on est connecté. Et je ne veux pas nécessairement créer de session pour lui non plus, parce qu’on n’est pas vraiment connecté en tant que lui. On essaie juste de faire du support client, par exemple. Alors parlons de comment on veut que ça marche. Dans current.rb, ce delegate :user, c’est exactement ce qu’on doit intercepter.3:54 Si on y réfléchit ainsi, on pourrait avoir un attribut pour l’utilisateur impersonné, qu’on assignerait, puis une méthode pour user qui retournerait soit l’utilisateur impersonné, soit l’utilisateur de la session. Et s’il n’y avait ni session ni utilisateur impersonné, ça retournerait nil, et on ne serait pas authentifié du tout. Ça marcherait donc plutôt bien, et ça ferait en sorte que ça fonctionne aussi avec le module d’authentification. On n’a donc plus vraiment besoin de cette ligne qui délègue user, puisqu’on va de toute façon devoir la surcharger ici. Maintenant, on pourrait bien sûr ajouter une méthode comme true_user si on voulait, en disant que c’est l’utilisateur de la session, pour toujours pouvoir contourner l’utilisateur impersonné, parce qu’on veut que l’utilisateur impersonné se comporte comme notre utilisateur courant, mais on pourrait avoir besoin d’afficher une bannière et de montrer l’utilisateur d’origine, pour laquelle on pourrait ajouter un petit helper ici aussi.5:01 C’est donc optionnel, pas forcément nécessaire, mais allons dans notre application.html.erb et ajoutons un petit élément qui dit : si on impersonne, on veut avoir un bouton pour arrêter l’impersonation. On aura besoin d’un chemin impersonate avec la méthode delete pour arrêter l’impersonation, puis on peut avoir un bouton pour impersonner un utilisateur, qui ira vers le chemin impersonate en passant l’ID d’un utilisateur. Ça devrait probablement être dynamique dans votre zone d’administration, et on va en faire une requête POST parce qu’on veut créer la session d’impersonation à ce moment-là. On doit donc implémenter une poignée de méthodes et un contrôleur pour y arriver. Alors allons voir nos routes.5:58 On ajoute une resource pour impersonate. On peut éditer app/controllers/impersonates_controller.rb pour créer ce ImpersonatesController, qui hérite de ApplicationController. Une chose qu’on veut faire, c’est ajouter un require_admin. On n’a pas de drapeau admin sur nos utilisateurs, mais c’est un peu un exemple de ce qu’on pourrait faire.6:27 On pourrait donc dire require_admin, et ça vérifierait si current_user est admin, et si ce n’est pas le cas, rediriger vers le chemin ou l’URL racine, ou ce que vous voulez faire. Vous voudrez peut-être y mettre une alerte ou autre chose, mais ici on doit créer nos deux actions. L’une créera la session d’impersonation, l’autre la supprimera. Ce sera quelque chose comme stop_impersonating, qui vous redirigera vers l’URL racine, et pour create, ce sera assez simple, plutôt similaire : on dira impersonate_user(User.find(params[:id])).7:13 Vous passez donc un ID dans l’URL. On impersonnera alors cet utilisateur, puis on vous redirigera vers la page d’accueil, que vous verrez désormais en tant que cet utilisateur. Maintenant on doit aller dans le module d’authentification et ajouter ces méthodes. Si on regarde ces autres méthodes, on peut construire quelque chose de similaire. On a besoin de la méthode impersonating?, qui retournera vrai ou faux.7:45 On a besoin d’un impersonate_user qui met ça en place. On a besoin d’un stop_impersonating, qui n’a besoin de rien de plus. Mais on va aussi avoir besoin d’un find_impersonated_user. C’est celle qu’on utilisera après avoir impersonné, quand vous êtes redirigé vers la page suivante.8:07 Cette requête va devoir retrouver l’utilisateur impersonné, tout comme elle retrouve votre session courante à partir du cookie. Alors commençons simplement. On dira : Current.impersonated_user est-il présent ? Oui, vous impersonnez.8:28 Non, vous ne l’êtes pas. Ensuite, quand on veut impersonner un utilisateur, on assigne cette variable, impersonated_user égal à cet utilisateur. Et on doit aussi définir quelque chose pour que l’ID de impersonated_user soit conservée pour la requête suivante. On utilisera la session pour ça. Et à la requête suivante, on pourra vérifier s’il y a un ID dans la session pour l’ID de l’utilisateur impersonné.8:58 S’il y en a un, on peut dire find_by avec l’ID et essayer de récupérer cet utilisateur. La façon dont je le configure ici, on vérifie que cet ID est présent, ce qui permet d’éviter complètement d’interroger Active Record s’il n’y a pas d’ID. On n’a pas besoin d’interroger la base pour un utilisateur dont l’ID est null, parce qu’on ne récupérera de toute façon aucun utilisateur pour ça. Ça ne sert donc à rien de faire cette requête. On retourne donc soit l’utilisateur, soit nil, et ensuite stop_impersonating est assez simple.9:29 L’utilisateur impersonné devrait maintenant être nil, et on veut supprimer impersonated_user_id de la session. Assez simple. Ce sont donc nos principaux éléments d’impersonation. Je vais ajouter une ligne vide ici. J’ai remarqué que c’est comme ça que le module d’authentification est déjà organisé.9:51 Un peu d’organisation avec des lignes vides supplémentaires. Et donc on doit maintenant intégrer ça dans le processus d’authentification réel. Et le principal processus d’authentification, c’est cette méthode require_authentication : reprendre une session ou en redémarrer une nouvelle. Et on vous envoie vers la page de connexion. Donc resume_session est l’endroit qu’on doit réellement modifier.10:10 On va donc mettre Current.impersonated_user égal à find_impersonated_user. Puis on définira la session. Il est important que ces deux lignes soient dans cet ordre précis, parce que la valeur de retour de resume_session détermine si on vous redirige vers la connexion ou non. Si vous mettiez ça après, et que vous n’étiez pas en train d’impersonner, on continuerait à vous envoyer vers require_authentication, parce que impersonated_user serait nil. Cette méthode retournerait nil ici, ce qui est falsy, ce qui déclencherait ce comportement.10:48 Et vous auriez l’impression de ne jamais pouvoir vous connecter. Il est donc important de bien mettre ces éléments en place pour que la valeur de retour de resume_session soit correctement définie. Et si tout se passe bien, la seule autre chose dont on a vraiment besoin, c’est une méthode helper pour impersonating?, pour qu’on puisse l’utiliser dans nos vues pour afficher ce bouton. Ensuite, vous voudrez probablement aussi vérifier si l’utilisateur est admin ou autre. Et je viens de remarquer qu’on doit s’assurer d’utiliser l’ID utilisateur comme clé partout où on accède à la session.11:27 On veut s’assurer que c’est cohérent. Sinon, vous rencontrerez des choses étranges. De plus, quand on termine la session, on veut vraiment s’assurer qu’on arrête aussi l’impersonation, parce que quand vous vous déconnectez puis vous reconnectez, vous ne voulez pas vous retrouver connecté à votre propre compte tout en impersonnant automatiquement la dernière personne que vous impersonniez. Ce serait étrange. On veut donc s’assurer d’arrêter l’impersonation aussi à la déconnexion.11:56 Alors essayons ça. Allons dans notre navigateur. On est sur sessions/new. On peut se connecter, chris@gorails.com. Ça nous connecte toujours, ce qui est bien.12:05 On n’a rien cassé. On n’impersonne pas, donc le bouton d’impersonation apparaît. On peut cliquer dessus. Ça va faire une requête POST vers le contrôleur d’impersonation, qui va atteindre create. Il aura l’ID 2.12:20 On l’a dans l’URL pour ça. Vous pourriez aussi mettre ça en place sur users/:id/impersonate si vous voulez. Je les ai simplement regroupés dans un seul contrôleur, et vous passez l’ID. Les deux fonctionnent, mais on peut impersonner. Et maintenant, Current.user.email_address est bob@bob.bob.12:37 Mais on est toujours connecté en tant que chris@gorails.com. Donc on sait maintenant qu’on impersonne, car on voit stop_impersonating. Si on voulait ouvrir la console Rails, laissez-moi augmenter la taille de la police. On peut dire Session.count. Et on verra qu’il n’y a qu’une seule session pour moi, chris@gorails.com.13:02 Si on dit Session.first et qu’on récupère l’utilisateur, ce sera l’utilisateur numéro un, que je ne montre pas ici. C’est filtré, mais l’adresse e-mail est chris@gorails.com. Il n’y a donc que moi de connecté, et mon impersonation n’est pas stockée comme une session, ce qui serait étrange si un client voyait que quelqu’un d’autre est connecté à son compte. On ne veut donc pas vraiment créer de session pour ça. Mais vous pourriez, si vous vouliez.13:31 Et le noter aussi sur l’enregistrement. Vous pourriez ajouter un champ supplémentaire au modèle Session pour garder une trace de ces sessions d’impersonation dans l’historique aussi. Il y a beaucoup de choses à développer à partir de là. Donc maintenant, si on arrête l’impersonation, ça va mettre Current.impersonated_user à nil, mais aussi retirer impersonated_user_id de la session. Et on sera de retour à chris@gorails.com.13:58 Et je peux repasser à bob@bob.bob quand je veux. Et ça n’a en fait demandé de modifier que resume_session et terminate_session à la fin pour y inclure nos changements d’impersonation. Et dans notre current.rb, au lieu de faire la délégation, on utilise l’utilisateur impersonné. Et bien sûr, on a toujours accès à true_user aussi. Donc si on va dans notre application.html.erb, on pourrait dire, en cas d’impersonation, vous aurez probablement l’adresse e-mail de l’utilisateur courant à cet endroit.14:39 Mais peut-être que si vous impersonnez, vous diriez Current.true_user.email_address, si vous voulez montrer le véritable utilisateur que vous êtes. On verra maintenant chris@gorails.com apparaître à cet endroit. Vous avez donc accès aux deux types d’utilisateurs, tout comme avec la gem Pretender pour Devise ou n’importe quelle autre bibliothèque d’impersonation qui existe pour l’authentification classique. Mais elles ne fonctionnent pas avec CurrentAttributes. Et donc c’est agréable de pouvoir construire ça nous-mêmes avec juste un peu de code.15:18 Il n’y a pas grand-chose ici. Et c’est assez simple à comprendre quand on regarde tout ça et qu’on se demande comment ça fonctionne. Je suis donc vraiment content du résultat. L’authentification Rails 8 est très, très flexible. Vous pouvez faire toutes sortes de choses avec et la personnaliser selon vos besoins, que ce soit pour l’impersonation, l’OAuth, ce que vous voulez.15:39 Il y a toutes sortes de flexibilité là-dedans. Et c’est très, très facile à lire avec ce module d’authentification. Voilà donc pour cet épisode. J’espère qu’il vous a plu. Et si vous voulez voir plus de contenu comme ça, faites-le-nous savoir dans les commentaires ci-dessous.15:56 Et on s’en servira comme inspiration pour nos prochaines leçons. Voilà, c’est tout. Passez une bonne journée. Et on se reparle bientôt. Salut.