Roles and permissions
Login answers "who are you", and authorization answers "what are you allowed to do". In the blog every logged-in user writes posts, but only an administrator may delete other people's posts. The user's role is already stored in the role field and goes into the token; it only remains to check it.
The Roles decorator
The controller method needs to be marked with a list of allowed roles. For that, create your own decorator based on SetMetadata() — a function that attaches arbitrary data (metadata) to a method:
// src/auth/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);Now @Roles('admin') above a method stores the array ['admin'] on it under the roles key. The decorator itself checks nothing — it only leaves a mark.
The roles guard
The guard reads the mark through the Reflector service and compares it with the user's role:
// src/auth/roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!roles || roles.length === 0) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return roles.includes(user?.role);
}
}getAllAndOverride() looks for the mark on the method first, then on the controller class. Without a mark, or with an empty list of roles, the route is open to every logged-in user. The user object was put into the request by the AuthGuard from the previous lesson.
Usage
// src/posts/posts.controller.ts
import { Controller, Delete, HttpCode, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/roles.guard';
import { PostsService } from './posts.service';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Delete(':id')
@HttpCode(204)
@Roles('admin')
@UseGuards(AuthGuard, RolesGuard)
remove(@Param('id', ParseIntPipe) id: number) {
return this.postsService.remove(id);
}
}Guards run in the order listed: first AuthGuard checks the token and puts the user into the request, then RolesGuard checks the role. A regular user gets 403 Forbidden resource for DELETE /posts/1.
Making an administrator
There is no route for changing roles yet, so the first administrator is set right in the database:
UPDATE user SET role = 'admin' WHERE email = 'anna@example.com';After that Anna has to log in again: the role is written into the token, and the old token holds the previous value.
Permissions on your own records
Roles do not solve "an author may edit only their own posts" — that depends on the specific post. Such a check is written in the service: load the post, compare its author id with user.sub, and throw ForbiddenException if they differ.
Common mistake
If RolesGuard comes before AuthGuard in @UseGuards(), there is no user in the request yet when the role is checked, and every request gets 403 — even from an administrator. The login guard must always come first.