Nebulous Puffy Marshmallows

    bookshelf-shield

    2.2.2 • Public • Published

    Shield

    bookshelf-shield

    Form a protective shield around your bookshelf models. This module adds ACL-based authorization, and a CRUD API to bookshelf models.

    Codeship Status for MRN-Code/bookshelf-shield

    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

    1. Set up your ACL
    relations.define('study', {
        PI: ['read_Study'],
        siteAdmin: [
            'read_Study',
            'update_Study',
            'create_Study',
            'delete_Study'
        ]
    });
    1. Set up you bookshelf models
    const models = {
        Study: bookshelf.Model.extend({tableName: 'mrs_studies'}),
        //...
    };
    1. 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
        },
        //...
    1. 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.

    1. create
        const user = { username: 'dylan' };
        const widget = new Widget({ color: 'blue' });
        widget.create(user).then((newWidget) => {
            //new Widget successfully created
        }).catch((error) => {
            //handle Error
        });
    1. read
        const user = { username: 'dylan' };
        const widget = new Widget({ id: '101' });
        widget.read(user).then((newWidget) => {
            //newWidget successfully read
        }).catch((error) => {
            //handle Error
        });
    1. 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
        });
    1. 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
        });
    1. 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
        });
    1. 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

    Keywords

    none

    Install

    npm i bookshelf-shield

    DownloadsWeekly Downloads

    24

    Version

    2.2.2

    License

    MIT

    Unpacked Size

    1.16 MB

    Total Files

    62

    Last publish

    Collaborators

    • cdaringe
    • dlandis
    • dylancwood
    • rsskmr
    • ruwang
    • swashcap