nestjsx / nest-access-control

Role and Attribute based Access Control for Nestjs 🔐
MIT License
1.13k stars 78 forks source link
access-control addon helper nestjs permissions typescript

Nest Access Control

MIT npm version Open Source Love PRs Welcome forthebadge forthebadge

A helper Module for building a Role and Attribute based Access Control System for Nestjs

TL;DR: recently our system was needing to have a Control Panel, so you can control, and monitor every thing from there, and it was really really needing some Role based access control system, so i build this module for that, it is really cool, so i'd love to share it with you, and any PR are more than welcome :heart:

This module is built on top of onury's accesscontrol library here is some of it's Core Features

What does this Module Provide?

In this module you will have all these features out of the box, but in nest-ish way.

Installation

npm install nest-access-control --save
yarn add nest-access-control

Example

See example folder for the more code

We need to build a Video service so users can share there videos with others, but we need some admins to control these videos.

  1. Let's first define our roles:

    To build our roles we will need the RolesBuilder class, it extends the AccessControl class from accesscontrol package.

    // app.roles.ts
    
    export enum AppRoles {
      USER_CREATE_ANY_VIDEO = 'USER_CREATE_ANY_VIDEO',
      ADMIN_UPDATE_OWN_VIDEO = 'ADMIN_UPDATE_OWN_VIDEO',
    }
    
    export const roles: RolesBuilder = new RolesBuilder();
    
    roles
      .grant(AppRoles.USER_CREATE_ANY_VIDEO) // define new or modify existing role. also takes an array.
        .createOwn('video') // equivalent to .createOwn('video', ['*'])
        .deleteOwn('video')
        .readAny('video')
      .grant(AppRoles.ADMIN_UPDATE_OWN_VIDEO) // switch to another role without breaking the chain
        .extend(AppRoles.USER_CREATE_ANY_VIDEO) // inherit role capabilities. also takes an array
        .updateAny('video', ['title']) // explicitly defined attributes
        .deleteAny('video');

Pro Tip :+1: : Keep all roles organized and in one file e,g: app.roles.ts

  1. Next let's use AccessControlModule in our Root module:
    // app.module.ts

    import { roles } from './app.roles';

    @Module({
      imports: [AccessControlModule.forRoles(roles)],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}

Until now everything is fine, but let's make our application, assume that we have list of video names, user can - according to our roles - create:own new video, and read:any video, so let's build it:

    // app.controller.ts
    ...
    @Controller()
    export class AppController  {
      constructor(private readonly appService: AppService)  {}
      @UseGuards(AuthGuard, ACGuard)
      @UseRoles({
        resource:  'video',
        action:  'read',
        possession:  'any',
      })
      @Get()
      root(@UserRoles() userRoles: any)  {
        return this.appService.root(userRoles);
      }
    }

ForRootAsync

Injecting providers for a RoleBuilder Factory (using a database to populate roles)

@Injectable()
class RoleProvider {

  getRoles(): Promise<string[]> {
    return Promise.resolve([
      'my-custom-role',
    ]);
  }
}

@Module({
  providers: [RoleProvider],
  exports: [RoleProvider],
})
class RoleModule {

}

@Module({
  imports: [
    AccessControlModule.forRootAsync({
      imports: [TestModule],
      inject: [RoleService],
      useFactory: async (roleService: RoleService): Promise<RolesBuilder> => {
        return new RolesBuilder(await roleService.getRoles());
      },
    }),
  ],
})
export class AccessModule {}

Notice the use of imports in the forRootAsync method. This will allow you to inject exported providers from the imported module. Injecting providers, provided in the same module as the imported AccessControlModule will result in the provider not being found. This is because the module is created before the providers.

So let's discuss what's going on!

First we introduced two new decorators, actually they are three, but let's see what they can do:

  1. Are you still there? Ok, that's it, you can go and run the application now, but wait, did someone asked for the AuthGuard? Ok let's discuss the LIMITATIONS.

Limitations

First of all, this module built with some assumptions

  1. The user object will exist in req.user
  2. It is up to you to build your own AuthGuard that will attach the user object to the req object, read more
  3. The AuthGuard must be registered before roles guard, in this case it's ACGuard, and of course you can combine the AuthGuard and ACGuard in one guard, and use it everywhere.

Secondly, i don't think these are limitations, since you can easily build your own guard and you don't need the built-in ones anymore.

CHANGELOG

See CHANGELOG for more information.

Contributing

You are welcome with this project for contributing, just make a PR.

Authors

See also the list of contributors who participated in this project.

License

This project is licensed under the MIT License - see the LICENSE.md file for details.