Using CASL in NestJS guards

Viewed 623

In this section of docs not all use-cases of guard usage explained clearly:
NestJS Docs - Claims-based authorization

CaslAbilityFactory implemented for these use-cases:

  • Admins can manage (create/read/update/delete) all entities
  • Users have read-only access to everything
  • Users can update their articles (article.authorId === userId)
  • Articles that are published already cannot be removed (article.isPublished === true)

and explained only the most trivial use-case:

Users have read-only access to everything

It's demonstrated with this controller method:

@Get()
@UseGuards(PoliciesGuard)
@checkPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
    return this.articlesService.findAll();
}

but how should I annotate a method to check the 3rd or 4th use-cases:

Articles that are published already cannot be removed:
(article.isPublished === true)

@Delete()
@UseGuards(PoliciesGuard)
@checkPolicies(?????????????????????????????)
delete(@Body() article: Article) {
    return this.articlesService.delete(article.id);
}

Is it possible, at all? For this requirement PoliciesGuard or handler declared in @checkPolicies should be able to access method arguments.

How can controller method arguments be accessed from a guard?

Of course a workaround solution if you call ability.can(...) directly from controller method:

@Delete()
@UseGuards(SomeGuards but NOT PoliciesGuard)
delete(@Body() article: Article) {
    const ability = this.caslAbilityFactory.createForUser(<<user from request>>);
    if (!ability.can(Action.Delete, article)) {
       throw new UnauthorizedException();
    }
    return this.articlesService.delete(article.id);
}

But this solution doesn't fit the original declarative pattern.

1 Answers

You can achieve this in the PolicyGuard. This is mentioned in NestJS docs here

your policy guard will be like this

@Injectable()
export class PoliciesGuard extends RequestGuard implements CanActivate {
  public constructor(private reflector: Reflector, private caslAbilityFactory: CaslAbilityFactory) {
    super();
  }

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandlers = this.reflector.get<PolicyHandler[]>(CHECK_POLICIES_KEY, context.getHandler()) || [];
    const request = this.getRequest(context);
    const { user } = request;
    const ability = await this.caslAbilityFactory.createForUser(user?.userId);

    return policyHandlers.every(handler => this.execPolicyHandler(handler, ability, request));
  }

  private execPolicyHandler(handler: PolicyHandler, ability: AppAbility, request: Request) {
    if (typeof handler === 'function') {
      return handler(ability, request);
    }
    return handler.handle(ability, request);
  }
}

then the checkPolicy will accept this function

export class ReadArticlePolicyHandler implements IPolicyHandler {
  handle(ability: AppAbility, request) {
    const { query } = request;
    const article = new Article();
    article.scope = query.scope;
    return ability.can(Action.Read, article) || ability.can(Action.Delete, article);
  }
}
Related