From 0cd6a98435b8e9a08106e12123ec79590d80a7f1 Mon Sep 17 00:00:00 2001 From: Mario Alberto Medina Nussbaum Date: Sat, 10 Dec 2016 06:41:45 -0500 Subject: [PATCH 1/5] Dist agregado --- .gitignore | 2 +- dist/api/controllers/ModelController.js | 10 + dist/api/controllers/PermissionController.js | 10 + dist/api/controllers/RoleController.js | 10 + dist/api/hooks/permissions/index.js | 155 +++++++ dist/api/models/Criteria.js | 25 ++ dist/api/models/Model.js | 35 ++ dist/api/models/Passport.js | 22 + dist/api/models/Permission.js | 98 ++++ dist/api/models/RequestLog.js | 40 ++ dist/api/models/Role.js | 38 ++ dist/api/models/SecurityLog.js | 21 + dist/api/models/User.js | 47 ++ dist/api/policies/AuditPolicy.js | 34 ++ dist/api/policies/CriteriaPolicy.js | 126 ++++++ dist/api/policies/ModelPolicy.js | 44 ++ dist/api/policies/OwnerPolicy.js | 34 ++ dist/api/policies/PermissionPolicy.js | 45 ++ dist/api/policies/RolePolicy.js | 62 +++ dist/api/services/ModelService.js | 27 ++ dist/api/services/PermissionService.js | 450 +++++++++++++++++++ dist/config/env/testing.js | 37 ++ dist/config/fixtures/model.js | 28 ++ dist/config/fixtures/permission.js | 91 ++++ dist/config/fixtures/role.js | 10 + dist/config/fixtures/user.js | 37 ++ dist/config/permissions.js | 11 + dist/config/policies.js | 58 +++ dist/config/session.js | 19 + 29 files changed, 1625 insertions(+), 1 deletion(-) create mode 100644 dist/api/controllers/ModelController.js create mode 100644 dist/api/controllers/PermissionController.js create mode 100644 dist/api/controllers/RoleController.js create mode 100644 dist/api/hooks/permissions/index.js create mode 100644 dist/api/models/Criteria.js create mode 100644 dist/api/models/Model.js create mode 100644 dist/api/models/Passport.js create mode 100644 dist/api/models/Permission.js create mode 100644 dist/api/models/RequestLog.js create mode 100644 dist/api/models/Role.js create mode 100644 dist/api/models/SecurityLog.js create mode 100644 dist/api/models/User.js create mode 100644 dist/api/policies/AuditPolicy.js create mode 100644 dist/api/policies/CriteriaPolicy.js create mode 100644 dist/api/policies/ModelPolicy.js create mode 100644 dist/api/policies/OwnerPolicy.js create mode 100644 dist/api/policies/PermissionPolicy.js create mode 100644 dist/api/policies/RolePolicy.js create mode 100644 dist/api/services/ModelService.js create mode 100644 dist/api/services/PermissionService.js create mode 100644 dist/config/env/testing.js create mode 100644 dist/config/fixtures/model.js create mode 100644 dist/config/fixtures/permission.js create mode 100644 dist/config/fixtures/role.js create mode 100644 dist/config/fixtures/user.js create mode 100644 dist/config/permissions.js create mode 100644 dist/config/policies.js create mode 100644 dist/config/session.js diff --git a/.gitignore b/.gitignore index 2ad0fad..b52a393 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ #api/policies/basicAuth.js #api/policies/sessionAuth.js #api/services/protocols/basic.js -dist/ +#dist/ .tmp *.sw* diff --git a/dist/api/controllers/ModelController.js b/dist/api/controllers/ModelController.js new file mode 100644 index 0000000..d490222 --- /dev/null +++ b/dist/api/controllers/ModelController.js @@ -0,0 +1,10 @@ +/** + * ModelController + * + * @description :: Server-side logic for managing models + * @help :: See http://links.sailsjs.org/docs/controllers + */ + +"use strict"; + +module.exports = {}; \ No newline at end of file diff --git a/dist/api/controllers/PermissionController.js b/dist/api/controllers/PermissionController.js new file mode 100644 index 0000000..18e89b2 --- /dev/null +++ b/dist/api/controllers/PermissionController.js @@ -0,0 +1,10 @@ +/** + * PermissionController + * + * @description :: Server-side logic for managing permissions + * @help :: See http://links.sailsjs.org/docs/controllers + */ + +"use strict"; + +module.exports = {}; \ No newline at end of file diff --git a/dist/api/controllers/RoleController.js b/dist/api/controllers/RoleController.js new file mode 100644 index 0000000..0b6c426 --- /dev/null +++ b/dist/api/controllers/RoleController.js @@ -0,0 +1,10 @@ +/** + * RoleController + * + * @description :: Server-side logic for managing roles + * @help :: See http://links.sailsjs.org/docs/controllers + */ + +"use strict"; + +module.exports = {}; \ No newline at end of file diff --git a/dist/api/hooks/permissions/index.js b/dist/api/hooks/permissions/index.js new file mode 100644 index 0000000..3eee844 --- /dev/null +++ b/dist/api/hooks/permissions/index.js @@ -0,0 +1,155 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _marlinspike = require('marlinspike'); + +var _marlinspike2 = _interopRequireDefault(_marlinspike); + +var permissionPolicies = ['passport', 'sessionAuth', 'ModelPolicy', 'OwnerPolicy', 'PermissionPolicy', 'RolePolicy']; + +var Permissions = (function (_Marlinspike) { + _inherits(Permissions, _Marlinspike); + + function Permissions(sails) { + _classCallCheck(this, Permissions); + + _get(Object.getPrototypeOf(Permissions.prototype), 'constructor', this).call(this, sails, module); + } + + _createClass(Permissions, [{ + key: 'configure', + value: function configure() { + if (!_lodash2['default'].isObject(sails.config.permissions)) sails.config.permissions = {}; + + /** + * Local cache of Model name -> id mappings to avoid excessive database lookups. + */ + this.sails.config.blueprints.populate = false; + } + }, { + key: 'initialize', + value: function initialize(next) { + var _this = this; + + var config = this.sails.config.permissions; + + this.installModelOwnership(); + this.sails.after(config.afterEvent, function () { + if (!_this.validateDependencies()) { + _this.sails.log.error('Cannot find sails-auth hook. Did you "npm install sails-auth --save"?'); + _this.sails.log.error('Please see README for installation instructions: https://github.com/tjwebb/sails-permissions'); + return _this.sails.lower(); + } + + if (!_this.validatePolicyConfig()) { + _this.sails.log.warn('One or more required policies are missing.'); + _this.sails.log.warn('Please see README for installation instructions: https://github.com/tjwebb/sails-permissions'); + } + }); + + this.sails.after('hook:orm:loaded', function () { + sails.models.model.count().then(function (count) { + if (count === _lodash2['default'].keys(_this.sails.models).length) return next(); + + return _this.initializeFixtures().then(function () { + next(); + }); + })['catch'](function (error) { + _this.sails.log.error(error); + next(error); + }); + }); + } + }, { + key: 'validatePolicyConfig', + value: function validatePolicyConfig() { + var policies = this.sails.config.policies; + return _lodash2['default'].all([_lodash2['default'].isArray(policies['*']), _lodash2['default'].intersection(permissionPolicies, policies['*']).length === permissionPolicies.length, policies.AuthController && _lodash2['default'].contains(policies.AuthController['*'], 'passport')]); + } + }, { + key: 'installModelOwnership', + value: function installModelOwnership() { + var models = this.sails.models; + if (this.sails.config.models.autoCreatedBy === false) return; + + _lodash2['default'].each(models, function (model) { + if (model.autoCreatedBy === false) return; + + _lodash2['default'].defaults(model.attributes, { + createdBy: { + model: 'User', + index: true + }, + owner: { + model: 'User', + index: true + } + }); + }); + } + + /** + * Install the application. Sets up default Roles, Users, Models, and + * Permissions, and creates an admin user. + */ + }, { + key: 'initializeFixtures', + value: function initializeFixtures() { + var _this2 = this; + + var fixturesPath = _path2['default'].resolve(__dirname, '../../../config/fixtures/'); + return require(_path2['default'].resolve(fixturesPath, 'model')).createModels().then(function (models) { + _this2.models = models; + _this2.sails.hooks.permissions._modelCache = _lodash2['default'].indexBy(models, 'identity'); + + return require(_path2['default'].resolve(fixturesPath, 'role')).create(); + }).then(function (roles) { + _this2.roles = roles; + var userModel = _lodash2['default'].find(_this2.models, { name: 'User' }); + return require(_path2['default'].resolve(fixturesPath, 'user')).create(_this2.roles, userModel); + }).then(function () { + return sails.models.user.findOne({ email: _this2.sails.config.permissions.adminEmail }); + }).then(function (user) { + _this2.sails.log('sails-permissions: created admin user:', user); + user.createdBy = user.id; + user.owner = user.id; + return user.save(); + }).then(function (admin) { + return require(_path2['default'].resolve(fixturesPath, 'permission')).create(_this2.roles, _this2.models, admin, _this2.sails.config.permissions); + })['catch'](function (error) { + _this2.sails.log.error(error); + }); + } + }, { + key: 'validateDependencies', + value: function validateDependencies() { + return !!this.sails.hooks.auth; + } + }]); + + return Permissions; +})(_marlinspike2['default']); + +exports['default'] = _marlinspike2['default'].createSailsHook(Permissions); +module.exports = exports['default']; \ No newline at end of file diff --git a/dist/api/models/Criteria.js b/dist/api/models/Criteria.js new file mode 100644 index 0000000..5ba1f0b --- /dev/null +++ b/dist/api/models/Criteria.js @@ -0,0 +1,25 @@ +/** + * @module Criteria + * + * @description + * Criteria specify limits on a permission, via a 'where' clause and an attribute blacklist. + * For the blacklist, if the request action is update or create, and there is a blacklisted attribute in the request, + * the request will fail. If the request action is read, the blacklisted attributes will be filtered. + * The blacklist is not relevant for delete requests. + * A where clause uses waterline query syntax to determine if a permission is allowed, ie where: { id: { '>': 5 } } + */ +'use strict'; + +module.exports = { + autoCreatedBy: false, + + description: 'Specifies more granular limits on a permission', + + attributes: { + where: 'json', + blacklist: 'array', + permission: { + model: 'Permission' + } + } +}; \ No newline at end of file diff --git a/dist/api/models/Model.js b/dist/api/models/Model.js new file mode 100644 index 0000000..3f21d9a --- /dev/null +++ b/dist/api/models/Model.js @@ -0,0 +1,35 @@ +/** + * @module Model + * + * @description + * Abstract representation of a Waterline Model. + */ +'use strict'; + +module.exports = { + description: 'Represents a Waterline collection that a User can create, query, etc.', + + autoPK: true, + autoCreatedBy: false, + autoCreatedAt: false, + autoUpdatedAt: false, + + attributes: { + name: { + type: 'string', + notNull: true, + unique: true + }, + identity: { + type: 'string', + notNull: true + }, + attributes: { + type: 'json' + }, + permissions: { + collection: 'Permission', + via: 'model' + } + } +}; \ No newline at end of file diff --git a/dist/api/models/Passport.js b/dist/api/models/Passport.js new file mode 100644 index 0000000..e0dda3e --- /dev/null +++ b/dist/api/models/Passport.js @@ -0,0 +1,22 @@ +'use strict'; + +var _ = require('lodash'); +var _super = require('sails-auth/api/models/Passport'); + +_.merge(exports, _super); +_.merge(exports, { + + autoCreatedBy: false + + // Extend with custom logic here by adding additional fields, methods, etc. + + /** + * For example: + * + * foo: function (bar) { + * bar.x = 1; + * bar.y = 2; + * return _super.foo(bar); + * } + */ +}); \ No newline at end of file diff --git a/dist/api/models/Permission.js b/dist/api/models/Permission.js new file mode 100644 index 0000000..35df9ea --- /dev/null +++ b/dist/api/models/Permission.js @@ -0,0 +1,98 @@ +/** + * @module Permission + * + * @description + * The actions a Role is granted on a particular Model and its attributes + */ +'use strict'; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +module.exports = { + autoCreatedBy: false, + + description: ['Defines a particular `action` that a `Role` can perform on a `Model`.', 'A `User` can perform an `action` on a `Model` by having a `Role` which', 'grants the necessary `Permission`.'].join(' '), + + attributes: { + + /** + * The Model that this Permission applies to. + */ + model: { + model: 'Model', + required: true + }, + + action: { + type: 'string', + index: true, + notNull: true, + /** + * TODO remove enum and support permissions based on all controller + * actions, including custom ones + */ + 'enum': ['create', 'read', 'update', 'delete'] + }, + + relation: { + type: 'string', + 'enum': ['role', 'owner', 'user'], + defaultsTo: 'role', + index: true + }, + + /** + * The Role to which this Permission grants create, read, update, and/or + * delete privileges. + */ + role: { + model: 'Role' + }, + + // Validate manually + //required: true + /** + * The User to which this Permission grants create, read, update, and/or + * delete privileges. + */ + user: { + model: 'User' + // Validate manually + }, + + /** + * A list of criteria. If any of the criteria match the request, the action is allowed. + * If no criteria are specified, it is ignored altogether. + */ + criteria: { + collection: 'Criteria', + via: 'permission' + } + }, + + afterValidate: [function validateOwnerCreateTautology(permission, next) { + if (permission.relation == 'owner' && permission.action == 'create') { + next(new Error('Creating a Permission with relation=owner and action=create is tautological')); + } + + if (permission.action === 'delete' && _lodash2['default'].filter(permission.criteria, function (criteria) { + return !_lodash2['default'].isEmpty(criteria.blacklist); + }).length) { + next(new Error('Creating a Permission with an attribute blacklist is not allowed when action=delete')); + } + + if (permission.relation == 'user' && permission.user === "") { + next(new Error('A Permission with relation user MUST have the user attribute set')); + } + + if (permission.relation == 'role' && permission.role === "") { + next(new Error('A Permission with relation role MUST have the role attribute set')); + } + + next(); + }] +}; \ No newline at end of file diff --git a/dist/api/models/RequestLog.js b/dist/api/models/RequestLog.js new file mode 100644 index 0000000..579c289 --- /dev/null +++ b/dist/api/models/RequestLog.js @@ -0,0 +1,40 @@ +/** +* RequestLog.js +* +* @description :: TODO: You might write a short summary of how this model works and what it represents here. +* @docs :: http://sailsjs.org/#!documentation/models +*/ + +'use strict'; + +module.exports = { + autoPK: false, + autoCreatedBy: false, + autoUpdatedAt: false, + + attributes: { + id: { + type: 'string', + primaryKey: true + }, + ipAddress: { + type: 'string' + }, + method: { + type: 'string' + }, + url: { + type: 'string', + url: true + }, + body: { + type: 'json' + }, + user: { + model: 'User' + }, + model: { + type: 'string' + } + } +}; \ No newline at end of file diff --git a/dist/api/models/Role.js b/dist/api/models/Role.js new file mode 100644 index 0000000..ce114e4 --- /dev/null +++ b/dist/api/models/Role.js @@ -0,0 +1,38 @@ +/** + * @module Role + * + * @description + * Roles endow Users with Permissions. Exposes Postgres-like API for + * resolving granted Permissions for a User. + * + * @see + */ +'use strict'; + +module.exports = { + autoCreatedBy: false, + + description: 'Confers `Permission` to `User`', + + attributes: { + name: { + type: 'string', + index: true, + notNull: true, + unique: true + }, + users: { + collection: 'User', + via: 'roles' + }, + active: { + type: 'boolean', + defaultsTo: true, + index: true + }, + permissions: { + collection: 'Permission', + via: 'role' + } + } +}; \ No newline at end of file diff --git a/dist/api/models/SecurityLog.js b/dist/api/models/SecurityLog.js new file mode 100644 index 0000000..40562e7 --- /dev/null +++ b/dist/api/models/SecurityLog.js @@ -0,0 +1,21 @@ +/** +* SecurityLog.js +* +* @description :: TODO: You might write a short summary of how this model works and what it represents here. +* @docs :: http://sailsjs.org/#!documentation/models +*/ + +'use strict'; + +module.exports = { + autoPK: false, + autoUpdatedAt: false, + autoCreatedAt: false, + + attributes: { + request: { + model: 'RequestLog', + primaryKey: true + } + } +}; \ No newline at end of file diff --git a/dist/api/models/User.js b/dist/api/models/User.js new file mode 100644 index 0000000..aca057d --- /dev/null +++ b/dist/api/models/User.js @@ -0,0 +1,47 @@ +'use strict'; + +var _ = require('lodash'); +var _super = require('sails-auth/api/models/User'); + +_.merge(exports, _super); +_.merge(exports, { + attributes: { + roles: { + collection: 'Role', + via: 'users', + dominant: true + }, + permissions: { + collection: "Permission", + via: "user" + } + }, + + /** + * Attach default Role to a new User + */ + afterCreate: [function setOwner(user, next) { + sails.log.verbose('User.afterCreate.setOwner', user); + User.update({ id: user.id }, { owner: user.id }).then(function (user) { + next(); + })['catch'](function (e) { + sails.log.error(e); + next(e); + }); + }, function attachDefaultRole(user, next) { + sails.log('User.afterCreate.attachDefaultRole', user); + User.findOne(user.id).populate('roles').then(function (_user) { + user = _user; + return Role.findOne({ name: 'registered' }); + }).then(function (role) { + user.roles.add(role.id); + return user.save(); + }).then(function (updatedUser) { + sails.log.silly('role "registered" attached to user', user.username); + next(); + })['catch'](function (e) { + sails.log.error(e); + next(e); + }); + }] +}); \ No newline at end of file diff --git a/dist/api/policies/AuditPolicy.js b/dist/api/policies/AuditPolicy.js new file mode 100644 index 0000000..2110218 --- /dev/null +++ b/dist/api/policies/AuditPolicy.js @@ -0,0 +1,34 @@ +'use strict'; + +var fnv = require('fnv-plus'); +var _ = require('lodash'); +var url = require('url'); + +module.exports = function (req, res, next) { + var ipAddress = req.headers['x-forwarded-for'] || req.connection && req.connection.remoteAddress; + req.requestId = fnv.hash(new Date().valueOf() + ipAddress, 128).str(); + + sails.models.requestlog.create({ + id: req.requestId, + ipAddress: ipAddress, + url: sanitizeRequestUrl(req), + method: req.method, + body: _.omit(req.body, 'password'), + model: req.options.modelIdentity, + user: (req.user || {}).id + }).exec(_.identity); + + // persist RequestLog entry in the background; continue immediately + next(); +}; + +function sanitizeRequestUrl(req) { + var requestUrl = url.format({ + protocol: req.protocol, + host: req.host || sails.getHost(), + pathname: req.originalUrl || req.url, + query: req.query + }); + + return requestUrl.replace(/(password=).*?(&|$)/ig, '$1$2'); +} \ No newline at end of file diff --git a/dist/api/policies/CriteriaPolicy.js b/dist/api/policies/CriteriaPolicy.js new file mode 100644 index 0000000..e98692e --- /dev/null +++ b/dist/api/policies/CriteriaPolicy.js @@ -0,0 +1,126 @@ +/** + * CriteriaPolicy + * @depends PermissionPolicy + * + * Verify that the User fulfills permission 'where' conditions and attribute blacklist restrictions + */ +'use strict'; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var wlFilter = require('waterline-criteria'); + +module.exports = function (req, res, next) { + var permissions = req.permissions; + + if (_lodash2['default'].isEmpty(permissions)) { + return next(); + } + + var action = PermissionService.getMethod(req.method); + + var body = req.body || req.query; + + // if we are creating, we don't need to query the db, just check the where clause vs the passed in data + if (action === 'create') { + if (!PermissionService.hasPassingCriteria(body, permissions, body)) { + return res.send(403, { + error: 'Can\'t create this object, because of failing where clause' + }); + } + return next(); + } + + // set up response filters if we are not mutating an existing object + if (!_lodash2['default'].contains(['update', 'delete'], action)) { + + // get all of the where clauses and blacklists into one flat array + // if a permission has no criteria then it is always true + var criteria = _lodash2['default'].compact(_lodash2['default'].flatten(_lodash2['default'].map(_lodash2['default'].pluck(permissions, 'criteria'), function (c) { + if (c.length == 0) { + return [{ where: {} }]; + } + return c; + }))); + + if (criteria.length) { + bindResponsePolicy(req, res, criteria); + } + return next(); + } + + PermissionService.findTargetObjects(req).then(function (objects) { + + // attributes are not important for a delete request + if (action === 'delete') { + body = undefined; + } + + if (!PermissionService.hasPassingCriteria(objects, permissions, body, req.user.id)) { + return res.send(403, { + error: 'Can\'t ' + action + ', because of failing where clause or attribute permissions' + }); + } + + next(); + })['catch'](next); +}; + +function bindResponsePolicy(req, res, criteria) { + res._ok = res.ok; + + res.ok = _lodash2['default'].bind(responsePolicy, { + req: req, + res: res + }, criteria); +} + +function responsePolicy(criteria, _data, options) { + var req = this.req; + var res = this.res; + var user = req.owner; + var method = PermissionService.getMethod(req); + var isResponseArray = _lodash2['default'].isArray(_data); + + var data = isResponseArray ? _data : [_data]; + + // remove undefined, since that is invalid input for waterline-criteria + data = data.filter(function (item) { + return item !== undefined; + }); + + var permitted = data.reduce(function (memo, item) { + criteria.some(function (crit) { + var filtered = wlFilter([item], { + where: { + or: [crit.where || {}] + } + }).results; + + if (filtered.length) { + + if (crit.blacklist && crit.blacklist.length) { + crit.blacklist.forEach(function (term) { + delete item[term]; + }); + } + memo.push(item); + return true; + } + }); + return memo; + }, []); + + if (isResponseArray) { + return res._ok(permitted, options); + } else if (permitted.length === 0) { + sails.log.silly('permitted.length === 0'); + return res.send(404); + } else { + res._ok(permitted[0], options); + } +} \ No newline at end of file diff --git a/dist/api/policies/ModelPolicy.js b/dist/api/policies/ModelPolicy.js new file mode 100644 index 0000000..8ff25a9 --- /dev/null +++ b/dist/api/policies/ModelPolicy.js @@ -0,0 +1,44 @@ +'use strict'; + +var _ = require('lodash'); + +/** + * Simplified version of sails/lib/hooks/blueprints/actionUtil + * see: https://github.com/balderdashy/sails/blob/b4eed1775d01f436b263362180eb3f8447af1b87/lib/hooks/blueprints/actionUtil.js#L302 + */ +function parseModel(req) { + return req.options.model || req.options.controller; +} + +/** + * Query the Model that is being acted upon, and set it on the req object. + */ +module.exports = function ModelPolicy(req, res, next) { + var modelCache = sails.hooks.permissions._modelCache; + req.options.modelIdentity = parseModel(req); + + if (_.isEmpty(req.options.modelIdentity)) { + return next(); + } + + req.options.modelDefinition = sails.models[req.options.modelIdentity]; + req.model = modelCache[req.options.modelIdentity]; + + if (_.isObject(req.model) && !_.isNull(req.model.id)) { + return next(); + } + + sails.log.warn('Model [', req.options.modelIdentity, '] not found in model cache'); + + // if the model is not found in the cache for some reason, get it from the database + Model.findOne({ identity: req.options.modelIdentity }).then(function (model) { + if (!_.isObject(model)) { + req.options.unknownModel = true; + + model = sails.models[req.options.modelIdentity]; + } + + req.model = model; + next(); + })['catch'](next); +}; \ No newline at end of file diff --git a/dist/api/policies/OwnerPolicy.js b/dist/api/policies/OwnerPolicy.js new file mode 100644 index 0000000..540576b --- /dev/null +++ b/dist/api/policies/OwnerPolicy.js @@ -0,0 +1,34 @@ +/** + * TODO - this is setting createdBy, not owner. + * The comment below, and the name of this file/function is confusing to me + * Ensure that the 'owner' property of an Object is set upon creation. + */ +'use strict'; + +module.exports = function OwnerPolicy(req, res, next) { + //sails.log('OwnerPolicy()'); + if (!req.user || !req.user.id) { + req.logout(); + return res.send(500, new Error('req.user is not set')); + } + + /* + sails.log.verbose('OwnerPolicy user', req.user); + sails.log.verbose('OwnerPolicy method', req.method); + sails.log.verbose('OwnerPolicy req.body', req.body); + */ + + if (req.options.modelDefinition.autoCreatedBy === false) { + // sails.log.verbose('OwnerPolicy hasOwnershipPolicy: false'); + return next(); + } + + if ('POST' == req.method) { + //req.body || (req.body = { }); + req.body.createdBy = req.user.id; + req.body.owner = req.user.id; + } + + //sails.log.verbose('OwnerPolicy req.model', req.model); + next(); +}; \ No newline at end of file diff --git a/dist/api/policies/PermissionPolicy.js b/dist/api/policies/PermissionPolicy.js new file mode 100644 index 0000000..cac46be --- /dev/null +++ b/dist/api/policies/PermissionPolicy.js @@ -0,0 +1,45 @@ +/** + * PermissionPolicy + * @depends OwnerPolicy + * @depends ModelPolicy + * + * In order to proceed to the controller, the following verifications + * must pass: + * 1. User is logged in (handled previously by sails-auth sessionAuth policy) + * 2. User has Permission to perform action on Model + * 3. User has Permission to perform action on Attribute (if applicable) [TODO] + * 4. User is satisfactorily related to the Object's owner (if applicable) + * + * This policy verifies #1-2 here, before any controller is invoked. However + * it is not generally possible to determine ownership relationship until after + * the object has been queried. Verification of #4 occurs in RolePolicy. + * + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +'use strict'; + +module.exports = function (req, res, next) { + var options = { + model: req.model, + method: req.method, + user: req.user + }; + + if (req.options.unknownModel) { + return next(); + } + + PermissionService.findModelPermissions(options).then(function (permissions) { + sails.log.silly('PermissionPolicy:', permissions.length, 'permissions grant', req.method, 'on', req.model.name, 'for', req.user.username); + + if (!permissions || permissions.length === 0) { + return res.send(403, { error: PermissionService.getErrorMessage(options) }); + } + + req.permissions = permissions; + + next(); + }); +}; \ No newline at end of file diff --git a/dist/api/policies/RolePolicy.js b/dist/api/policies/RolePolicy.js new file mode 100644 index 0000000..f697e78 --- /dev/null +++ b/dist/api/policies/RolePolicy.js @@ -0,0 +1,62 @@ +/** + * RolePolicy + * @depends PermissionPolicy + * @depends OwnerPolicy + * @depends ModelPolicy + * + * Verify that User is satisfactorily related to the Object's owner. + * By this point, we know we have some permissions related to the action and object + * If they are 'owner' permissions, verify that the objects that are being accessed are owned by the current user + */ +'use strict'; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +module.exports = function (req, res, next) { + var permissions = req.permissions; + var relations = _lodash2['default'].groupBy(permissions, 'relation'); + var action = PermissionService.getMethod(req.method); + + // continue if there exist role Permissions which grant the asserted privilege + if (!_lodash2['default'].isEmpty(relations.role)) { + return next(); + } + if (req.options.unknownModel) { + return next(); + } + + /* + * This block allows us to filter reads by the owner attribute, rather than failing an entire request + * if some of the results are not owned by the user. + * We don't want to take this same course of action for an update or delete action, we would prefer to fail the entire request. + * There is no notion of 'create' for an owner permission, so it is not relevant here. + */ + if (!_lodash2['default'].contains(['update', 'delete'], action) && req.options.modelDefinition.attributes.owner) { + // Some parsing must happen on the query down the line, + // as req.query has no impact on the results from PermissionService.findTargetObjects. + // I had to look at the actionUtil parseCriteria method to see where to augment the criteria + req.params.all().where = req.params.all().where || {}; + req.params.all().where.owner = req.user.id; + req.query.owner = req.user.id; + _lodash2['default'].isObject(req.body) && (req.body.owner = req.user.id); + } + + PermissionService.findTargetObjects(req).then(function (objects) { + // PermissionService.isAllowedToPerformAction checks if the user has 'user' based permissions (vs role or owner based permissions) + return PermissionService.isAllowedToPerformAction(objects, req.user, action, ModelService.getTargetModelName(req), req.body).then(function (hasUserPermissions) { + if (hasUserPermissions) { + return next(); + } + if (PermissionService.hasForeignObjects(objects, req.user)) { + return res.send(403, { + error: 'Cannot perform action [' + action + '] on foreign object' + }); + } + next(); + }); + })['catch'](next); +}; \ No newline at end of file diff --git a/dist/api/services/ModelService.js b/dist/api/services/ModelService.js new file mode 100644 index 0000000..28ff443 --- /dev/null +++ b/dist/api/services/ModelService.js @@ -0,0 +1,27 @@ +'use strict'; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var pluralize = require('pluralize'); + +module.exports = { + /** + * Return the type of model acted upon by this request. + */ + getTargetModelName: function getTargetModelName(req) { + // TODO there has to be a more sails-y way to do this without including + // external modules + if (_lodash2['default'].isString(req.options.alias)) { + sails.log.silly('singularizing', req.options.alias, 'to use as target model'); + return pluralize.singular(req.options.alias); + } else if (_lodash2['default'].isString(req.options.model)) { + return req.options.model; + } else { + return req.model && req.model.identity; + } + } +}; \ No newline at end of file diff --git a/dist/api/services/PermissionService.js b/dist/api/services/PermissionService.js new file mode 100644 index 0000000..fd894a1 --- /dev/null +++ b/dist/api/services/PermissionService.js @@ -0,0 +1,450 @@ +'use strict'; + +var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })(); + +var _ = require('lodash'); + +var methodMap = { + POST: 'create', + GET: 'read', + PUT: 'update', + DELETE: 'delete' +}; + +var wlFilter = require('waterline-criteria'); + +module.exports = { + + /** + * Given an object, or a list of objects, return true if the list contains + * objects not owned by the specified user. + */ + hasForeignObjects: function hasForeignObjects(objects, user) { + if (!_.isArray(objects)) { + return PermissionService.isForeignObject(user.id)(objects); + } + return _.any(objects, PermissionService.isForeignObject(user.id)); + }, + + /** + * Return whether the specified object is NOT owned by the specified user. + */ + isForeignObject: function isForeignObject(owner) { + return function (object) { + //sails.log.verbose('object', object); + //sails.log.verbose('object.owner: ', object.owner, ', owner:', owner); + return object.owner !== owner; + }; + }, + + /** + * Find objects that some arbitrary action would be performed on, given the + * same request. + * + * @param options.model + * @param options.query + * + * TODO this will be less expensive when waterline supports a caching layer + */ + findTargetObjects: function findTargetObjects(req) { + + // handle add/remove routes that have :parentid as the primary key field + var originalId; + if (req.params.parentid) { + originalId = req.params.id; + req.params.id = req.params.parentid; + } + + return new Promise(function (resolve, reject) { + sails.hooks.blueprints.middleware.find(req, { + ok: resolve, + serverError: reject, + // this isn't perfect, since it returns a 500 error instead of a 404 error + // but it is better than crashing the app when a record doesn't exist + notFound: reject + }); + }).then(function (result) { + if (originalId !== undefined) { + req.params.id = originalId; + } + return result; + }); + }, + + /** + * Query Permissions that grant privileges to a role/user on an action for a + * model. + * + * @param options.method + * @param options.model + * @param options.user + */ + findModelPermissions: function findModelPermissions(options) { + var action = PermissionService.getMethod(options.method); + + //console.log('findModelPermissions options', options) + //console.log('findModelPermissions action', action) + + return User.findOne(options.user.id).populate('roles').then(function (user) { + var permissionCriteria = { + model: options.model.id, + action: action, + or: [{ role: _.pluck(user.roles, 'id') }, { user: user.id }] + }; + + return Permission.find(permissionCriteria).populate('criteria'); + }); + }, + + /** + * Given a list of objects, determine if they all satisfy at least one permission's + * where clause/attribute blacklist combination + * + * @param {Array of objects} objects - The result of the query, or if the action is create, + * the body of the object to be created + * @param {Array of Permission objects} permissions - An array of permission objects + * that are relevant to this particular user query + * @param {Object} attributes - The body of the request, in an update or create request. + * The keys of this object are checked against the permissions blacklist + * @returns boolean - True if there is at least one granted permission that allows the requested action, + * otherwise false + */ + hasPassingCriteria: function hasPassingCriteria(objects, permissions, attributes, user) { + // return success if there are no permissions or objects + if (_.isEmpty(permissions) || _.isEmpty(objects)) return true; + + if (!_.isArray(objects)) { + objects = [objects]; + } + + var criteria = permissions.reduce(function (memo, perm) { + if (perm) { + if (!perm.criteria || perm.criteria.length == 0) { + // If a permission has no criteria then it passes for all cases + // (like the admin role) + memo = memo.concat([{ where: {} }]); + } else { + memo = memo.concat(perm.criteria); + } + if (perm.relation === 'owner') { + perm.criteria.forEach(function (criteria) { + criteria.owner = true; + }); + } + return memo; + } + }, []); + + if (!_.isArray(criteria)) { + criteria = [criteria]; + } + + if (_.isEmpty(criteria)) { + return true; + } + + // every object must have at least one permission that has a passing criteria and a passing attribute check + return objects.every(function (obj) { + return criteria.some(function (criteria) { + var match = wlFilter([obj], { + where: criteria.where + }).results; + var hasUnpermittedAttributes = PermissionService.hasUnpermittedAttributes(attributes, criteria.blacklist); + var hasOwnership = true; // edge case for scenario where a user has some permissions that are owner based and some that are role based + if (criteria.owner) { + hasOwnership = !PermissionService.isForeignObject(user)(obj); + } + return match.length === 1 && !hasUnpermittedAttributes && hasOwnership; + }); + }); + }, + + hasUnpermittedAttributes: function hasUnpermittedAttributes(attributes, blacklist) { + if (_.isEmpty(attributes) || _.isEmpty(blacklist)) { + return false; + } + return _.intersection(Object.keys(attributes), blacklist).length ? true : false; + }, + + /** + * Return true if the specified model supports the ownership policy; false + * otherwise. + */ + hasOwnershipPolicy: function hasOwnershipPolicy(model) { + return model.autoCreatedBy; + }, + + /** + * Build an error message + */ + getErrorMessage: function getErrorMessage(options) { + var user = options.user.email || options.user.username; + return ['User', user, 'is not permitted to', options.method, options.model.name].join(' '); + }, + + /** + * Given an action, return the CRUD method it maps to. + */ + getMethod: function getMethod(method) { + return methodMap[method]; + }, + + /** + * create a new role + * @param options + * @param options.name {string} - role name + * @param options.permissions {permission object, or array of permissions objects} + * @param options.permissions.model {string} - the name of the model that the permission is associated with + * @param options.permissions.criteria - optional criteria object + * @param options.permissions.criteria.where - optional waterline query syntax object for specifying permissions + * @param options.permissions.criteria.blacklist {string array} - optional attribute blacklist + * @param options.users {array of user names} - optional array of user ids that have this role + */ + createRole: function createRole(options) { + + var ok = Promise.resolve(); + var permissions = options.permissions; + + if (!_.isArray(permissions)) { + permissions = [permissions]; + } + + // look up the model id based on the model name for each permission, and change it to an id + ok = ok.then(function () { + return Promise.all(permissions.map(function (permission) { + return Model.findOne({ + name: permission.model + }).then(function (model) { + permission.model = model.id; + return permission; + }); + })); + }); + + // look up user ids based on usernames, and replace the names with ids + ok = ok.then(function (permissions) { + if (options.users) { + return User.find({ + username: options.users + }).then(function (users) { + options.users = users; + }); + } + }); + + ok = ok.then(function (users) { + return Role.create(options); + }); + + return ok; + }, + + /** + * + * @param options {permission object, or array of permissions objects} + * @param options.role {string} - the role name that the permission is associated with, + * either this or user should be supplied, but not both + * @param options.user {string} - the user than that the permission is associated with, + * either this or role should be supplied, but not both + * @param options.model {string} - the model name that the permission is associated with + * @param options.action {string} - the http action that the permission allows + * @param options.criteria - optional criteria object + * @param options.criteria.where - optional waterline query syntax object for specifying permissions + * @param options.criteria.blacklist {string array} - optional attribute blacklist + */ + grant: function grant(permissions) { + if (!_.isArray(permissions)) { + permissions = [permissions]; + } + + // look up the models based on name, and replace them with ids + var ok = Promise.all(permissions.map(function (permission) { + var findRole = permission.role ? Role.findOne({ + name: permission.role + }) : null; + var findUser = permission.user ? User.findOne({ + username: permission.user + }) : null; + return Promise.all([findRole, findUser, Model.findOne({ + name: permission.model + })]).then(function (_ref) { + var _ref2 = _slicedToArray(_ref, 3); + + var role = _ref2[0]; + var user = _ref2[1]; + var model = _ref2[2]; + + permission.model = model.id; + if (role && role.id) { + permission.role = role.id; + } else if (user && user.id) { + permission.user = user.id; + } else { + return Promise.reject(new Error('no role or user specified')); + } + }); + })); + + ok = ok.then(function () { + return Permission.create(permissions); + }); + + return ok; + }, + + /** + * add one or more users to a particular role + * TODO should this work with multiple roles? + * @param usernames {string or string array} - list of names of users + * @param rolename {string} - the name of the role that the users should be added to + */ + addUsersToRole: function addUsersToRole(usernames, rolename) { + if (_.isEmpty(usernames)) { + return Promise.reject(new Error('One or more usernames must be provided')); + } + + if (!_.isArray(usernames)) { + usernames = [usernames]; + } + + return Role.findOne({ + name: rolename + }).populate('users').then(function (role) { + return User.find({ + username: usernames + }).then(function (users) { + role.users.add(_.pluck(users, 'id')); + return role.save(); + }); + }); + }, + + /** + * remove one or more users from a particular role + * TODO should this work with multiple roles + * @params usernames {string or string array} - name or list of names of users + * @params rolename {string} - the name of the role that the users should be removed from + */ + removeUsersFromRole: function removeUsersFromRole(usernames, rolename) { + if (_.isEmpty(usernames)) { + return Promise.reject(new Error('One or more usernames must be provided')); + } + + if (!_.isArray(usernames)) { + usernames = [usernames]; + } + + return Role.findOne({ + name: rolename + }).populate('users').then(function (role) { + return User.find({ + username: usernames + }, { + select: ['id'] + }).then(function (users) { + users.map(function (user) { + role.users.remove(user.id); + }); + return role.save(); + }); + }); + }, + + /** + * revoke permission from role + * @param options + * @param options.role {string} - the name of the role related to the permission. This, or options.user should be set, but not both. + * @param options.user {string} - the name of the user related to the permission. This, or options.role should be set, but not both. + * @param options.model {string} - the name of the model for the permission + * @param options.action {string} - the name of the action for the permission + * @param options.relation {string} - the type of the relation (owner or role) + */ + revoke: function revoke(options) { + var findRole = options.role ? Role.findOne({ + name: options.role + }) : null; + var findUser = options.user ? User.findOne({ + username: options.user + }) : null; + var ok = Promise.all([findRole, findUser, Model.findOne({ + name: options.model + })]); + + ok = ok.then(function (_ref3) { + var _ref32 = _slicedToArray(_ref3, 3); + + var role = _ref32[0]; + var user = _ref32[1]; + var model = _ref32[2]; + + var query = { + model: model.id, + action: options.action, + relation: options.relation + }; + + if (role && role.id) { + query.role = role.id; + } else if (user && user.id) { + query.user = user.id; + } else { + return Promise.reject(new Error('You must provide either a user or role to revoke the permission from')); + } + + return Permission.destroy(query); + }); + + return ok; + }, + + /** + * Check if the user (out of role) is granted to perform action on given objects + * @param objects + * @param user + * @param action + * @param model + * @param body + * @returns {*} + */ + isAllowedToPerformAction: function isAllowedToPerformAction(objects, user, action, model, body) { + if (!_.isArray(objects)) { + return PermissionService.isAllowedToPerformSingle(user.id, action, model, body)(objects); + } + return Promise.all(objects.map(PermissionService.isAllowedToPerformSingle(user.id, action, model, body))).then(function (allowedArray) { + return allowedArray.every(function (allowed) { + return allowed === true; + }); + }); + }, + + /** + * Resolve if the user have the permission to perform this action + * @param user + * @param action + * @param model + * @param body + * @returns {Function} + */ + isAllowedToPerformSingle: function isAllowedToPerformSingle(user, action, model, body) { + return function (obj) { + return new Promise(function (resolve, reject) { + Model.findOne({ + identity: model + }).then(function (model) { + return Permission.find({ + model: model.id, + action: action, + relation: 'user', + user: user + }).populate('criteria'); + }).then(function (permission) { + if (permission.length > 0 && PermissionService.hasPassingCriteria(obj, permission, body)) { + resolve(true); + } else { + resolve(false); + } + })['catch'](reject); + }); + }; + } +}; \ No newline at end of file diff --git a/dist/config/env/testing.js b/dist/config/env/testing.js new file mode 100644 index 0000000..7f9e057 --- /dev/null +++ b/dist/config/env/testing.js @@ -0,0 +1,37 @@ +'use strict'; + +var _ = require('lodash'); +var path = require('path'); + +/** + * Testing environment settings + * + * This file can include shared settings for a development team, + * such as API keys or remote database passwords. If you're using + * a version control solution for your Sails app, this file will + * be committed to your repository unless you add it to your .gitignore + * file. If your repository will be publicly viewable, don't add + * any private information to this file! + * + */ +module.exports = { + log: { level: 'debug' }, + models: { + migrate: 'drop', + connection: 'testing' + }, + connections: { + testing: { + adapter: 'waterline-postgresql' + } + }, + hooks: { grunt: false }, + port: 1336, + routes: { + "DELETE /role/:parentid/users/:id": { + controller: 'RoleController', + action: 'remove', + alias: 'users' + } + } +}; \ No newline at end of file diff --git a/dist/config/fixtures/model.js b/dist/config/fixtures/model.js new file mode 100644 index 0000000..07eb3b3 --- /dev/null +++ b/dist/config/fixtures/model.js @@ -0,0 +1,28 @@ +/** + * Creates database representations of the Model types. + * + * @public + */ +'use strict'; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +exports.createModels = function () { + sails.log.verbose('sails-permissions: syncing waterline models'); + + var models = _lodash2['default'].compact(_lodash2['default'].map(sails.models, function (model, name) { + return model && model.globalId && model.identity && { + name: model.globalId, + identity: model.identity, + attributes: _lodash2['default'].omit(model.attributes, _lodash2['default'].functions(model.attributes)) + }; + })); + + return Promise.all(_lodash2['default'].map(models, function (model) { + return sails.models.model.findOrCreate({ name: model.name }, model); + })); +}; \ No newline at end of file diff --git a/dist/config/fixtures/permission.js b/dist/config/fixtures/permission.js new file mode 100644 index 0000000..4bafe18 --- /dev/null +++ b/dist/config/fixtures/permission.js @@ -0,0 +1,91 @@ +'use strict'; + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } + +var _ = require('lodash'); + +var grants = { + admin: [{ action: 'create' }, { action: 'read' }, { action: 'update' }, { action: 'delete' }], + registered: [{ action: 'create' }, { action: 'read' }], + 'public': [{ action: 'read' }] +}; + +var modelRestrictions = { + registered: ['Role', 'Permission', 'User', 'Passport'], + 'public': ['Role', 'Permission', 'User', 'Model', 'Passport'] +}; + +// TODO let users override this in the actual model definition + +/** + * Create default Role permissions + */ +exports.create = function (roles, models, admin, config) { + return Promise.all([grantAdminPermissions(roles, models, admin, config), grantRegisteredPermissions(roles, models, admin, config)]).then(function (permissions) { + //sails.log.verbose('created', permissions.length, 'permissions'); + return permissions; + }); +}; + +function grantAdminPermissions(roles, models, admin, config) { + var adminRole = _.find(roles, { name: 'admin' }); + var permissions = _.flatten(_.map(models, function (modelEntity) { + //var model = sails.models[modelEntity.identity]; + grants.admin = _.get(config, 'grants.admin') || grants.admin; + + return _.map(grants.admin, function (permission) { + var newPermission = { + model: modelEntity.id, + action: permission.action, + role: adminRole.id + }; + return sails.models.permission.findOrCreate(newPermission, newPermission); + }); + })); + + return Promise.all(permissions); +} + +function grantRegisteredPermissions(roles, models, admin, config) { + var registeredRole = _.find(roles, { name: 'registered' }); + var basePermissions = [{ + model: _.find(models, { name: 'Permission' }).id, + action: 'read', + role: registeredRole.id + }, { + model: _.find(models, { name: 'Model' }).id, + action: 'read', + role: registeredRole.id + }, { + model: _.find(models, { name: 'User' }).id, + action: 'update', + role: registeredRole.id, + relation: 'owner' + }, { + model: _.find(models, { name: 'User' }).id, + action: 'read', + role: registeredRole.id, + relation: 'owner' + }]; + + // XXX copy/paste from above. terrible. improve. + var permittedModels = _.filter(models, function (model) { + return !_.contains(modelRestrictions.registered, model.name); + }); + var grantPermissions = _.flatten(_.map(permittedModels, function (modelEntity) { + + grants.registered = _.get(config, 'grants.registered') || grants.registered; + + return _.map(grants.registered, function (permission) { + return { + model: modelEntity.id, + action: permission.action, + role: registeredRole.id + }; + }); + })); + + return Promise.all([].concat(basePermissions, _toConsumableArray(grantPermissions)).map(function (permission) { + return sails.models.permission.findOrCreate(permission, permission); + })); +} \ No newline at end of file diff --git a/dist/config/fixtures/role.js b/dist/config/fixtures/role.js new file mode 100644 index 0000000..42a68f7 --- /dev/null +++ b/dist/config/fixtures/role.js @@ -0,0 +1,10 @@ +/** + * Creates default Roles + * + * @public + */ +'use strict'; + +exports.create = function () { + return Promise.all([sails.models.role.findOrCreate({ name: 'admin' }, { name: 'admin' }), sails.models.role.findOrCreate({ name: 'registered' }, { name: 'registered' }), sails.models.role.findOrCreate({ name: 'public' }, { name: 'public' })]); +}; \ No newline at end of file diff --git a/dist/config/fixtures/user.js b/dist/config/fixtures/user.js new file mode 100644 index 0000000..d3f546a --- /dev/null +++ b/dist/config/fixtures/user.js @@ -0,0 +1,37 @@ +/** + * Create admin user. + * @param adminRole - the admin role which grants all permissions + */ +'use strict'; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +exports.create = function (roles, userModel) { + if (_lodash2['default'].isEmpty(sails.config.permissions.adminUsername)) { + throw new Error('sails.config.permissions.adminUsername is not set'); + } + if (_lodash2['default'].isEmpty(sails.config.permissions.adminPassword)) { + throw new Error('sails.config.permissions.adminPassword is not set'); + } + if (_lodash2['default'].isEmpty(sails.config.permissions.adminEmail)) { + throw new Error('sails.config.permissions.adminEmail is not set'); + } + return sails.models.user.findOne({ username: sails.config.permissions.adminUsername }).then(function (user) { + if (user) return user; + + sails.log.info('sails-permissions: admin user does not exist; creating...'); + return sails.models.user.register({ + username: sails.config.permissions.adminUsername, + password: sails.config.permissions.adminPassword, + email: sails.config.permissions.adminEmail, + roles: [_lodash2['default'].find(roles, { name: 'admin' }).id], + createdBy: 1, + owner: 1, + model: userModel.id + }); + }); +}; \ No newline at end of file diff --git a/dist/config/permissions.js b/dist/config/permissions.js new file mode 100644 index 0000000..2abf9ba --- /dev/null +++ b/dist/config/permissions.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports.permissions = { + name: 'permissions', + + adminEmail: process.env.ADMIN_EMAIL || 'admin@example.com', + adminUsername: process.env.ADMIN_USERNAME || 'admin', + adminPassword: process.env.ADMIN_PASSWORD || 'admin1234', + + afterEvents: ['hook:auth:initialized'] +}; \ No newline at end of file diff --git a/dist/config/policies.js b/dist/config/policies.js new file mode 100644 index 0000000..16a99ca --- /dev/null +++ b/dist/config/policies.js @@ -0,0 +1,58 @@ +/** + * Policy Mappings + * (sails.config.policies) + * + * Policies are simple functions which run **before** your controllers. + * You can apply one or more policies to a given controller, or protect + * its actions individually. + * + * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed + * below by its filename, minus the extension, (e.g. "authenticated") + * + * For more information on how policies work, see: + * http://sailsjs.org/#/documentation/concepts/Policies + * + * For more information on configuring policies, check out: + * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.policies.html + */ + +'use strict'; + +module.exports.policies = { + + '*': ['basicAuth', 'passport', 'sessionAuth', 'ModelPolicy', 'AuditPolicy', 'OwnerPolicy', 'PermissionPolicy', 'RolePolicy', 'CriteriaPolicy'], + + AuthController: { + '*': ['passport'] + } + +}; +/*************************************************************************** +* * +* Default policy for all controllers and actions (`true` allows public * +* access) * +* * +***************************************************************************/ + +// '*': true, + +/*************************************************************************** +* * +* Here's an example of mapping some policies to run before a controller * +* and its actions * +* * +***************************************************************************/ +// RabbitController: { + +// Apply the `false` policy as the default for all of RabbitController's actions +// (`false` prevents all access, which ensures that nothing bad happens to our rabbits) +// '*': false, + +// For the action `nurture`, apply the 'isRabbitMother' policy +// (this overrides `false` above) +// nurture : 'isRabbitMother', + +// Apply the `isNiceToAnimals` AND `hasRabbitFood` policies +// before letting any users feed our rabbits +// feed : ['isNiceToAnimals', 'hasRabbitFood'] +// } \ No newline at end of file diff --git a/dist/config/session.js b/dist/config/session.js new file mode 100644 index 0000000..cd6abb0 --- /dev/null +++ b/dist/config/session.js @@ -0,0 +1,19 @@ +/** + * Session Configuration + * (sails.config.session) + * + * Sails session integration leans heavily on the great work already done by + * Express, but also unifies Socket.io with the Connect session store. It uses + * Connect's cookie parser to normalize configuration differences between Express + * and Socket.io and hooks into Sails' middleware interpreter to allow you to access + * and auto-save to `req.session` with Socket.io the same way you would with Express. + * + * For more information on configuring the session, check out: + * http://links.sailsjs.org/docs/config/session + */ + +'use strict'; + +module.exports.session = { + secret: '00000000000000000000000000000000' +}; \ No newline at end of file From ae658aa046a3a0b16993ff788ee5410f46f957b1 Mon Sep 17 00:00:00 2001 From: Mario Medina Date: Mon, 13 Mar 2017 21:16:43 -0500 Subject: [PATCH 2/5] Fixing error: TypeError: Cannot set property 'owner' of undefined --- api/policies/RolePolicy.js | 6 ++++-- dist/api/policies/RolePolicy.js | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/api/policies/RolePolicy.js b/api/policies/RolePolicy.js index 6f436d2..6546e56 100644 --- a/api/policies/RolePolicy.js +++ b/api/policies/RolePolicy.js @@ -33,8 +33,10 @@ module.exports = function(req, res, next) { // Some parsing must happen on the query down the line, // as req.query has no impact on the results from PermissionService.findTargetObjects. // I had to look at the actionUtil parseCriteria method to see where to augment the criteria - req.params.all().where = req.params.all().where || {}; - req.params.all().where.owner = req.user.id; + //req.params.all().where = req.params.all().where || {}; + //req.params.all().where.owner = req.user.id; + req.params.where = req.params.where || {}; + req.params.where.owner = req.user.id; req.query.owner = req.user.id; _.isObject(req.body) && (req.body.owner = req.user.id); } diff --git a/dist/api/policies/RolePolicy.js b/dist/api/policies/RolePolicy.js index f697e78..06f701e 100644 --- a/dist/api/policies/RolePolicy.js +++ b/dist/api/policies/RolePolicy.js @@ -39,8 +39,10 @@ module.exports = function (req, res, next) { // Some parsing must happen on the query down the line, // as req.query has no impact on the results from PermissionService.findTargetObjects. // I had to look at the actionUtil parseCriteria method to see where to augment the criteria - req.params.all().where = req.params.all().where || {}; - req.params.all().where.owner = req.user.id; + //req.params.all().where = req.params.all().where || {}; + //req.params.all().where.owner = req.user.id; + req.params.where = req.params.where || {}; + req.params.where.owner = req.user.id; req.query.owner = req.user.id; _lodash2['default'].isObject(req.body) && (req.body.owner = req.user.id); } From ec72b04614404abd6d1bc9da89dd543ac1aff420 Mon Sep 17 00:00:00 2001 From: Mario Medina Date: Fri, 7 Apr 2017 03:51:20 -0500 Subject: [PATCH 3/5] Fixing a bug on custom methods in controllers with owner privileges --- api/policies/RolePolicy.js | 1 + dist/api/policies/RolePolicy.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/policies/RolePolicy.js b/api/policies/RolePolicy.js index 6546e56..fece158 100644 --- a/api/policies/RolePolicy.js +++ b/api/policies/RolePolicy.js @@ -38,6 +38,7 @@ module.exports = function(req, res, next) { req.params.where = req.params.where || {}; req.params.where.owner = req.user.id; req.query.owner = req.user.id; + req.query.where = req.query.where || {}; _.isObject(req.body) && (req.body.owner = req.user.id); } diff --git a/dist/api/policies/RolePolicy.js b/dist/api/policies/RolePolicy.js index 06f701e..e0d9a1f 100644 --- a/dist/api/policies/RolePolicy.js +++ b/dist/api/policies/RolePolicy.js @@ -44,6 +44,7 @@ module.exports = function (req, res, next) { req.params.where = req.params.where || {}; req.params.where.owner = req.user.id; req.query.owner = req.user.id; + req.query.where = req.query.where || {}; _lodash2['default'].isObject(req.body) && (req.body.owner = req.user.id); } @@ -61,4 +62,4 @@ module.exports = function (req, res, next) { next(); }); })['catch'](next); -}; \ No newline at end of file +}; From edf3abc3b869b5cb94be581f377d20a8c0dc513d Mon Sep 17 00:00:00 2001 From: Mario Medina Date: Fri, 7 Apr 2017 03:51:25 -0500 Subject: [PATCH 4/5] 2.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8cff0f9..f0b900f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sails-permissions", - "version": "2.2.0", + "version": "2.2.1", "description": "Comprehensive user permissions and entitlements system for sails.js and Waterline. Supports user authentication with passport.js, role-based permissioning, object ownership, and row-level security.", "main": "dist/api/hooks/permissions/index.js", "scripts": { From 910677f197ee2ec3bab9ee2a6af3d47e018187ba Mon Sep 17 00:00:00 2001 From: Mario Medina Date: Fri, 7 Apr 2017 04:38:29 -0500 Subject: [PATCH 5/5] Fixing a bug on custom methods in controllers with owner privileges --- api/policies/RolePolicy.js | 4 +++- dist/api/policies/RolePolicy.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/policies/RolePolicy.js b/api/policies/RolePolicy.js index fece158..aedc42a 100644 --- a/api/policies/RolePolicy.js +++ b/api/policies/RolePolicy.js @@ -37,8 +37,10 @@ module.exports = function(req, res, next) { //req.params.all().where.owner = req.user.id; req.params.where = req.params.where || {}; req.params.where.owner = req.user.id; + req.params.owner= req.user.id; // Nuevo + req.query.where = req.query.where || {}; // Nuevo + req.query.where.owner = req.user.id; // Nuevo req.query.owner = req.user.id; - req.query.where = req.query.where || {}; _.isObject(req.body) && (req.body.owner = req.user.id); } diff --git a/dist/api/policies/RolePolicy.js b/dist/api/policies/RolePolicy.js index e0d9a1f..325803d 100644 --- a/dist/api/policies/RolePolicy.js +++ b/dist/api/policies/RolePolicy.js @@ -43,8 +43,10 @@ module.exports = function (req, res, next) { //req.params.all().where.owner = req.user.id; req.params.where = req.params.where || {}; req.params.where.owner = req.user.id; + req.params.owner= req.user.id; // Nuevo + req.query.where = req.query.where || {}; // Nuevo + req.query.where.owner = req.user.id; // Nuevo req.query.owner = req.user.id; - req.query.where = req.query.where || {}; _lodash2['default'].isObject(req.body) && (req.body.owner = req.user.id); }