days
0
-24
-4
hours
-2
0
minutes
-4
-8
seconds
-4
-4
search
Interview with Sergii Stotskyi

“CASL is an isomorphic JavaScript permission management library”

Ann-Cathrin Klose
CASL
© Shutterstock / vladwel

CASL is a library that is designed to make managing permissions easier. We spoke to CASL’s developer Sergii Stotskyi about the library. When do you need CASL, what has changed with version 4.0 and what are some typical stumbling blocks?

JAXenter: Hi Sergii, you are the developer of CASL, so could you tell us what it is and who should be using it?

CASL is an isomorphic JavaScript permission management library.

Sergii Stotskyi: CASL is an isomorphic JavaScript permission management library. The fancy word “isomorphic” means that you can use the library on both frontend and backend in exactly the same way.

What else can I say about CASL?

CASL is versatile, you can start with a simple claim based access control and scale your solution to fully-featured attribute based access control.

CASL is declarative, it allows you to define permissions in the memory using a domain-specific language that matches your business requirements almost word for word.

CASL is TypeSafe, it’s written in TypeScript, this makes apps safer and developer experience more enjoyable.

CASL is small, it’s just ~4.5KB mingzipped and can be even smaller, thanks to tree-shaking! The minimum size is ~1.5KB.

SEE ALSO: How long does it take to learn JavaScript?

JAXenter: When should you use CASL?

Sergii Stotskyi: Whenever you have a requirement to implement Access Control in the application. CASL, in its core, implements ABAC (i.e., Attribute Based Access Control), but it can be successfully used to implement RBAC (Role Based Access Control) and even Claim based access control.

Moreover, CASL can be integrated with databases, so you can use it to query accessible records! Currently it officially supports MongoDB and Mongoose. Support for SQL is planned to be implemented in the nearest future. From what I know, there are successful integrations of CASL with Objection.js, Sequelize and GraphQL.

JAXenter: With CASL 4.0 you did a re-write in TypeScript. Quite a lot of JavaScript libraries and frameworks make that step at some point. What was the reason you decided to do that for CASL too?

Sergii Stotskyi: Well, TypeScript has been one of the hottest topics in the JavaScript community in recent years. From my experience, enterprise applications are usually built using statically typed languages. This allows to ensure that written code is valid on the build step, just by checking types. Moreover, a modern IDE provides hints almost instantaneously, so the developer can spot the error even before the build step. This brings higher confidence in the resulting app. I want users of CASL to be confident that their app is safe.

CASL supported TypeScript from the early versions but it was by handwritten declaration files. It was tedious to update them and I used to forget to do that when a new feature was released.

CASL 4.0 is rewritten in TypeScript.

CASL 4.0 is rewritten in TypeScript. Now, I’m sure that types are in sync with the latest features. Moreover, new types are more advanced and helpful in comparison to the handwritten ones. They allow an IDE to give you hints on what actions and/or subjects can be used, and what MongoDB operators you can use in conditions, so you are protected from making typo mistakes in action or subject names.

JAXenter: What other changes were published in CASL 4.0 that users should definitely know about?

The main goals of the 4.0 release were

  • comprehensive TypeScript support
  • better documentation
  • better tree-shaking support

TypeScript support was improved a lot! In 4.0, the Ability class accepts 2 optional generic parameters. The 1st parameter restricts which actions can be done on which subjects and the 2nd defines the shape of the conditions object. By default, the Ability class uses MongoDB conditions, so you need to specify only one parameter – application abilities.


For example, in a blog app, we have Article, Comment, and User on which we can do CRUD operations:

import { Ability } from '@casl/ability';

type AppAbilities = [
'read' | 'update' | 'delete' | 'create',
'Article' | 'Comment' | 'User'
];
const ability = new Ability<AppAbilities>();

ability.can('raed', 'Post'); // typo is intentional

