Skip to content

nestbolt/permissions

Repository files navigation

@nestbolt/permissions

Roles & permissions (RBAC) for NestJS applications with TypeORM support.

npm version npm downloads tests license


This package provides a role-based access control (RBAC) system for NestJS that lets you manage roles and permissions with TypeORM, protect routes with guards, and check permissions programmatically.

Once installed, using it is as simple as:

@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles('admin')
@Get('admin/dashboard')
dashboard() { return { message: 'Welcome, admin!' }; }

Table of Contents

Installation

Install the package via npm:

npm install @nestbolt/permissions

Or via yarn:

yarn add @nestbolt/permissions

Or via pnpm:

pnpm add @nestbolt/permissions

Peer Dependencies

This package requires the following peer dependencies, which you likely already have in a NestJS project:

@nestjs/common    ^10.0.0 || ^11.0.0
@nestjs/core      ^10.0.0 || ^11.0.0
@nestjs/typeorm   ^10.0.0 || ^11.0.0
typeorm           ^0.3.0
class-validator   ^0.14.0
class-transformer ^0.5.0
reflect-metadata  ^0.1.13 || ^0.2.0

Optional:

@nestjs/event-emitter ^2.0.0 || ^3.0.0   # For permission/role lifecycle events

Quick Start

1. Implement the PermissionUserRepository

Create a repository that bridges your User entity with the permission system:

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import {
  PermissionUserRepository,
  UserHasRolesEntity,
  UserHasPermissionsEntity,
} from "@nestbolt/permissions";

@Injectable()
export class TypeOrmPermissionUserRepository implements PermissionUserRepository {
  constructor(
    @InjectRepository(UserHasRolesEntity)
    private readonly userRolesRepo: Repository<UserHasRolesEntity>,
    @InjectRepository(UserHasPermissionsEntity)
    private readonly userPermissionsRepo: Repository<UserHasPermissionsEntity>,
  ) {}

  async getRoleIds(userId: string): Promise<string[]> {
    const rows = await this.userRolesRepo.find({ where: { userId } });
    return rows.map((r) => r.roleId);
  }

  async getDirectPermissionIds(userId: string): Promise<string[]> {
    const rows = await this.userPermissionsRepo.find({ where: { userId } });
    return rows.map((r) => r.permissionId);
  }

  async attachRoles(userId: string, roleIds: string[]): Promise<void> {
    const entities = roleIds.map((roleId) =>
      this.userRolesRepo.create({ userId, roleId }),
    );
    await this.userRolesRepo.save(entities);
  }

  async detachRoles(userId: string, roleIds: string[]): Promise<void> {
    for (const roleId of roleIds) {
      await this.userRolesRepo.delete({ userId, roleId });
    }
  }

  async detachAllRoles(userId: string): Promise<void> {
    await this.userRolesRepo.delete({ userId });
  }

  async attachPermissions(
    userId: string,
    permissionIds: string[],
  ): Promise<void> {
    const entities = permissionIds.map((permissionId) =>
      this.userPermissionsRepo.create({ userId, permissionId }),
    );
    await this.userPermissionsRepo.save(entities);
  }

  async detachPermissions(
    userId: string,
    permissionIds: string[],
  ): Promise<void> {
    for (const permissionId of permissionIds) {
      await this.userPermissionsRepo.delete({ userId, permissionId });
    }
  }

  async detachAllPermissions(userId: string): Promise<void> {
    await this.userPermissionsRepo.delete({ userId });
  }

  async userExists(userId: string): Promise<boolean> {
    // Implement based on your User entity
    return true;
  }
}

2. Register the module in AppModule

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PermissionsModule } from "@nestbolt/permissions";
import { TypeOrmPermissionUserRepository } from "./repositories/permission-user.repository";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      /* your config */
    }),
    PermissionsModule.forRoot({
      userRepository: TypeOrmPermissionUserRepository,
    }),
  ],
})
export class AppModule {}

3. Use guards and decorators on your routes

import { Controller, Get, UseGuards } from "@nestjs/common";
import { RequireRoles, RolesGuard } from "@nestbolt/permissions";
import { JwtAuthGuard } from "@nestbolt/authentication";

@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
  @RequireRoles("admin")
  @Get("dashboard")
  dashboard() {
    return { message: "Welcome, admin!" };
  }
}

