/nestjs-iacry

An Identity and Access Control (Management) module for Nest framework (node.js) highly inspired by the AWS IAM

Primary LanguageTypeScriptMIT LicenseMIT

Nest Logo

An Identity and Access Control (Management) module for Nest framework (node.js) highly inspired by the AWS IAM.

NPM Version Package License NPM Downloads

Installation

npm install --save nestjs-iacry sequelize-typescript
#or
yarn add nestjs-iacry sequelize-typescript

Important: sequelize-typescript is required if you are using sequelize policy storage model.

Configuration

Configure policy storage using sequelize adapter:

// models/policies-storage.model.ts
import { PoliciesStorageSequelizeModel } from 'nestjs-iacry';

export default class PoliciesStorage extends PoliciesStorageSequelizeModel<PoliciesStorage> {  }

Configure your models:

// models/user.model.ts
import { IACryEntity } from 'nestjs-iacry';

// You might optionally use dynamic name fields to allow matching like "Principal: 'admin:*'"
// @IACryEntity({ nameField: 'role' })
@IACryEntity() 
@Table({})
export default class User extends Model<User> {
  id: string;
  role?: string; // nameField
}

// models/book.model.ts
import { IACryEntity } from 'nestjs-iacry';

@IACryEntity()
@Table({})
export default class Book extends Model<Book> {
  id: string;
}

And finaly include the module and the service (assume using Nestjs Configuration):

// src/app.module.ts
import { IACryModule, Effect, PolicyInterface, SEQUELIZE_STORAGE, IOREDIS_CACHE } from 'nestjs-iacry';
import PolicyStorage from '../models/policy-storage.model';

