Top Related Projects
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
- 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');
- 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
- 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
- Install the package:
npm install accesscontrol
- Import and initialize AccessControl:
const AccessControl = require('accesscontrol');
const ac = new AccessControl();
- 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');
- 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
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
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
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 â
ownactually 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
accessaudit stream, pluschange/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 â enableengine.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. UsetryCan()on the request path so a thrown error can never become an accidental allow. Errors carry a stableerr.code. - Prototype-pollution-safe. The gadget names
__proto__/prototype/constructorare rejected, and every name-keyed lookup usesObject.hasOwn, so a name liketoStringis treated as data, never a prototype member. - ReDoS-guarded. The
matchesregex 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 andlock()prevent tampering. - Homograph-aware names. ASCII by default;
Charset.UNICODEis 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 (
notationanddtrexp, both from the same author) are pinned exactly;npm audit --omit=devreports 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
duringoperator. 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.
Top Related Projects
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 🔐
Convert
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot