Managing User Permissions in an Angular Application

Managing User Permissions in an Angular Application

Managing User Permissions in an Angular Application - In this story, I want to share an alternative way to implement permission management by using a neat library which is called CASL...

Managing User Permissions in an Angular Application - In this story, I want to share an alternative way to implement permission management by using a neat library which is called CASL...

Modern applications usually displays only what is visible to the user based on their role. For example, a guest user may read stories but can’t write comments on Medium. Or another example, an authorized user can write and remove drafts. But that user is not allowed to see or remove somebody’s else drafts. And that’s pretty reasonable, isn’t it? :)

It all sounds cool but sometimes managing such accessibility may become a nightmare. You probably have written or seen code like this before:

<div *ngIf="loggedInUser.role === ADMIN || user.auth && post.author === loggedInUser.id">
    <button (click)="deletePost()">Delete</button>
</div>

Later, this code is spread over the application and becomes a big problem when you need to add a new role in the app or change permissions of existing role. Eventually you need to change some of *ngIf checks or in the worst case, change them all.

In this story, I want to share an alternative way to implement permission management by using a neat library which is called CASL*.*It makes managing user permissions much simpler, and allows to rewrite the previous example to:

<div *ngIf="post | can: 'delete'">
  <button (click)="deletePost()">Delete</button>
</div>

First time you’ve heard about *[CASL](https://github.com/stalniy/casl "*CASL")? You may want to read *[“What is CASL?”](https://medium.com/@sergiy.stotskiy/what-is-casl-or-how-can-you-build-a-castle-around-your-application-4d2daa0b1ab4 "*“What is CASL?”").

Demo application

To illustrate how to use CASL, I decided to use well known Todo appication with small additions:

  • you can assign tasks to users (“me” represents current user)
  • you can switch between roles

There are two roles in application:

  • “member” (i.e., regular user). Can read all Todos and CRUD (i.e., create, read, update, delete) only on assigned to him
  • “admin”. Can CRUD any Todo

This logic is defined by using AbilityBuilder class which allows to define user permissions by using declarative function calls:

export function defineAbilitiesFor(role) {
  const { can, rules } = AbilityBuilder.extract()

  if (role === 'admin') {
    can('manage', 'all')
  } else {
    can('read', 'all')
    can('manage', 'Todo', { assignee: 'me' })
  }

  return rules
}

In order to understand this function, we need to dive a bit deeper into CASL details. To do this, let’s go through each line of defineAbilitiesFor(role) function.

AbilityBuilder.extract() creates an instance of AbilityBuilderand extracts rulesarray and its can, cannot methods. With help of can(or cannot which is not used here), it’s possible to define user permissions. can (and cannot) both accepts 4 arguments, 2 of which are required: action name and subject name. Action name represents a user’s action in application (in most cases it will be standard CRUD) and subject name represents model name which this rule is defined for.

Let’s take a look a the code again.

If role argument of defineAbilitiesFor(role) equals admin, then user can do anything in the system (i.e., super admin rights). This is specified by can('manage', 'all') statement. As I said before the first argument is an action name, manage is a special keyword which represents any action in the system. And the second argument is a subject name, in this case it’s all, which is a reserved alias for any subject name. So, if user has role admin he can perform any action on any model.

In case user is not an admin, he can read any model (this is specified by can('read', 'all')) and can do any action on instances of Todomodel where model’s assignee property equals to me (i.e., can('manage', 'Todo', { assignee: 'me' })).

As you can see, the last permission declaration has 3 arguments. The third arguments represents conditions object. In this case, it clarifies which instances of Todo user is allowed to manage.

To understand it clearly, let’s look at this example:

class Todo {
  constructor({ title, assignee }) {
    this.title = title;
    this.assignee = assignee;
  }
}

const myTodo = new Todo({
  title: "buy food",
  assignee: "me"
});

const johnTodo = new Todo({
  title: "read the book",
  assignee: "John"
});

const ability = new Ability(defineAbilitiesFor("member"));

console.log("can read my todo?", ability.can("read", myTodo)); // true
console.log("can close my todo?", ability.can("close", myTodo)); // true

console.log("can read John's todo", ability.can("read", johnTodo));  // true
console.log("can close John's todo?", ability.can("close", johnTodo)); // false

Here I created a separate class Todo which represents a task to be done. This class accepts an object which must contain 2 fields: task’s title and who should work on this task. Later, I create 2 instances of Todo class: 1 that represent task assigned to me (remember conditions object?) and another one which is assigned to John. I also create an Ability instance with permissions for user with member role.

Finally, I check permissions on these 2 todos. As you can see ability.can("read", myTodo) and ability.can("close", myTodo) returns true, this is because early we defined that users who are not admins can do anything with tasks assigned to them (i.e., can('manage', 'Todo', { assignee: 'me' })). Due to exactly the same reason, ability.can("close", johnTodo) returns falsebecause we can only read his todo not close (i.e., can('read', 'all')).

Do you see how closely explanation in console.log reflects the actual check? They are almost identical (except that first one just a string and the last one do a complex permission check :)):