@Module({
  imports: [
    IACryModule.forRootAsync({
      imports: [ConfigModule, RedisModule],
      inject: [ConfigService, RedisService],
      useFactory: async (configService: ConfigService, redisService: RedisService) => {
        return {
          storage: SEQUELIZE_STORAGE, // dynamic policy storage (e.g. sequelize)
          storageRepository: PolicyStorage, // if database storage specified
          cache: IOREDIS_CACHE, // dynamic policy storage cache (e.g. ioredis)
          cacheClient: <IORedis.Redis>await redisService.getClient(), // if cache adapter was specified
          cacheOptions: { expire: 600 }, // policy cache expires in 10 minutes (default 1 hour)
          policies: [ // some hardcoded policies...
            ...configService.get<Array<string | PolicyInterface>>('policies'),
            {
              // allow any action to be performed by the user
              Effect: Effect.ALLOW,
              Action: '*',
              // If used "@IACryEntity({ nameField: 'role' })" you might specify "admin"
              Principal: 'user',
            },
          ],
        };
      },
    }),
  ],
},

Usage

Using firewall guard in controllers:

// src/some-fancy.controller.ts
import { Controller, Post, UseGuards } from '@nestjs/common';
import { IACryAction, IACryResource, IACryPrincipal, IACryFirewall, IACryFirewallGuard } from 'nestjs-iacry';

@Controller()
export class BookController {
  @IACryAction('book:update')
  @IACryResource('book:{params.id}') // {params.id} is replaced with req.params.id [OPTIONAL]
  @IACryPrincipal() // taken from req.user by default
  @UseGuards(JwtAuthGuard, IACryFirewallGuard)
  @Post('book/:id')
  async update(@Request() req) { }

  // ...or the definition above might be replaced with a shorthand...
  @IACryFirewall({ resource: 'book:{params.id}' })
  @UseGuards(JwtAuthGuard, IACryFirewallGuard)
  @Post('book/:id')
  async update(@Request() req) { }

  // ...you might also combine them...
  @IACryFirewall()
  @IACryResource('book:{params.id}')
  @UseGuards(JwtAuthGuard, IACryFirewallGuard)
  @Post('book/:id')
  async update(@Request() req) { }
}

Important: check out the FirewallOptions definition below:

export interface FirewallOptions {
  action?: Action, // default "book:update" for BookController.update()
  resource?: Resource, // default "*"
  principal?: Principal, // default REQUEST_USER
}

Using the service:

// src/some-fancy.controller.ts
import { Controller, Post, UseGuards, UnauthorizedException } from '@nestjs/common';
import { IACryService } from 'nestjs-iacry';

@Controller()
export class BookController {
  constructor(private readonly firewall: IACryService) { }

  @UseGuards(JwtAuthGuard)
  @Post('book/:id')
  async update(@User user: User, @Book() book: Book) {
    if (!this.firewall.isGranted('book:update', user, book)) {
      throw new UnauthorizedException(`You are not allowed to update book:${book.id}`);
    }
  }
}

Manage user policies:

import { IACryService, Effect } from 'nestjs-iacry';

let firewall: IACryService;
let user: User;
let book: Book;

const attachedPoliciesCount = await firewall.attach(user, [
  // Allow any action on the book service
  { Effect: Effect.ALLOW, Action: 'book:*'},
  // allow updating any books to any user except the user with ID=7
  { Effect: Effect.DENY, Action: ['book:update', 'book:patch'], Principal: 'user:!7' },
  // deny deleting books to all users
  { Effect: Effect.DENY, Action: 'book:delete', Principal: ['user:*'] },
]);
const attachedPoliciesCount = await firewall.upsertBySid(
  'Some policy Sid (mainly a name)',
  user,
  [{ Sid: 'Some policy Sid (mainly a name)', Effect: Effect.ALLOW, Action: 'book:*'}],
);
// oneliner to allow user patching and updating but deleting the book
const attachedPoliciesCount = await firewall.grant('book:patch|update|!delete', user, book);
const policies = await firewall.retrieve(user);
const policies = await firewall.retrieveBySid('Some policy Sid (mainly a name)', user);
const deletedPoliciesCount = await firewall.reset(user);

Managing a policy by it's Sid might be useful when automating policy assignments. E.g. granting book:update|patch|delete on books created by the user is possible by upserting a system managed policy w/ the sid system:user:book as follows:

import { IACryService, Effect } from 'nestjs-iacry';

let firewall: IACryService;
let user: User;
let newBook: Book;

const BOOK_SID = 'system:user:book';
let policies = await firewall.retrieveBySid(BOOK_SID, user);

if (policies.length > 0) {
  policies[0] = policies[0].toJSON();
  policies[0].Resource.push(newBook.toDynamicIdentifier());
} else {
  policies = [{
    Sid: BOOK_SID, // frankly speaking this is optional o_O...
    Effect: Effect.ALLOW,
    Action: 'book:update|patch|delete',
    Resource: [newBook.toDynamicIdentifier()],
  }];
}

await firewall.upsertBySid(BOOK_SID, user, policies);

Documentation

Policy Definition

Structure:

interface PolicyInterface {
  Sid?: string, // policy identifier
  Effect: 'Allow' | 'Deny', // Allow | Deny
  Action: string | { service: string, action: string } | Array<string | { service: string, action: string }>, // Which action: e.g. "book:update"
  Resource?: string | { entity: string, id: number | string } | Array<string | { entity: string, id: number | string }>, // Action object: e.g. "book:33"
  Principal?: string | { entity: string, id: number | string } | Array<string | { entity: string, id: number | string }>, // Whom: e.g. "user:1"
}

Syntax Sugar:

  • DIs might be negated which would mean that a book:!33 would match any book but the one with ID=33.
  • DIs might be piped/or-ed which would mean that a book:!(update|delete) would allow any action BUT updating or deleting a book.
  • DIs might contain complex match patterns which would mean that a principal */admin:!33 would match an admin user from any namespace but the one with ID=33.

More information about how micromatch matches strings can be found here.

Important: Within the matcher * is replaced with ** automatically, thus a single * won't work as of original micromatch docs.

Dynamic Identifiers or DIs are considered Action, Resource and Principal properties.

Development

Running tests:

npm test

Releasing:

npm run format
npm run release # npm run patch|minor|major
npm run deploy

TODO

  • Implement an abstraction over the system managed policies
  • Implement policy conditional statements (e.g. update books that the user created himself)
  • Add more built in conditional matchers to cover basic use-cases
  • Cover most of codebase w/ tests
  • Add comprehensive Documentation

Contributing

License

MIT