UNPKG

fakeblock

Version:

Field-level ACL library for Node.js and NoSql document stores

191 lines (163 loc) 6.15 kB
/** /** * Copyright 2013 GetHuman LLC * Author: Jeff Whelpley * Date: 8/18/13 * * This module works off of a acl file that has all * the security aclurations for CRUD operations on * a given collection */ var _ = require('lodash'); var fieldFilter = require('./field.filter'); /** * Set the values for Fakeblock on a per ACL and per user basis * @param opts * @constructor */ var Fakeblock = function (opts) { opts = opts || {}; this.name = opts.name || 'unknown'; this.userId = opts.userId; this.userRole = opts.userRole || 'anonymous'; this.acl = opts.acl || {}; this.permissions = opts.permissions || []; }; // expose static properties Fakeblock.filter = fieldFilter; /** * Get a value from a specific acl * @param key * @returns {*} */ Fakeblock.prototype.getValue = function (key) { key = key || ''; var keyParts = key.split('.'); var pointer = this.acl; for (var i = 0; i < keyParts.length; i++) { if (pointer[keyParts[i]]) { pointer = pointer[keyParts[i]]; } else { return undefined; } } return pointer; }; /** * Check to see if a user has access to a method (i.e. create/find/update/remove) * * @param methodName * @returns {boolean} */ Fakeblock.prototype.canUserAccessMethod = function (methodName) { var accessList = this.getValue(methodName + '.access') || []; var role = this.userRole; var permissions = this.permissions; var hasAccess = accessList.indexOf(role) >= 0; // if don't have access and is a partner, check for permissions if (!hasAccess && role === 'partner') { for (var i = 0; i < permissions.length; i++) { if (accessList.indexOf('partner:' + permissions[i]) >= 0) { return true; } } } return hasAccess; }; /** * Check to see if a user is restricted by * @param data * @param methodName */ Fakeblock.prototype.checkValues = function (data, methodName) { var role = this.userRole; var name = this.name; var prefix = methodName + '.values.'; var restrictedFields = this.getValue(prefix + 'restricted.' + role); var allowedFields = this.getValue(prefix + 'allowed.' + role); if (!data) { return; } // if no restrictions/allowed with the given role, then look for the allroles if (!restrictedFields) { restrictedFields = this.getValue(prefix + 'restricted.allroles'); } if (!allowedFields) { allowedFields = this.getValue(prefix + 'allowed.allroles'); } if (restrictedFields) { _.each(restrictedFields, function (restrictedValues, key) { if (data[key] && restrictedValues.indexOf(data[key]) >= 0) { throw new Error('Value [' + data[key] + '] not valid for ' + name + ' ' + methodName + ' with role ' + role); } }); } else if (allowedFields) { _.each(allowedFields, function (validValues, key) { if (data[key] && validValues.indexOf(data[key]) < 0) { throw new Error('Value ' + data[key] + ' not valid. Must be ' + JSON.stringify(validValues) + ' for role ' + role); } }); } }; /** * This is the primary function for filtering data. If the user supplies data that * conflicts with the ACL, an error is throw. If no data is given and there is * a default in the ACL, the default is used. * * @param data The data that will be filtered * @param methodName [create|find|update|remove] * @param dataType [fields|select|where|sort] Default is 'fields' * @returns {{}} The filtered data is returned */ Fakeblock.prototype.applyAcl = function (data, methodName, dataType) { data = data || {}; dataType = dataType || 'fields'; if (!this.acl || !this.userRole || !methodName) { throw new Error('Fakeblock missing required input: role=' + this.userRole + ' method=' + methodName); } var prefix = methodName + '.' + dataType + '.'; var onlyMineRoles = this.getValue(prefix + 'onlyMine.roles') || []; var onlyMineField = this.getValue(prefix + 'onlyMine.field') || 'createUserId'; var defaultFields = this.getValue(prefix + 'default.' + this.userRole); var defaultFieldsAll = this.getValue(prefix + 'default.allroles'); var restrictedFields = this.getValue(prefix + 'restricted.' + this.userRole); var allowedFields = this.getValue(prefix + 'allowed.' + this.userRole); // if the dataType is onlyMine, then we need to make sure the restriction is there if (onlyMineRoles.indexOf(this.userRole) >= 0) { if (!this.userId) { throw new Error('onlyMine requires userId set in Fakeblock constructor'); } else { data[onlyMineField] = this.userId; } } // if there is no data, return one of the defaults if (!data || (_.isObject(data) && _.isEmpty(data))) { return defaultFields || defaultFieldsAll || data; } // if no restrictions/allowed with the given role, then look for the allroles if (!_.isArray(restrictedFields)) { restrictedFields = this.getValue(prefix + 'restricted.allroles'); } if (!_.isArray(allowedFields)) { allowedFields = this.getValue(prefix + 'allowed.allroles'); } // create a copy of the data object before we start messing with it var dataCopy = JSON.parse(JSON.stringify(data)); data = JSON.parse(JSON.stringify(data)); data = fieldFilter.applyFilter(data, restrictedFields, allowedFields); // if the data is not equal, then something was filtered out, so error if (!_.isEqual(data, dataCopy)) { throw new Error('Fakeblock blocked some data for name=' + this.name + ' userId=' + this.userId + ' role=' + this.userRole + ' method=' + methodName + ' dataType=' + dataType + ' original=' + JSON.stringify(dataCopy) + ' filtered=' + JSON.stringify(data)); } // else if we get here, we are all good return data; }; module.exports = Fakeblock;