Managing Access Rights in Expressjs with CASL



In modern applications that support authentication, we often want to change what is visible to the user, depending on their role. For example, a guest user can see an article, but only a registered user or an administrator sees a button for deleting this article.


Managing this visibility can be a complete nightmare with increasing roles. You probably already wrote or saw code like this:


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!' }) } 

This code is distributed throughout the application and usually becomes a big problem when the customer changes requirements or asks to add additional roles. In the end, you need to go through all such if-s and add additional checks.


In this article, I will show an alternative way to implement permissions management in the Expressjs API using a library called CASL . It greatly simplifies the management of access rights and allows you to rewrite the previous example to something like this:


 if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) } 

First hear about CASL? I recommend to read. What is CASL?


Note: This article was originally published on Medium.


Demo application


As a test application, I made a fairly simple REST API for a blog . The application consists of 3 entities ( User , Post and Comment ) and 4 modules (one module for each entity and one more for verification of authorization). All modules can be found in the src/modules folder.


The application uses mongoose models, passportjs authentication and authorization (or authorization) based on CASL. Using the API, the user can:



To install this application, simply clone it from github and run npm install and npm start . You also need to start the MongoDB server, the application connects to mongodb://localhost:27017/blog . After everything is ready, you can play a little, and to make it more fun, import the basic data from the db/ folder:


 mongorestore ./db 

Alternatively, you can follow the instructions in the project's readme file or use my Postman collection .


What is the trick?


First of all, a big advantage of CASL is that it allows you to define access rights in one place, for all users! Secondly, CASL focuses not on who the user is, but on what he can do, i.e. on its capabilities. This allows you to distribute these capabilities to different roles or groups of users without any extra effort. This means that we can register access rights for authorized and non-authorized users:


 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() } 

Let's now analyze the code written above. The defineAbilitiesFor(user) function creates an instance of AbilityBuilder -a, its extract method splits this object into 2 simple functions can and cannot and an array of rules (it cannot be used in this code). Next, using the can function calls, we determine what the user can do: the first argument passes the action (or an array of actions), the second argument is the type of the object the action is being taken on (or an array of types), and the third optional argument can be passed the condition object. The condition object is used when checking permissions on a class instance, i.e. it checks to see if the author property of the post object and user._id are equal; if they are equal, then it returns true , otherwise false . For greater clarity, I will give an example:


 // 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 

Further, with the help of if (user) we determine the access rights for the authorized user (if the user is not authorized, then we do not know who he is and do not have an object with information about the user). In the end, we return an instance of the Ability class, with the help of which we will check access rights.


Next we create the ANONYMOUS_ABILITY constant; it is the instance of the Ability class for non-authorized users. Finally, we export the express middleware, which is responsible for creating the instance Ability for a specific user.


Testing API


Let's test what we did using Postman . First you need to get accessToken, for this send a request:


 POST /session { "session": { "email": "casl@medium.com", "password": "password" } } 

In return, you will get something like this:


 { "accessToken": "...." } 

This token must be inserted into the Authorization header and sent with all subsequent requests.


Now let's try to update the 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 } } 

Everything works well. And what if we update someone else's article?


 PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" } 

Got a mistake! As expected :)


And now let's imagine that for the authors of our blog we want to create a page where they can see all the posts that they can update. From the point of view of specific logic, this is easy, just need to select all articles in which author equals user._id . But we have already registered such logic with the help of CASL, it would be very convenient to get all such articles from the database without writing unnecessary queries, and if the rights change, then you will have to change the query - too much work :).


Fortunately, CASL has an additional npm package - @ casl / mongoose . This package allows you to request entries from MongoDB in accordance with certain access rights! For mongoose, this package provides a plugin that adds an accessibleBy(ability, action) method to the model. With this method we will request records from the database (for more, read the CASL documentation and the README package file ).


This is exactly how handler for /posts implemented (I also added the ability to specify for which action the access rights should be checked):


 Post.accessibleBy(req.ability, req.query.action) 

So, in order to solve the problem described earlier, just add the action=update parameter:


 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 } ] } 

Finally


Thanks to CASL, we have a really good way to manage access rights. I am more than sure that the construction type


 if (ability.can('read', post)) ... 

much clearer and easier than


 if (user.role === ADMIN || user.auth && todo.author === user.id) ... 

With CASL, we can be more clear about what our code does. In addition, such checks will certainly be used elsewhere in our application, and it is here that CASL helps to avoid code duplication.


I hope you were just as interested in reading about CASL, as far as I was interested in creating it. CASL has pretty good documentation , you will surely find a lot of useful information there, but do not hesitate to ask questions if there is something in the gitter chat and add an asterisk on the githab ;)

Source: https://habr.com/ru/post/414951/


All Articles