console.log(
  "can read my todo?",
  ability.can("read", myTodo)
)

Of course, now some of you may have questions like these:

  1. What other restrictions can be enforced by conditions object? You can learn about this in documentation here.
  2. Shall I always define a class for all my models on UI? The short answer is NO but if you don’t use classes (and I think majority of devs don’t use them on frontend), you need to let CASL know how to detect subject name based on the object instance passed in ability.can(action, subject). You can learn about this here.

CASL and Angular

CASL is shipped together with complementary package for Angular 2+. So, you can quickly add it into your Angular app.

Let’s start from installation

npm i @casl/ability @casl/angular
# or 
yarn add @casl/ability @casl/angular

Later, include AbilityModule in your AppModule (use

AbilityModule.forRoot() if you include it in root AppModule):

import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AbilityModule } from '@casl/angular';
    import { Ability } from '@casl/ability';


    import { defineAbilitiesFor, createAbility } from '../services/ability';
    import { AppRoutingModule } from './routing.module';


    @NgModule({
      declarations: [
        ...
      ],
      imports: [
        ...,
        BrowserModule,
        AppRoutingModule,
        AbilityModule.forRoot(),
      ],
      providers: [
        { provide: Ability, useFactory: createAbility }
      ],
      bootstrap: [App]
    })
    export class AppModule { }

@casl/angular provides the empty instance of Ability by default. Here I specified own provider which defines Ability based on provided rules (i.e., permissions). Alternatevely it’s possible to update rules of provided by default instance

Afterwards, you can inject Ability instance in your component to check permissions or update them:

import { Component, Input } from '@angular/core';
import { Ability } from '@casl/ability';
import { defineAbilitiesFor } from '../../services/ability';

@Component({
  selector: 'todo-footer',
  template: `
    ...
      <ul class="filters">
        <li class="help">Switch roles</li>
        <li><a href="#" [class.selected]="role == 'admin'" (click)="setRole('admin')">Admin</a></li>
        <li><a href="#" [class.selected]="role == 'member'" (click)="setRole('member')">Member</a></li>
      </ul>
    ...
  `,
})
export default class TodoFooter {
  @Input() items = [];

  role = 'member';

  constructor(private ability: Ability) {}

  setRole(name) {
    if (this.role !== name) {
      this.role = name;
      this.ability.update(defineAbilitiesFor(name))
    }
  }
}

Alternatevely you can use can pipe in templates for simple checks (more details here). So, you can write code like this in Angular’s templates:

<header class="header">
    <h1>{{ title }}</h1>
    <todo-form (newTodo)="addTodo($event)" *ngIf="'Todo' | can: 'create'"></todo-form>
</header>

With that, we have a really nice way to manage permissions in Angular app. The full example of the Angular based app with CASL can be found on github or codesandbox:

If you like CASL, please star it on **[github](https://github.com/stalniy/casl "github") and **share the article with your friends :)

Angular 9 Tutorial: Learn to Build a CRUD Angular App Quickly

What's new in Bootstrap 5 and when Bootstrap 5 release date?

Brave, Chrome, Firefox, Opera or Edge: Which is Better and Faster?

How to Build Progressive Web Apps (PWA) using Angular 9

What is new features in Javascript ES2020 ECMAScript 2020

How to Use Cookies in Angular for Storing user’s Credentials

In this post, I will be explaining about using Cookies in Angular for Storing user’s Credentials

Angular Tutorial - Learn Angular from Scratch

Angular Tutorial - Learn Angular from Scratch: This course is for beginners who are curious on how to get started with Angular. In this course you will learn how to download, install and play around with Angular. We teach you the main components of Angular, so that you can get up and running with it asap. You will learn now to start building applications with Angular.

An Angular Roadmap — The Past, Present, and Future of Angular

✅Interested in being an Angular developer in 2019? ... blog post it's most likely that you've written some code in javaScript in the past.