@nestbolt/permissions
Decorators and Guards
Protect routes using @RequireRoles(), @RequirePermissions(), @RequireRolesOrPermissions() decorators and their corresponding guards.
The package provides three decorators for declaring authorization requirements on controller routes and three corresponding guards that enforce those requirements at runtime.
Guards Overview
| Guard | Decorator | Logic |
|---|---|---|
RolesGuard | @RequireRoles() | User must have any of the listed roles |
PermissionsGuard | @RequirePermissions() | User must have all of the listed permissions |
RolesOrPermissionsGuard | @RequireRolesOrPermissions() | User must have any of the listed roles or any of the listed permissions |
All guards follow the same pattern:
- Read the metadata set by the decorator.
- Extract
request.userfrom the HTTP context. - Call
PermissionRegistrarServiceto verify authorization. - Throw a
ForbiddenExceptionif the check fails.
If no metadata is set (the decorator is not present), the guard allows the request through.
Authentication Guard Order
Every route that uses a permission or role guard must also use an authentication guard (e.g., JwtAuthGuard) that populates request.user. The authentication guard must be listed before the permission/role guard in @UseGuards() so that the user is available when the authorization check runs.
// Correct -- JwtAuthGuard runs first, then RolesGuard
@UseGuards(JwtAuthGuard, RolesGuard)
// Incorrect -- RolesGuard cannot find request.user
@UseGuards(RolesGuard, JwtAuthGuard)The user object on the request must have an id property. If request.user is missing or does not have an id, the guard throws:
ForbiddenException: User is not authenticated.@RequireRoles()
Declares that a route requires the user to have any one of the specified roles. If the user has at least one of the listed roles, access is granted.
Signature
RequireRoles(...roles: string[])Usage
import { Controller, Get, UseGuards } from "@nestjs/common";
import { RequireRoles, RolesGuard } from "@nestbolt/permissions";
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
@Controller("admin")
export class AdminController {
// Single role
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles("admin")
@Get("dashboard")
dashboard() {
return { message: "Admin dashboard" };
}
// Multiple roles (user needs ANY of them)
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles("admin", "editor")
@Get("content")
manageContent() {
return { message: "Content management" };
}
}Class-Level Decorator
You can apply @RequireRoles() at the class level to protect all routes in a controller:
@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles("admin")
export class AdminController {
@Get("dashboard")
dashboard() {
return { message: "Admin dashboard" };
}
@Get("settings")
settings() {
return { message: "Admin settings" };
}
}Handler-Level Override
When both class-level and handler-level decorators are present, the handler-level decorator takes precedence (via Reflector.getAllAndOverride()):
@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles("admin")
export class AdminController {
// This route requires "admin" (inherited from class)
@Get("dashboard")
dashboard() {
return { message: "Admin dashboard" };
}
// This route overrides: requires "admin" or "editor"
@RequireRoles("admin", "editor")
@Get("posts")
managePosts() {
return { message: "Manage posts" };
}
}RolesGuard Behavior
The RolesGuard uses PermissionRegistrarService.userHasAnyRole() internally. When the check fails:
- If
displayRoleInExceptionisfalse(default):"User does not have the required roles." - If
displayRoleInExceptionistrue:"User does not have the required roles: [admin, editor]."
Metadata Key
The metadata key used by @RequireRoles() is exported as REQUIRED_ROLES_KEY:
import { REQUIRED_ROLES_KEY } from "@nestbolt/permissions";
// Value: "requiredRoles"@RequirePermissions()
Declares that a route requires the user to have all of the specified permissions. The user must possess every listed permission for access to be granted.
Signature
RequirePermissions(...permissions: string[])Usage
import { Controller, Post, Delete, UseGuards, Param } from "@nestjs/common";
import { RequirePermissions, PermissionsGuard } from "@nestbolt/permissions";
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
@Controller("posts")
@UseGuards(JwtAuthGuard)
export class PostsController {
// Single permission
@UseGuards(PermissionsGuard)
@RequirePermissions("posts.create")
@Post()
createPost() {
return { message: "Post created" };
}
// Multiple permissions (user needs ALL of them)
@UseGuards(PermissionsGuard)
@RequirePermissions("posts.edit", "posts.delete")
@Delete(":id")
deletePost(@Param("id") id: string) {
return { message: `Post ${id} deleted` };
}
}Permission Resolution
The PermissionsGuard uses PermissionRegistrarService.userHasAllPermissions() to check the user. This method checks permissions through three channels:
- Direct permissions -- Permissions assigned directly to the user.
- Role-inherited permissions -- Permissions the user inherits through their assigned roles.
- Wildcard permissions -- If
enableWildcardPermissionsistrue, wildcard patterns likeposts.*are evaluated.
All three channels are checked for each required permission. The user must satisfy every required permission through any combination of these channels.
PermissionsGuard Behavior
When the check fails:
- If
displayPermissionInExceptionisfalse(default):"User does not have the required permissions." - If
displayPermissionInExceptionistrue:"User does not have the required permissions: [posts.edit, posts.delete]."
Metadata Key
import { REQUIRED_PERMISSIONS_KEY } from "@nestbolt/permissions";
// Value: "requiredPermissions"@RequireRolesOrPermissions()
Declares that a route can be accessed if the user has any of the specified roles or any of the specified permissions. The strings passed to this decorator are checked as both role names and permission names.
Signature
RequireRolesOrPermissions(...rolesOrPermissions: string[])Usage
import { Controller, Post, Get, UseGuards, Body } from "@nestjs/common";
import {
RequireRolesOrPermissions,
RolesOrPermissionsGuard,
} from "@nestbolt/permissions";
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
@Controller("content")
@UseGuards(JwtAuthGuard)
export class ContentController {
// Access granted if user has the "admin" role OR the "posts.create" permission
@UseGuards(RolesOrPermissionsGuard)
@RequireRolesOrPermissions("admin", "posts.create")
@Post()
createContent(@Body() dto: CreateContentDto) {
return { message: "Content created" };
}
// Access granted if user has the "admin" role OR the "analytics.view" permission
@UseGuards(RolesOrPermissionsGuard)
@RequireRolesOrPermissions("admin", "analytics.view")
@Get("analytics")
viewAnalytics() {
return { message: "Analytics data" };
}
}RolesOrPermissionsGuard Behavior
The guard checks in two steps:
- First, it calls
PermissionRegistrarService.userHasAnyRole()with all the provided strings. If the user has any matching role, access is granted immediately. - If no role matches, it calls
PermissionRegistrarService.userHasAnyPermission()with all the provided strings. If the user has any matching permission, access is granted. - If neither check passes, a
ForbiddenExceptionis thrown.
The error message includes the role/permission names if either displayRoleInException or displayPermissionInException is true.
Metadata Key
import { REQUIRED_ROLES_OR_PERMISSIONS_KEY } from "@nestbolt/permissions";
// Value: "requiredRolesOrPermissions"Combining Guards
You can use different guards on different routes within the same controller:
@Controller("content")
@UseGuards(JwtAuthGuard)
export class ContentController {
@UseGuards(RolesGuard)
@RequireRoles("admin")
@Get("admin-only")
adminOnly() {
return { restricted: true };
}
@UseGuards(PermissionsGuard)
@RequirePermissions("content.publish")
@Post("publish")
publish() {
return { published: true };
}
@UseGuards(RolesOrPermissionsGuard)
@RequireRolesOrPermissions("editor", "content.view")
@Get()
viewContent() {
return { content: [] };
}
}Routes Without Decorators
If a guard is applied but no corresponding decorator is present on the handler or class, the guard allows the request through. This means you can apply a guard at the controller level and selectively protect only certain routes:
@Controller("posts")
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class PostsController {
// No @RequirePermissions -- accessible to any authenticated user
@Get()
listPosts() {
return [];
}
// Protected -- requires "posts.create"
@RequirePermissions("posts.create")
@Post()
createPost() {
return { created: true };
}
}Custom Guard Composition
If you need to require a role AND a specific permission (not OR), apply both guards in sequence:
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
@RequireRoles("editor")
@RequirePermissions("posts.publish")
@Post("publish")
publish() {
// User must be an "editor" AND have "posts.publish" permission
return { published: true };
}In this case, the RolesGuard runs first and checks the role requirement. If it passes, the PermissionsGuard runs and checks the permission requirement. Both must pass for the route to be accessible.