CASL. Authorization for JavaScript application
Nowadays, almost every application has the concept of access rights and provides various functions for different user groups (for example, admin, member, subscriber, etc.). These groups are commonly called "roles."
From my own experience, I will say that the logic of access rights for most applications is built around roles (the verification is as follows: if the user has this role, then he can do something) and ultimately we have a massive system with many complex checks that are difficult to maintain. This problem can be solved with CASL.
CASL is a JavaScript authorization library that makes you think about what the user can do on the system, and not what role he has (the check is like this: if the user has this ability, then he can do it). For example, in a blogging application, a user can create, edit, delete, view articles and comments. Let's share these abilities between two groups of users: anonymous users (those who are not identified in the system) and writers (those who are identified in the system).
Anonymous users can only read articles and comments. Writers can do the same plus manage their articles and comments (in this case, “manage” means create, read, update and delete). With CASL, you can write it like this:
import { AbilityBuilder } from 'casl'
const user = whateverLogicToGetUser()
const ability = AbilityBuidler.define(can => {
can('read', ['Post', 'Comment'])
if (user.isLoggedIn) {
can('create', 'Post')
can('manage', ['Post', 'Comment'], { authorId: user.id })
}
})Thus, you can determine what the user can do not only based on roles, but also on the basis of any other criteria. For example, we can allow users to moderate other comments or messages based on their reputation, only allow people who have confirmed that they are 18 years old to view the content, etc. With CASL, all of this can be described in one place!
In addition, some operators from the query language for MongoDB can be used to determine conditions. For example, you can give articles to delete, provided that they have no comments:
can('delete', 'Post', { 'comments.0': { $exists: false } })Checking the possibilities
There are 3 methods on an instance Abilitythat allow you to check permissions:
import { ForbiddenError } from 'casl'
ability.can('update', 'Post')
ability.cannot('update', 'Post')
try {
ability.throwUnlessCan('update', 'Post')
} catch (error) {
console.log(error instanceof Error) // true
console.log(error instanceof ForbiddenError) // true
}The first method will return false, the second true, and the third will throw ForbiddenErrorfor an anonymous user, since they do not have the right to update articles. As a second argument, these methods can take an instance of the class:
const post = new Post({ title: 'What is CASL?' })
ability.can('read', post)In this case, it can ('read', post)returns true, because in the abilities we determined that the user can read all articles. The type of object is calculated based on constructor.name. It can be redefined by creating a static property modelNameon the class Post, this may be needed if function names are minified for assembly production. You can also write your own function to determine the type of object and pass it as an option to the constructor Ability:
import { Ability } from 'casl'
function subjectName(subject) {
// custom logic to detect subject name, should return string or undefined
}
const ability = new Ability([], { subjectName })Let's now check the case when a user tries to update another user's article (I will refer to the identifier of another author as anotherIdwell as to the identifier of the current user as myId):
const post = new Post({ title: 'What is CASL?', authorId: 'anotherId' })
ability.can('update', post)In this case, can('update', post)returns false, because we have determined that the user can only update his own articles. Of course, if you check the same thing on your own article we will get true. For more information about access checks, see the Check Abilities section in the official documentation.
Database Integration
CASL provides functions that allow you to convert the described permissions into database queries. Thus, it is quite easy to get all the records from the database to which the user has access. Currently, the library only supports MongoDB and provides tools for writing integration with other query languages.
To convert access rights to Mongo request, there is a toMongoQueryfunction:
import { toMongoQuery } from 'casl'
const query = toMongoQuery(ability.rulesFor('read', 'Post'))In this case, it querywill be an empty object, because the user can read all articles. Let's check what will be the output for the update operation:
// { $or: [{ authorId: 'myId' }] }
const query = toMongoQuery(ability.rulesFor('update', 'Post'))Now it querycontains a query that should return only those records that were created by me. All the usual rules go through the logical OR chain, which is why you see the $oroperator as a result of the request.
CASL also provides a mongoose plugin that adds a accessibleBymethod to models. This method calls the function under the hood toMongoQueryand passes the result to the findmongoose-a method .
const { mongoosePlugin, AbilityBuilder } = require('casl')
const mongoose = require('mongoose')
mongoose.plugin(mongoosePlugin)
const Post = mongoose.model('Post', mongoose.Schema({
title: String,
author: String,
content: String,
createdAt: Date
}))
// by default it asks for `read` rules and returns mongoose Query, so you can chain it
Post.accessibleBy(ability).where({ createdAt: { $gt: Date.now() - 24 * 3600 } })
// also you can call it on existing query to enforce visibility.
// In this case it returns empty array because rules does not allow to read Posts of `someoneelse` author
Post.find({ author: 'someoneelse' }).accessibleBy(ability, 'update').exec()By default, it accessibleBywill create a request based on readaccess rights. To build a query for another action, just pass it a second argument. More details can be found in the Database Integration section .
And finally
CASL is written in pure ES6, so it can be used for authorization both on the API and on the UI side. An additional plus is that the UI can request all access rights from the API, and use them to show or hide buttons or entire sections on the page.