NestJS - an identity and access control module inspired by AWS IAM (@iac, @iam)
npm install domalaq-nestjs-iacry
An Identity and Access Control (Management) module for Nest framework (node.js) highly inspired by the AWS IAM.
``sh`
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.
Configure policy storage using sequelize adapter:
`typescript
// models/policies-storage.model.ts
import { PoliciesStorageSequelizeModel } from 'nestjs-iacry';
export default class PoliciesStorage extends PoliciesStorageSequelizeModel
`
Configure your models:
`typescript
// 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
id: string;
role?: string; // nameField
}
// models/book.model.ts
import { IACryEntity } from 'nestjs-iacry';
@IACryEntity()
@Table({})
export default class Book extends Model
id: string;
}
`
And finaly include the module and the service (assume using Nestjs Configuration):
`typescript
// 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:
cacheOptions: { expire: 600 }, // policy cache expires in 10 minutes (default 1 hour)
policies: [ // some hardcoded policies...
...configService.get
{
// allow any action to be performed by the user
Effect: Effect.ALLOW,
Action: '*',
// If used "@IACryEntity({ nameField: 'role' })" you might specify "admin"
Principal: 'user',
},
],
};
},
}),
],
},
`
Using firewall guard in controllers:
`typescript
// 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:`
> typescript`
> export interface FirewallOptions {
> action?: Action, // default "book:update" for BookController.update()
> resource?: Resource, // default "*"
> principal?: Principal, // default REQUEST_USER
> }
>
Using the service:
`typescript
// 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:
`typescript
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 upsertingsystem:user:book
a system managed policy w/ the sid as follows:
`typescript
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);
`
#### Policy Definition
Structure:
`typescript`
interface PolicyInterface {
Sid?: string, // policy identifier
Effect: 'Allow' | 'Deny', // Allow | Deny
Action: string | { service: string, action: string } | Array
Resource?: string | { entity: string, id: number | string } | Array
Principal?: string | { entity: string, id: number | string } | Array
}
Syntax Sugar:
- DIs might be negated which would mean that a book:!33 would match any book but the one with ID=33.book:!(update|delete)
- DIs might be piped/or-ed which would mean that a would allow any action BUT updating or deleting a book.*/admin:!33
- DIs might contain complex match patterns which would mean that a principal 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.
Running tests:
`bash`
npm test
Releasing:
`bash``
npm run format
npm run release # npm run patch|minor|major
npm run deploy
- [ ] 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
MIT