@nestbolt/permissions
Permission Management
Create, find, delete, and manage permissions using PermissionService and PermissionRegistrarService.
Permission management is split across two services:
PermissionServicehandles CRUD operations on permission records.PermissionRegistrarServicehandles the relationship between users and permissions -- granting, revoking, syncing, and checking permissions for a specific user.
PermissionService
Inject PermissionService to create, find, and delete individual permission records.
import { Injectable } from "@nestjs/common";
import { PermissionService } from "@nestbolt/permissions";
@Injectable()
export class MyService {
constructor(private readonly permissionService: PermissionService) {}
}create()
Creates a new permission. Throws PermissionAlreadyExistsException if a permission with the same name and guard already exists. Emits a permissions.permission-created event.
create(name: string, guardName?: string): Promise<PermissionEntity>const perm = await this.permissionService.create("posts.create");
// { id: "...", name: "posts.create", guardName: "default", createdAt: ..., updatedAt: ... }
// Create for a specific guard
const apiPerm = await this.permissionService.create("posts.create", "api");The guardName parameter defaults to the defaultGuardName configured in the module options (which itself defaults to "default").
findByName()
Finds a permission by its name and guard. Throws PermissionDoesNotExistException if no matching permission is found.
findByName(name: string, guardName?: string): Promise<PermissionEntity>const perm = await this.permissionService.findByName("posts.create");
const apiPerm = await this.permissionService.findByName("posts.create", "api");findById()
Finds a permission by its UUID. Throws PermissionDoesNotExistException if not found.
findById(id: string, guardName?: string): Promise<PermissionEntity>const perm = await this.permissionService.findById(
"550e8400-e29b-41d4-a716-446655440000",
);When guardName is provided, the lookup is further filtered by guard name.
findOrCreate()
Finds an existing permission by name and guard, or creates it if it does not exist. This is the recommended method for seeding permissions, since it is safe to call multiple times without throwing.
findOrCreate(name: string, guardName?: string): Promise<PermissionEntity>// Idempotent -- safe to run on every application startup
const perm = await this.permissionService.findOrCreate("posts.create");
const perm2 = await this.permissionService.findOrCreate("posts.edit");
const perm3 = await this.permissionService.findOrCreate("posts.delete");findAll()
Returns all permissions, optionally filtered by guard name. Results are sorted by name in ascending order.
findAll(guardName?: string): Promise<PermissionEntity[]>// All permissions across all guards
const all = await this.permissionService.findAll();
// Only permissions for the "api" guard
const apiPerms = await this.permissionService.findAll("api");findByNames()
Finds multiple permissions by their names in a single call. Each permission must exist -- PermissionDoesNotExistException is thrown if any name is not found.
findByNames(names: string[], guardName?: string): Promise<PermissionEntity[]>const perms = await this.permissionService.findByNames([
"posts.create",
"posts.edit",
"posts.delete",
]);delete()
Deletes a permission by name or ID. Accepts either the permission name or its UUID. The method first tries to find by ID, then falls back to find by name. Throws PermissionDoesNotExistException if the permission is not found. Emits a permissions.permission-deleted event.
delete(nameOrId: string, guardName?: string): Promise<void>// Delete by name
await this.permissionService.delete("posts.create");
// Delete by ID
await this.permissionService.delete("550e8400-e29b-41d4-a716-446655440000");PermissionRegistrarService -- Permission Operations
The PermissionRegistrarService manages user-permission associations. Inject it to grant, revoke, sync, and check permissions for individual users.
import { Injectable } from "@nestjs/common";
import { PermissionRegistrarService } from "@nestbolt/permissions";
@Injectable()
export class UserPermissionService {
constructor(private readonly registrar: PermissionRegistrarService) {}
}givePermissionTo()
Grants one or more direct permissions to a user. Permissions that the user already has are silently skipped. Flushes the permission cache and emits a permissions.permission-attached event.
givePermissionTo(userId: string, ...permissionNames: string[]): Promise<void>// Grant a single permission
await this.registrar.givePermissionTo("user-uuid", "posts.create");
// Grant multiple permissions at once
await this.registrar.givePermissionTo("user-uuid", "posts.create", "posts.edit", "posts.delete");The permissions must already exist in the database. If a permission name is not found, PermissionDoesNotExistException is thrown.
revokePermissionTo()
Revokes one or more direct permissions from a user. Flushes the cache and emits a permissions.permission-detached event.
revokePermissionTo(userId: string, ...permissionNames: string[]): Promise<void>await this.registrar.revokePermissionTo("user-uuid", "posts.delete");
// Revoke multiple
await this.registrar.revokePermissionTo("user-uuid", "posts.edit", "posts.delete");Note that this only revokes direct permissions. Permissions inherited through roles are unaffected. To remove an inherited permission, you must either remove the role from the user or remove the permission from the role.
syncPermissions()
Replaces all direct permissions on a user with the provided set. All existing direct permissions are removed, and the new permissions are assigned. Flushes the cache.
syncPermissions(userId: string, ...permissionNames: string[]): Promise<void>// User will have exactly these direct permissions
await this.registrar.syncPermissions("user-uuid", "posts.create", "posts.view");This does not affect role-inherited permissions.
userHasPermissionTo()
Checks whether a user has a specific permission. This is the most comprehensive check -- it evaluates all three channels:
- Direct permissions assigned to the user.
- Role-inherited permissions from the user's assigned roles.
- Wildcard permissions (if
enableWildcardPermissionsistrue).
userHasPermissionTo(userId: string, permission: string, guardName?: string): Promise<boolean>const canCreate = await this.registrar.userHasPermissionTo("user-uuid", "posts.create");
// true if user has the permission directly, via a role, or via a wildcarduserHasAnyPermission()
Checks whether a user has at least one of the specified permissions.
userHasAnyPermission(userId: string, permissions: string[]): Promise<boolean>const canDoSomething = await this.registrar.userHasAnyPermission("user-uuid", [
"posts.create",
"posts.edit",
"posts.delete",
]);userHasAllPermissions()
Checks whether a user has all of the specified permissions.
userHasAllPermissions(userId: string, permissions: string[]): Promise<boolean>const canManagePosts = await this.registrar.userHasAllPermissions("user-uuid", [
"posts.create",
"posts.edit",
"posts.delete",
]);userHasDirectPermission()
Checks whether a user has a specific permission assigned directly (not via a role). This is useful when you need to distinguish between direct and inherited permissions.
userHasDirectPermission(userId: string, permission: string, guardName?: string): Promise<boolean>const directlyGranted = await this.registrar.userHasDirectPermission(
"user-uuid",
"posts.create",
);getUserAllPermissions()
Returns all permissions for a user -- both directly assigned and inherited through roles. Duplicates are deduplicated by permission ID.
getUserAllPermissions(userId: string): Promise<PermissionEntity[]>const allPerms = await this.registrar.getUserAllPermissions("user-uuid");
for (const perm of allPerms) {
console.log(perm.name, perm.guardName);
}getUserDirectPermissions()
Returns only the permissions that are directly assigned to a user (not inherited through roles).
getUserDirectPermissions(userId: string): Promise<PermissionEntity[]>const directPerms = await this.registrar.getUserDirectPermissions("user-uuid");getUserPermissionsViaRoles()
Returns only the permissions that a user inherits through their assigned roles.
getUserPermissionsViaRoles(userId: string): Promise<PermissionEntity[]>const inheritedPerms = await this.registrar.getUserPermissionsViaRoles("user-uuid");flushCache()
Manually clears the in-memory permission cache. Emits a permissions.cache-flushed event. The cache is also flushed automatically whenever roles or permissions are modified through the service methods.
flushCache(): voidthis.registrar.flushCache();Complete Example: Permission-Based Feature Flags
import { Injectable } from "@nestjs/common";
import {
PermissionService,
PermissionRegistrarService,
} from "@nestbolt/permissions";
@Injectable()
export class FeatureFlagService {
constructor(
private readonly permissionService: PermissionService,
private readonly registrar: PermissionRegistrarService,
) {}
// Create a feature flag as a permission
async createFeatureFlag(featureName: string) {
return this.permissionService.findOrCreate(`feature.${featureName}`);
}
// Enable a feature for a user
async enableFeature(userId: string, featureName: string) {
await this.registrar.givePermissionTo(userId, `feature.${featureName}`);
}
// Disable a feature for a user
async disableFeature(userId: string, featureName: string) {
await this.registrar.revokePermissionTo(userId, `feature.${featureName}`);
}
// Check if a user has access to a feature
async hasFeature(userId: string, featureName: string): Promise<boolean> {
return this.registrar.userHasPermissionTo(userId, `feature.${featureName}`);
}
// Get all features available to a user
async getUserFeatures(userId: string): Promise<string[]> {
const allPerms = await this.registrar.getUserAllPermissions(userId);
return allPerms
.filter((p) => p.name.startsWith("feature."))
.map((p) => p.name.replace("feature.", ""));
}
}Direct vs. Role-Inherited Permissions
Understanding the distinction between direct and role-inherited permissions is important for managing access correctly:
| Type | Assigned via | Revoked via | Checked by |
|---|---|---|---|
| Direct | registrar.givePermissionTo() | registrar.revokePermissionTo() | registrar.userHasDirectPermission(), registrar.userHasPermissionTo() |
| Role-inherited | registrar.assignRole() + roleService.givePermissionTo() | registrar.removeRole() or roleService.revokePermissionTo() | registrar.userHasPermissionTo() |
The general-purpose userHasPermissionTo() method checks both channels (plus wildcards). Use userHasDirectPermission() when you need to know specifically whether a permission was granted directly.
Exception Handling
| Exception | When |
|---|---|
PermissionAlreadyExistsException | PermissionService.create() is called with a name that already exists for the given guard. |
PermissionDoesNotExistException | PermissionService.findByName(), findById(), findByNames(), or delete() cannot find the permission. Also thrown by PermissionRegistrarService methods when a referenced permission name does not exist. |
import {
PermissionAlreadyExistsException,
PermissionDoesNotExistException,
} from "@nestbolt/permissions";
try {
await this.permissionService.create("posts.create");
} catch (error) {
if (error instanceof PermissionAlreadyExistsException) {
// Permission already exists -- use findOrCreate() instead
}
}
try {
await this.registrar.givePermissionTo("user-uuid", "nonexistent.permission");
} catch (error) {
if (error instanceof PermissionDoesNotExistException) {
// The permission must be created first
}
}