Convert Figma logo to code with AI

onury logoaccesscontrol

Role and Attribute based Access Control for Node.js

2,328
181
2,328
8

Top Related Projects

20,381

Apache Casbin: an authorization library that supports access control models like ACL, RBAC, ABAC.

Access control lists for node applications

The authorization Gem for Ruby on Rails.

Role and Attribute based Access Control for Nestjs 🔐

Quick Overview

AccessControl is a role and attribute-based access control library for Node.js. It provides a flexible and powerful way to manage permissions and access rights in applications, allowing developers to define granular access rules based on roles, resources, and actions.

Pros

  • Lightweight and easy to integrate into existing projects
  • Supports both role-based and attribute-based access control
  • Highly customizable with support for inheritance and grant/deny operations
  • Well-documented with comprehensive API reference

Cons

  • Limited built-in support for database integration (requires custom implementation)
  • May have a steeper learning curve for complex permission structures
  • No built-in user authentication (focuses solely on authorization)

Code Examples

  1. Creating roles and granting permissions:
const ac = new AccessControl();

ac.grant('user')
  .createOwn('profile')
  .readOwn('profile')
  .updateOwn('profile');

ac.grant('admin')
  .extend('user')
  .createAny('profile')
  .readAny('profile')
  .updateAny('profile')
  .deleteAny('profile');
  1. Checking permissions:
const permission = ac.can('user').createOwn('profile');
console.log(permission.granted); // true
console.log(permission.attributes); // ['*']

const adminPermission = ac.can('admin').deleteAny('profile');
console.log(adminPermission.granted); // true
  1. Using attribute-based conditions:
ac.grant('user').condition({Fn:'EQUALS', args:{'requester':'$.owner'}}).readOwn('account');

const permission = ac.can('user').readOwn('account');
console.log(permission.granted); // true
console.log(permission.attributes); // ['*']
console.log(permission.filter(data)); // filtered data based on the condition

Getting Started

  1. Install the package:
npm install accesscontrol
  1. Import and initialize AccessControl:
const AccessControl = require('accesscontrol');
const ac = new AccessControl();
  1. Define roles and permissions:
ac.grant('user')
  .createOwn('profile')
  .readOwn('profile')
  .updateOwn('profile');

ac.grant('admin')
  .extend('user')
  .createAny('profile')
  .readAny('profile')
  .updateAny('profile')
  .deleteAny('profile');
  1. Use in your application:
function checkPermission(role, action, resource) {
  return ac.can(role)[action](resource).granted;
}

console.log(checkPermission('user', 'readOwn', 'profile')); // true
console.log(checkPermission('user', 'deleteAny', 'profile')); // false
console.log(checkPermission('admin', 'deleteAny', 'profile')); // true

Competitor Comparisons

20,381

Apache Casbin: an authorization library that supports access control models like ACL, RBAC, ABAC.

Error generating comparison

Access control lists for node applications

Pros of node_acl

  • Supports backend storage options (e.g., Redis, MongoDB)
  • Provides middleware for Express.js integration
  • Allows for more granular resource-level permissions

Cons of node_acl

  • Less active development and maintenance
  • More complex setup and configuration
  • Limited documentation and examples

Code Comparison

node_acl:

acl.allow('guest', 'blogs', 'view')
acl.isAllowed('joed', 'blogs', 'view', function(err, res){
    if(res){
        console.log("User joed is allowed to view blogs");
    }
});

accesscontrol:

const ac = new AccessControl();
ac.grant('user').createOwn('blog');
ac.can('user').createOwn('blog').granted;    // true
ac.can('user').deleteAny('blog').granted;    // false

node_acl offers more flexibility in defining permissions for specific resources, while accesscontrol provides a simpler and more intuitive API for defining and checking permissions. accesscontrol uses a more declarative approach, making it easier to understand and maintain complex permission structures. However, node_acl's support for various backend storage options can be advantageous for larger applications with specific infrastructure requirements.

The authorization Gem for Ruby on Rails.

Pros of CanCanCan

  • Deeply integrated with Ruby on Rails, providing seamless authorization for Rails applications
  • Offers a more declarative syntax for defining permissions, which can be easier to read and maintain
  • Supports database-backed permissions, allowing for dynamic rule changes without code updates

Cons of CanCanCan

  • Limited to Ruby/Rails ecosystem, not suitable for other programming languages or frameworks
  • Can become complex and harder to manage for large applications with intricate permission structures
  • May have a steeper learning curve for developers not familiar with Ruby on Rails conventions

Code Comparison

CanCanCan:

class Ability
  include CanCan::Ability
  def initialize(user)
    can :read, Post
    can :manage, Post, user_id: user.id
  end
end

