| name | authz-enforcement |
| description | Enforce authorization in @dudousxd/nestjs-authz with the @Can and @Roles guards and the programmatic Gate. @Can('update', Post) resolves a Post instance via the ResourceResolver and runs PostPolicy.update; @Can('create', Post, { classLevel: true }) skips loading; @Can('access-admin') hits an ad-hoc gate. @Roles('admin','teacher') is the coarse role check. Both guards are auto-registered as APP_GUARD and are inert on un-annotated routes. Covers the ResourceResolver seam (default IdParamResourceResolver reads :id vs a real ORM resolver bound to RESOURCE_RESOLVER) and the Gate API: authorize/allows/denies/allowsMany, forUser(user) BoundGate, define(ability, fn), hasRole/hasAnyRole.
|
| metadata | {"type":"core","library":"@dudousxd/nestjs-authz","library_version":"0.6.3","framework":"nestjs"} |
Enforcing authorization
Two paths: declarative guards (@Can, @Roles) and the programmatic Gate.
Both guards are registered as APP_GUARD by AuthzModule.forRoot, and both are inert
on routes without their decorator (no metadata → allow).
Setup
import { Controller, Param, Patch, Post as HttpPost } from '@nestjs/common';
import { Can, Roles } from '@dudousxd/nestjs-authz';
import { Post } from './post.entity';
@Controller('posts')
export class PostController {
@Patch(':id')
@Can('update', Post)
update(@Param('id') id: string) {}
@HttpPost()
@Can('create', Post, { classLevel: true })
create() {}
@HttpPost('purge')
@Roles('admin', 'teacher')
purge() {}
}
Core patterns
Programmatic Gate
Inject Gate for checks inside services. authorize throws ForbiddenException on deny;
allows/denies return a boolean.
import { Injectable } from '@nestjs/common';
import { Gate } from '@dudousxd/nestjs-authz';
@Injectable()
export class PostService {
constructor(private readonly gate: Gate) {}
async update(post: Post) {
await this.gate.authorize('update', post);
if (await this.gate.allows('delete', post)) { }
}
}
forUser(user) returns a BoundGate that bypasses the context accessor — use it when no
nestjs-context is wired, or to check a user other than the current one:
await this.gate.forUser(someUser).authorize('update', post);
allowsMany batch-checks one user in a single pass (resolves the user once, shares a
permission cache — kills the N+1 on a list page):
const results = await this.gate.allowsMany([
{ ability: 'update', resource: post },
{ ability: 'delete', resource: post },
]);
Ad-hoc gates
Register a model-less, named ability with define, then guard or check it:
this.gate.define('access-admin', (user) => (user as User).role === 'staff');
await this.gate.allows('access-admin');
Custom ResourceResolver
@Can('update', Post) needs an instance. The default IdParamResourceResolver builds a
{ id } shim from the route :id. For real entities, implement ResourceResolver and
bind RESOURCE_RESOLVER (or pass resourceResolver to forRoot):
import type { ResourceResolver } from '@dudousxd/nestjs-authz';
import { RESOURCE_RESOLVER } from '@dudousxd/nestjs-authz';
import type { ExecutionContext, Type } from '@nestjs/common';
class OrmResourceResolver implements ResourceResolver {
async resolve(resource: Type<unknown>, ctx: ExecutionContext) {
const req = ctx.switchToHttp().getRequest<{ params: { id: string } }>();
return this.repos.get(resource).findOneBy({ id: Number(req.params.id) });
}
}
Common mistakes
1. Class-level ability without { classLevel: true }
@HttpPost()
@Can('create', Post)
create() {}
@HttpPost()
@Can('create', Post, { classLevel: true })
create() {}
Mechanism: with a resource and no classLevel, the guard loads an instance via the
resolver; a undefined instance is treated as not-found → deny.
Source: packages/core/src/guard/can.guard.ts (canActivate), packages/core/src/decorator/can.decorator.ts.
2. Using the default resolver and expecting real entity fields
AuthzModule.forRoot({ policies: [PostPolicy] });
AuthzModule.forRoot({ policies: [PostPolicy], resourceResolver: new OrmResourceResolver(...) });
Mechanism: the default resolver produces a Object.create(resource.prototype) shim with
only id set (enough for instance.constructor matching, not for column reads).
Source: packages/core/src/resource-resolver.ts (IdParamResourceResolver).
3. Ambiguous class-level ability across multiple policies
await this.gate.allows('create');
await this.gate.allows('create', Post);
Mechanism: with no resource the Gate scans all policies for the method; >1 match is an
ambiguous (arbitrary) choice, so it throws instead of guessing.
Source: packages/core/src/gate.ts (resolvePolicy, AmbiguousAbilityException).