Nowadays, almost every application has the concept of access rights and provides different functions for different groups of users (for example, admin, member, subscriber, etc.). These groups are usually called "roles."
From my own experience I will say that the logic of access rights of most applications is built around roles (the test sounds like this: if the user has this role, then he can do something) and eventually we have a massive system, with many complex checks that are difficult to maintain. This problem can be solved with the help of CASL.
CASL is a library for authorization in JavaScript, which makes you think about what the user can do in the system, and not what role he has (the test sounds 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 divide these abilities between two groups of users: anonymous users (those who have not identified in the system) and writers (those who identified in the system).
Anonymous users can only read articles and comments. Writers can do the same thing plus manage their articles and comments (in this case, “manage” means to 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, it is possible to determine what the user can do not only on the basis of 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, and so on, to view the content. With CASL, all this can be described in one place!
In addition, you can use certain operators from the query language for MongoDB to define conditions. For example, you can give delete articles, provided that they have no comments:
can('delete', 'Post', { 'comments.0': { $exists: false } })
There are 3 methods in the Ability
instance that 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 returns false
, the second true
, and the third throws ForbiddenError
for an anonymous user, since they are not allowed to update articles. As a second argument, these methods can take an instance of a class:
const post = new Post({ title: 'What is CASL?' }) ability.can('read', post)
In this case, can ('read', post)
returns true
, because in the capabilities we determined that the user can read all the articles. Object type is calculated based on constructor.name
. It can be overridden by creating a static modelName
property on the Post
class, this may be necessary if the production of the assembly uses the minification of function names. You can also write your own function to determine the type of the object and pass it as an option to the Ability
constructor:
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 other author’s anotherId
as anotherId
and to the current user myId
as myId
):
const post = new Post({ title: 'What is CASL?', authorId: 'anotherId' }) ability.can('update', post)
In this case, can('update', post)
returns false
, since we have determined that the user can update only his own articles. Of course, if you check the same on your own article, then we get true
. More information about access checks can be found in the section Check Abilities in the official documentation.
CASL provides functions that allow you to convert the described access rights into database queries. Thus, it is quite easy to get all the records from the database to which the user has access. At the moment, the library only supports MongoDB and provides tools for writing integration with other query languages.
To convert permissions to Mongo, the request exists toMongoQuery
function:
import { toMongoQuery } from 'casl' const query = toMongoQuery(ability.rulesFor('read', 'Post'))
In this case, the query
will be an empty object, because the user can read all the articles. Let's check what will be the output for the update operation:
// { $or: [{ authorId: 'myId' }] } const query = toMongoQuery(ability.rulesFor('update', 'Post'))
Now the query
contains a query that should return only those records that were created by me. All the usual rules go through a chain of logical OR, so you see the $or
operator as a result of the query.
CASL also provides a plug-in for mongoose , which adds an accessibleBy
method to models. This method under the hood calls the toMongoQuery
function and passes the result to the find
mongoose 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, accessibleBy
will create a request based on read
access rights. To build a query for another action, just pass it a second argument. More details can be found in the Database Integration section.
CASL is written on pure ES6, so it can be used for authorization both on the API and on the UI side. An additional advantage is that the UI can request all access rights from the API, and use them to show or hide buttons or whole sections on the page.
Source: https://habr.com/ru/post/334076/
All Articles