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