Role-based Access Control
⭐ Interview Importance: LOW
⏱️ Revision Time: 7 min
Role-Based Access Control (RBAC) is an authorization paradigm where permissions are grouped into “Roles”, and users are assigned those Roles.
Overview
RBAC is the most common way to handle authorization in modern web applications. Instead of saying “User A can delete posts and edit posts”, you say “User A has the ADMIN role. The ADMIN role is allowed to delete and edit posts.”
In NestJS, implementing RBAC involves three parts:
- A custom decorator (e.g.,
@Roles()) to attach metadata to a route. - The
Reflectorservice to read that metadata. - A Guard to compare the metadata against the user’s role.
Key Concepts
- Metadata: NestJS uses the
Reflectorclass to read arbitrary data (like an array of roles) attached to a class or method via a decorator. - The User Object: RBAC assumes that an Authentication Guard has already run and attached the user’s current role to
request.user.role.
Code Examples
1. Create the Roles Decorator
This decorator allows you to easily tag routes with the required roles.
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export enum Role {
USER = 'user',
ADMIN = 'admin',
SUPER_ADMIN = 'super_admin',
}
// We use 'roles' as the key to store the metadata
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
2. Create the Roles Guard
This Guard reads the metadata and compares it to the authenticated user.
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Read the required roles from the route handler
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
// If no roles are required, allow access
if (!requiredRoles) {
return true;
}
// Get the user from the request (attached by JwtAuthGuard)
const { user } = context.switchToHttp().getRequest();
// Check if the user's role exists in the array of required roles
return requiredRoles.some((role) => user.role?.includes(role));
}
}
3. Protect the Route
Now apply both the Authentication Guard and the Roles Guard to the controller.
// admin.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles, Role } from './roles.decorator';
@Controller('admin')
// Apply globally to the whole controller
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('dashboard')
@Roles(Role.ADMIN, Role.SUPER_ADMIN) // Only Admins can access this!
getDashboard() {
return 'Admin Dashboard Data';
}
@Get('billing')
@Roles(Role.SUPER_ADMIN) // Even regular Admins are blocked from this!
getBilling() {
return 'Billing Data';
}
}
Best Practices
- Use Enums: Never use magic strings for roles (e.g.,
@Roles('admin')). Always define anenumand use it everywhere. If the spelling of a role changes, you only have to update the enum, not search through 50 controllers. - Hierarchy vs Flat Roles: The RBAC example above uses a “flat” array check. In complex systems, you might want a hierarchy (e.g.,
SUPER_ADMINautomatically has all permissions ofADMIN). You can implement this logic inside theRolesGuardby having a predefined map of role inheritances.