Module Configuration

Static Configuration (forRoot)

PermissionsModule.forRoot({
  userRepository: TypeOrmPermissionUserRepository,
  defaultGuardName: "default",
  enableWildcardPermissions: false,
  cache: { enabled: true, ttl: 86400000 },
  displayPermissionInException: false,
  displayRoleInException: false,
});

Async Configuration (forRootAsync)

PermissionsModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    userRepository: TypeOrmPermissionUserRepository,
    enableWildcardPermissions: config.get("ENABLE_WILDCARD_PERMISSIONS", false),
  }),
});

The module is registered as global — you only need to import it once.

Using the Guards & Decorators

Require specific permissions

@UseGuards(JwtAuthGuard, PermissionsGuard)
@RequirePermissions('posts.create', 'posts.edit')
@Post('posts')
createPost() { /* user must have ALL listed permissions */ }

Require specific roles

@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles('admin', 'editor')
@Get('manage')
manage() { /* user must have ANY of the listed roles */ }

Require roles OR permissions

@UseGuards(JwtAuthGuard, RolesOrPermissionsGuard)
@RequireRolesOrPermissions('admin', 'posts.create')
@Post('posts')
createPost() { /* user must have the role 'admin' OR the permission 'posts.create' */ }

Important: Always put JwtAuthGuard (or your auth guard) before the permission/role guard so request.user is populated.

Using the Service Directly

Inject PermissionRegistrarService for programmatic access:

import {
  PermissionRegistrarService,
  PermissionService,
  RoleService,
} from "@nestbolt/permissions";

@Injectable()
export class SetupService {
  constructor(
    private readonly registrar: PermissionRegistrarService,
    private readonly permissionService: PermissionService,
    private readonly roleService: RoleService,
  ) {}

  async seedRolesAndPermissions() {
    // Create permissions
    await this.permissionService.create("posts.create");
    await this.permissionService.create("posts.edit");
    await this.permissionService.create("posts.delete");

    // Create roles and assign permissions
    await this.roleService.create("admin");
    await this.roleService.givePermissionTo(
      "admin",
      "posts.create",
      "posts.edit",
      "posts.delete",
    );

    await this.roleService.create("editor");
    await this.roleService.givePermissionTo(
      "editor",
      "posts.create",
      "posts.edit",
    );

    // Assign roles to users
    await this.registrar.assignRole("user-uuid", "admin");

    // Check permissions
    await this.registrar.userHasPermissionTo("user-uuid", "posts.create"); // true
    await this.registrar.userHasRole("user-uuid", "admin"); // true
  }
}

PermissionRegistrarService Methods

Method Returns Description
assignRole(userId, ...roles) Promise<void> Assign roles to a user
removeRole(userId, ...roles) Promise<void> Remove roles from a user
syncRoles(userId, ...roles) Promise<void> Replace all user roles with given ones
givePermissionTo(userId, ...perms) Promise<void> Give direct permissions to a user
revokePermissionTo(userId, ...perms) Promise<void> Revoke direct permissions from a user
syncPermissions(userId, ...perms) Promise<void> Replace all direct user permissions
userHasRole(userId, role) Promise<boolean> Check if user has a specific role
userHasAnyRole(userId, roles) Promise<boolean> Check if user has any of the roles
userHasAllRoles(userId, roles) Promise<boolean> Check if user has all roles
userHasPermissionTo(userId, perm) Promise<boolean> Check permission (direct + via role + wildcard)
userHasAnyPermission(userId, perms) Promise<boolean> Check if user has any permission
userHasAllPermissions(userId, perms) Promise<boolean> Check if user has all permissions
userHasDirectPermission(userId, perm) Promise<boolean> Check only direct permission
getUserRoleNames(userId) Promise<string[]> Get all role names for a user
getUserAllPermissions(userId) Promise<PermissionEntity[]> Get all permissions (direct + via roles)
getUserPermissionsViaRoles(userId) Promise<PermissionEntity[]> Get permissions inherited from roles
getUserDirectPermissions(userId) Promise<PermissionEntity[]> Get directly assigned permissions
flushCache() void Clear the permission cache

PermissionService Methods

