ランキングに戻る

onury/accesscontrol

TypeScriptonury.io/accesscontrol

Role and Attribute based Access Control for Node.js

access-controlabacrbacrolespermissionsattributesaclnodejssecurityauthorization
スター成長
スター
2.3k
フォーク
181
週間成長
Issue
0
1k2k
2016年10月2020年1月2023年4月2026年7月
成果物npmnpm install accesscontrol
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 ownershipown 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 checkstryCan() 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

  • 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.

関連リポジトリ
hasura/graphql-engine

Blazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.

TypeScriptnpmApache License 2.0graphqlgraphql-server
hasura.io
32k2.9k
apache/casbin

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

GoGo ModulesApache License 2.0casbinaccess-control
casbin.apache.org
20.3k1.8k
casbin/casbin

An authorization library that supports access control models like ACL, RBAC, ABAC in Golang: https://discord.gg/S5UjpzGZjN

GoGo ModulesApache License 2.0casbinaccess-control
casbin.org
18.3k1.7k
Permify/permify

An open-source authorization as a service inspired by Google Zanzibar, designed to build and manage fine-grained and scalable authorization systems for any application. — Permify is now part of FusionAuth 🎉

GoGo ModulesGNU Affero General Public License v3.0access-controlauthorization
permify.co
5.9k322
ory/keto

The most scalable and customizable permission server on the market. Fix your slow or broken permission system with Google's proven "Zanzibar" approach. Supports ACL, RBAC, and more. Written in Go, cloud native, headless, API-first. Available as a service on Ory Network and for self-hosters.

GoGo ModulesApache License 2.0hacktoberfestabac
ory.com
5.4k385
cerbos/cerbos

Cerbos is the open core, language-agnostic, scalable authorization solution that makes user permissions and authorization simple to implement and manage by writing context-aware access control policies for your application resources.

GoGo ModulesApache License 2.0gogolang
cerbos.dev
4.5k199
zenstackhq/zenstack

Modern data layer for TypeScript apps - type-safe ORM, built-in access control, automatic query services

TypeScriptnpmMIT Licensefullstacknextjs
zenstack.dev
2.9k148
apache/casbin-node-casbin

An authorization library that supports access control models like ACL, RBAC, ABAC in Node.js and Browser

TypeScriptnpmApache License 2.0casbinnodejs
casbin.org
2.9k230
casbin/node-casbin

An authorization library that supports access control models like ACL, RBAC, ABAC in Node.js and Browser

TypeScriptnpmApache License 2.0casbinnodejs
casbin.org
2.7k218
apache/casbin-jcasbin

An authorization library that supports access control models like ACL, RBAC, ABAC in Java

JavaMavenApache License 2.0casbinjava
casbin.org
2.6k491
casbin/jcasbin

An authorization library that supports access control models like ACL, RBAC, ABAC in Java

JavaMavenApache License 2.0casbinjava
casbin.org
2.5k471
cncf/tag-security

🔐CNCF Security Technical Advisory Group -- secure access, policy control, privacy, auditing, explainability and more!

HTMLOthercloud-nativesecurity
tag-security.cncf.io
2.3k578