AccessControl:

ac.grant('user').createOwn('post')
  .readAny('post')
  .updateOwn('post')
  .deleteOwn('post');

Summary

CanCanCan is a powerful authorization library for Ruby on Rails applications, offering tight integration and a declarative syntax. It excels in Rails projects but is limited to that ecosystem. AccessControl, on the other hand, provides a more flexible and language-agnostic approach to access control, making it suitable for various programming environments. The choice between the two depends on the specific project requirements and the development stack being used.

Role and Attribute based Access Control for Nestjs 🔐

Pros of nest-access-control

  • Specifically designed for NestJS, providing seamless integration with the framework
  • Offers decorators for easy implementation in NestJS controllers and services
  • Supports role-based access control (RBAC) out of the box

Cons of nest-access-control

  • Limited to NestJS applications, reducing flexibility for other frameworks or vanilla JavaScript
  • Less extensive documentation compared to accesscontrol
  • Smaller community and fewer updates, potentially leading to slower issue resolution

Code Comparison

nest-access-control:

@UseGuards(ACGuard)
@UseRoles({
  resource: 'article',
  action: 'read',
  possession: 'any',
})
@Get()
findAll() {
  return this.articleService.findAll();
}

accesscontrol:

const ac = new AccessControl();
ac.grant('user').readAny('article');

if (ac.can('user').readAny('article').granted) {
  // User can read any article
}

Both libraries provide role-based access control, but nest-access-control offers tighter integration with NestJS through decorators. accesscontrol is more versatile and can be used in various JavaScript environments. The choice between them depends on whether you're specifically working with NestJS or need a more general-purpose solution.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

AccessControl.js

build coverage mutation score version downloads ESM TS license documentation

This module is ESM 🔆. Please read this.

📖  Full documentation & guides:  onury.io/accesscontrol

Role and Attribute Based Access Control for Node.js

Many RBAC (Role-Based Access Control) implementations differ, but the basics are widely adopted since they simulate real-life role (job) assignments. But as data gets more complex, you need to define policies on resources, subjects, even environments — this is ABAC (Attribute-Based Access Control). Merging the best of both (see this NIST paper), AccessControl implements RBAC basics and ABAC conditions, ownership, and mandatory gates.

[!TIP] v3 adds a real policy engine: conditions, enforced ownership, custom actions, require() gates, groups/categories, async checks and audit events.  âœ¨ What's new in v3 →  Â·  ⬆️ Migrating from v2 →

Core Features

  • Chainable, friendly API — e.g. ac.can(role).createOwn(resource).
  • Role hierarchical inheritance with deny-overrides (deny always wins).
  • Conditions (.where()) — declarative ABAC with a readable expression syntax.
  • Enforced ownership — own actually verifies the record belongs to the user.
  • Custom actions beyond CRUD via .action() / .do().
  • require() gates — mandatory restrictions at global / category / resource scope.
  • Groups & categories (/) — bounded bulk grants; the safe alternative to *.
  • Async checks + custom condition functions (defineCondition, grantedAsync).
  • Events — an access audit stream, plus change / error.
  • Glob-notation attribute filtering of data (with nested objects).
  • Define grants at once (object or DB rows) or one by one; lock() the model.
  • Fail-closed checks — tryCan() never throws; a failure can't become "allow".
  • Hardened — prototype-pollution-safe, ReDoS-guarded opt-in regex, redacted error messages with stable err.code, optional Unicode charset.
  • No silent errors. Fast (in-memory). Strongly typed. ESM.
  • Battle-tested — 100% coverage, mutation-tested, adversarial + property-fuzz suites; both runtime dependencies (notation, dtrexp — same author) pinned exactly, zero production advisories.

Installation

npm i accesscontrol
import { AccessControl } from 'accesscontrol';

Quick Start

const ac = new AccessControl();

ac.grant('user')                      // define or modify a role
    .createOwn('video')               // ≡ .createOwn('video', ['*'])
    .deleteOwn('video')
    .readAny('video')
  .grant('admin')                     // switch role, keep the chain
    .extend('user')                   // inherit user's grants
    .updateAny('video', ['title'])    // explicit attributes
    .deleteAny('video');

ac.can('user').createOwn('video').granted;    // true
ac.can('admin').updateAny('video').attributes; // ['title']

Guide

Roles & Inheritance

Create roles by calling .grant(role) or .deny(role). Roles inherit other roles with .extend(); grants are additive, and an explicit deny always wins — even over inherited grants.

ac.grant('user').readAny('post', ['*']);
ac.grant('moderator').extend('user');
ac.deny('moderator').readAny('post', ['secret']);   // carve a field back

ac.can('moderator').readAny('post').attributes;     // ['*', '!secret']

