Creating a NestJs CASL Authorization guard that utilizes user attributes and policies for access control

I am currently working on implementing a generic policy-based guard in NestJS and CASL for list/get endpoints. I have been referring to the documentation available at https://docs.nestjs.com/security/authorization#integrating-casl.

In this implementation, the goal is to ensure that users can only read articles or books if their group memberships match the specific `groupId` associated with each entity (Article or Books).

Despite my efforts, it seems like I am encountering some issues with getting everything to work seamlessly.

// Sample User Object - obtained from request.user after authentication via AuthGuard('jwt')

user = {
  groups: ['department-1', 'department-2']
}
// Article Entity
export class Article extends UUIDBase {
  @Column({ nullable: false })
  groupId: string

  @Column({ nullable: true })
  description: string

  @Column({ nullable: true })
  name: string
}
// Books Entity
export class Books extends UUIDBase {
  @Column({ nullable: false })
  groupId: string

  @Column({ nullable: true })
  description: string

  @Column({ nullable: true })
  name: string

  @Column({ nullable: true })
  description: string
}
// CASL-ability.factory.ts
type Subjects = typeof Articles | typeof User | Articles | User | 'all'

export type AppAbility = Ability<[Action, Subjects]>;

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: any) {
    const { can, build } = new AbilityBuilder<Ability<[Action, Subjects]>>(
      Ability as AbilityClass<AppAbility>,
    )

    console.log('USER IS ADMIN', user.groups)
    if (user.groups.includes(Groups.ADMIN)) {
      can(Action.Manage, 'all') // read-write access to everything
    } else {
      can(Action.Read, 'all') // read-only access to everything
    }

    return build()
  }
}
// @CheckPolicies decorator
interface IPolicyHandler {
  handle(ability: AppAbility): boolean
}

type PolicyHandlerCallback = (ability: AppAbility, can?, user?) => boolean

export declare type PolicyHandler = IPolicyHandler | PolicyHandlerCallback

import { SetMetadata } from '@nestjs/common'

export const CHECK_POLICIES_KEY = 'check_policy'
export const CheckPolicies: any = (...handlers: PolicyHandler[]) =>
  SetMetadata(CHECK_POLICIES_KEY, handlers)
// PoliciesGuard.ts

@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private caslAbilityFactory: CaslAbilityFactory,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandlers =
      this.reflector.get<PolicyHandler[]>(
        CHECK_POLICIES_KEY,
        context.getHandler(),
      ) || []

    const { user } = context.switchToHttp().getRequest()
    const ability = this.caslAbilityFactory.createForUser(user)

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

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

Usage in Articles controller:

@ApiTags('Articles')
@Controller('Articles')
@UseGuards(AuthGuard('jwt'))
export class ArticlesController {
  constructor(public service: ArticlesService) {}
  
  @Get('/articles/:id')
  @UseGuards(PoliciesGuard)
  @CheckPolicies((ability: AppAbility) => {
    ability.can(Action.Read, Articles)
  })
  async listArticles(@Query() query, @Param() param) {
    return this.service.listArticles(query, param)
  }

  @Get('/articles/:id')
  @UseGuards(PoliciesGuard)
  @CheckPolicies((ability: AppAbility) => {
    ability.can(Action.Read, Articles)
  })
  async getArticle(@Param() params, @Query() query) {
    return this.service.getArticle(params, query)
  }

Usage in Books controller:

@ApiTags('Books')
@Controller('Books')
@UseGuards(AuthGuard('jwt'))
export class BooksController {
  constructor(public service: BooksService) {}
  
  @Get('/books/:id')
  @UseGuards(PoliciesGuard)
  @CheckPolicies((ability: AppAbility) => {
    ability.can(Action.Read, Books)
  })
  async listBooks(@Query() query, @Param() param, @Body() body) {
    return this.service.listBooks(query, param, body)
  }

