Integrating CASL with NestJS: A Step-by-Step Guide

Integrating CASL with NestJS: A Step-by-Step Guide

Seamlessly Managing Permissions and Authorizations in Your NestJS Application Using CASL

In modern web applications, managing permissions and authorizations is crucial. CASL (Code Access Security Library) provides a powerful way to handle these permissions declaratively. This guide will walk you through integrating CASL with a NestJS application, ensuring your resources are protected efficiently.

Introduction to CASL

CASL is a highly flexible and customizable library for handling authorization in JavaScript applications. By defining abilities (permissions) declaratively, developers can easily control access to various parts of their applications.

Setting Up the NestJS Application

To begin, a new NestJS application will be set up. This can be done using the NestJS CLI:

nest new nest-casl-app

Installing Dependencies

Next, the necessary dependencies will be installed. These include @casl/ability for defining abilities and @casl/ability/extra for additional utilities.

npm install @casl/ability @casl/ability/extra

Creating the CASL Ability Factory

A factory will be created to define the abilities. This factory will determine what actions a user can perform on specific resources.

src/casl/casl-ability.factory.ts:

import { Injectable } from '@nestjs/common';
import { Ability, AbilityBuilder, AbilityClass, ExtractSubjectType, InferSubjects } from '@casl/ability';
import * as fs from 'fs';
import * as path from 'path';

// Define the actions available
export enum Actions {
  Manage = 'manage', // wildcard for any action
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

// Define the subjects (resources) that actions can be performed on
export type Subjects = InferSubjects<typeof User | 'all'>;
export type AppAbility = Ability<[Actions, Subjects]>;

// User entity definition
export class User {
  id: number;
  isAdmin: boolean;
  roles: string[];
  // additional properties as needed
}

// Define the CASL Ability Factory as a NestJS injectable service
@Injectable()
export class CaslAbilityFactory {
  private rolesData = JSON.parse(fs.readFileSync(path.join(__dirname, '../../data/roles.json'), 'utf-8'));

  createForUser(user: User) {
    // Use CASL's AbilityBuilder to construct the ability
    const { can, cannot, build } = new AbilityBuilder<Ability<[Actions, Subjects]>>(
      Ability as AbilityClass<AppAbility>,
    );

    // Define abilities based on the user's roles
    user.roles.forEach(role => {
      const roleAbilities = this.rolesData[role]?.actions;
      if (roleAbilities) {
        roleAbilities.forEach((action: string) => {
          can(action as Actions, 'all'); // Adjust according to actual resource types
        });
      }
    });

    // Specific abilities
    if (user.isAdmin) {
      can(Actions.Manage, 'all'); // Admins can manage everything
    } else {
      can(Actions.Read, 'all'); // Regular users can read everything
      cannot(Actions.Delete, User).because('Only admins can delete users'); // Regular users cannot delete users
    }

    // Build the ability
    return build({
      detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects>,
    });
  }
}
  • Actions Enum: Defines possible actions (manage, create, read, update, delete).

  • Subjects Type: Defines the resources (subjects) on which actions can be performed.

  • CaslAbilityFactory Service: Creates abilities for a user based on their roles or attributes.

Protecting Routes with Guards

Guards will be used to protect the routes. These guards will check the user’s abilities before allowing access to the route.

src/guards/permissions.guard.ts:

import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { CaslAbilityFactory, Actions } from '../casl/casl-ability.factory';
import { CHECK_POLICIES_KEY } from '../decorators/check-policies.decorator';

// PermissionsGuard to enforce abilities on routes
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(
    private reflector: Reflector, // Used to access metadata
    private caslAbilityFactory: CaslAbilityFactory, // CASL Ability Factory
  ) {}

  canActivate(context: ExecutionContext): boolean {
    // Retrieve policies from metadata
    const requiredPolicies = this.reflector.get(CHECK_POLICIES_KEY, context.getHandler());
    if (!requiredPolicies) {
      return true; // If no policies are required, allow access
    }

    const { user } = context.switchToHttp().getRequest();
    const ability = this.caslAbilityFactory.createForUser(user);

    // Check each policy
    const policyHandlers = requiredPolicies.map(policy => policy(ability));

    // If any policy check fails, deny access
    if (policyHandlers.some(can => !can)) {
      throw new ForbiddenException('You do not have permission to perform this action');
    }

    return true; // Allow access if all policy checks pass
  }
}
  • PermissionsGuard: Checks the user’s abilities before allowing access to the route.

  • Reflector: Used to access metadata defined on routes.

Applying Guards and Custom Decorators

To check policies at the route level, custom decorators will be used.

src/decorators/check-policies.decorator.ts:

import { SetMetadata } from '@nestjs/common';
import { AppAbility } from '../casl/casl-ability.factory';

export const CHECK_POLICIES_KEY = 'checkPolicies';
export type PolicyHandler = (ability: AppAbility) => boolean;

// Custom decorator to define policies on routes
export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers);
  • CHECK_POLICIES_KEY: Key for storing policy metadata.

  • CheckPolicies Decorator: Used to define policies on routes.

Implementing the User Resource

A sample resource (Users) will be implemented. This will include routes that are protected by CASL.

src/users/users.controller.ts:

import { Controller, Get, Post, Body, Param, Delete, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './user.entity';
import { CheckPolicies } from '../decorators/check-policies.decorator';
import { PermissionsGuard } from '../guards/permissions.guard';
import { Actions } from '../casl/casl-ability.factory';

@Controller('users')
@UseGuards(PermissionsGuard)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @CheckPolicies((ability) => ability.can(Actions.Create, User))
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  @CheckPolicies((ability) => ability.can(Actions.Read, User))
  findAll() {
    return this.usersService.findAll();
  }

  @Delete(':id')
  @CheckPolicies((ability) => ability.can(Actions.Delete, User))
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}
  • UsersController: Contains routes for managing users.

  • CheckPolicies Decorator: Applied to routes to enforce policies.

Dummy Users and Roles

To test the permissions, a JSON file with dummy users and roles will be created.

data/users.json:

[
  {
    "id": 1,
    "name": "Admin User",
    "isAdmin": true,
    "roles": ["admin"]
  },
  {
    "id": 2,
    "name": "Regular User",
    "isAdmin": false,
    "roles": ["user"]
  }
]

data/roles.json:

{
  "admin": {
    "actions": ["manage"]
  },
  "user": {
    "actions": ["read"]
  }
}
  • users.json: Defines dummy users, including an admin user and a regular user.

  • roles.json: Defines roles and their associated actions.

Conclusion
By integrating CASL with NestJS, permissions and authorizations can be managed declaratively and efficiently. The ability to define roles and permissions centrally ensures that security policies are consistent and easier to maintain. Through this approach, robust and scalable authorization mechanisms can be implemented in NestJS applications.