bookshelf-shield
v2.2.2
Published
Access control list based authorization for bookshelf models
Downloads
11
Keywords
Readme
bookshelf-shield
Form a protective shield around your bookshelf models. This module adds ACL-based authorization, and a CRUD API to bookshelf models.
Dependencies
relations
As of right now, bookshelf-shield only can interact with the ACL module called relations Provides an intuitive interface for storing and querying Access Conrtol Lists. Relations is used to determine whether a user has been granted access to perform an action on the model.
ES6
This module utilizes ES6 features, including classes, arrow functions and Promises. As a result node 4.0.0+ is required.
Usage
- Set up your ACL
relations.define('study', {
PI: ['read_Study'],
siteAdmin: [
'read_Study',
'update_Study',
'create_Study',
'delete_Study'
]
});
- Set up you bookshelf models
const models = {
Study: bookshelf.Model.extend({tableName: 'mrs_studies'}),
//...
};
- Create a shieldConfig for each model (see configuration section for more)
const config = [
{
defaults: {
modelName: 'Study',
authKey: 'id',
aclContextName: 'study'
},
//optional action-specific configs here
},
//...
- Shields up!
const shield = require('bookshelf-shield');
shield({
config: config,
acl: relations,
models: models
});
API
Once a model has been shielded, you can interact with it using a standard CRUD API, rather than the traditional fetch, save, destroy
bookshelf API. This was implemented to more easily map to user's permissions.
- create
const user = { username: 'dylan' };
const widget = new Widget({ color: 'blue' });
widget.create(user).then((newWidget) => {
//new Widget successfully created
}).catch((error) => {
//handle Error
});
- read
const user = { username: 'dylan' };
const widget = new Widget({ id: '101' });
widget.read(user).then((newWidget) => {
//newWidget successfully read
}).catch((error) => {
//handle Error
});
- readAll
const user = { username: 'dylan' };
const widget = new Widget();
widget.query({color: 'blue'});
widget.readAll(user).then((newWidgets) => {
//widgets successfully read into newWidgets collection
}).catch((error) => {
//handle Error
});
- update (note: by default, read access is required to perform an update)
const user = { username: 'dylan' };
widget.set('color', 'red');
widget.update(user).then((newWidget) => {
//widget successfully updated
}).catch((error) => {
//handle Error
});
- delete (note: by default, read access is required to perform a delete)
const user = { username: 'dylan' };
const widget = new Widget({ id: '101' });
widget.delete(user).then((newWidget) => {
//widgets successfully deleted (newWidget should now be empty)
}).catch((error) => {
//handle Error
});
- bypass
const widget = new Widget({ id: '101' });
widget.bypass('fetch').then((newWidget) => {
//new Widget successfully created
}).catch((error) => {
//handle Error
});
Configuration
Each model to be shielded requires a config object. During initialization, these config objects should be provided as an array. Here is an example config object:
module.exports = {
defaults: { // These defaults will be applied to all CRUD access methods, unless overridden below.
modelName: 'Study', // The name of the model: must match the key associated with the model in the models object passed to shield init.
authKey: 'id', // The property that should be used for authorization.
aclContextName: 'study' // The name of the ACL (relations) context to be used
},
create: { //specifying any CRUD method allows you to override the defaults secified above
authKey: 'siteId', //alternative auth key to be used when evaluating create access
aclContextName: 'site',
method: function validateSiteAdmin(user) {
// this is a cusom authentication method that will be invoked instead of the generic method.
// `this` refers to the current instance of the bookshelf model
const siteId = this.get('siteId');
// data stored on the shield can be accessed through the current object's constructor (the bookshelf Model).
const Model = this.constructor;
const aclContext = Model.shield.acl.site;
const aclQuestion = 'can ' +
user.username +
' create_Study from ' +
siteId;
if (!siteId) {
return Promise.reject(
new Error('Study has no valide siteId')
);
}
return aclContext(aclQuestion).then(function checkAuth(allow) {
let errorMsg;
if (allow) {
return allow;
}
errorMsg =
user.username +
' cannot create studies in Site ' +
siteId;
throw new Error(errorMsg);
});
}
}
};
Because there are no configuration objects specified for read
, update
and delete
operations, those operations will be protected using the generic method (see below).
Generic Auth Method
Unless a custom method is specified in the Model's config, the following generic method will be applied:
// Note options is the config for the current Model and action
function genericAuthMethod(user) {
const authVal = this.get(options.authKey);
const aclQuestion = 'can ' +
user.username +
' ' +
permissionName +
' from ' +
authVal;
const aclContext = options.acl[aclContextName];
//TODO: optimize to cache perms instead of loading from redis
return aclContext(aclQuestion).then(function checkAuth(allow) {
let errorMsg;
if (allow) {
return allow;
}
errorMsg = [
user.username,
'cannot',
permissionName.replace('_', ' '),
'in',
options.authKey,
'`' + authVal + '`'
].join(' ');
throw new AuthError(errorMsg);
});
Examples
See test/integration/main.js
for a full example
Tests
Fully unit and integration tested
Contributing
Please follow the MRN Javascript Style Guide (forked from AirBnB). Use grunt lint
to check yo-self