sergey-telpuk / nestjs-rbac

Awesome RBAC for NestJs
Other
426 stars 36 forks source link
javascript nestjs nodejs npm rbac

npm version Scrutinizer Code Quality codecov npm RBAC CI RBAC CD

Join our discord server: Link

Description

The rbac module for Nest. Support NestJS ^8.0.0 || ^9.0.0 || ^10.0.0

Installation

npm i --save nestjs-rbac

Quick Start

For using RBAC there is need to implement IStorageRbac

export interface IStorageRbac {
  roles: string[];
  permissions: object;
  grants: object;
  filters: { [key: string]: any | IFilterPermission };
}

For instance:

export const RBAC: IStorageRbac = {
    roles: ['admin', 'user'],
    permissions: {
        permission1: ['create', 'update', 'delete'],
        permission2: ['create', 'update', 'delete'],
        permission3: ['filter1', 'filter2', RBAC_REQUEST_FILTER],
        permission4: ['create', 'update', 'delete'],
        permission5: ['ASYNC_filter1', 'ASYNC_filter2', ASYNC_RBAC_REQUEST_FILTER],
    },
    grants: {
        admin: [
            '&user',
            'permission1',
            'permission3',
            'permission5',
        ],
        user: ['&userRoot', 'permission2', 'permission1@create', 'permission3@filter1', 'permission5@ASYNC_filter1'],
        userRoot: ['permission4'],

    },
    filters: {
        filter1: TestFilterOne,
        filter2: TestFilterTwo,
        ASYNC_filter1: TestAsyncFilterOne,
        ASYNC_filter2: TestAsyncFilterTwo,
        [RBAC_REQUEST_FILTER]: RequestFilter,
        [ASYNC_RBAC_REQUEST_FILTER]: RequestAsyncFilter,
    },
};

Storage consists of the following keys:

roles: array of roles

permissions: objects of permissions which content actions

grants: objects of assigned permission to roles

filters: objects of customized behavior

prefix ASYNC_ use for async operations

Grant symbols

&: extends grant by another grant, for instance admin extends user (only support one level inheritance)

@: a particular action from permission, for instance permission1@update

Using RBAC like an unchangeable storage

import { Module } from '@nestjs/common';
import { RBAcModule } from 'nestjs-rbac';

@Module({
  imports: [
    RBAcModule.forRoot(IStorageRbac),
  ],
  controllers: []
})
export class AppModule {}

Using RBAC like a dynamic storage

There is enough to implement IDynamicStorageRbac interface.

import { Module } from '@nestjs/common';
import { RBAcModule } from 'nestjs-rbac';

@Module({
  imports: [
    RBAcModule.forDynamic(DynamicStorageService),
  ],
  controllers: []
})
export class AppModule {}
// implement dynamic storage
import { IDynamicStorageRbac, IStorageRbac } from 'nestjs-rbac';
@Injectable()
export class  DynamicStorageService implements IDynamicStorageRbac {
  constructor(
    private readonly repository: AnyRepository
  ) {

  }
  async getRbac(): Promise<IStorageRbac> {
//use any persistence storage for getting `RBAC`
      return  await this.repository.getRbac();
  }
}

Using for routers RBAcPermissions

import {RBAcPermissions, RBAcGuard} from 'nestjs-rbac';
import {RBAcAsyncPermissions} from "./rbac.permissions.decorator";

@Controller()
export class RbacTestController {

    @RBAcPermissions('permission', 'permission@create')
    @UseGuards(
// Any Guard for getting & adding user to request which implements `IRole` interface from `nestjs-rbac`:
//*NOTE:
//  const request = context.switchToHttp().getRequest();
//  const user: IRole = request.user;
        GuardIsForAddingUserToRequestGuard,
        RBAcGuard,
    )
    @Get('/')
    async test1(): Promise<boolean> {
        return true;
    }
}

// example Async 
@Controller()
export class RbacAsyncTestController {

    @RBAcAsyncPermissions('permission1')
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-permission1')
    async test1(): Promise<boolean> {
        return true;
    }

    @RBAcAsyncPermissions('permission2', 'permission1')
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-permission1-and-permission2')
    async test2(): Promise<boolean> {
        return true;
    }

    @RBAcAsyncPermissions('permission4')
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-permission4')
    async test3(): Promise<boolean> {
        return true;
    }