Method Returns Description
create(name, guardName?) Promise<PermissionEntity> Create a permission
findByName(name, guardName?) Promise<PermissionEntity> Find by name (throws if not found)
findById(id, guardName?) Promise<PermissionEntity> Find by ID (throws if not found)
findOrCreate(name, guardName?) Promise<PermissionEntity> Find or create a permission
findAll(guardName?) Promise<PermissionEntity[]> List all permissions
findByNames(names, guardName?) Promise<PermissionEntity[]> Find multiple by names
delete(nameOrId, guardName?) Promise<void> Delete a permission

RoleService Methods

Method Returns Description
create(name, guardName?) Promise<RoleEntity> Create a role
findByName(name, guardName?) Promise<RoleEntity> Find by name (throws if not found)
findById(id, guardName?) Promise<RoleEntity> Find by ID (throws if not found)
findOrCreate(name, guardName?) Promise<RoleEntity> Find or create a role
findAll(guardName?) Promise<RoleEntity[]> List all roles
delete(nameOrId, guardName?) Promise<void> Delete a role
givePermissionTo(role, ...perms) Promise<RoleEntity> Attach permissions to a role
revokePermissionTo(role, ...perms) Promise<RoleEntity> Detach permissions from a role
syncPermissions(role, ...perms) Promise<RoleEntity> Replace all role permissions
hasPermissionTo(role, perm) Promise<boolean> Check if role has a permission

Implementing PermissionUserRepository

The PermissionUserRepository interface is the bridge between this package and your User entity. You must implement it and pass it to the module configuration.

Method Description
getDirectPermissionIds(userId) Return IDs of directly assigned permissions
getRoleIds(userId) Return IDs of assigned roles
attachRoles(userId, roleIds) Insert user-role associations
detachRoles(userId, roleIds) Remove user-role associations
detachAllRoles(userId) Remove all roles from user
attachPermissions(userId, permissionIds) Insert user-permission associations
detachPermissions(userId, permissionIds) Remove user-permission associations
detachAllPermissions(userId) Remove all direct permissions from user
userExists(userId) Check if user exists

See Quick Start for a full TypeORM implementation example.

Configuration Options

Option Type Default Description
userRepository Type<PermissionUserRepository> (required) Class implementing PermissionUserRepository
defaultGuardName string "default" Default guard name for roles/permissions
enableWildcardPermissions boolean false Enable wildcard permission matching
cache.enabled boolean true Enable in-memory permission cache
cache.ttl number 86400000 Cache TTL in milliseconds (default: 24 hours)
displayPermissionInException boolean false Include permission names in 403 error messages
displayRoleInException boolean false Include role names in 403 error messages

Features

Wildcard Permissions

Enable wildcard matching to define broad permissions:

PermissionsModule.forRoot({
  userRepository: TypeOrmPermissionUserRepository,
  enableWildcardPermissions: true,
});
// Give a user the wildcard permission
await permissionService.create("articles.*");
await registrar.givePermissionTo(userId, "articles.*");

// Now these checks all return true:
await registrar.userHasPermissionTo(userId, "articles.create"); // true
await registrar.userHasPermissionTo(userId, "articles.edit"); // true
await registrar.userHasPermissionTo(userId, "articles.delete"); // true

// Super-admin wildcard
await permissionService.create("*");
await registrar.givePermissionTo(userId, "*");
await registrar.userHasPermissionTo(userId, "anything.at.all"); // true

Events

If @nestjs/event-emitter is installed, the package emits events on role/permission changes:

Event Payload When
permissions.role-attached { userId, roleIds } Role(s) assigned to user
permissions.role-detached { userId, roleIds } Role(s) removed from user
permissions.permission-attached { userId, permissionIds } Permission(s) given to user
permissions.permission-detached { userId, permissionIds } Permission(s) revoked from user
permissions.role-created { role } New role created
permissions.role-deleted { role } Role deleted
permissions.permission-created { permission } New permission created
permissions.permission-deleted { permission } Permission deleted
permissions.cache-flushed {} Permission cache cleared

Caching

The in-memory cache stores permission data to avoid repeated database queries. It is automatically flushed when roles or permissions are modified. You can also flush it manually:

registrar.flushCache();

Testing

npm test

Run tests in watch mode:

npm run test:watch

Generate coverage report:

npm run test:cov

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.

License

The MIT License (MIT). Please see License File for more information.

About

Roles & permissions (RBAC) for NestJS applications with TypeORM support.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors