javascript - NestJs CASL Authorization guard based on user attributes and policies - Stack Overflow

I am trying to implement a generic policy based guard in NestJS and CASL for listget endpoints.I am

I am trying to implement a generic policy based guard in NestJS and CASL for list / get endpoints. I am following the documentation here .

A user could be part of multiple groups. I need to check if Article or Books entity which has groupId matches any of the user's groups. And only allow user to read them if the user's list of groups matches Article.id and Books.id

However I can't get it seems to be working

// User Object - from request.user after decoded from 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/mon'

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)
  }
}

And I am using it like this 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)
  }

And I am using it like this 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)
  }

I am trying to implement a generic policy based guard in NestJS and CASL for list / get endpoints. I am following the documentation here https://docs.nestjs./security/authorization#integrating-casl.

A user could be part of multiple groups. I need to check if Article or Books entity which has groupId matches any of the user's groups. And only allow user to read them if the user's list of groups matches Article.id and Books.id

However I can't get it seems to be working

// User Object - from request.user after decoded from 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/mon'

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)
  }
}

And I am using it like this 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)
  }

And I am using it like this 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)
  }

Share Improve this question asked Dec 3, 2020 at 11:45 MonteCristoMonteCristo 1,5502 gold badges21 silver badges44 bronze badges 1
  • 1 how did you fix this issue? – rukshiin Commented Sep 21, 2021 at 8:57
Add a ment  | 

1 Answer 1

Reset to default 5

Try to change @CheckPolicies((ability: AppAbility) => {ability.can(Action.Read, Books)}) by adding a return before ability.can... or removing the brackets.

With what you have, that policy handler always returns undefined, so it always gives you an Unauthorized.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744291897a4567072.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信