When you check abilities, your IDE will suggest which options you have and TypeScript will ensure that you haven’t made a typo! The example above won’t compile with an error that the raed action does not exist. That’s not all and you can make your code even stricter by defining possible combinations of action and subject. To get more details on CASL TypeScript support, check the documentation.

33% of all issues are questions in the CASL repository. This was a good hint that the documentation needs to improve. The docs’ app is written from scratch using rollup and lit-element but it’s a different story. Now, CASL’s documentation is more beginner-friendly and has a cookbook section, so you have a source of recommendations that explain when, how and why you should approach permissions logic in your application.

I strive to make CASL to be very powerful and at the same time with minimal impact on the resulting bundle size.

I strive to make CASL to be very powerful and at the same time with minimal impact on the resulting bundle size. This is important for frontend applications. That’s why 4.0 is smaller and brings better tree-shaking support. To achieve this, I needed to introduce several breaking changes:

  • The AbilityBuilder.extract method was replaced by its constructor
  • AbilityBuilder.define was replaced by the defineAbility function. Usually, this function is not used in application code, so now it can be removed thanks to tree-shaking.
  • Ability.addAlias was replaced by createAliasResolver which is more clear in usage. So, instead of:
import { Ability } from '@casl/ability';

Ability.addAlias('modify', ['create', 'update']);
const ability = new Ability();

ability.can('modify', 'Post');

We can write:

import { Ability, createAliasResolver } from '@casl/ability';

const resolveAction = createAliasResolver({ modify: ['create', 'update'] });
const ability = new Ability([], { resolveAction });

ability.can('modify', 'Post');
  • as aliasing functionality was refactored to be tree-shakable, default `crud` alias was removed. Now, you need to define it manually.
  • minor breaking changes in complementary packages. By minor, I mean changes in types that reflects changes in @casl/ability types and removal of default instantiation of the Ability instance in all packages.

As usual, you can find all breaking changes and a migration guide in the CHANGELOG.md file of the corresponding complementary package.

One of the powerful new features that were added is Ability customization. From 4.0, we can implement our own conditions matcher, so instead of the MongoDB query language we can use regular functions or the JSON Schema. Actually, we can use any object validation library (e.g., joi), we are restricted only by our imagination and the TypeScript interface ;) To get more details about how to do this, you can read in Customize Ability.

JAXenter: CASL offers individual packages for frameworks like Vue.js or Angular. Are all the packages on the same level, featurewise? If you want to use the latest features with an Angular project, how do you approach that?

Sergii Stotskyi: I strive to keep complementary packages up to date with the latest changes in frameworks. I personally have commercial experience only with Vue and Angular but I read a lot about React and Aurelia. I also help my friends with their projects in my free time. This is what allows me to test complementary packages in terms of Developer Experience (DX).

I strive to keep complementary packages up to date with the latest changes in frameworks.

Packages for Vue and React provide the <Can> component that allows you to toggle the visibility of UI elements based on users’ permission to see them. This works good in the majority of cases but sometimes we need to write imperative code. Previously, it was easier in Vue apps because we could just use the $can method:

export default {
  methods: {
    createPost() {
      if (!this.$can('create', 'Post')) {
        alert('You are not allowed to create posts');
        return;
      }
      // implementation
    }
  }
}

But with the release of React’s hooks, and thanks to David Acevedo’s contribution, we recently released the useAbility hook in @casl/react which simplifies the imperative usage of CASL in React apps. It allows to get the Ability instance and update React’s corresponding component when Ability rules are updated:

import { useAbility } from '@casl/react';
import { AbilityContext } from './Can';
 
export default () =&gt; {
  const ability = useAbility(AbilityContext);
  const createPost = () =&gt; { /* implementation */ };
 
  return ability.can('create', 'Post')
    ? &lt;button onClick={createPost}&gt;Create Post&lt;/button&gt;
    : null;
};

The request to update @casl/angular to Angular 9.0 (released on 6 Feb) was created on 13 Feb. On the same day, that request was implemented and closed. However, there is one thing which I don’t like about Angular integration, it’s done using impure pipe. Impure pipes may become a performance bottleneck at some point. The request to allow pure pipes to subscribe to async source and update itself was made on Mar 9, 2017, but even today, on Apr 20, 2020, it has not been implemented yet! It’s a pity. That’s why I created an issue to add support for the can structural directive to @casl/angular. This directive works in the same way as pipe but its change detection cycle is run with better performance.

@casl/aurelia is probably the less used complementary package, probably due to the relatively small Aurelia community. As far as I remember, there was not any request to fix or update something in the code related to Aurelia for the last 3 years. Anyway, Aurelia is very similar to Angular, that’s why support for Aurelia is similar to Angular’s. @casl/aurelia provides a value converter (this is analogous to Angular’s pipe). At least, Aurelia doesn’t have the performance issue which Angular impure pipes have :)

JAXenter: Is there any practical advice you’d like to give our readers on how to get started with CASL?

When you work with CASL, think in terms of what a user can do in your application and not who he is or which role he has.

Sergii Stotskyi: After all this, you probably have a question where to find more. I’d recommend to start from the CASL guide. Then read about the complementary package for the framework of your choice. Finally, if you want to find examples of integrations with popular frameworks (including frontend and backend ones), check my medium blog and the CASL examples monorepo (a work in progress).

Finally, let’s talk about what to keep in mind and common pitfalls:

When you work with CASL, think in terms of what a user can do in your application and not who he is or which role he has. Roles can be easily mapped to groups of actions. This level of indirection helps to add new roles into the application as easy as a pie.

There is one pitfall which developers usually get into:

Ability and AbilityBuilder have methods with the same name (can and cannot) but it’s crucial to understand that they are different in meaning and functionality! If we define our permissions:

import { defineAbility } from '@casl/ability';
 
const ability = defineAbility((can) =&gt; {
  can('read', 'Article', { userId: 1 });
});

We cannot check it in exactly the same way, so the next code is wrong:

ability.can('read', 'Article', { userId: 1 });

At first glance, it looks like nonsense, it seems so logical for permissions to be defined and checked in the same way. But there are 2 reasons why this won’t be implemented:

1. The conditions object (3rd argument of AbilityBuilder.can) is not a plain object, it can contain a subset of a MongoDB query. So, what are your expectations for this permissions check?

import { defineAbility } from '@casl/ability';
 
const ability = defineAbility((can) =&gt; {
  can('read', 'Article', {
    userId: { $eq: 1 },
    createdAt: { $lte: Date.now() }
  });
});

2. I prefer objects to contain information about what they are. So, we can distinguish whether an object is an article, page or comment. The issue may become more complex if you have a bunch of objects whose shapes intersect, even TypeScript can’t help here!

interface Comment {
  body: string
  authorId: number
}
 
interface Article {
  title: string
  body: string
  authorId: number
}
 
const article: Article = {
  "title": "CASL",
  "body": "...",
  "authorId": 1
};
const comment: Comment = article; // bug here! No error from TypeScript. Did you know that?
console.log(comment);

SEE ALSO: Deno 1.0 – “Deno is a web browser for command-line scripts”

This is not the case for classes however. An instance of a class always has a reference to the constructor that was used to create it. This allows you to easily get the type from an instance.

These are important reasons checking permissions in CASL looks like this (the correct usage for the example above):

import { subject as an } from '@casl/ability';
 
ability.can('read', an('Article', { userId: 1 }));

But don’t worry, CASL throws a runtime error when it detects an attempt of incorrect usage. If you want to know more about built-in Subject Type Detection logic, read about in the documentation.

Hopefully, you enjoyed reading this and are going to use CASL at least on your next project :)

JAXenter: Thank you for the interview!

Author
Ann-Cathrin Klose

Ann-Cathrin Klose

All Posts by Ann-Cathrin Klose

Ann-Cathrin Klose is an editor and has been working for S&S Media since 2015. Before joining the team she studied General Linguistics at Johannes Gutenberg University Mainz.

Leave a Reply

avatar
400