Files
What you'll learn
- Say when a role is not enough: permissions that depend on the record being touched
- Build an ability with CASL's AbilityBuilder: can and cannot, a subject class, conditions on its fields, and why a later cannot beats an earlier can
- Check an ability against a subject type in a guard and against an instance in the service, and know what each answer means
- Attach policies to routes with metadata and a guard that runs them all
Policies
Roles answered lesson 3's question: may this kind of user do this kind of thing? They stop answering when the thing itself matters. Ada may rename a cat, but only a cat she adopted. An admin may delete cats, but not one that has been adopted, because the shelter keeps the record of every adoption. Neither rule is about a role; both are about a relationship between the user and the record. Encoding them as if statements scattered through the service works until the third rule, and then nobody can say what a user may do without reading every handler. An ability says it in one place.
Abilities with CASL
CASL is a library for exactly this. An ability is a list of rules built for one user, each rule an action on a subject, optionally with conditions on the subject's fields:
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
can(Action.Read, Article); // any article
can(Action.Update, Article, { authorId: user.id }); // only their own
cannot(Action.Delete, Article, { isPublished: true }); // nobody, even after a can
can(Action.Manage, 'all'); // an admin: every action on everything
manage is CASL's word for every action, 'all' for every subject. Conditions use the same syntax as a MongoDB query, { authorId: 7 }, { age: { $gte: 2 } }, matched against the fields of a plain object or a class instance. Rules are read in order and a later rule wins, which is what makes cannot useful: can(Manage, 'all') followed by cannot(Delete, Article, { isPublished: true }) is an admin who can do everything except that. build() turns the list into an ability, and detectSubjectType tells it how to name a subject given an instance, here its class.
Asking is ability.can(action, subject), and the subject can be either of two things, with different meanings:
ability.can(Action.Update, Article); // the class: may they update some article?
ability.can(Action.Update, oneArticle); // an instance: may they update this one?
Against the class, a conditional rule counts as a yes, because there exist articles they may update; the conditions cannot be checked without a record. Against an instance, the conditions are evaluated against its fields. So a guard, which runs before the handler loads anything, can only ask the first question; the second belongs where the record is, in the service. Both are needed: the first stops a user who may never delete anything before a database query, the second stops the admin from deleting the one cat the rules protect.
The factory, the decorator, the guard
The docs wrap ability-building in an injectable CaslAbilityFactory with one method, createForUser(user), so a guard or a service can ask for the current user's ability anywhere. Policies attach to routes the way roles did, as metadata: @CheckPolicies() takes one or more handlers, each a function of the ability (or an object with a handle method, for a policy worth naming), and the guard reads them back:
@CheckPolicies((ability) => ability.can(Action.Read, Article))
PoliciesGuard has three steps: read the handlers off the handler with reflector.get() (no handlers, nothing to check), build the ability for request.user, and pass only if every handler returns true. Returning false gets Nest's 403 Forbidden resource, as in lesson 3. The guard is bound with @UseGuards(PoliciesGuard) on the routes that declare policies; it needs the factory injected, so the module that uses it imports CaslModule.
Where it sits
The global AuthGuard runs first and leaves the verified token on the request; a route's own guards run after the global ones, so PoliciesGuard finds request.user ready. Then the handler, which for a record-level decision builds the same ability again and hands it to the service with the record.
Your task
Ada looks after Tom; nobody deletes an adopted cat.
- In
casl/casl-ability.factory.ts, build the ability: an admin manages all; everyone else reads all and updates the cats whoseownerIdis their own id (user.sub); nobody deletes a cat whoseadoptedistrue. - In
casl/policies.guard.ts, read the route's policy handlers, build the user's ability, and pass only if every handler approves. - In
cats/cats.service.ts, ask the ability about the specific cat before renaming (403You may only rename a cat you adopted) and before deleting (403An adopted cat stays on the record). - In
cats/cats.controller.ts, guard creating and deleting with the policies guard and a policy each: may this user create cats at all, delete cats at all? Renaming gets no guard: every member of staff may update some cat, so the class-level answer is always yes and only the service's question about this cat can refuse.
Do step 1 and 4 first, then Run: ada is refused on the class-level checks, but john can still delete the adopted cat, because a guard never sees the record. Step 3 closes that.
When it fails
- John deletes the adopted cat with a 204: the service never asked about the instance, or
cannotwas written beforecan(Manage, 'all')and the later rule won. - Ada renames Luna with a 200: the update rule has no condition, or the condition names a field the entity does not have (
ownerforownerId), which matches nothing and everything. - Ada gets 403 renaming her own cat: the condition compares
ownerIdwith the wrong thing; the token's user id issub. - Every guarded route is 403, for john too: the guard reads handlers that were never attached, or checks them with
someinstead ofevery; or the ability was built forundefinedbecause the guard read the user from the wrong place. Nest can't resolve dependencies of the PoliciesGuard (Reflector, ?): the controller's module does not importCaslModule.- The instance check always says yes:
ability.can(Action.Update, Cat)was asked with the class where the instance was meant. - A policy on the rename route changes nothing: it asks the class-level question, which every signed-in user passes; a guard that cannot refuse anyone is noise, and the mutation harness behind this course flags it as one.
Remember
- An ability is one user's rules:
can/cannot, action, subject, conditions; a later rule wins. can(action, Class)asks whether some record is allowed;can(action, instance)checks this record's fields.- The guard answers the class question before the handler; the service answers the instance question with the record in hand.
- Policies are metadata on the route; the guard runs every handler and returns false for Nest's 403.
Stuck? Show a hint
casl-ability.factory.ts: if (user.roles.includes(Role.Admin)) can(Action.Manage, 'all'); else can(Action.Read, 'all'); can(Action.Update, Cat, { ownerId: user.sub }); cannot(Action.Delete, Cat, { adopted: true }). policies.guard.ts: this.reflector.get<PolicyHandler[]>(CHECK_POLICIES_KEY, context.getHandler()) || []; ability = this.caslAbilityFactory.createForUser(request['user']); handlers.every((h) => this.execPolicyHandler(h, ability)). cats.service.ts: if (!ability.can(Action.Update, cat)) throw new ForbiddenException(...). cats.controller.ts: @UseGuards(PoliciesGuard) and @CheckPolicies((ability: AppAbility) => ability.can(Action.Create, Cat)) on create, Delete on remove.
Press Run tests to start the app. Its log appears here.Graded endpoints
A user, not an admin
His ability will be manage all
Her ability: read all, update her own cats
The token payload the guards read: id, name, roles
The policy asks whether she may create cats at all; no rule says so, so the guard returns false and Nest answers 403
manage on all covers create on Cat
A second cat, which nobody will adopt
Reading stays public
Any member of staff may; the cat now carries her id
The guard passed her (some cats are hers to update); the service checked this cat, whose ownerId matches
Any member of staff, as in lesson 2; no policy involved
Decrypted for staff
Past the guard, refused by the service: the condition ownerId is not hers
manage all, no condition
No rule lets a user delete any cat, so the guard stops her before the service
The guard passed him (he may delete cats); the service asked about this cat, and the cannot rule on adopted cats beats manage all
Not adopted, so the cannot rule does not match
Tom Jr, adopted by ada, is still on the record