Why would I need a CASL in my app
If I would ask you to add managing user permissions in the existing React app, how do you solve it in the first place? In my case, it would have been the most react-ish way:
- Write a hook that encapsulates the logic of getting a user role.
- Use this hook inside a component to check the user role for correspondence.
const useRole = () => {
const user = useUser()
return user.role
}
const Component = () => {
const role = useRole()
if (role === 'admin') {
return <AdminComponent />
}
return <PublicComponent />
}
As much as the application grows, the logic of displaying or hiding some parts of the application becomes more complex. Tomorrow you will have two more roles that will need to check, the next day the logic of role permissions will change, for example, you would need to also check the status of the subscription. And for every permissions-based functionality, you will need to change all conditions to correspond to the new requirements. Sounds like we're doing something wrong, isn't it?
It is. We're breaking the separation of concerns principle.
We're mixing the logic that handles permissions for different roles and the logic that handles providing abilities to users that has permissions for them.
And that's where CASL barge in!
CASL is a JavaScript library that allows to limit some actions for users based on their attributes. It can be not only their role but pretty much any attribute they have. Let's try to build some permissions manager using CASL.
How to start with CASL
First of all, let's add CASL to our project:
npm install @casl/ability @casl/react
# or
yarn add @casl/ability @casl/react
CASL itself is not attached to any framework, so it needs an adapter to work with React elements.
Now, let's create ability.ts
file and define abilities for the user:
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
export function defineAbility() {
const { can, build } = new AbilityBuilder(createMongoAbility)
can('read', 'all')
can('update', 'article')
return build()
};
So, here we're defining what exactly our user could do. To do it, we create an instance of AbilityBuilder
class, and describe permissions using can
function. We want our users to be able to view everything in the app and edit the article.
CASL is operating such a thing as ability. Ability is the description of permission, which describes using 4 parameters, but for now, we only need two: Action and Subject. Action is what we can do, and Subject is what we can interact with.
What can be Action and Subject? Basically - anything! It's a layer of abstraction, so you can define what Actions and Subjects are more suitable for your needs. Also, there is a special keywords that represent any Action and Subject: manage
is representing all Actions, and all
represents all Subjects.
Currently, we have the same ability for all users. Let's define more abilities and base them on the user's attributes and roles. Let our app be a blog where different users can post their articles and read other articles. Also, we will add admin and manager roles, which will have wider abilities.
type User = {
role: 'admin' | 'manager' | 'author'
isAuthorised: boolean
}
type CrudActions = 'create' | 'read' | 'update' | 'delete' | 'manage'
type Ability =
| [CrudActions, 'article']
| [CrudActions, 'profile']
| [CrudActions, 'all']
export type AppAbility = PureAbility<Ability, MongoQuery>
export function defineAbilityFor(user: User) {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility)
can("read", "all");
can("create", "profile");
if (user.role === "admin") {
can("manage", "all");
}
if (user.role === "manager") {
can("manage", "article");
}
if (user.isAuthorised) {
can("update", "profile");
can(["create", "update"], "article");
cannot("create", "profile");
}
return build()
};
We update the function and now it takes a user and manages abilities based on user attributes such as roles and authorization. We also used cannot
function which is an inverted can
function.
Let's check how our manager works by checking permissions for different kinds of users.
const admin = defineAbilityFor({ isAuthorised: true, role: "admin" });
admin.can("read", "article") // true
admin.can("update", "profile") // true
admin.can("delete", "article") // true
admin.can("delete", "profile") // true
admin.can("create", "profile") // false
const manager = defineAbilityFor({ isAuthorised: true, role: "manager" });
manager.can("read", "article") // true
manager.can("update", "profile") // true
manager.can("delete", "article") // true
manager.can("delete", "profile") // false
manager.can("create", "profile") // false
const author = defineAbilityFor({ isAuthorised: true, role: "author" });
author.can("read", "article") // true
author.can("update", "profile") // true
author.can("delete", "article") // false
author.can("delete", "profile") // false
author.can("create", "profile") // false
const reader = defineAbilityFor({ isAuthorised: false });
reader.can("read", "article") // true
reader.can("update", "profile") // false
reader.can("delete", "article") // false
reader.can("delete", "profile") // false
reader.can("create", "profile") // true
You can use both can
and cannot
to check ability or its absence, manage
and all
keywords can be used too.
OK, now we are sure that every user type has permissions he should have. What's next?
How to use it in React app
You can already use it with React by providing an ability instance to component and conditionally do something:
const Article = () => {
const user = useUser()
const { can, cannot } = defineAbilityFor(user)
return (
<nav>
<a href="/article/all">All articles</a>
{can('create', 'article') && (
<a href="/article/new">Write an article</a>
)}
{can('read', 'profile') && (
<a href="/profile">My profile</a>
)}
{cannot('read', 'profile') && (
<a href="/login">Login</a>
)}
<nav>
)
}
But using @casl/react
, it can be even better. This package contains Can
component which does the same as the example above - show or hide elements.
const Article = () => {
const user = useUser()
const ability = defineAbilityFor(user)
return (
<nav>
<a href="/article/all">All articles</a>
<Can I="create" an="article" ability={ability}>
<a href="/article/new">Write an article</a>
</Can>
<Can I="read" a="profile" ability={ability}>
<a href="/profile">My profile</a>
</Can>
<Can not I="read" a="profile" ability={ability}>
<a href="/login">Login</a>
</Can>
<nav>
)
}
Wow, this becomes a lot more human-readable with these I
and a
props. But it only aliases to pass subjects and actions. Actually, instead of I
, you can use do
, and instead of a
- an
, this
, and on
.
You can get rid of passing ability
every time using Can
by bounding ability to it or even bound a context that contains ability. This bounding is covered in documentation.
Conclusion
CASL is great library that allows to manage permissions in most efficient way, it has well documented API and adaptation to many frontend libraries and frameworks. In this article I covered the basic usage of it, in the next I'll cover more advanced usage cases. See you later!
Top comments (1)
Simple and clear. Great article.