@yanfoo/rbac-a
v2.2.4
Published
RBAC-A/ABAC dynamic plugin roles implementation
Downloads
19
Maintainers
Readme
RBAC-A (ABAC)
Role Based Access Control with Attributes and dynamic plugin roles implementation. This module follows the NIST RBAC model and offer a flexible solution to allow or restrict user operations.
Introduction
In an RBAC system, permissions are assigned to roles, not users. Therefore, roles act as a ternary relation between permissions and users. Permissions are static, defined in the applications. Roles, on the other hand, are dynamic and can be defined from an application interface (API), or user interface (UI), and saved in a datastore.
This module is not dependent on an authentication, a user session, or a datastore system. The relation between the
user and it's roles are specified by a Provider
. It is the application's responsibility to implement such provider.
See providers for more information.
Rules are applied in consideration with the roles hierarchy. Top level roles always have priority over inherited
roles. When validating users against given permissions, the best role priority matching the permissions is returned.
Therefore, "allowed" users will always resolve with a positive integer, and "restricted" users will always resolve
with a non-numeric value (i.e. NaN
). See usage for more information, or
how to restrict users with this module.
Usage
import RBAC, { JsonRBACProvider, RBACValidationError } from '@yanfoo/rbac-a';
import permissionData from './permission-data.json';
/*
permissionData = {
roles: {
editor: {
permissions: [
{ can: 'create' },
{ can: 'edit' } // no restriction from attributes
],
inherited: [ 'contributor' ]
},
contributor: {
permissions: [
{ can: 'edit', when: { timeInterval: { from: "8:00", to: "17:00" } } }
],
inherited: [ 'authenticatedUser' ]
},
authenticatedUser: {
permissions: [
{ can: 'login' }
]
}
},
users: {
1: ['editor'],
2: ['contributor'],
3: ['authenticatedUser']
}
}
*/
const rbac = new RBAC({
provider: new JsonRBACProvider(permissionData), // mandatory
attributes: { // optional
timeInterval: ({ from, to }) => checkCurrentTimeBetween(from, to)
},
checkOptions: {
onError: err => { // optional
if (err instanceof RBACValidationError) {
console.error('Error while checking %s with user roles %s',
JSON.stringify(err.user),
JSON.stringify(err.role),
err
);
} else {
console.error(err);
}
},
// onSuccess: (role, level) => ...
// ignoreMissingAttributes: false
}
});
// check permission for user #3
await rbac.check(3, 'login');
// -> 0 (the access level)
// check double permissions for user #2
await rbac.check(2, 'create, edit');
// -> NaN because a contributor can edit, but not create
// check double permissions for user #1 with some restrictions
await rbac.check(2, 'edit');
// -> 0 only if current time is between 8am and 5pm
The method rbac.check
will resolve with a positive numeric value if the given user has access to the specified
roles, or NaN
otherwise.
If for ever reason the validation should fail, either at the provider level, or during an attribute validation,
then the method will return NaN
.
Definitions
Users
When invoking rbac.check
, the argument user
is an arbitrary value that is only checked within the specified
providers. For this reason, the value should normally be numeric or string, however it may very well be an
Object
. Whatever the value, it should be considered immutable at all times. Check the provider implementation
for more information.
Roles
A role is an organizational unit defining a group of permissions assignable to users. Roles consitute de bridge between actual permissions and users. Roles are hierarchichal, meaning that they may have a child to parent relationship.
Attributes
Attributes are used to conditionally authorize certain roles' permissions. For example, an attribute that would dynamically check the user's device and enable the permission depending on the device currently in use. This is useful, for example, to grant users with login permissions only at very specific times depending on their roles.
Since attributes are evaluated, they are provided as option parameters in the role definitions, and as asynchronous callbacks at the provider level. This not only allows making roles persistent in a database or a cache (i.e. preventing code injection), but also makes the same attributes reusable by various roles with different parameters.
const rbac = new RBAC({
provider,
attributes: {
syncAttrib: options => { /* ... */ },
asyncAttrib: async options => { /* ... */ }
}
});
The above defines two attributes, called syncAttrib
and asyncAttrib
respectively.
For more control over attribute options, or if attributes should be stateful, consider using an implementation of
RBACAttribute
. For example :
import RBAC, { RBACAttribute } from '@yanfoo/rbac-a';
import users from '/path/to/models/users';
import permissionData from './permission-data.json';
/* permissionData =
{
roles: {
authenticatedUser: {
permissions: [{ can: 'login', when: { userActive: true }}]
}
}
users: {
1: ['authenticatedUser']
}
}
*/
class UserActiveAttribute extends RBACAttribute {
async check(isActive, user) {
const userActive = await users.isActive(user);
return userActive === isActive;
}
}
const rbac = new RBAC({
provider: new JsonRBACProvider(permissionData),
attributes: {
userActive: new UserActiveAttribute()
},
...
});
await rbac.check(1, 'login');
// will call the attribute 'userActive' with the arguments `true` and `1`
When multiple attributes are specified for a role permission, all must be valid for the permission to be valid.
Data relations
+--------+ 1. n. +--------+ 1. n. +--------------+
| User |---------| Role |---------| Permission |
+--------+ +--------+ +--------------+
1. | n.
+------[ Attribute ]
API
RBAC
interface RBACOptions {
provider:RBACProvider,
attributes:Object<String,Function> | Map<String,Function>,
options:Object = {}
}
interface RBACProviderOptions {}
interface RBACCheckSuccessEvent {
user:Any,
role:String,
level:Number
}
interface RBACCheckOptions {
ignoreMissingAttributes:Boolean,
providerOptions:RBACProviderOptions,
onError:Function(err:RBACValidationError),
onSuccess:Function(e:RBACCheckSuccessEvent)
}
interface Role {
role:String
level:Number
permissions:Array<Permission> | Set<Permission>
}
interface Permission {
// the permission strings
can:String | Array<String> | Set<String>
// the attributes
when:Map<String,Any> | Object<String,Any>
}
class RBAC {
/**
* Create a new instance of RBAC
*/
constructor(options:RBACOptions)
/**
* Validate the given user
*/
async check(
user:Any,
permissions:String | Array<String> | Set<String>,
options:RBACCheckOptions = {}
):Promise<Number|NaN>
}
interface RBACProvider {
/**
* Retrive all permission rules to validate users against
**/
async getRoles(user:Any, options:RBACProviderOptions):Promise<Array<Role> | Set<Role>>
}
Custom RBACProvider
While the above example might work well when roles are static, it might be necessary for an application
to implement their own providers, for example, when roles are managed externally. In the JsonRBACProvider
implementation, roles are specified separate from users, so they aren't immediately compatible with how
the RBAC
instance consume them. The RBACProvider
is responsible to provider only the roles for a given
user.
Consider, in the example below, the input and output of the provider :
const provider = new JsonRBACProvider({
roles: {
editor: {
permissions: [
{ can: 'create' },
{ can: 'edit' } // no restriction from attributes
],
inherited: [ 'contributor' ]
},
contributor: {
permissions: [
{ can: 'edit', when: { timeInterval: { from: "8:00", to: "17:00" } } }
],
inherited: [ 'authenticatedUser' ]
},
authenticatedUser: {
permissions: [
{ can: 'login' }
]
}
},
users: {
1: ['editor'],
2: ['contributor'],
3: ['authenticatedUser']
}
});
const userRoles1 = await provider.getRoles(1);
/* [
{ role: 'editor', level: 0, permissions: [
{ can: 'create' },
{ can: 'edit' }
]},
{ role: 'contributor', level: 1, permissions: [
{ can: 'edit', when: { timeInterval: { from: "8:00", to: "17:00" } } }
]},
{ role: 'authenticatedUser', level: 2, permissions: [
{ can: 'login' }
]}
] */
const userRoles2 = await provider.getRoles(2);
/* [
{ role: 'contributor', level: 0, permissions: [
{ can: 'edit', when: { timeInterval: { from: "8:00", to: "17:00" } } }
]},
{ role: 'authenticatedUser', level: 1, permissions: [
{ can: 'login' }
]}
] */
const userRoles4 = await provider.getRoles(4); // no known user
/* [] */
Each user has their own level
value per role because, even if all belong to the same roles, not all
have the same associated role hierarchy; some users may have more privileges within the same roles. In
the above example, user 1
is not restricted to the edit
permission compared to user 2
, who has a
time restriction, allowed only between "8:00"
and "17:00"
.
The following would be a custom RBACProvider
fetching user roles from a database :
class RemoteRBACProvider extends RBACProvider {
constructor(db) {
this.db = db;
}
// the server is responsible to return Array<Role>
async getRoles(user/*, options */) {
return user?.id ? db.fetchRoles(user.id) : [];
}
}
If if the system does not need a hierarchy, and can cache roles :
// NOTE : the following code is for illustration only, it is neither optimal or ideal!
class RemoteRBACProvider extends RBACProvider {
users = new Map();
roles = new Map();
constructor(db) {
this.db = db;
}
// the server is responsible to return Array<Role>
async getRoles(user/*, options */) {
const result = [];
let userRoles, role;
if (users.has(user?.id)) {
userRoles = users.get(user.id);
} else if (user?.id) {
userRoles = await db.fetchUserRoles(user.id) || [];
users.set(user.id, userRoles);
} else {
userRoles = [];
}
// userRoles = ['role1', 'role2', ...];
for (const userRole of userRoles) {
if (roles.has(userRole)) {
role = roles.get(userRole);
} else {
role = await db.fetchRole(userRole) || {}
roles.set(userRole, role);
}
// role = { permissions: ['foo', 'bar', ...] }
// NOTE : we omit the property `level`, it will default to 0 by the RBAC instance
result.push({ role: userRole, permissions: role.permissions?.map(can => ({ can })) || [] });
}
return result;
// userRoles = [{ role:'role1', permissions:[{ can:'foo' },{ can:'bar'}], ... }]
}
}
TL;DR;
Check out the source code for more implementation information.
Contribution
All contributions welcome! Every PR must be accompanied by their associated unit tests!
License
The MIT License (MIT)
Copyright (c) 2015 Mind2Soft [email protected]
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.