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.
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. 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.
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.
by Manfred Steyer (SOFTWAREarchitekt.at)
by Andrey Goncharov (Hazelcast)
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 () => {
const ability = useAbility(AbilityContext);
const createPost = () => { /* implementation */ };
return ability.can('create', 'Post')
? <button onClick={createPost}>Create Post</button>
: 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) => {
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) => {
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);
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!
The post “CASL is an isomorphic JavaScript permission management library” appeared first on JAXenter.