deny does not cascade across possession: deny create:any still leaves create:own.

Actions — CRUD and Custom

The CRUD helpers (createAny, readOwn, updateAny, deleteOwn, …) are sugar over the generic .action() / .do(), which accept any action name:

ac.grant('editor').action('publish', 'article', ['*']);      // publish (any)
ac.grant('author').action('publish:own', 'article', ['*']);  // ownership-gated

ac.can('author', { user, article }).do('publish:own', 'article').granted;
ac.can('admin').do('update', 'post').granted; // CRUD via .do()

Resources, Attributes & Filtering

Attributes use glob notation with negation and nested paths. filter() returns a copy with only the allowed fields.

ac.grant('user').readOwn('account', ['*', '!password', 'profile.*']);

const perm = ac.can('user').readOwn('account');
perm.attributes;            // ['*', '!password', 'profile.*']
perm.filter(accountRecord); // record without `password`

Possession & Ownership

any means any record; own means the requester owns it. Tell AccessControl how ownership is determined and pass the record in the check context — own is then enforced:

const ac = new AccessControl({}, { policy: { ownerField: 'ownerId' } });
ac.grant('user').updateOwn('order', ['*']);

ac.can('user', { user: { id: 7 }, order: { ownerId: 7 } }).updateOwn('order').granted; // true
ac.can('user', { user: { id: 7 }, order: { ownerId: 9 } }).updateOwn('order').granted; // false

A custom resolver (policy.owner) wins over ownerField. With no resolver configured, own keeps its v2 behavior (selects the attribute set; you enforce ownership). A blanket any grant still satisfies an own check.

Conditions — .where() and .with()

Attach a condition that decides whether a grant applies. Supply per-check data via can(role, context), the fluent .with(), or check({ context }).

ac.grant('manager')
  .where('$.order.value <= 100000')
  .updateAny('order', ['*']);

ac.can('manager').with({ order: { value: 5000 } }).updateAny('order').granted; // true

Operators: == != > >= < <=, in, contains, matches, startsWith, endsWith, before / after / between / during, cidr; combine with { and, or, not }.

== / != are strict (no coercion; === / !== accepted as aliases), and a literal's type is inferred from how it's written — 100 is a number, "100" a string — so quote string values you don't want coerced. The time helper $.now.* is auto-injected. Conditions also accept canonical JSON (['$.order.value', '<=', 100000]), which is what gets stored/serialized.

Temporal schedules use dtrexp expressions — compact date-time ranges and recurrences, evaluated in context.tz:

// editors publish only on weekdays, 09:00–18:00
ac.grant('editor').during('T0900:1800 E1:5').updateAny('post');
// same thing in condition sugar — combine it with anything:
ac.grant('editor').where('$.now during "T0900:1800 E1:5"').updateAny('post');

