Skip to Content

Authorization

NestJS CRUD Automator now ships with a subscriber-style RBAC system. Policies are declared once, discovered automatically, and applied to securable controllers without manual guard wiring.

Enable the Authorization Module

Import ApiAuthorizationModule in your root module to boot the discovery service, registry, guard, and engine:

app.module.ts
import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { ApiAuthorizationModule } from "@elsikora/nestjs-crud-automator"; @Module({ imports: [ TypeOrmModule.forRoot(/* ... */), ApiAuthorizationModule, // 👈 enables discovery + guard ], }) export class AppModule {}

The authorization guard is attached automatically. Mark controllers with @ApiControllerSecurable() to enable policy evaluation; no @UseGuards(ApiAuthorizationGuard) calls are required.

Create a Policy

Policies mirror the subscriber system: decorate a class and extend ApiAuthorizationPolicyBase. The decorator only needs the entity; policy IDs and priorities default to predictable values.

policies/user-access.policy.ts
import type { IApiAuthorizationRuleContext, IApiAuthorizationScope, TApiAuthorizationPolicyBeforeGetListResult, TApiAuthorizationPolicyBeforeGetResult, TApiAuthorizationPolicyCustomActionResult, TApiAuthorizationRuleResultTransform, TApiAuthorizationRuleScopeResolver } from "@elsikora/nestjs-crud-automator"; import { ApiAuthorizationPolicy, ApiAuthorizationPolicyBase } from "@elsikora/nestjs-crud-automator"; import { EUserRole } from "../user-role.enum"; import { UserEntity } from "../user.entity"; @ApiAuthorizationPolicy<UserEntity>({ entity: UserEntity, priority: 200 }) export class UserAccessPolicy extends ApiAuthorizationPolicyBase<UserEntity> { public onBeforeGet(): TApiAuthorizationPolicyBeforeGetResult<UserEntity> { const resultTransform: TApiAuthorizationRuleResultTransform<UserEntity, UserEntity> = (result: UserEntity, context: IApiAuthorizationRuleContext<UserEntity>): UserEntity => { if (!context.subject.roles.includes(EUserRole.ADMIN)) { result.email = "***"; } return result; }; return this.allowForRoles([EUserRole.ADMIN], { description: "Admins can fetch users by id", scope: (context: IApiAuthorizationRuleContext<UserEntity>): IApiAuthorizationScope<UserEntity> => ({ where: { id: context.subject.id }, }), resultTransform, }); } public onBeforeGetList(): TApiAuthorizationPolicyBeforeGetListResult<UserEntity> { const scope: TApiAuthorizationRuleScopeResolver<UserEntity> = (context: IApiAuthorizationRuleContext<UserEntity>): IApiAuthorizationScope<UserEntity> => ({ where: { ownerId: context.subject.id }, }); return this.allowForRoles([EUserRole.ADMIN], { description: "Admins can list users in their tenant", scope, }); } public getCustomActionRule(action: "promote"): TApiAuthorizationPolicyCustomActionResult<UserEntity, "promote"> { if (action === "promote") { return this.allowForRoles([EUserRole.ADMIN]); } return []; } }

Inside any policy hook, the context argument exposes:

  • DATA — typed container (includes subject and optional authenticationRequest);
  • action — a string action name (for example get, create, promote);
  • routeType — an EApiRouteType value for standard CRUD hooks;
  • entity and entityMetadata — the model constructor and its metadata (columns, primary key, table name).

Hook Summary

Hook names align with EApiRouteType:

  • onBeforeCreate, onBeforeGet, onBeforeGetList, onBeforeUpdate, onBeforePartialUpdate, onBeforeDelete
  • getCustomActionRule(action) handles non-standard methods such as solve, promote, etc. Return an empty array when no rules apply.

Every hook can return:

  • this.allow() / this.deny() helpers
  • arrays of rules (priority is policyPriority + rulePriority)
  • an empty array ([]) when no rules apply

To make return types explicit, use TApiAuthorizationPolicyBefore*Result and TApiAuthorizationPolicyCustomActionResult.

Subject Resolution

ApiAuthorizationGuard normalizes request.user through AuthorizationResolveDefaultSubject, so every policy receives the same IApiAuthorizationSubject shape:

  • id — the first non-empty string among id, uuid, or email;
  • roles — the values coming from roles (array) or role (single string);
  • permissions — either an array from permissions or a single permission string;
  • attributes — the original request.user object (helpful for tenant/organization data, etc.).

Just make sure your auth layer injects roles and permissions into request.user:

auth.strategy.ts
const client = await this.clientsService.resolveFromToken(token); return { id: client.id, roles: client.roles ?? ["client"], permissions: client.permissions ?? ["challenge:read"], tenantId: client.tenantId, };

If you need a different structure, override the resolver — the only requirement is to return a valid IApiAuthorizationSubject.

Policy Caching (Optional)

Policies are built per request by default. If your policies are static and do not depend on the subject or request, you can enable caching:

app.module.ts
import { ApiAuthorizationPolicyRegistry } from "@elsikora/nestjs-crud-automator"; import { Injectable, OnModuleInit } from "@nestjs/common"; @Injectable() export class AppModule implements OnModuleInit { constructor(private readonly policyRegistry: ApiAuthorizationPolicyRegistry) {} onModuleInit() { this.policyRegistry.configureCache({ isEnabled: true, ttlMs: 60_000 }); } }

Leave caching disabled if policies read dynamic data or depend on the current subject. Policy-level cache options override the registry defaults. Omit ttlMs to cache indefinitely.

You can also configure caching per policy:

policies/user-access.policy.ts
@ApiAuthorizationPolicy<UserEntity>({ entity: UserEntity, cache: { isEnabled: true, ttlMs: 30_000 }, }) export class UserAccessPolicy extends ApiAuthorizationPolicyBase<UserEntity> { // ... }

Decision Resource Lifecycle

  1. The guard runs before the controller, so the engine initially gets resource: undefined.
  2. The controller method validates input, performs service.get / update / create, and only after success calls AuthorizationDecisionAttachResource to store the resource in authorizationDecision.resource.
  3. BEFORE-subscribers see decision metadata; AFTER/AFTER_ERROR hooks and AuthorizationDecisionApplyResult receive the same entity instance returned by the service.
  4. This keeps the guard free from business logic duplication and ensures transforms/subscribers operate on the real data.
  5. authorizationDecision.policyIds contains all policy IDs that contributed rules to the aggregated policy.

Context, Scope, and Result Transform

Each rule can define:

  • condition(context) → async boolean
  • scope(context) → returns FindOptionsWhere fragments (merged with existing filters)
  • resultTransform(result, context) → sequential transformations for masking or enrichment

The engine evaluates rules in priority order. The first matching DENY short-circuits; otherwise all matching ALLOW rules contribute scope and transforms. Scopes are applied to controller queries (GET, GET_LIST, UPDATE, DELETE, etc.) before hitting your service.

Result Transform Example

resultTransform(result, ruleContext) is the right place to mask fields or adjust the response based on ruleContext.subject and (on AFTER stages) ruleContext.resource:

policies/challenge.policy.ts
onBeforeGet() { return this.allow({ resultTransform: (result, { subject }) => { if (result && !subject.roles.includes("admin")) { delete (result as Challenge).secretKey; } return result; }, }); } onBeforeGetList() { return this.allow({ resultTransform: (result) => { if (Array.isArray(result)) { for (const item of result) { delete (item as Challenge).secretKey; } } return result; }, }); }
  • You may return several rules with resultTransform — the engine appends them to decision.transforms and executes sequentially.
  • Transforms receive the same context as conditions/scopes, so you can rely on roles, permissions, and the loaded resource.

Typed Result Payloads

Every standard hook now knows the precise result type, so resultTransform receives a strongly typed result without extra casts:

RouteResult type RNotes
onBeforeCreate / onBeforeGet / onBeforePartialUpdate / onBeforeUpdateE
onBeforeDeletevoid
onBeforeGetListIApiGetListResponseResult<E>
Custom actionsTApiAuthorizationRuleTransformPayload<E> (fallback)
  • DELETE returns void, so transforms there can only trigger side effects.
  • Custom actions fall back to the union type; specify this.allow<TCustomResult>({ ... }) if you need to narrow it.

Controller Opt-In

Mark controllers once with @ApiControllerSecurable() to enable policy evaluation. The guard:

  1. Resolves the entity from controller metadata
  2. Infers the action from the handler name
  3. Builds/loads the aggregated policy from the registry
  4. Resolves the subject from request.user with smart fallbacks (id, uuid, email, roles, permissions)
  5. Applies scope + transforms to the generated methods

Decisions are stored on the request and injected into subscriber contexts, so you can inspect context.DATA.authorizationDecision inside existing hooks.

Migration Tips

Existing manual guards can be phased out gradually:

  1. Import ApiAuthorizationModule.
  2. Implement policies using helper methods (allowForRoles, scopeToOwner, etc.).
  3. Remove legacy controller configuration (no authorization blocks required).

See the API Reference for the complete contract and helper methods exposed by ApiAuthorizationPolicyBase.

Last updated on