NestJS — Enterprise Node.js

Advanced Authorization: Policies & CASL

18 min Lesson 30 of 30

Advanced Authorization: Policies & CASL

RBAC answers "does this user have the admin role?" But real apps need finer rules: "can this user edit this specific article?" That depends on the user, the action, and the resource together. Policy-based (or attribute-based) authorization handles this, and CASL is the most popular library for it in the NestJS world.

Why RBAC is not enough

Consider editing an article. The rule is "a user can edit an article they authored, OR any article if they are an admin." Pure RBAC cannot express the ownership half — the decision depends on comparing the user to the resource. This is where policies shine.

Defining abilities with CASL

CASL describes permissions as abilities: combinations of an action (read, update, delete) and a subject (a model), optionally with conditions:

import { AbilityBuilder, createMongoAbility } from '@casl/ability'; function defineAbilitiesFor(user) { const { can, cannot, build } = new AbilityBuilder(createMongoAbility); if (user.isAdmin) { can('manage', 'all'); // admins can do everything } else { can('read', 'Article'); // anyone can read articles can('update', 'Article', { authorId: user.id }); // only their own } return build(); }

manage and all are CASL wildcards meaning "any action" and "any subject". The condition { authorId: user.id } is the ownership rule RBAC could not express.

Checking an ability

const ability = defineAbilitiesFor(currentUser); if (ability.can('update', article)) { // allowed — either they own this article, or they are an admin } else { throw new ForbiddenException(); }
The check is resource-aware. ability.can('update', article) evaluates the condition against the actual article instance — comparing its authorId to the user. That is the leap beyond role checks.

Integrating with NestJS

The idiomatic pattern is a CaslAbilityFactory provider that builds abilities for a user, plus a policies guard that evaluates a declared policy. A @CheckPolicies() decorator attaches the rule, and the guard enforces it:

@UseGuards(AuthGuard('jwt'), PoliciesGuard) @CheckPolicies((ability) => ability.can('update', 'Article')) @Patch(':id') update(@Param('id') id: string) {}

Choosing the right model

  • RBAC — simple role gates ("admins only"). Use it when permissions map cleanly to roles.
  • Policy/ABAC (CASL) — rules involving ownership, conditions, or resource attributes. Use it when "it depends on the resource".
Combine them. Most production apps use RBAC for coarse gates (is this an admin area?) and policies for fine-grained, ownership-aware checks (can this user touch this record?). They are complementary, not competing.
Always enforce authorization on the server. Hiding a button in the UI is not security — the same ability checks must run in your guards/services. A determined client can call any endpoint directly, so the server is the only place authorization truly counts.

Summary

Policy-based authorization (with CASL) expresses rules that depend on the action, the subject, and resource conditions — like editing only your own content — which RBAC cannot. Define abilities with can/cannot, check them resource-aware with ability.can(action, resource), and integrate via a policies guard and @CheckPolicies(). Use RBAC for coarse roles and policies for fine ownership rules. This completes Phase 6 — your APIs are now properly authenticated and authorized. Next: API design.

Tutorial Complete!

Congratulations! You have completed all lessons in this tutorial.