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.