npm package: @s1seven/nestjs-tools-access-control
Description: S1Seven ACL engine
Author: getlarge
Latest version: 0.1.15

# Nest ACL

[![npm][npm-image]][npm-url]

[npm-image]: https://img.shields.io/npm/v/@s1seven/microservices-access-control.svg?style=flat
[npm-url]: https://npmjs.org/package/@s1seven/microservices-access-control

Fork of [nest-access-control](https://github.com/nestjsx/nest-access-control), instead making use of [role-acl](https://github.com/tensult/role-acl).
It offers a great flexibility on how to build condition to grant or deny access to specific resources and related actions.

## Installation

```bash
npm i @s1seven/microservices-access-control
```

## Example

### Constants

In ./src/data-transfer-object/index.ts

```ts
export const Actions = {
  ReadOne: 'readOne',
  CreateOne: 'CreateOne',
};
export const Resources = {
  FILE: 'file',
};
export const Roles = {
  USER: 'user',
};
```

### Custom Conditions

In ./src/access-control/data/conditions.ts

```ts
import { IDictionary, IFunctionCondition } from '@s1seven/microservices-access-control';

export const conditions: IDictionary<IFunctionCondition> = {
  isWorkspaceMember(context: { user: any; workspaceId: number | string }, _args: any): boolean {
    const { user, workspaceId } = context;
    return workspaceId === 'yeah-right';
  },

  async isWorkspaceOwner(context: { user: any; workspaceId: number | string }, _args: any): Promise<boolean> {
    const { user, workspaceId } = context;
    return Promise.resolve(workspaceId === 'hell-yeah');
  },
};
```

### Grants

In ./src/access-control/data/grants.ts

```ts
import { Actions, Resources, Roles } from '../../data-transfer-object';

const customConditions = { isWorkspaceMember: 'custom:isWorkspaceMember', isWorkspaceOwner: 'custom:isWorkspaceOwner' };

export const grants = {
  [Roles.USER]: {
    grants: [
      { resource: Resources.FILE, action: [Actions.CreateOne], attributes: ['*'] },
      {
        resource: Resources.FILE,
        action: Actions.ReadOne,
        attributes: ['*'],
        condition: {
          Fn: customConditions.isWorkspaceMember,
          args: { resource: 'workspace' },
        },
      },
    ],
  },
};
```

### ACLGuard

In ./src/access-control/access-control.guard.ts

```ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { getAction, getFeature } from '@nestjsx/crud';
import {
  getAction as GetAclAction,
  getResource,
  InjectRulesBuilder,
  RulesBuilder,
} from '@s1seven/microservices-access-control';
import { Actions, Resources, Roles } from '../data-transfer-object';

@Injectable()
export class ACLGuard<User extends any = any> implements CanActivate {
  constructor(@InjectRulesBuilder() private readonly ruleBuilder: RulesBuilder) {}

  protected async getUser(context: ExecutionContext): Promise<any> {
    const request = context.switchToHttp().getRequest();
    const { user } = request;
    return { user };
  }

  protected async getUserRoles(context: ExecutionContext): Promise<{ user: any; roles: Roles[] }> {
    const { user } = await this.getUser(context);
    if (!user) {
      throw new UnauthorizedException();
    }
    let { roles } = user;
    return { user, roles };
  }

  isSuperAdmin(roles: Roles[]) {
    return roles.some((role) => role === Roles.SUPER_ADMIN);
  }

  protected getPermissionContextByResource = {
    [Resources.FILE]: this.applyWorkspaceContext,
  };

  protected applyWorkspaceContext(_action: Actions, resource: Resources, context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const workspaceId = request.headers['current-workspace'];
    return { workspaceId };
  }

  protected getPermissionContext(_action: Actions, resource: Resources, context: ExecutionContext) {
    return this.getPermissionContextByResource[resource]
      ? this.getPermissionContextByResource[resource](_action, resource, context)
      : {};
  }

  protected getActionAndResource(context: ExecutionContext): { action: Actions; resource: Resources } {
    const handler = context.getHandler();
    const controller = context.getClass();
    const resource = getFeature(controller) || getResource(controller);
    const action = getAction(handler) || GetAclAction(handler);
    return { action, resource };
  }

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const { user, roles: userRoles } = await this.getUserRoles(context);
    if (this.isSuperAdmin(userRoles)) {
      return true;
    }
    const { action, resource } = this.getActionAndResource(context);
    if (!resource || !action) {
      return true;
    }
    const permissionContext = { user, ...this.getPermissionContext(action, resource, context) };
    const permission = await this.ruleBuilder.can(userRoles).context(permissionContext).execute(action).on(resource);
    return permission.granted;
  }
}
```

### Controller

In ./src/file/file.controller.ts

```ts
import { Actions, Resources, Roles } from '../data-transfer-object';
import { ACLGuard } from '../access-control/access-control.guard';
import { Action, Resource } from '@s1seven/microservices-access-control/decorators';

@Resource(Resources.FILE)
@UseGuards(AuthGuard('jwt'), ACLGuard)
export class FileController {
  @Action(Actions.ReadOne)
  @Get('preview/:id')
  preview(@Req() request) {
    return 'done';
  }
}
```

### Application module

In ./src/app.module.ts

```ts
import { AccessControlModule, RulesBuilder } from '@s1seven/microservices-access-control';
import { Module } from '@nestjs/common';
import { ACLGuard } from './access-control/access-control.guard';
import { conditions, grants } from './access-control/data';
import { FileController } from './file/file.controller';

@Module({
  imports: [
    AccessControlModule.forRootAsync({
      useFactory: (): RulesBuilder => new RulesBuilder(grants, conditions),
    }),
  ],
  providers: [ACLGuard],
  controllers: [FileController],
})
export class AppModule {}
```