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:
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.
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 (includessubjectand optionalauthenticationRequest);action— a string action name (for exampleget,create,promote);routeType— anEApiRouteTypevalue for standard CRUD hooks;entityandentityMetadata— the model constructor and its metadata (columns, primary key, table name).
Hook Summary
Hook names align with EApiRouteType:
onBeforeCreate,onBeforeGet,onBeforeGetList,onBeforeUpdate,onBeforePartialUpdate,onBeforeDeletegetCustomActionRule(action)handles non-standard methods such assolve,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 amongid,uuid, oremail;roles— the values coming fromroles(array) orrole(single string);permissions— either an array frompermissionsor a singlepermissionstring;attributes— the originalrequest.userobject (helpful for tenant/organization data, etc.).
Just make sure your auth layer injects roles and permissions into request.user:
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:
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:
@ApiAuthorizationPolicy<UserEntity>({
entity: UserEntity,
cache: { isEnabled: true, ttlMs: 30_000 },
})
export class UserAccessPolicy extends ApiAuthorizationPolicyBase<UserEntity> {
// ...
}Decision Resource Lifecycle
- The guard runs before the controller, so the engine initially gets
resource: undefined. - The controller method validates input, performs
service.get / update / create, and only after success callsAuthorizationDecisionAttachResourceto store the resource inauthorizationDecision.resource. - BEFORE-subscribers see decision metadata; AFTER/AFTER_ERROR hooks and
AuthorizationDecisionApplyResultreceive the same entity instance returned by the service. - This keeps the guard free from business logic duplication and ensures transforms/subscribers operate on the real data.
authorizationDecision.policyIdscontains all policy IDs that contributed rules to the aggregated policy.
Context, Scope, and Result Transform
Each rule can define:
condition(context)→ async booleanscope(context)→ returnsFindOptionsWherefragments (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:
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 todecision.transformsand 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:
| Route | Result type R | Notes |
|---|---|---|
onBeforeCreate / onBeforeGet / onBeforePartialUpdate / onBeforeUpdate | E | |
onBeforeDelete | void | |
onBeforeGetList | IApiGetListResponseResult<E> | |
| Custom actions | TApiAuthorizationRuleTransformPayload<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:
- Resolves the entity from controller metadata
- Infers the action from the handler name
- Builds/loads the aggregated policy from the registry
- Resolves the subject from
request.userwith smart fallbacks (id,uuid,email, roles, permissions) - 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:
- Import
ApiAuthorizationModule. - Implement policies using helper methods (
allowForRoles,scopeToOwner, etc.). - Remove legacy controller configuration (no
authorizationblocks required).
See the API Reference for the complete contract and helper methods exposed by ApiAuthorizationPolicyBase.