  @Get('/books/:id')
  @UseGuards(PoliciesGuard)
  @CheckPolicies((ability: AppAbility) => {
    ability.can(Action.Read, Books)
  })
  async getBooks(@Param() params, @Query() query) {
    return this.service.getBooks(params, query)
  }

Answer №1

You can modify the code snippet

@CheckPolicies((ability: AppAbility) => {ability.can(Action.Read, Books)})
by either inserting a return statement before ability.can... or eliminating the curly brackets.

As it stands, the policy handler consistently outputs undefined, resulting in an automatic Unauthorized response.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Creating a stream of observables in RxJs and subscribing to only the latest one after a delay: A comprehensive guide

I am trying to create a stream of Observables with delay and only subscribe to the last one after a specified time. I have three HostListeners in which I want to use to achieve this. I would like to avoid using Observable form event and instead rely on H ...

Unable to find the matching closing parenthesis ')" before reaching the end

I've been struggling to create a regular expression that can identify strings like the ones below: var p1=@VAL([Test1Q1].[Bandwidth]) var p2=@VAL([Test1Q1].[Usages (KB)]) The criteria is to find strings starting with @VAL( and ending before the firs ...

HTML - Reloading form inputs - varying functionality in Internet Explorer versus Android Phones

I created a form that includes a list box and multiple text boxes. When a user chooses an item from the list box, the text in the text boxes is automatically filled out. This functionality is handled by a function that runs when the onclick event of the li ...

Ensuring each div element has a unique bullet icon: A step-by-step guide

I am looking to dynamically create bullet icons based on the number of div elements present in a slider. For instance, if there are 2 slider divs, I want to generate 2 bullet icons within another div. If there are no div elements, then the bullets should ...

TSLint is unable to locate a custom module containing a custom d.ts file

I've been searching for a solution to this issue, but haven't found a definitive answer yet. My current challenge involves using the suncalc module with TypeScript. Since it doesn't come with its own typing file, I created one and saved it ...

Which is quicker when utilizing jQuery selectors: selecting by .classname or div.classname?

Which is quicker: using $(".classname"). or adding the tag to search for as well? $("div.classname") In my opinion, using just the classname would be faster because jQuery can simply loop through all classnames directly, whereas in the second example i ...

What is the best method for implementing intersection types within an angular template?

While working with intersection types in an Angular template, I faced a challenge. In my component's TypeScript file, I defined the following input: export class ExampleClassComponent { ... @Input() items: Array<InfoItem> | Array<InfoItem ...

Create a canvas element to display an image within a picture frame

I am currently developing a customized framing website and aiming to showcase a frame example on the screen as customers input their dimensions. Progress has been made, but I am facing a challenge in cropping the image's corners using an HTML Canvas e ...

Designated location for downloading specified by the user

I've been searching for a while now and I can't seem to find a satisfactory answer. Essentially, I have a website where users can input values and download a specific file based on those values. Everything is functional, but I want to give the u ...

Adjusting the size of several images individually with jquery

Currently, I am working on a jQuery script that enables me to resize any image by simply clicking on it. The goal is to have the ability to click on one image and resize it, then click on another image and resize it independently. Here is the code I have b ...

Adding an element to an array does not automatically reflect on the user interface

After fetching JSON data from the endpoint, I am attempting to update an array but not seeing the expected results on the frontend. export class LocationSectionComponent implements OnInit{ myControl = new FormControl(); options : string[] = [' ...

The iPad screen displays the image in a rotated position while it remains

Recently, I developed a mini test website that enables users to upload pictures and immediately see them without navigating back to the server. It seemed quite simple at first. $('input').on('change', function () { var file = this. ...

Setting default selections for mat-select component in Angular 6

I've been attempting to preselect multiple options in a mat-select element, but I haven't been successful so far. Below is the snippet of HTML code: <mat-dialog-content [formGroup]="form"> <mat-form-field> <mat-select pla ...

What is the best way to combine two JavaScript objects?

One object I am currently working with resembles the following structure let ob1 = { 'item_data':{ 'stack':{ 'purchase':'12345', 'order':'22222' } } } Additionally, I have anothe ...

Utilizing arrays in the label field of Chart.js

Creating a straightforward bar chart using Chartjs 3.x The process involves fetching JSON data from the server, parsing it, and storing specific parts in arrays. See the code snippet below: serverData = JSON.parse(http.responseText); console.log(serverDa ...

Objects that are included into an array will end up with an undefined value

Currently, I am in the process of coding a command for my Discord bot that involves adding items to an array of objects stored within a JSON file. To achieve this functionality, I have implemented the following code snippet: let rawdata = fs.readFileSync(& ...

Switch between two AppBars simultaneously while scrolling in Material UI

In my Header.js component, I have two AppBars. The first one is sticky and the second one is not initially visible. As we scroll down, I want the second AppBar to collapse and the first one to stay stickied at the top of the screen. I looked at the Materi ...

Guide for bringing in a complete npm library into React Native beyond just a single file access

Currently, I am facing an issue with importing the sjcl library into my project. Even though there are multiple files within the library, I am only able to import one file successfully. This is what I have tried from the command line: npm install sjcl -- ...

Manually Enroll Node Module

Question: I am tackling a challenge in my TypeScript project where I need to interact with multiple APIs that are not available locally on my computer, but exist on the web. The code compiles without issues on my local machine as I have all the API declar ...

Unlocking the secrets of the Fibonacci function through mathematical equations

Looking for a better way to calculate the fibonacci function using a mathematical formula. I've tried the following formula without success: fib(n) = ((1 + 5^0.5) / 2)^n - ((1 - 5^0.5) / 2)^n / 5^0.5 const fib=(n)=>{ return ((1+(5**0.5))/2)* ...