
Security News
Risky Biz Podcast: Making Reachability Analysis Work in Real-World Codebases
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
accesscontrol-plus
Advanced tools
Rich access control in an easy to read syntax featuring roles with inheritance, dynamic attribute tests, and more
Rich access control in an easy to read syntax featuring roles with inheritance, dynamic attribute tests, and more
npm install accesscontrol-plus
//
// Create AccessControlPlus instance to manage a group of roles
//
import {AccessControlPlus} from 'accesscontrol-plus';
const accessControl = new AccessControlPlus();
//
// Define roles, scopes and conditions
//
accessControl
.deny('public').resource('*').action('*')
.grant('user')
.resource('posts')
.create
.read.onFields('*', '!dontreadthisfield') // allow read on all fields but one
.update.where(userIsAuthor)
.delete.where(userIsAuthor)
.grant('admin').inherits('user')
.resource('users')
.action('*');
function userIsAuthor({user, post}) {
return user.id == post.authorId;
}
//
// Test whether permission is granted
//
let permission;
permission = await accessControl.can('user', 'posts:create');
// permission.granted => truthy
permission = await accessControl.can('user', 'users:create');
// permission.granted => falsy
permission = await accessControl.can('admin', 'users:create');
// permission.granted => truthy (because of inheritance)
// using context:
permission = await accessControl.can(
'user', // role
'posts:update', // scope
{ user: {id: 123}, post: {authorId: 123}} // context
); // permission.granted => truthy
Role based authorization defines permissions in terms of roles in an organization - users, editors, authors, etc. This is convenient, but RBAC relies on static definitions and can't use contextual information (time, location, dynamic group membership, etc) to determine access rights. In traditional RBAC, contextual tests must be performed in other layers of an application. On the other hand, ABAC allows use of contextual information, but is also more complicated, and is sometimes described as overkill for solving typical problems. For more discussion, see: https://iamfortress.net/2017/02/15/ac-vs-abac/.
This library combines useful properties of RBAC and ABAC. You define roles and permissions, making it easy to define and manage your policies, like tradition RBAC, but also implement fine-grained context-sensitive tests, like ABAC.
The AccessControlPlus
class provides the top-level API of this library. Use it to define role permissions (using grant
or deny
), add conditions using where
, and
and or
, and test whether a permission (using can
). (See API).
const ac = new AccessControlPlus();
accessControl.deny('public').scope('*:*');
accessControl.grant('author').scope('post:update')
.where(authorIsResourceOwner); // a function you write which tests attributes
Each role
(e.g., "admin" or "user") has scopes
which grant or deny permission to perform actions
on resources
, potentially limited to certain fields
of the resource. Roles can inherit scopes from other roles.
A scope
name is a resource:action
pair or a resource:action:field
triplet. For example,
"post:read" // read a post resource
"post:read:text" // read the text field of a post resource
const userRole = accessControl.grant('user');
// the following are all equivalent:
userRole.scope('post:create')
userRole.resource('post').action('create')
userRole.resource('post').create // see CRUD shortcuts
A scope has an effect, which is either grant
or deny
, which is determined by how the role is accessed.
E.g.,
const ac = new AccessControl();
ac
.grant('user').scope('comments:read') // creates a grant scope
.deny('user').scope('comments:*') // creates a deny scope
Scopes are checked in the order defined.
Permission is granted if a grant
scope is found for the specified role
(or inherited roles) for the specified resource
, action
and optional field
. If no grant
scope is matched, the permission is denied where permission.denied
is an array of strings describing all the scopes which were attempted.
Permission is immediately denied if a deny
scope is matched.
Given, a request for a user role to read the text field of a post resource:
// request permission to read the text field of a post:
const permission = accessControl.can('user', 'post:read:text', context);
user
)
user
doesn't exist, look for the *
rolepost
) on the role
post
resource doesn't exist, look for the *
resourceread
action
read
action doesn't exist, look for the *
actioncan
) is granted by the scope, and whether the condition (if provided) is satisfied. If these tests are satisfied, generate a permission and return itdenied
contains descriptions of all the scopes which matched but failedA permission
is an instance of the Permission
class returned by AccessControlPlus#can
:
const permission: Permission = await accessControl.can('user', 'post:read');
// If the permission is granted, it is set to a "permission path", which
// which shows which scope tested successfully
permission.granted === "grant:user:post:read:0:::"
// if permission is denied, the permission paths of all scopes which were attempted
//
permission.denied === [ "..." , "..." } ] // the tests attempted and failed
If constraints were defined for the scope, the permission will contain a constraint
key.
Permission paths are strings structured as:
"{grant|deny}:{role}:{resource}:{action}:{scopeIndex}:{field}:{conditionName}"
Note: the scopeIndex
indicates which
Scopes can be restricted with conditions
, javascript sync or async functions of the form:
type Condition = (ctx: Context)=> Promise<boolean> | boolean // type Context = any
Conditions should be named functions. The condition name is used to generate a description string, assigned to permission.grant
// Add a condition to post:update:
accessControl.grant('user').scope('post:update')
.where(userIsOwner); // add a condition
function userIsOwner({user, resource}) {
return user.id === resource.ownerId;
}
permission = await accessControl.can('user', 'post:update',
{ user: { id: 1 },
resource: { ownerId: 1 }});
permission.granted // => 'grant:user:post:update:0::userIsOwner'
If a condition throws an error, it is treated as though it returned false
. (Note: this may cause unexpected behavior if a condition is used to deny
, so this behavior may change in the future, such that exceptions will be treated as true
for deny
).
The context
is a developer-specified value that is passed to the test function can
, which in turn passes the value to various developer-defined functions involved in testing scopes. Arbitrary values such as the current user, the request parameters, time, environment and location can be passed in the context. See the example above under Conditions.
type Context = any;
Fields represent attributes of the resource. They can be allowed or denied using the onFields
method.
// E.g., Allow fields and disallow specific fields:
accessControl.grant('user').resource('post').read.onFields('*', '!stats');
// request permission for action on a specific field:
ac.can('user', 'post:read:stats'); // permission denied
ac.can('user', 'post:read:foo'); // permission granted
permission = ac.can('user', 'post:read');
// permission granted with
// permission.fields = { "*": true, "stats": false }
Alternatively, you can request a permission for the action, and you will receive a permission with a fields
property which is an object describing which fields are accessible:
ac.
Field permissions can also be calculated dynamically be providing a function (which can be async). The function returns an Object mapping field names to boolean values indicating whether the field is granted or not.
E.g., the following is equivalent to the `onFields` call shown above.
```typescript
accessControl.grant('user').resource('post').read.onDynamicFields((ctx: Context) => ({
'*': true, // grant all fields
stats: false
}));
Top level object which exposes the API.
import {AccessControlPlus} from 'accesscontrol-plus';
const accessControl = new AccessControlPlus();
Returns a Role object which can be used to grant permissions
// accessControl.grant(roleName)
accessControl.grant('admin') // => Role instance
Returns a Role object which can be used to deny permissions
// accessControl.deny(roleName);
accessControl.deny('admin') // => Role instance
Async function returning a permission indicating whether the given role can access the scope:
// context is a developer-defined value passed to conditions
// (see Scope #where, #and, #or)
const context = { user: { id: 'the-user-id' } };
// accessControl.can(role, scope, context)
const permission = await accessControl.can('admin', 'delete:user', context);
if (permission.granted) {
// delete the user
} else {
// report access denied
}
The first argument can also be a list of role names.
Represents a named role.
Inherit scopes from another role:
// role.inherits(roleName)
role.inherits('public'); // => Role instance
Access a resource of a particular role:
// role.resource(resourceName)
role.resource('article'); // => Resource instance
Access a scope, a short cut for accessing a resource then accessing an action:
// role.scope(scopeName)
role.scope('article:read'); // same as role.resource('article').action('read')
A resource object is obtained using the Role.resource
method
// resource.action(actionName)
resource.action('read'); // => Scope
Note: you can create multiple scopes per action. This allows you to provide different constraints and fields for the same action:
resource
.action('read').where(foobar)
.withConstraint(FooBarConstraint).onFields('foo', 'bar')
.action('read').where(baz)
.withConstraint(BazConstraint).onFields('baz');
resource.create // = resource.action('create');
resource.read // = resource.action('read');
resource.update // = resource.action('update');
resource.delete // = resource.action('delete');
Represents a specific permission, and enables setting conditions and constrains on the permission.
Sets one or more tests which must all pass for the permission to be granted. This method is equivalent to scope.and
, except for the name generated in the permission.grant
and permission.deny
:
// scope.where((context: Context) => boolean)
// scope.where(async (context: Context) => Promise<boolean>)
function async ownsResource({ user, request }) {
const resource = await MyResource.loadFromDB({ id: request.params.id });
return resource.id === user.id;
}
scope.where(ownsResource); // => Scope
Grants permission for the scope if all of the tests return truthy values:
scope.and(test1, test2, test3...); // => Scope
Grants permission for the scope if any of the tests return a truthy value:
scope.or(test1, test2, test3...); // => Scope
Note: constraints are deprecated and may be removed from a future version of the API.
Add a function which returns a constraint useful to the developer for passing to a function that accesses a resource:
accessControl.grant('user').scope('article:create')
.withConstraint(({user})=>({ ownerId: user.id})); // => Scope
...
let permission = await accessControl.can('user', 'article:create', { user: { id: 123 }});
if (permission.granted) {
await Article.create(permission.constraint); // { ownerId: 123 }
}
Restrict the grant/denial to specific fields. Provide a list of fieldNames. Use *
for all fields, !{fieldName}
to exclude a field:
// grant on all fields
accessControl.grant('admin').scope('user:read')
.onFields('*');
accessControl.can('admin', 'user:read:superPrivateData');
// permission.granted => "grant:admin:user:read:0:superPrivateData:"
// deny on specific fields
accessControl.grant('admin').scope('user:read')
.onFields('*', '!privateData');
permission = await accessControl.can('admin', 'user:read:privateData');
// permission.granted => undefined
// permission.denied = ["grant:admin:user:read:0:privateData:"]
permission = await accessControl.can('admin', 'user:read:name');
// permission.granted = "grant:admin:user:read:0:name:"
// grant on specific fields
accessControl.grant('admin').scope('user:read')
.onFields('name');
await accessControl.can('admin', 'user:read:name');
// permission.granted => yes
await accessControl.can('admin', 'user:read:phoneNumber'); // permission.granted => no
Generate field grants dynamically, given a context. You can use async calls, if needed:
accessControl.grant('admin').scope('user:read')
.onDynamicFields(async ({admin, user}: Context) => {
const permissive = await myBackend.adminHasPermissionFromUser(admin, user);
if (permissive) {
return { '*': true };
} else {
return { 'id': true, 'userName': true, 'phoneNumber': true };
}
});
Object returned by AccessControlPlus#can
If permission granted this will be a string describing the scope granted.
If permission denied, this is set to an array of objects that contain
Tests whether permission was granted for the specified field. Accounts for wildcards and denied fields (!foo
) provided in .onFields
.
permission.field('foo') // => true or false
import AccessControlPlus from 'accesscontrol-plus';
function userIsResourceOwner({user, resource}) {
return user.id === resource.ownerId;
}
function userImpersonatesResourceOwner({user, resource}) {
return user.impersonationId === resource.ownerId;
}
function articleIsPublished({resource}) {
return resource.state === 'published';
}
const ac = new AccessControlPlus();
//
// 4 roles in this scenario: public, author, admin, superadmin
//
ac
// Define roles:
//
// PUBLIC
//
.deny('public') // start by disallowing the public access to everything
.scope('*:*')
.grant('public')
.scope('article:read')
.where(articleIsPublished)
.onFields('*', '!viewers') // allow all fields except viewers
//
// AUTHOR
//
.grant('author').inherits('public')
.resource('article')
.action('create')
// add a constraint - to include when creating the article:
.withConstraint(({user})=>({ ownerId: user.id }))
.action('read') // === .scope('article:read')
.where(userIsResourceOwner)
.action('update') // === .scope('article:update')
.where(userIsResourceOwner)
//
// ADMIN
//
.grant('admin').inherits('author')
.resource('article')
.action('read').where(userImpersonatesResourceOwner)
//
// SUPERADMIN
//
.grant('superadmin').inherits('admin')
.resource('user')
.action('*');
//
// The following are objects which are generated by your code
// during a request - users, resources, etc:
//
const user = { id: 1234 }; // determined by request authentication
const draft = { ownerId: 1234, state: 'draft', text: '...' }; // retrieved from db
const published = { ownerId: 1234, state: 'published', text: '...' }; // retrieved from db
const adminUser = { id: 999, impersonationId: 1234 };
const superAdmin = { id: 222 };
async function testPermissions {
let permission;
// public can read published articles
permission = await accessControl.can('public', 'article:read', { user: null, resource: published });
// permission.granted => truthy
// public can't read draft articles
permission = await accessControl.can('public', 'article:read', { user: null, resource: draft });
// permission.granted => falsy
// permission.denied = ['public:article:read:articleIsPublished']
// author can read their own draft article
permission = accessControl.can('author', 'article:read', { user, resource: draft });
// permission.granted => truthy
// auth can update their own article
permission = accessControl.can('user', 'article:update', { user: user, resource: draft });
// permission.granted => truthy
// admin cannot update an author's article, even if they are impersonating them
permission = accessControl.can('admin', 'article:update', { user: adminUser, resource: draft});
// permission.granted => falsy
// permision.denied = [ 'author:article:update:userIsResourceOwner' ]
// admin can read a draft article if they are impersonating the author
permission = accessControl.can('admin', 'article:read', { user: adminUser, resource: draft});
// permission.granted => truthy
// superadmin can do anything to user resources
permission = accessControl.can('superadmin', 'user:delete', { user: superAdmin, resource: user });
// permission.granted => truthy
}
FAQs
Rich access control in an easy to read syntax featuring roles with inheritance, dynamic attribute tests, and more
We found that accesscontrol-plus demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socket’s AI scanner detected the supply chain attack and flagged the malware.
Security News
CISA’s 2025 draft SBOM guidance adds new fields like hashes, licenses, and tool metadata to make software inventories more actionable.