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
Middleware
or anInterceptor
should take care of setting theuser
property on theRequest
. But fact is that@nestjs/passport
does 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/passport
If 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 ExtraGuard2
Note that you can use
bindPolicyDecorators
to bind bothPolicy
andPoliciesMask
This is tested, of course. Just see by yourself.
{@codeblock folded use-with-guards/recommended-test.e2e-spec.ts}
Generated using TypeDoc