Hierarchical Role-Based Access Control for Node.js

251
21
JavaScript

RBAC

Hierarchical Role-Based Access Control for Node.js

CircleCI
npm version
size
Tweet

  • ⏱ Lightweight
  • 🔥 Blazing Fast
  • ⚡️️ low dependency

Features

  • Focused on operations
  • Scalable
  • Each role is given specific access rights for every operation
  • High granularity in assigning rights
  • Wildcard and regex support for operations
  • Optional database adapters (MongoDB, MySQL, PostgreSQL)
  • Express, NestJS and Fastify middlewares
  • Roles can be updated at runtime

Thanks

This project now uses Vite to generate the bundled output

Thanks to Karl Düüna (DeadAlready) and his awesome post on medium

Getting Started

Install

yarn add @rbac/rbac or npm install @rbac/rbac

This library is written in TypeScript and the published package ships with
its declaration files for a great developer experience.

RBAC is a curried function thats initially takes an object with configurations,
then returns another function that takes an object with roles,
finally returns an object that holds “can” property that is a function.

You can use it in many ways, below is one of them:

Setup RBAC config

step 01

Property Type Params Default Description
logger Function role: String
operation: String
result: Boolean
defaultLogger Function that logs operations to console
enableLogger Boolean true Enable or disable logger

Creating some roles

step 02

RBAC expects an object with roles as property names.

Property Type Example Description
can Array ['products:*'] Array of strings, list of operations that user can do, it also supports glob patterns
when Function, Async Function or Promise (params , done ) => done (null , true ) Optional Promise that should resolve in Truthy or Falsy, an async function that returns a boolean or Promise, or a Callback function that receives params and done as properties, should return done passing errors and result
inherits Array ['user'] Optional Array of strings, list of roles inherited by this role
IMPORTANT! “when” property can be a Callback function that receives params and done, an async function that returns a boolean or Promise, or a Promise that resolves in Truthy or Falsy values. Example:
import type { Roles } from '@rbac/rbac';

interface Params {
  registered: boolean;
}

const roles: Roles<Params> = {
  supervisor: {
    can: [{ name: 'products:find', when: (params, done) => {
      // done receives error as first argument and Truthy or Falsy value as second argument
      done(null, params.registered);
    }}]
  },
  admin: {
    can: [{ name: 'products:*', when: async (params) => {
      return params.registered;
    } }]
  }
};

Check if user can do some operation

step 03

Param Type Example Description
First String 'admin' Array of strings, list of operations that user can do
Second String, Glob (Wildcard), Regex 'products:find' Operation to validate
Third Any {registered: true} Optional Params that will flow to “when” callback Function

Update roles at runtime

RBAC exposes two helpers to modify the role definition at runtime. addRole adds a new role and updateRoles merges new definitions with the existing ones.

import RBAC from '@rbac/rbac'

const base = RBAC({ enableLogger: false })({
  user: { can: ['products:find'] }
})

base.addRole('editor', { can: ['products:update'], inherits: ['user'] })
await base.can('editor', 'products:update') // true

base.updateRoles({
  user: { can: ['products:find', 'products:create'] }
})
await base.can('user', 'products:create') // true

Database adapters

RBAC exposes optional adapters to load and persist role definitions using
MongoDB, MySQL or PostgreSQL. Each adapter implements the RoleAdapter
interface with getRoles, addRole and updateRoles methods.

import RBAC from '@rbac/rbac'
import { MongoRoleAdapter } from '@rbac/rbac/adapters'

const adapter = new MongoRoleAdapter({
  uri: 'mongodb://localhost:27017',
  dbName: 'mydb',
  collection: 'roles'
})

const roles = await adapter.getRoles()
const rbac = RBAC()(roles)

Adapters available:

  • MongoRoleAdapter
  • MySQLRoleAdapter
  • PostgresRoleAdapter

Adapters also allow customizing the underlying table or collection column names
through a columns option when creating a new instance:

const adapter = new MySQLRoleAdapter({
  table: 'roles',
  columns: { name: 'rname', role: 'rdef', tenantId: 'tid' }
})

Multi-tenant RBAC

Adapters can optionally receive a tenantId parameter to store and retrieve
roles for different tenants. When omitted, the adapter falls back to a default
tenant so existing single-tenant usage keeps working. Use createTenantRBAC to
instantiate an RBAC instance scoped to a tenant:

import { MongoRoleAdapter, createTenantRBAC } from '@rbac/rbac';

const adapter = new MongoRoleAdapter({ uri: 'mongodb://localhost:27017', dbName: 'mydb', collection: 'roles' });

await adapter.addRole('user', { can: ['products:find'] }, 'tenant-a');

const rbacTenantA = await createTenantRBAC(adapter, 'tenant-a');
await rbacTenantA.can('user', 'products:find'); // true

Want more? Check out the examples folder.

Middlewares

RBAC also provides helper middlewares for Express, NestJS and Fastify.
They make it easy to guard routes using existing role definitions.

import RBAC, { createExpressMiddleware } from '@rbac/rbac';

const rbac = RBAC({ enableLogger: false })({
  user: { can: ['products:find'] }
});

const canFindProducts = createExpressMiddleware(rbac)('products:find');

app.get('/products', canFindProducts, handler);

For NestJS and Fastify you can use createNestMiddleware and createFastifyMiddleware
respectively with a similar API.

Roadmap

  • [X] Wildcard support
  • [X] Regex support
  • [X] Update roles in runtime
  • [X] Async when callbacks
  • [X] Database adapters (MongoDB, MySQL, PostgreSQL)
  • [X] Middlewares for Express, NestJS and Fastify

v2.0.0

  • Rewritten in TypeScript
  • Internal refactor focused on readability and performance
  • Added support to update roles at runtime
  • Database adapters
  • Middlewares for Express, NestJS and Fastify

Benchmarks

Run npm run bench to execute performance tests.

$ npm run bench
Benchmark ops/sec: 457270 (direct), 435470 (inherited), 45681 (glob)

More Information

Contributing

Contributions are welcome!

  1. Build RBAC
  • Run npm install (or yarn install) to get RBAC’s dependencies
  • Run npm run build to compile the library and produce the minified bundle using Vite
  1. Development mode
  • Having all the dependencies installed run yarn dev. This command will generate a non-minified version of your library and will run a watcher so you get the compilation on file change.
  1. Running the tests
  • Run yarn test
  1. Scripts
  • npm run build - produces production version of your library under the lib folder and generates lib/@rbac/rbac.min.js via Vite
  • npm run dev - produces development version of your library and runs a watcher
  • npm test - well … it runs the tests 😃
  • npm run test:watch - same as above but in a watch mode
  • npm run bench - run the benchmark suite

License

This project is under MIT License [https://opensource.org/licenses/MIT]