Options
All
  • Public
  • Public/Protected
  • All
Menu

Better type constraints

So far, we were able to configure your policies. But we can't yet check that our actions or subjects are actually matching what we declared.

Define the ability type

Let's configure our ability types:

From ./test/demo/better-types/ability.ts

import { Ability } from '@casl/ability';

export type MyAbilities =
| ['admin', 'ImportantData']
| ['create' | 'read' | 'update' | 'delete', 'PublicData' ];
export type MyAbility = Ability<MyAbilities>;

Using MyAbility, we can't pass anything to can or cannot: types are constrained.

From ./test/demo/better-types/ability.typecheck.e2e-spec.ts

import { expectTypeOf } from 'expect-type';

import { MyAbility } from './ability';

it( 'ability should have correct typings', () => {
expectTypeOf<['read', 'PublicData']>().toMatchTypeOf<Parameters<MyAbility['can']>>();
expectTypeOf<['create', 'PublicData']>().toMatchTypeOf<Parameters<MyAbility['can']>>();
expectTypeOf<['admin', 'ImportantData']>().toMatchTypeOf<Parameters<MyAbility['can']>>();

expectTypeOf<['admin', 'PublicData']>().not.toMatchTypeOf<Parameters<MyAbility['can']>>();
expectTypeOf<['read', 'ImportantData']>().not.toMatchTypeOf<Parameters<MyAbility['can']>>();
} );

That's great. This will greatly reduce our chances of typos. Moreover, your IDE might now suggest actions & subjects for you.

Use the ability type

With the CaslAbilityFactory

Let's now use this type in your CaslAbilityFactory:

From src/ability-factory.service.ts

import { AbilityBuilder, PureAbility } from '@casl/ability';
import { Injectable } from '@nestjs/common';

import { CaslAbilityFactory } from '@knodes/nest-casl';

import { MyAbility } from './ability';

@Injectable()
export class AbilityFactory implements CaslAbilityFactory<MyAbility> {
// Here, \`request\` is the express or fastify request. You might get infos from it.
public createFromRequest( _request: unknown ): MyAbility {
const { user } = ( _request as any );
const abilityBuilder = new AbilityBuilder<MyAbility>( PureAbility );
if( user?.role === 'admin' ) {
abilityBuilder.can( 'admin', 'ImportantData' );
}
return abilityBuilder.build();
}
}

With decorators

You can pass your ability as a type parameter to your decorators to constraint your actions and subjects:

From src/test.controller.ts

import { Controller, Get } from '@nestjs/common';

import { Policy } from '@knodes/nest-casl';

import { MyAbility } from './ability';

@Controller()
// @ts-expect-error -- \`something\` is not a valid subject
@Policy<MyAbility>( { action: 'admin', subject: 'something' } )
// @ts-expect-error -- \`rick-roll\` is not a valid action
@Policy<MyAbility>( { action: 'rick-roll', subject: 'ImportantData' } )
// @ts-expect-error -- \`read\` is not a valid action on \`ImportantData\`
@Policy<MyAbility>( { action: 'read', subject: 'ImportantData' } )
@Policy<MyAbility>( { action: 'read', subject: 'PublicData' } )
// ...
export class TestController {
@Get()
public method(){
// ...
}
}

But the boring part here is that you have to pass your ability type to every decorator in order to constrain them. Hopefully, you can solve this:

From src/my-policies.ts

import { bindPolicyDecorators } from '@knodes/nest-casl';

import { MyAbility } from './ability';

export const MyPolicies = bindPolicyDecorators<MyAbility>( /* you can even pass some guards here ! */ );

Then, simply enjoy type contraints !

From src/test-bound.controller.ts

import { Controller, Get } from '@nestjs/common';

import { MyPolicies } from './my-policies';

@Controller()
@MyPolicies.PoliciesMask( {
'*': { action: 'admin', subject: 'ImportantData' },
'read': { action: 'read', subject: 'PublicData' },
'create': { action: 'create', subject: 'PublicData' },
} )
export class TestController {
@Get()
@MyPolicies.Policy( { handle: ability => ability.can( 'read', 'PublicData' ) } )
public create(){
// ...
}
@Get()
public read(){
// ...
}

@Get()
public admin(){
// ...
}
}

Generated using TypeDoc