So far, we were able to configure your policies. But we can't yet check that our action
s or subject
s are actually matching what we declared.
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 action
s & subject
s for you.
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();
}
}
You can pass your ability as a type parameter to your decorators to constraint your action
s and subject
s:
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