Should a large JavaScript application be isomorphic

Interview with Sergii Stotskyi

Developer: Hello Sergii, you are the developer of CASL and we want to talk a little about that now. Let's start from the basics: What is CASL anyway?

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

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 a comprehensive variant based on attributes.

CASL is declarative. It allows the definition of permissions in-memoy with a domain-specific language that corresponds almost word for word to business requirements.

CASL is type safe. It's written in TypeScript, which makes apps safer and the developer experience more enjoyable.

CASL is small - it's only ~ 4.5kb mingzipped and can get even smaller thanks to treeshaking. The minimum size is ~ 1.5kb.

Developer: What kind of project should CASL be used for?

Stotskyi: Whenever one of the requirements of an application is that access control must be implemented. CASL implements ABAC (Attribute Based Access Control) at its core, but it can also be used successfully to implement RBAC (Role Based Access Control) and even works for claim-based versions.

CASL can also be integrated with databases so that it can be used to query documents that can be accessed. MongoDB and Mongoose are currently supported. The implementation of SQL support is planned for the near future. As far as I know, there are also successful integrations of CASL with Objection.js, Sequelize and GraphQL.

Developer: You rewrote CASL 4.0 in TypeScript. This is done a lot in the JavaScript ecosystem. Why did you choose this step?

Stotskyi: TypeScript has been one of the hottest topics in the JavaScript community in recent years. In my experience, enterprise applications are mostly written in statically typed languages. This way you can ensure that the written code on the build stage is then also valid by checking the types. In addition, modern IDEs offer hints almost immediately, so that developers can discover errors even before the build phase. This increases confidence in the application that is created. I want CASL users to be sure that their app is secure.

CASL has supported TypeScript since the early versions, but used handwritten declaration files. It was a hassle to update and I kept forgetting about it every time I released a new feature.

CASL 4.0 is completely rewritten in TypeScript. Now I can be sure that the types match the latest features. In addition, the new types are more advanced and helpful compared to the ones you wrote yourself. This allows IDEs to output information about which actions or subjects can be used or which MongoDB operators can be used in conditions. This prevents typos from developing in these places.

Developer: What other important innovations does CASL 4.0 bring with it?

Stotskyi: The main goals for version 4.0 were:

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

TypeScript support just got a lot better! In 4.0, the Ability class accepts two optional generic parameters. The first of these limits which actions are possible with which subjects; the second defines the shape of the Conditions object. By default, the Ability class uses MongoDB Conditions, so you only have to specify one parameter - the Application Abilities.

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

import {Ability} from '@ casl / ability'; type AppAbilities = ['read' | 'update' | 'delete' | 'create', 'Article' | 'Comment' | 'User']; const ability = new Ability (); ability.can ('raed', 'Post'); // typo is intentional

If you check the abilities, the IDE will now make suggestions as to which options are available and TypeScript ensures that you have made a typo! So the example above will not compile and will give an error that there is no action named raed. That is not all. You can make your code even stricter by defining the possible combinations of action and subject. Further information is available in the documentation.

33 percent of the issues in the CASL repository are questions. From this it was concluded that the documentation needs to be improved. The Docs app was rewritten from the ground up and now uses rollup and lit-element, but that's a different story. The CASL documentation is now more beginner-friendly and has a cookbook section that gives recommendations on when, how and why to look at the permissions logic of your application.

It is my goal to make CASL very extensive and at the same time to have a minimal influence on the resulting bundle size. This is important for front-end applications. That's why 4.0 is smaller and has better tree-shaking support. To do that, I had to make some breaking changes:

  • AbilityBuilder.extract: the method has been replaced by its constructor
  • AbilityBuilder.define has been replaced by the defineAbility function. The function is normally not used in applications, so it can now be removed thanks to tree shaking.
  • Ability.addAlias ​​has been replaced by createAliasResolver whose usage is clearer. The following code was used before:
import {Ability} from '@ casl / ability'; Ability.addAlias ​​('modify', ['create', 'update']); const ability = new Ability (); ability.can ('modify', 'Post'); `` Now we can write the following: import {Ability, createAliasResolver} from '@ casl / ability'; const resolveAction = createAliasResolver ({modify: ['create', 'update']}); const ability = new Ability ([], {resolveAction}); ability.can ('modify', 'Post');
  • since the aliasing functionality has been refactored and is now also tree-shakable, the standard alias crud has been removed and must now be defined manually.
  • Minor breaking changes in the supplementary packages. By “smaller” I mean changes that reflect the type changes in @ casl / ability and the removal of the standard instantiation of the Ability instance in all packages.

