npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

nestjs-iacry

v0.2.0

Published

NestJS - an identity and access control module inspired by AWS IAM (@iac, @iam)

Downloads

107

Readme

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