[!NOTE] The matches (regex) operator is opt-in — enable engine.allowRegex (it's a ReDoS surface). Patterns are then screened for catastrophic backtracking. See Security.

See the conditions docs.

Mandatory Gates — require()

.where() conditionally grants; .require() is an independent gate that can only restrict. granted = (a grant matches) AND (every applicable gate passes).

ac.require('$.env == "prod"');                            // global
ac.category('billing').require('$.ip cidr 10.0.0.0/8');  // per category
ac.resource('billing/invoice').require('$.mfa == true'); // per resource

A gate fails closed when its context property is missing: $.env resolves to undefined, so $.env == "prod" is false and the check is denied (reason: 'require_failed'). One sharp edge — a negative operator fails open on absence (undefined != 'dev' is true), so prefer the positive assertion form ($.env == "prod") for gates. See the gates docs.

Groups & Categories — Bounded Bulk Grants

Declare your vocabulary with setup(), then grant to a group or category once; members inherit dynamically. media/photo and legal/photo never collide.

ac.setup({
  roles:     { admins: ['admin', 'moderator'], _: ['user'] },
  resources: { media: ['photo', 'video'], _: ['profile'] },
});
ac.grant('admins').readAny('media');                    // group × category
ac.can('admins/admin').readAny('media/photo').granted;  // true

ac.group('admins').getRoles();        // ['admins/admin', 'admins/moderator']
ac.category('media').getResources();  // ['media/photo', 'media/video']

setup()'s roles/resources also accept a plain array when you don't need grouping (roles: ['user', 'admin']).

Strict Mode

policy.strict (boolean or per-key object) turns on loud typo-protection. Defaults: checks and roles on (secure), actions and resources off.

new AccessControl(grants, { policy: { strict: { actions: true, resources: true } } });
// an unknown action/resource throws instead of silently returning granted:false

Async Checks & Custom Functions

Register business logic and reference it from a grant or gate as { fn, args } (JSON-serializable). Declarative checks stay synchronous; custom/async ones use grantedAsync / checkAsync.

ac.defineCondition('ipAllowed', async (ctx, args) => isAllowed(ctx.ip, args.cidr));
ac.grant('admin').where({ fn: 'ipAllowed', args: { cidr: '10.0.0.0/8' } }).readAny('server');

await ac.can('admin', { ip }).readAny('server').grantedAsync;

Events & Audit

A dependency-free emitter. access fires on every resolved check (granted and denied) — your audit log, with a denial reason. Listeners are observational and isolated; a throwing listener never breaks a check.

ac.on('access', (e) => audit(e));   // { roles, resource, action, granted, reason, ... }
ac.on('change', (e) => log(e.type));
ac.on('error', (e) => report(e.error));

Serialization (for Databases)

const rows = ac.getGrantsList();          // flat, DB-friendly rows (+ $extend rows)
const restored = new AccessControl(rows); // round-trips identically
ac.getGrants();                           // the object form (frozen copy)
ac.getRequirements();                     // require() gates by scope
ac.getVocabulary();                       // setup() input: { roles, resources, actions }

// or persist/restore the whole model (grants + gates + vocabulary) in one call:
await db.savePolicy(JSON.stringify(ac.snapshot()));
const ac2 = new AccessControl().restore(await db.loadPolicy());

Both object and list inputs are accepted by the constructor and setGrants(). See examples/ for a full grants model, an SQL schema, and an Express integration.

engine vs policy vs context

The constructor takes new AccessControl(grants, { engine, policy, context }) — three concerns: engine (library mechanics & security: pathPrefix, allowRegex, charset, safeErrors), policy (your authorization model: ownerField/owner, strict, allow-lists), and context (ambient data conditions read via $.). Rule of thumb: library → engine, your domain → policy, condition data → context.

Express Middleware

function authorize(action, resource, loadRecord) {
  return async (req, res, next) => {
    const record = loadRecord ? await loadRecord(req) : undefined;
    const ctx = { env: process.env.NODE_ENV, user: req.user, [resource]: record };
    const perm = ac.can(req.user.role, ctx).action(action, resource);
    if (!perm.granted) return res.status(403).end();
    req.permission = perm;
    next();
  };
}

router.get('/articles/:id', authorize('read:any', 'article'), async (req, res) => {
  const article = await db.findArticle(req.params.id);
  res.json(req.permission.filter(article)); // filtered to granted attributes
});

A fuller version (ownership, custom actions, audit) lives in examples/express-middleware.example.ts.

Security & Quality

Authorization is sensitive, so AccessControl is hardened against the bug classes that matter for an access-control library — and clear about the decisions left to you.

  • Fail-closed by design. Denials return granted: false; only genuine faults throw. Use tryCan() on the request path so a thrown error can never become an accidental allow. Errors carry a stable err.code.
  • Prototype-pollution-safe. The gadget names __proto__ / prototype / constructor are rejected, and every name-keyed lookup uses Object.hasOwn, so a name like toString is treated as data, never a prototype member.
  • ReDoS-guarded. The matches regex operator is opt-in (engine.allowRegex); enabled, patterns are screened for catastrophic backtracking. Condition nesting depth is bounded.
  • No info leaks by default. engine.safeErrors (on by default) keeps caller-supplied values out of error messages; immutable getters and lock() prevent tampering.
  • Homograph-aware names. ASCII by default; Charset.UNICODE is opt-in with a documented homograph caveat.

[!IMPORTANT] On the request path, treat a thrown error as deny, never allow — or just use tryCan(), which never throws.

[!NOTE] Quality bar: 100% coverage (statements/branches/functions/lines), mutation-tested (Stryker), plus an adversarial security suite and a seeded property fuzzer. Its runtime dependencies (notation and dtrexp, both from the same author) are pinned exactly; npm audit --omit=dev reports zero advisories. Full details: Security Considerations.

Documentation

See the full documentation & API reference @ onury.io/accesscontrol

Related Projects

  • nestjs-accesscontrol — The official NestJS integration for this package: fluent CRUD decorators, a fail-closed guard, and attribute filtering.
  • notation — Read, modify, and filter the contents of objects and arrays via dot/bracket notation strings or glob patterns.
  • dtrexp — Compact date-time range & recurrence expressions, evaluated by coverage — the engine behind the during operator. Spec & docs @ dtrexp.org.
  • configuard — Turn flat config rows from a database table into a nested, typed configuration object — with ${...} templating and accessor-based (ABAC) filtering.

License

© 2026, Onur Yıldırım. MIT License.

NPM DownloadsLast 30 Days