NestboltNestbolt

@nestbolt/permissions

Role Management

Create, find, delete, and manage roles using RoleService and PermissionRegistrarService.

Role management is split across two services:

  • RoleService handles CRUD operations on role records and manages which permissions are attached to a role.
  • PermissionRegistrarService handles the relationship between users and roles -- assigning, removing, syncing, and checking roles for a specific user.

RoleService

Inject RoleService to create, find, and delete roles and to manage which permissions belong to a role.

import { Injectable } from "@nestjs/common";
import { RoleService } from "@nestbolt/permissions";

@Injectable()
export class MyService {
  constructor(private readonly roleService: RoleService) {}
}

create()

Creates a new role. Throws RoleAlreadyExistsException if a role with the same name and guard already exists.

create(name: string, guardName?: string): Promise<RoleEntity>
const admin = await this.roleService.create("admin");
const apiAdmin = await this.roleService.create("admin", "api");

The guardName parameter defaults to the defaultGuardName configured in the module options (which itself defaults to "default").

findByName()

Finds a role by its name. Throws RoleDoesNotExistException if no matching role is found.

findByName(name: string, guardName?: string): Promise<RoleEntity>
const admin = await this.roleService.findByName("admin");
const apiAdmin = await this.roleService.findByName("admin", "api");

findById()

Finds a role by its UUID. Throws RoleDoesNotExistException if no matching role is found.

findById(id: string, guardName?: string): Promise<RoleEntity>
const role = await this.roleService.findById("550e8400-e29b-41d4-a716-446655440000");

When guardName is provided, the lookup is further filtered by guard name.

findOrCreate()

Finds an existing role by name and guard, or creates it if it does not exist. This is useful for seeding.

findOrCreate(name: string, guardName?: string): Promise<RoleEntity>
// Safe to call multiple times -- will not throw if the role already exists
const admin = await this.roleService.findOrCreate("admin");
const editor = await this.roleService.findOrCreate("editor");

findAll()

Returns all roles, optionally filtered by guard name. Results are sorted by name in ascending order.

findAll(guardName?: string): Promise<RoleEntity[]>
// Get all roles
const allRoles = await this.roleService.findAll();

// Get roles for a specific guard
const apiRoles = await this.roleService.findAll("api");

delete()

Deletes a role by name or ID. Accepts either the role name or its UUID. Throws RoleDoesNotExistException if the role is not found.

delete(nameOrId: string, guardName?: string): Promise<void>
// Delete by name
await this.roleService.delete("temporary-role");

// Delete by ID
await this.roleService.delete("550e8400-e29b-41d4-a716-446655440000");

givePermissionTo()

Attaches one or more permissions to a role. Permissions that are already attached are silently skipped. The permissions must already exist (they are looked up by name via PermissionService.findByNames()).

givePermissionTo(roleNameOrId: string, ...permissionNames: string[]): Promise<RoleEntity>
await this.roleService.givePermissionTo("admin", "posts.create", "posts.edit", "posts.delete");
await this.roleService.givePermissionTo("editor", "posts.create", "posts.edit");

The first argument can be either the role name or its UUID. The method resolves the role internally by first trying findById(), then falling back to findByName().

revokePermissionTo()

Detaches one or more permissions from a role. Permissions that are not currently attached are silently ignored.

revokePermissionTo(roleNameOrId: string, ...permissionNames: string[]): Promise<RoleEntity>
// Remove the delete permission from the editor role
await this.roleService.revokePermissionTo("editor", "posts.delete");

syncPermissions()

Replaces all permissions on a role with the provided set. Any permissions currently attached to the role that are not in the new list are removed, and any new permissions are added.

syncPermissions(roleNameOrId: string, ...permissionNames: string[]): Promise<RoleEntity>
// The editor role will have exactly these two permissions after this call
await this.roleService.syncPermissions("editor", "posts.create", "posts.edit");

hasPermissionTo()

Checks whether a role has a specific permission attached.

hasPermissionTo(roleNameOrId: string, permission: string, guardName?: string): Promise<boolean>
const canDelete = await this.roleService.hasPermissionTo("admin", "posts.delete");
// true

const editorCanDelete = await this.roleService.hasPermissionTo("editor", "posts.delete");
// false

getRolePermissions()

Returns all permissions attached to a role, identified by the role's UUID.

getRolePermissions(roleId: string): Promise<PermissionEntity[]>
const role = await this.roleService.findByName("admin");
const permissions = await this.roleService.getRolePermissions(role.id);
// [{ id: "...", name: "posts.create", ... }, { id: "...", name: "posts.edit", ... }, ...]

PermissionRegistrarService -- Role Operations

The PermissionRegistrarService manages user-role associations. Inject it to assign, remove, sync, and check roles for users.

import { Injectable } from "@nestjs/common";
import { PermissionRegistrarService } from "@nestbolt/permissions";

@Injectable()
export class UserRoleService {
  constructor(private readonly registrar: PermissionRegistrarService) {}
}

assignRole()

Assigns one or more roles to a user. Roles that the user already has are silently skipped. Flushes the permission cache and emits a permissions.role-attached event.