As always, the breaking changes and the migration guide can be found in the Changelog.md of the respective packages.

One of the most powerful new features that have been added is the ability to customize Ability. From 4.0 it is possible to implement your own Conditions Matcher. Instead of the MongoDB query language, we can use normal functions or JSON schema. We can even use any Object Validation Library (e.g. joi). The only limits are our imagination and the TypeScript interface 😉 More details can be found in the documentation.

Developer: CASL offers individual packages for frameworks such as Vue.js or Angular. Are all of them on the same feature level or are there differences that have to be considered when using them?

Stotskyi: I try to keep the complementary packages on the same level as the framework. Personally, I've only had commercial experience with Vue and Angular, but I've read a lot about React and Aurelia. In my free time I also help friends with their projects. This allows me to test the additional packages in terms of developer experience (DX).

The packages for Vue and React include a component with which the visibility of UI elements can be switched on and off based on the consent of the user. This works fine in most cases, but sometimes you have to write imperative code. This used to be easier in Vue apps because we could just write a $ can method:

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

But since React now has hooks, and thanks to the contribution by David Acevedo, we recently published the useAbility Hook in @ casl / react, which simplifies the imperative use of CASL in React applications. The hook allows us to use the Ability instance and update the appropriate React component when the rules of Ability are updated.

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

The request to update @ casl / angular to Angular 9.0 (released February 6th) was made on February 13th. The request was implemented and closed on the same day. However, there is one thing that bothers me about the Angular integration. It was written with Impure Pipes. Impure pipes could become a performance bottleneck at some point. The request to allow pure pipes to subscribe to asynchronous sources and update themselves was created on March 9, 2017, but has not yet been implemented on April 20, 2020. This is a pity. That's why I created an issue to add support for the can Structural Directive for @ casl / angular. This directive works just like pipes, but the change detection cycle has better performance.

@ casl / aurelia is probably the least used supplementary package, presumably because the Aurelia community itself is comparatively small. As far as I can remember, there has not been a single request in the past three years for bugs that need to be fixed or for something to be updated in the code related to Aurelia. Aurelia is very similar to Angular, which is why the Aurelia support is very similar to Angular's. @ casl / aurelia provides a value converter (analogous to Angulars Pipe). At least Aurelia doesn't have the performance problem that arises from Angular's Impure Pipes. 🙂

Developer: Do you have a practical tip for people getting started with CASL?

Stotskyi: After all of this, some are probably wondering where to get more information. I would recommend starting with the CASL Guide. Then you should read something about the supplementary package for the framework of choice. If you are looking for examples of integrations with the most popular frameworks (frontend and backend), you should take a look at my medium blog and the CASL Examples Monorepo (still in progress)

Finally, I would like to talk about a few typical stumbling blocks:

When working with CASL, you should think about what the user can do in the application, not who he is or what role he has. Roles can easily be mapped to groups of actions. This kind of detour helps to add new roles to an application with ease.

There is one trap that developers very often fall into:

Ability and AbilityBuilder have methods that are named the same (can and cannot), but it is important to understand that they mean something different and have different functions. If we define our permissions like this:

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

... we can't check this in exactly the same way, so the following code is wrong:

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

At first glance, that looks nonsensical. It makes sense that you should be able to define and check permissions in the same way. But there are two reasons why this is not implemented:

1. The Conditions Object (3rd argument of AbilityBuilder.can) is not a pure object, but can contain a subset of a MongoDB query. So what do we expect from this permissions check?

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

2. I prefer objects to contain information about what they are so that one can distinguish whether an object is an article, a page, or a comment. This problem could become more complex when you have a number of objects whose shapes overlap. Even TypeScript doesn't help!

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

This is not the case with classes, by the way. An instance of a class always includes a reference to the constructor that was used to create it. This means that the type can easily be read from an instance.

These are important reasons why the permissions check in CASL looks like this (as the correct version of the example above):

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

But don't worry, CASL won't throw a runtime error if it detects an attempt to use it incorrectly. If you want to know more about the built-in subject-type detection logic, you can read something about it in the documentation.

I hope you enjoyed reading it and you will use CASL in your next project!

Developer: Thank you for the interview!

Sergii Stotskyi is a technical lead with more than 11 years of experience in web development, 5 years of which he managed teams of 3-7 people. He likes open source, reading books and skating. Hates laziness 🙂