    @RBAcAsyncPermissions(`permission5@${ASYNC_RBAC_REQUEST_FILTER}`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-request-filter')
    async test4(): Promise<boolean> {
        return true;
    }

    @RBAcAsyncPermissions(`permission4`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-extends-userRoot')
    async test5(): Promise<boolean> {
        return true;
    }

    @RBAcAsyncPermissions(`permission1@create`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-permission1@create')
    async test7(): Promise<boolean> {
        return true;
    }

    @RBAcAsyncPermissions(`permission1@delete`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-permission1@delete')
    async test8(): Promise<boolean> {
        return true;
    }

    @RBAcAnyAsyncPermissions(
        [`permission1@delete`],
        [`permission1@create`]
    )
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-permission1@deleteOrCreate')
    async test9(): Promise<boolean> {
        return true;
    }
}

// example 
export class RbacTestController {

    @RBAcPermissions('permission1')
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-permission1')
    async test1(): Promise<boolean> {
        return true;
    }

    @RBAcPermissions('permission2', 'permission1')
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-permission1-and-permission2')
    async test2(): Promise<boolean> {
        return true;
    }

    @RBAcPermissions('permission4')
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-permission4')
    async test3(): Promise<boolean> {
        return true;
    }

    @RBAcPermissions(`permission3@${RBAC_REQUEST_FILTER}`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/admin-request-filter')
    async test4(): Promise<boolean> {
        return true;
    }

    @RBAcPermissions(`permission4`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-extends-userRoot')
    async test5(): Promise<boolean> {
        return true;
    }

    @RBAcPermissions(`permission1@create`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-permission1@create')
    async test7(): Promise<boolean> {
        return true;
    }

    @RBAcPermissions(`permission1@delete`)
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-permission1@delete')
    async test8(): Promise<boolean> {
        return true;
    }

    @RBAcAnyPermissions(
        [`permission1@delete`],
        [`permission1@create`]
    )
    @UseGuards(
        AuthGuard,
        RBAcGuard,
    )
    @Get('/user-permission1@deleteOrCreate')
    async test9(): Promise<boolean> {
        return true;
    }

Variety of the decorators

@RBAcPermissions: obtain 'permission', 'permission@create'

@RBAcAnyPermissions: obtain ['permission'], ['permission@create']

@RBAcAsyncPermissions: obtain ['permission'], ['permission@create']

@RBAcAnyAsyncPermissions obtain ['permission'], ['permission@create'] and async filter

Async filter

For using async filter add ASYNC_

Using for a whole controller

It's applicable with the crud library, for example nestjsx/crud

import { RBAcPermissions, RBAcGuard } from 'nestjs-rbac';

@Crud({
    model: {
        type: Company,
    },
})
@RBAcPermissions('permission2')
@UseGuards(
        AuthGuard,
        RBAcGuard,
)
@Controller('companies')
export class CompaniesController implements CrudController<Company> {
    constructor(public service: CompaniesService) {}
}

one more example

@Crud({
    model: {
        type: Company,
    },
    routes: {
        getManyBase: {
            interceptors : [],
            decorators: [RBAcPermissions('permission1')],
        },
        createOneBase: {
            interceptors : [],
            decorators: [RBAcPermissions('permission2')],
        },
    },
})
@UseGuards(
        AuthGuard,
        RBAcGuard,
)
@Controller('companies')
export class CompaniesController implements CrudController<Company> {
    constructor(public service: CompaniesService) {
    }
}

Using like service

import { RbacService } from 'nestjs-rbac';

@Controller()
export class RbacTestController {

  constructor(
    private readonly rbac: RbacService
  ){}

  @Get('/')
  async test1(): Promise<boolean> {
    return await (await this.rbac.getRole(role)).can('permission', 'permission@create');
    return true;
  }
}

Using the custom filters

filter is a great opportunity of customising behaviour RBAC. For creating filter, there is need to implement IFilterPermission interface, which requires for implementing can method, and bind a key filter with filter implementation, like below:

export const RBAC: IStorageRbac = {
  roles: ['role'],
  permissions: {
    permission1: ['filter1', 'filter2'],
  },
  grants: {
    role: [
      `permission1@filter1`
      `permission1@filter2`
    ],
  },
  filters: {
    filter1: TestFilter,
    filter2: TestAsyncFilter,
  },
};
//===================== implementing filter
import { IFilterPermission } from 'nestjs-rbac';

export class TestFilter implements IFilterPermission {

  can(params?: any[]): boolean {
    return params[0];
  }

}

//===================== implementing async filter
import { IFilterPermission } from 'nestjs-rbac';

@Injectable()
export class TestAsyncFilter implements IFilterPermission {
  constructor(private readonly myService: MyService) {}

  async canAsync(params?: any[]): Promise<boolean> {
    const myResult = await this.myService.someAsyncOperation()
    // Do something with myResult
    return myResult;
  }
}

:warning: - A single filter can implement both can and canAsync. If you use the RBAcGuard, they will be evaluated with an AND condition.

ParamsFilter services for passing arguments into particular filter:

const filter = new ParamsFilter();
filter.setParam('filter1', some payload);

const res = await (await rbacService.getRole('admin', filter)).can(
  'permission1@filter1',
);

Also RBAC has a default filter RBAC_REQUEST_FILTER which has request object as argument:

Example:
//===================== filter
export class RequestFilter implements IFilterPermission {

  can(params?: any[]): boolean {
    return params[0].headers['test-header'] === 'test';
  }
}
//===================== storage
export const RBAC: IStorageRbac = {
  roles: ['role'],
  permissions: {
    permission1: ['filter1', 'filter2', RBAC_REQUEST_FILTER],
  },
  grants: {
    role: [
      `permission1@${RBAC_REQUEST_FILTER}`
    ],
  },
  filters: {
    [RBAC_REQUEST_FILTER]: RequestFilter,
  },
};
//===================== using for routes
  @RBAcPermissions(`permission1@${RBAC_REQUEST_FILTER}`)
  @UseGuards(
    AuthGuard,
    RBAcGuard,
  )
  @Get('/')
  async test4(): Promise<boolean> {
    return true;
  }

Performance

By default, RBAC storage always parses grants for each request, in some cases, it can be a very expensive operation. The bigger RBAC storage, the more taking time for parsing. For saving performance RBAC has built-in a cache, based on node-cache

Using cache

import { RbacCache } from 'nestjs-rbac';

@Module({
  imports: [
    RBAcModule.useCache(RbacCache, {KEY: 'RBAC', TTL: 400}).forDynamic(AsyncService),
  ],
})

if you need to change a cache storage, there is enough to implement ICacheRBAC

ICacheRBAC

export interface ICacheRBAC {
  KEY: string;
  TTL: number;

  get(): object | null;

  /**
   *
   * @param value
   */
  set(value: object): void;

  del(): void;
}

Inject ICacheRBAC

import { ICacheRBAC } from 'nestjs-rbac';
...
@Inject('ICacheRBAC') cache: ICacheRBAC