Back to Home

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

Managing Access Rights in Expressjs with CASL

  • Tutorial



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 administrator sees a button to delete 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!' })
}

New to 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, Postand Comment) and 4 modules (one module for each entity and one more to verify authorization). All modules can be found in the folder src/modules.


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


  • read all articles and comments
  • create user (i.e., register)
  • manage your own articles (create, edit, delete) if authorized
  • update personal information if authorized
  • manage your own comments if authorized

To install this app, just clone it from github and run npm installand 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 folder db/:


mongorestore ./db

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


What is the trick?


Firstly, the big plus 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')
functiondefineAbilitiesFor(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 })
  }
  returnnew Ability(rules)
}
const ANONYMOUS_ABILITY = defineAbilitiesFor(null)
module.exports = functioncreateAbilities(req, res, next) {
  req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY
  next()
}

Let's now analyze the code written above. The function defineAbilitiesFor(user)creates an instance AbilityBuilder-a, his method extract breaks the object into 2 simple functions canand cannotand an array rules(in this code cannotis not used). Then, using function calls, canwe 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 over which the action is performed (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 property of the authorobject is equal postand user._id, if equal, it will return true, otherwisefalse. For greater clarity, I will give an example:


// Post is a mongoose modelconst post = await Post.findOne()
const user = await User.findOne()
const ability = defineAbilitiesFor(user)
console.log(ability.can('update', post)) // если post.author === user._id, то вернется true

Next, with the help 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). At the end, we return an instance of the class Ability, with the help of which we will check the access rights.


Next, create a constant ANONYMOUS_ABILITY; it is a Abilityclass instance for unauthorized users. At the end, we export express middleware, which is responsible for creating an Abilityinstance 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": "[email protected]",
    "password": "password"
  }
}

In return, you will get something like this:


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

This token must be inserted into Authorization headerand 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"
  }
}
200Ok
{
  "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 :)


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 is equal 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 extra requests, and even if the rights change, then you will have to change the request - 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 a method accessibleBy(ability, action)to the model. Using this method, we will request records from the database (for more, read the CASL documentation and the README package file ).


This is exactly how it is implemented handlerfor /posts(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, it is enough to add a parameter action=update:


GET http://localhost:3030/posts?action=update200 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 confident 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 duplication of code.


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 githaba ;)

Read Next