
Dans les applications modernes qui prennent en charge l'authentification, nous souhaitons souvent modifier ce qui est visible pour l'utilisateur, en fonction de son rôle. Par exemple, un utilisateur invité peut voir un article, mais seul un utilisateur enregistré ou un administrateur voit un bouton pour supprimer cet article.
La gestion de cette visibilité peut être un cauchemar complet avec des rôles croissants. Vous avez probablement écrit ou vu du code comme celui-ci:
if (user.role === ADMIN || user.auth && post.author === user.id) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
Ce code est distribué dans toute l'application et devient généralement un gros problème lorsque le client modifie les exigences ou demande d'ajouter des rôles supplémentaires. En fin de compte, vous devez passer par tous ces if-s et ajouter des vérifications supplémentaires.
Dans cet article, je vais montrer une autre façon d'implémenter la gestion des autorisations dans l' API Expressjs à l' aide d'une bibliothèque appelée CASL . Il simplifie considérablement le contrôle d'accès et vous permet de réécrire l'exemple précédent en quelque chose comme ceci:
if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
Nouveau sur CASL? Je recommande de lire Qu'est-ce que la CASL?
Remarque: Cet article a été initialement publié sur Medium.
Application de démonstration
En tant qu'application de test, j'ai créé une API REST assez simple pour le blog . L'application se compose de 3 entités ( User
, Post
et Comment
) et 4 modules (un module pour chaque entité et un autre pour la vérification des autorisations). Tous les modules se trouvent dans le dossier src/modules
.
L'application utilise des modèles mangouste , une authentification passeport et une autorisation CASL (ou contrôle d'accès). En utilisant l'API, l'utilisateur peut:
- lire tous les articles et commentaires
- créer un utilisateur (c'est-à-dire s'inscrire)
- gérer vos propres articles (créer, éditer, supprimer), si autorisé
- mettre à jour les informations personnelles si autorisé
- gérer vos propres commentaires si autorisé
Pour installer cette application, il suffit de la cloner à partir de github et d'exécuter npm install
et npm start
. Vous devez également démarrer le serveur MongoDB, l'application se connecte à mongodb://localhost:27017/blog
. Une fois que tout est prêt, vous pouvez jouer un peu et pour le rendre plus amusant, importez les données de db/
dossier db/
:
mongorestore ./db
Vous pouvez également suivre les instructions du fichier de projet README ou utiliser ma collection Postman .
Quel est le truc?
Premièrement, le grand avantage de CASL est qu'il vous permet de déterminer les droits d'accès en un seul endroit, pour tous les utilisateurs! Deuxièmement, CASL ne se concentre pas sur qui est l'utilisateur, mais sur ce qu'il peut faire, c'est-à-dire sur ses capacités. Cela vous permet de distribuer ces capacités à différents rôles ou groupes d'utilisateurs sans effort inutile. Cela signifie que nous pouvons enregistrer des droits d'accès pour les utilisateurs autorisés et non autorisés:
const { AbilityBuilder, Ability } = require('casl') function defineAbilitiesFor(user) { const { rules, can } = AbilityBuilder.extract() can('read', ['Post', 'Comment']) can('create', 'User') if (user) { can(['update', 'delete', 'create'], ['Post', 'Comment'], { author: user._id }) can(['read', 'update'], 'User', { _id: user._id }) } return new Ability(rules) } const ANONYMOUS_ABILITY = defineAbilitiesFor(null) module.exports = function createAbilities(req, res, next) { req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY next() }
Analysons maintenant le code écrit ci-dessus. Une instance de AbilityBuilder
-a est créée dans la fonction defineAbilitiesFor(user)
, sa méthode d'extraction divise cet objet en 2 fonctions simples can
et cannot
et un tableau de rules
( cannot
être utilisé dans ce code). Ensuite, en utilisant les appels à la fonction can
, nous déterminons ce que l'utilisateur peut faire: le premier argument transmet l'action (ou un tableau d'actions), le deuxième argument est le type de l'objet sur lequel l'action (ou un tableau de types) est effectuée et l'objet condition peut être passé comme troisième argument facultatif. L'objet condition est utilisé lors de la vérification des droits d'accès sur une instance de la classe, c'est-à-dire il vérifie si la propriété author
de l'objet post
et user._id
sont égales, si elles le sont, alors true
sera retourné, sinon false
. Pour plus de clarté, je vais donner un exemple:
// Post is a mongoose model const post = await Post.findOne() const user = await User.findOne() const ability = defineAbilitiesFor(user) console.log(ability.can('update', post)) // post.author === user._id, true
Ensuite, en utilisant if (user)
nous déterminons les droits d'accès de l'utilisateur autorisé (si l'utilisateur n'est pas autorisé, nous ne savons pas qui il est et nous n'avons pas d'objet contenant des informations sur l'utilisateur). À la fin, nous retournons une instance de la classe Ability
, à l'aide de laquelle nous vérifierons les droits d'accès.
Ensuite, nous créons la constante ANONYMOUS_ABILITY
, c'est une instance de la classe Ability
pour les utilisateurs non autorisés. Au final, nous exportons le middleware express, qui est responsable de la création d'une instance de Ability
pour un utilisateur spécifique.
API de test
Testons ce qui s'est passé avec Postman . Vous devez d'abord obtenir accessToken, pour cela, envoyez une demande:
POST /session { "session": { "email": "casl@medium.com", "password": "password" } }
Vous obtenez quelque chose comme ça en réponse:
{ "accessToken": "...." }
Ce jeton doit être inséré dans l'en- Authorization header
et envoyé avec toutes les demandes suivantes.
Essayons maintenant de mettre à jour l'article.
PATCH http://localhost:3030/posts/597649a88679237e6f411ae6 { "post": { "title": "[UPDATED] my post title" } } 200 Ok { "post": { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } }
Tout fonctionne bien. Mais que se passe-t-il si nous mettons à jour l'article de quelqu'un d'autre?
PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" }
Vous avez une erreur! Comme prévu :)
Imaginons maintenant que pour les auteurs de notre blog, nous voulons créer une page où ils peuvent voir tous les articles qu'ils peuvent mettre à jour. Du point de vue de la logique spécifique, ce n'est pas difficile, il vous suffit de sélectionner tous les articles dans lesquels l'auteur est égal à user._id
. Mais nous avons déjà enregistré une telle logique avec l'aide de CASL, il serait très pratique d'obtenir tous ces articles de la base de données sans écrire de demandes supplémentaires, et si les droits changent, alors la demande devra être modifiée - travail supplémentaire :).
Heureusement, CASL a un package npm supplémentaire - @ casl / mongoose . Ce package vous permet d'interroger les enregistrements de MongoDB selon des autorisations spécifiques! Pour mangouste, ce package fournit un plugin qui ajoute la méthode accessibleBy(ability, action)
au modèle. En utilisant cette méthode, nous demanderons également des enregistrements à la base de données (en savoir plus à ce sujet dans la documentation CASL et le fichier de package README ).
C'est exactement comment le handler
de /posts
implémenté (j'ai également ajouté la possibilité de spécifier pour quelle action vous devez vérifier les autorisations):
Post.accessibleBy(req.ability, req.query.action)
Ainsi, afin de résoudre le problème décrit précédemment, ajoutez simplement le paramètre action=update
:
GET http://localhost:3030/posts?action=update 200 Ok { "posts": [ { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } ] }
En conclusion
Grâce à CASL, nous avons un très bon moyen de gérer les droits d'accès. Je suis plus que sûr que la construction de type
if (ability.can('read', post)) ...
beaucoup plus clair et plus facile que
if (user.role === ADMIN || user.auth && todo.author === user.id) ...
Avec CASL, nous pouvons être plus clairs sur ce que fait notre code. De plus, ces vérifications seront certainement utilisées ailleurs dans notre application, et c'est ici que CASL aidera à éviter la duplication de code.
J'espère que vous avez été aussi intéressé par la lecture de la LCAP que par sa création. CASL a une assez bonne documentation , vous y trouverez probablement beaucoup d'informations utiles, mais n'hésitez pas à poser des questions si dans le chat gitter et à ajouter un astérisque sur le github ;)