External policy engines
Delegate decisions to Cerbos, OPA or any policy decision point. The DecisionProvider seam can allow, deny or abstain, batches checks, and maps query plans onto gate.scope().
Policies and RBAC cover most apps. Some organizations, though, already run a policy decision
point (PDP), such as Cerbos, OPA or OpenFGA, and want authz to defer to
it. The DecisionProvider seam does that.
Unlike the grant-only permission seam, a decision provider is authoritative and resource-aware:
import type { DecisionProvider } from '@dudousxd/nestjs-authz';
const provider: DecisionProvider = {
// true / { allowed: true } → allow · false / { allowed: false, message } → deny · undefined → abstain
decide(user, ability, resource) { /* ... */ },
// optional: one round-trip for a whole gate.allowsMany(...) batch
async decideMany(user, requests) { /* ... */ },
// optional: gate.scope(Entity, ability) → a ScopeConstraint (undefined abstains)
planScope(user, entity, ability) { /* ... */ },
};Register it under the DECISION_PROVIDER token:
{ provide: DECISION_PROVIDER, useValue: provider }.
Precedence
- The global
superAdminhook. It now also receives the resource as a third argument (the entity class forgate.scope). - The decision provider. Allow or deny short-circuits. Returning
undefinedabstains. - The permission-provider grant (RBAC).
- The policy
beforehook, then the policy method, or an ad-hoc gate. - The global
afterhook, which only runs when nothing above had an opinion.
The provider is also consulted for anonymous users, so the engine can allow public access.
Abstain to keep authz's default of denying anonymous callers. A denial made by the provider
surfaces in the ForbiddenException as reason: 'decision-provider', together with its message.
Cerbos
The Cerbos adapter ships inside the core package as the @dudousxd/nestjs-authz/cerbos subpath.
Its client types are structural, so you bring your own @cerbos/http or @cerbos/grpc client and
nothing extra gets installed:
import { DECISION_PROVIDER } from '@dudousxd/nestjs-authz';
import { CerbosDecisionProvider } from '@dudousxd/nestjs-authz/cerbos';
import { HTTP } from '@cerbos/http';
@Module({
providers: [
{
provide: DECISION_PROVIDER,
useValue: new CerbosDecisionProvider({
// A client, or (user) => per-tenant client. Resolving undefined abstains for that user.
client: new HTTP(process.env.CERBOS_URL!),
principal: (u) => u && { id: u.id, roles: u.roles, attr: { tenantId: u.tenantId } },
// Which checks Cerbos governs. undefined abstains, so policies and RBAC decide.
resource: (ability, r) =>
r instanceof Post ? { kind: 'app:post', id: r.id, attr: { ownerId: r.ownerId } } : undefined,
// Query plans for gate.scope(Post, 'read').
scope: { resource: (entity) => (entity === Post ? { kind: 'app:post' } : undefined) },
}),
},
],
})
export class AppModule {}gate.allows/gate.authorizemap toCheckResources.gate.allowsManysends one request: resources are de-duplicated, their actions merged, and large batches split into chunks ofmaxBatchSize(default 50).gate.scope(Post, 'read')maps toPlanResources, translated to the scope AST that every ORM adapter compiles into aWHERE:ALWAYS_ALLOWEDbecomes allow-all andALWAYS_DENIEDbecomes deny-all.and/orbecome groups, andnotis pushed down to the leaves.eq ne lt gt le ge incomparisons onrequest.resource.attr.*(orrequest.resource.id) become leaf conditions.- To rename attributes to columns, use
scope.field.
The adapter fails closed: an unreachable PDP denies, and a plan that uses something the scope
AST can't express (arithmetic, lambdas, membership in an attribute's array) scopes to no rows. Set
onFailure: 'abstain' to fall back to your own policies instead, and use onError to log.
RBAC (roles & permissions)
The opt-in, persisted roles-and-permissions layer — coarse @Roles checks in the core, and database-backed providers via the typeorm / mikro-orm / prisma / drizzle adapters.
Ecosystem & integrations
The glue packages that make authz disappear into your stack — context, Inertia, codegen, React, Telescope, and the filter integration.