CASL: How to manage user permissions in React app - Basics

misha2479k

Mykhailo Kokadii

Posted on June 16, 2023

CASL: How to manage user permissions in React app - Basics

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:

  1. Write a hook that encapsulates the logic of getting a user role.
  2. 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 />
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
};
Enter fullscreen mode Exit fullscreen mode

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()
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
misha2479k
Mykhailo Kokadii

Posted on June 16, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related