NestboltNestbolt

@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

GuardDecoratorLogic
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:

  1. Read the metadata set by the decorator.
  2. Extract request.user from the HTTP context.
  3. Call PermissionRegistrarService to verify authorization.
  4. Throw a ForbiddenException if 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 displayRoleInException is false (default): "User does not have the required roles."
  • If displayRoleInException is true: "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:

  1. Direct permissions -- Permissions assigned directly to the user.
  2. Role-inherited permissions -- Permissions the user inherits through their assigned roles.
  3. Wildcard permissions -- If enableWildcardPermissions is true, wildcard patterns like posts.* 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 displayPermissionInException is false (default): "User does not have the required permissions."
  • If displayPermissionInException is true: "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:

  1. First, it calls PermissionRegistrarService.userHasAnyRole() with all the provided strings. If the user has any matching role, access is granted immediately.
  2. If no role matches, it calls PermissionRegistrarService.userHasAnyPermission() with all the provided strings. If the user has any matching permission, access is granted.
  3. If neither check passes, a ForbiddenException is 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.