assignRole(userId: string, ...roleNames: string[]): Promise<void>
// Assign a single role
await this.registrar.assignRole("user-uuid", "editor");

// Assign multiple roles at once
await this.registrar.assignRole("user-uuid", "editor", "viewer");

removeRole()

Removes one or more roles from a user. Flushes the cache and emits a permissions.role-detached event.

removeRole(userId: string, ...roleNames: string[]): Promise<void>
await this.registrar.removeRole("user-uuid", "editor");

// Remove multiple roles at once
await this.registrar.removeRole("user-uuid", "editor", "viewer");

syncRoles()

Replaces all roles on a user with the provided set. All existing roles are removed, and the new roles are assigned. Flushes the cache.

syncRoles(userId: string, ...roleNames: string[]): Promise<void>
// User will have exactly these roles and no others
await this.registrar.syncRoles("user-uuid", "admin");

// Reset to multiple roles
await this.registrar.syncRoles("user-uuid", "editor", "viewer");

userHasRole()

Checks whether a user has a specific role.

userHasRole(userId: string, role: string, guardName?: string): Promise<boolean>
const isAdmin = await this.registrar.userHasRole("user-uuid", "admin");

userHasAnyRole()

Checks whether a user has at least one of the specified roles.

userHasAnyRole(userId: string, roles: string[]): Promise<boolean>
const canManage = await this.registrar.userHasAnyRole("user-uuid", [
  "admin",
  "editor",
]);

userHasAllRoles()

Checks whether a user has all of the specified roles.

userHasAllRoles(userId: string, roles: string[]): Promise<boolean>
const isSuperEditor = await this.registrar.userHasAllRoles("user-uuid", [
  "editor",
  "reviewer",
]);

getUserRoleNames()

Returns the names of all roles assigned to a user.

getUserRoleNames(userId: string): Promise<string[]>
const roles = await this.registrar.getUserRoleNames("user-uuid");
// ["admin", "editor"]

Complete Example: Role Seeding and Assignment

import { Injectable, OnModuleInit, Logger } from "@nestjs/common";
import {
  RoleService,
  PermissionService,
  PermissionRegistrarService,
} from "@nestbolt/permissions";

@Injectable()
export class RoleSetupService implements OnModuleInit {
  private readonly logger = new Logger(RoleSetupService.name);

  constructor(
    private readonly roleService: RoleService,
    private readonly permissionService: PermissionService,
    private readonly registrar: PermissionRegistrarService,
  ) {}

  async onModuleInit() {
    // Step 1: Create permissions
    await this.permissionService.findOrCreate("users.view");
    await this.permissionService.findOrCreate("users.create");
    await this.permissionService.findOrCreate("users.edit");
    await this.permissionService.findOrCreate("users.delete");
    await this.permissionService.findOrCreate("posts.view");
    await this.permissionService.findOrCreate("posts.create");
    await this.permissionService.findOrCreate("posts.edit");
    await this.permissionService.findOrCreate("posts.delete");
    await this.permissionService.findOrCreate("analytics.view");
    await this.permissionService.findOrCreate("settings.manage");

    // Step 2: Create roles
    await this.roleService.findOrCreate("super-admin");
    await this.roleService.findOrCreate("admin");
    await this.roleService.findOrCreate("editor");
    await this.roleService.findOrCreate("viewer");

    // Step 3: Assign permissions to roles
    await this.roleService.syncPermissions(
      "super-admin",
      "users.view", "users.create", "users.edit", "users.delete",
      "posts.view", "posts.create", "posts.edit", "posts.delete",
      "analytics.view", "settings.manage",
    );

    await this.roleService.syncPermissions(
      "admin",
      "users.view", "users.create", "users.edit",
      "posts.view", "posts.create", "posts.edit", "posts.delete",
      "analytics.view",
    );

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

    await this.roleService.syncPermissions(
      "viewer",
      "posts.view", "users.view",
    );

    this.logger.log("Roles and permissions seeded successfully.");
  }

  // Called when a new user registers
  async onUserCreated(userId: string) {
    await this.registrar.assignRole(userId, "viewer");
  }

  // Called by an admin to promote a user
  async promoteUser(userId: string, roleName: string) {
    await this.registrar.assignRole(userId, roleName);
    this.logger.log(`User ${userId} promoted to ${roleName}.`);
  }

  // Replace all roles -- used when the admin explicitly sets a user's role
  async setUserRole(userId: string, roleName: string) {
    await this.registrar.syncRoles(userId, roleName);
    this.logger.log(`User ${userId} role set to ${roleName}.`);
  }
}

Exception Handling

The following exceptions can be thrown during role operations:

ExceptionWhen
RoleAlreadyExistsExceptionRoleService.create() is called with a name that already exists for the given guard.
RoleDoesNotExistExceptionRoleService.findByName(), findById(), or delete() cannot find the role. Also thrown when PermissionRegistrarService methods reference a non-existent role name.
import {
  RoleAlreadyExistsException,
  RoleDoesNotExistException,
} from "@nestbolt/permissions";

try {
  await this.roleService.create("admin");
} catch (error) {
  if (error instanceof RoleAlreadyExistsException) {
    // Role already exists -- handle gracefully
  }
}