The nestjs-acl module purpose is to check several access rules at the same time, depending on the current context.
It allows to store, manage and check scenarios of access rules and to check them at any time.
In our case, we needed to finely filter access to different types of resources for different types of users. Because some scenarios are repetitive, it was important to store these rules without repeating tones of code lines.
nestjs-acl does not replace the nest-access-control module, they are fully compatible and complementary. nest-access-control is a module to protect methods using decorators. It also allows you to implement your own access logic using the Nestjs Guards, which covers most cases.
npm install nestjs-acl --save
yarn add nestjs-acl
Create a file that exports an AccessControl instance. You can refer to the npm package accesscontrol to learn how to manipulate roles
Note: The roles instance could be manipulate at runtime
// roles.ts
import { AccessControl } from 'nestjs-acl';
// See npm package accesscontrol for details
export const roles = new AccessControl({
ADMIN: {
doSomething: {
'create:any': ['*']
},
doSomethingElse: {
'create:any': ['*']
}
},
USER: {
doSomething: {
'create:own': ['*']
}
}
});
- The AclService will search for matching AclRulesCreator and execute them passing the context.
- If a AclRulesCreator is not found, the check passes (only if option rejectIfNoRule === false)
- If a AclRulesCreator is found, The creator is executed. if option rejectIfNoRule === true and no rule was returned by the creator, the check will fail.
- Returned Rules from AclRulesCreator are tested
- The first rule that is granted will validate the test.
- The first rule that throw an Error will stop the chain and invalidate the test.
- Rules returning false are ignored
- Rules returning Error are concatenated, returned as a single Error if no granted rule was found
// rules.ts
/**
* Based on roles
* users with the USER role will pass this scenarios only if they own the data (check returns true)
* users with the ADMIN role will pass this scenarios
*/
export const userCanDoSomething = (opts) => {
const {
context: { user }, // the context passed to the test
data, // the data passed to the test
sourceData // the source data passed to the test
} = opts;
return [
// rule 1
{
req: opts.rolesBuilder.can(opts.context.user.roles).createOwn('doSomething'),
// do an extra check if with context
check: () => opts.data.user === context.user
},
// rule 2
{
req: opts.rolesBuilder.can(opts.context.user.roles).createAny('doSomething')
}
];
};
/**
* Based on roles, only users with ADMIN role will be ale to pass this scenarios
*/
export const userCanDoSomethingElse = (opts) => {
return [
{
req: opts.rolesBuilder.can(opts.context.user.roles).createAny('doSomethingElse')
}
];
};
The AclModule is global and need to be registered once, imported modules can inject the AclService without the need to register the module again.
// mymodule.ts
import { AclModule, AclService } from 'nestjs-acl';
import { roles } from './roles';
import { userCanDoSomething, userCanDoSomethingElse } from './rules';
import { MyProvider } from './provider';
@Module({
imports: [AclModule.register(roles), MyProvider]
})
export class MyModule {
construtor(protected acl: AclService) {
// register acl rules creators
this.acl
.registerRules('userCanDoSomething', userCanDoSomething)
.registerRules('userCanDoSomethingElse', userCanDoSomethingElse);
}
}
In your modules, providers or controllers you can now inject the AclService instance and use it to check acl rules.
// myprovider.ts
import { Injectable } from '@nestjs/common';
import { AclService } from 'nestjs-acl';
@Injectable()
export class MyProvider {
constructor(protected acl: AclService) {}
/**
* This method is protected by a rule with id userCanDoSomething
*/
async doSomething(user: AclRoles<string>) {
const data = { foo: 'bar', user };
// The AclService will throw a ForbiddenException if the check fails.
const { rule, data } = await this.acl.check({
id: 'userCanDoSomething',
data,
context: {
user: {
roles: user.roles
}
}
});
// Do something...
}
/**
* This method is protected by a rule with id userCanDoSomethingElse
*/
async doSomethingElse(user: AclRoles<string>) {
const { rule, data } = await this.acl.check({
id: 'userCanDoSomethingElse',
context: {
user: {
roles: user.roles
}
}
});
// Do something else...
}
}