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';
export enum Actions {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
export type Subjects = InferSubjects<typeof User | 'all'>;
export type AppAbility = Ability<[Actions, Subjects]>;
export class User {
id: number;
isAdmin: boolean;
roles: string[];
}
@Injectable()
export class CaslAbilityFactory {
private rolesData = JSON.parse(fs.readFileSync(path.join(__dirname, '../../data/roles.json'), 'utf-8'));
createForUser(user: User) {
const { can, cannot, build } = new AbilityBuilder<Ability<[Actions, Subjects]>>(
Ability as AbilityClass<AppAbility>,
);
user.roles.forEach(role => {
const roleAbilities = this.rolesData[role]?.actions;
if (roleAbilities) {
roleAbilities.forEach((action: string) => {
can(action as Actions, 'all');
});
}
});
if (user.isAdmin) {
can(Actions.Manage, 'all');
} else {
can(Actions.Read, 'all');
cannot(Actions.Delete, User).because('Only admins can delete users');
}
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';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(
private reflector: Reflector,
private caslAbilityFactory: CaslAbilityFactory,
) {}
canActivate(context: ExecutionContext): boolean {
const requiredPolicies = this.reflector.get(CHECK_POLICIES_KEY, context.getHandler());
if (!requiredPolicies) {
return true;
}
const { user } = context.switchToHttp().getRequest();
const ability = this.caslAbilityFactory.createForUser(user);
const policyHandlers = requiredPolicies.map(policy => policy(ability));
if (policyHandlers.some(can => !can)) {
throw new ForbiddenException('You do not have permission to perform this action');
}
return true;
}
}
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;
export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers);
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);
}
}
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.