Most of the times, you want to execute some guards before checking CASL policies, to extract some user informations before generating its abilities.
This page will show you the various way to do.
Let's assume you have the following CaslAbilityFactory:
From src/ability-factory.service.ts
import { AbilityBuilder, PureAbility } from '@casl/ability';
import { Injectable } from '@nestjs/common';
import { CaslAbilityFactory } from '@knodes/nest-casl';
@Injectable()
export class AbilityFactory implements CaslAbilityFactory {
// Here, \`request\` is the express or fastify request. You might get infos from it.
public createFromRequest( _request: unknown ): PureAbility {
const { user } = ( _request as any );
const abilityBuilder = new AbilityBuilder( PureAbility );
if( user?.role === 'admin' ) {
abilityBuilder.can( 'admin', 'something' );
}
return abilityBuilder.build();
}
}
This method is not recommanded. You might skip directly to the recommended approach
This method does not require any external dependency, and is as close to NestJS as possible.
From src/guards/naive.guard.ts
import { CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
export class NaiveGuard implements CanActivate {
public canActivate( context: ExecutionContext ): boolean {
const request = context.switchToHttp().getRequest<Request>();
const authorization = request.header( 'Authorization' );
if( !authorization ){
// **BEWARE**: if the guard returns \`false\`, Nest will throw a \`ForbiddenException\` which is semantically incorrect here.
// The user is not *missing the right to do*, it is *not authenticated at all*.
throw new UnauthorizedException( 'Missing authorization header' );
}
const user = this._getUserFromAuthorization( authorization );
if( !user ){
return false;
}
( request as any ).user = user;
return true;
}
private _getUserFromAuthorization( authorization?: string ){
if( authorization === 'admin' ){
return { role: 'admin' };
} else if( authorization === 'user' ){
return { role: 'user' };
} else {
return undefined;
}
}
}
Assuming the guard does the following (see above for an example implementation):
Authorization header will see an Unauthorized exception.Authorization header will see a Forbidden exception.Authorization header will be allowed to continue, and the property user is set on the request.Yeah, I'm aware that in an ideal world, a
Middlewareor anInterceptorshould take care of setting theuserproperty on theRequest. But fact is that@nestjs/passportdoes it in a guard
From src/controllers/naive.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Policy } from '@knodes/nest-casl';
import { NaiveGuard } from './naive.guard';
@Controller( '/naive' )
@Policy( { action: 'admin', subject: 'something' } ) // **MUST** be above the guard extracting infos from your request.
@UseGuards( NaiveGuard )
export class NaiveTestController {
@Get()
public method(){
// ...
}
}
Now,
Authorization header will see an Unauthorized exception (from the NaiveGuard).Authorization header will see a Forbidden exception (from the NaiveGuard).Authorization header that does not give them access to the ressource will see a Forbidden exception (from the {@link PoliciesGuard PoliciesGuard}).Authorization header that give them access to the ressource will succeed.@nestjs/passportIf you're using passport, you can delegate the user retrieval. Let's say you have a JWT passport strategy.
From src/strategies/jwt-passport.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt';
@Injectable()
export class JwtPassportStrategy extends PassportStrategy( Strategy, 'jwt' ) {
public static readonly KEY = 'this-is-a-secret';
public constructor(){
super( {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JwtPassportStrategy.KEY,
} as StrategyOptions );
}
public validate( user: any ){
return user;
}
}
Still using the naive approach, you can then declare the controller like this:
From src/controllers/passport-naive.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Policy } from '@knodes/nest-casl';
@Controller( '/passport/naive' )
@Policy( { action: 'admin', subject: 'something' } ) // **MUST** be above the guard extracting infos from your request.
@UseGuards( AuthGuard( 'jwt' ) )
export class PassportNaiveTestController {
@Get()
public method(){
// ...
}
}
Wanna see the behavior as tests ? Here you are !
From ./test/demo/use-with-guards/naive-test.e2e-spec.ts
import { HttpStatus, INestApplication } from '@nestjs/common';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { CaslModule } from '@knodes/nest-casl';
import { AbilityFactory } from './ability-factory.service';
import { JwtPassportStrategy } from './jwt-passport.strategy';
import { NaiveTestController } from './naive.controller';
import { PassportNaiveTestController } from './passport-naive.controller';
describe( 'Use with guards (naive)', () => {
let app: INestApplication;
describe( 'NaiveTestController', () => {
beforeAll( async () => {
const moduleRef = await Test.createTestingModule( {
imports: [
CaslModule.withConfig( ( { abilityFactory: AbilityFactory } ) ),
],
controllers: [ NaiveTestController ],
} ).compile();
app = moduleRef.createNestApplication();
await app.init();
} );
it( 'should send an UNAUTHORIZED error if no authorization set', () => request( app.getHttpServer() )
.get( '/naive' )
.expect( HttpStatus.UNAUTHORIZED )
.expect( { statusCode: HttpStatus.UNAUTHORIZED, message: 'Missing authorization header', error: 'Unauthorized' } ) );
it( 'should send a FORBIDDEN error if invalid authorization', () => request( app.getHttpServer() )
.get( '/naive' )
.set( 'Authorization', 'invalid' )
.expect( HttpStatus.FORBIDDEN )
.expect( { statusCode: HttpStatus.FORBIDDEN, message: 'Forbidden resource', error: 'Forbidden' } ) );
it( 'should send a FORBIDDEN error if invalid capabilities', () => request( app.getHttpServer() )
.get( '/naive' )
.set( 'Authorization', 'user' )
.expect( HttpStatus.FORBIDDEN )
.expect( { statusCode: HttpStatus.FORBIDDEN, message: 'Invalid authorizations: Can\\'t "admin" on "something"', error: 'Forbidden' } ) );
it( 'should work', () => request( app.getHttpServer() )
.get( '/naive' )
.set( 'Authorization', 'admin' )
.expect( HttpStatus.OK ) );
} );
describe( 'PassportNaiveTestController', () => {
beforeAll( async () => {
const moduleRef = await Test.createTestingModule( {
imports: [
CaslModule.withConfig( ( { abilityFactory: AbilityFactory } ) ),
JwtModule.register( { secret: JwtPassportStrategy.KEY } ),
],
providers: [ JwtPassportStrategy ],
controllers: [ PassportNaiveTestController ],
} ).compile();
app = moduleRef.createNestApplication();
await app.init();
} );
it( 'should send an UNAUTHORIZED error if no authorization set', () => request( app.getHttpServer() )
.get( '/passport/naive' )
.expect( HttpStatus.UNAUTHORIZED )
.expect( { statusCode: HttpStatus.UNAUTHORIZED, message: 'Unauthorized' } ) );
it( 'should send a FORBIDDEN error if invalid capabilities', () => request( app.getHttpServer() )
.get( '/passport/naive' )
.set( 'Authorization', \`Bearer ${app.get( JwtService ).sign( { role: 'user' } )}\` )
.expect( HttpStatus.FORBIDDEN )
.expect( { statusCode: HttpStatus.FORBIDDEN, message: 'Invalid authorizations: Can\\'t "admin" on "something"', error: 'Forbidden' } ) );
it( 'should work', () => request( app.getHttpServer() )
.get( '/passport/naive' )
.set( 'Authorization', \`Bearer ${app.get( JwtService ).sign( { role: 'admin' } )}\` )
.expect( HttpStatus.OK ) );
} );
} );
What's the problem about the naïve approach above ? Well, there are 2.
UseGuards decorator is placed below the Policy decorator, but is ran before it.You can use Policy.usingGuard or PoliciesMask.usingGuard to prepended guards to the decorator factory. Those calls are cumulative, and you can do multiple subsequent calls to join guards using an and condition. If you provide an array of guards, they be evaluated using an or condition.
From src/controllers/recommended.controller.ts
@Controller( '/recommended' )
@Policy( { action: 'admin', subject: 'something' } )
.usingGuard( AuthGuard( 'jwt' ) ) // The policy will run the guard before doing its own checks.
export class RecommendedTestController {
@Get()
public method(){
// ...
}
}
You can even prepare a policy decorator with guards or PolicyDescriptor:
From src/policies.ts
export const AdminViaJwtPolicy = Policy( { action: 'admin', subject: 'something' } )
.usingGuard( AuthGuard( 'jwt' ) );
export const ViaJwtPolicy = Policy.usingGuard( AuthGuard( 'jwt' ) );
Then, use those presets in your controller:
From src/recommended-bound.controller.ts
@Injectable()
export class ExtraGuard1 implements CanActivate {
public canActivate( context: ExecutionContext ): boolean {
if( !context.switchToHttp().getRequest().user.allowed1 ){
throw new BadRequestException( 'Not allowed from ExtraGuard1' );
}
return true;
}
}
@Injectable()
export class ExtraGuard2 implements CanActivate {
public canActivate( context: ExecutionContext ): boolean {
return context.switchToHttp().getRequest().user.allowed2 ?? false;
}
}
@Controller( '/recommended/bound' )
export class RecommendedBoundTestController {
@Get( 'method1' )
@AdminViaJwtPolicy
.usingGuard( [ ExtraGuard1, ExtraGuard2 ] ) // Add guards. When passed as an array, if either one can activate, it will continue
public method1(){
// ...
}
@Get( 'method2' )
@ViaJwtPolicy( { action: 'admin', subject: 'something' } )
.usingGuard( [ ExtraGuard1, ExtraGuard2 ] ) // Add guards. When passed as an array, if either one can activate, it will continue
public method2(){
// ...
}
}
Now, both method1 and method2 will run if
jwt strategyExtraGuard1 or ExtraGuard2Note that you can use
bindPolicyDecoratorsto bind bothPolicyandPoliciesMask
This is tested, of course. Just see by yourself.
{@codeblock folded use-with-guards/recommended-test.e2e-spec.ts}
Generated using TypeDoc