├── .gitignore ├── .jshintrc ├── .npmignore ├── .sailsrc ├── .travis.yml ├── LICENSE ├── README.md ├── api ├── controllers │ ├── ModelController.js │ ├── PermissionController.js │ └── RoleController.js ├── hooks │ └── permissions │ │ └── index.js ├── models │ ├── Criteria.js │ ├── Model.js │ ├── Passport.js │ ├── Permission.js │ ├── RequestLog.js │ ├── Role.js │ ├── SecurityLog.js │ └── User.js ├── policies │ ├── AuditPolicy.js │ ├── CriteriaPolicy.js │ ├── ModelPolicy.js │ ├── OwnerPolicy.js │ ├── PermissionPolicy.js │ └── RolePolicy.js └── services │ ├── ModelService.js │ └── PermissionService.js ├── config ├── env │ └── testing.js ├── fixtures │ ├── model.js │ ├── permission.js │ ├── role.js │ └── user.js ├── permissions.js ├── policies.js └── session.js ├── generator.js ├── gulpfile.js ├── package.json └── test ├── bootstrap.test.js ├── mocha.opts └── unit ├── controllers ├── ModelController.test.js ├── PermissionController.test.js └── UserController.test.js └── services └── PermissionService.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | #api/policies/basicAuth.js 2 | #api/policies/sessionAuth.js 3 | #api/services/protocols/basic.js 4 | dist/ 5 | 6 | .tmp 7 | *.sw* 8 | # Logs 9 | logs 10 | *.log 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # Deployed apps should consider commenting this line out: 31 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 32 | node_modules 33 | 34 | .idea 35 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 2, 3 | "maxdepth": 6, 4 | "maxlen": 200, 5 | "esnext": true, 6 | "expr": true, 7 | "trailing": true, 8 | "node": true, 9 | "esnext": false, 10 | "globals": { 11 | "_": true, 12 | "sails": true, 13 | "describe": true, 14 | "it": true, 15 | "before": true, 16 | "Permission": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailsjs/sails-permissions/826d504711662bf43a5c266667dd4487553a8a41/.npmignore -------------------------------------------------------------------------------- /.sailsrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "grunt": false 4 | }, 5 | "generators": { 6 | "modules": { 7 | "auth-api": "sails-auth" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | - '4.2' 5 | - 'stable' 6 | 7 | addons: 8 | postgresql: "9.4" 9 | 10 | sudo: false 11 | 12 | env: 13 | global: 14 | - REDIS_URL=redis://:@localhost:6379 15 | 16 | services: 17 | - redis-server 18 | 19 | notifications: 20 | email: false 21 | 22 | deploy: 23 | provider: npm 24 | email: me@traviswebb.com 25 | api_key: 26 | secure: AitUbALDH1eqxEuTIUMynFcaumHP2jFVZyCqUOqVmoNmHo6T5l+S6fwE1AlnPaeEic8q0gosDnMYG6W3GhQuXd0UkfAB7Gz72SHfNvNe29/svNN5exP7c/PWZZSqBxarnYM6NU7rkqdP11sF+UO6oqzd0QguT7VJPylg5EXg5I8= 27 | on: 28 | tags: true 29 | all_branches: true 30 | repo: tjwebb/sails-permissions 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Travis Webb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sails-permissions 2 | 3 | [![Gitter][gitter-image]][gitter-url] 4 | [![NPM version][npm-image]][npm-url] 5 | [![Build status][travis-image]][travis-url] 6 | [![Dependency Status][daviddm-image]][daviddm-url] 7 | 8 | Comprehensive sails.js user permissions and entitlements system. Supports user authentication with passport.js, role-based permissioning, object ownership, and row-level security. 9 | 10 | ## Install 11 | ```sh 12 | $ npm install sails-permissions sails-auth --save 13 | ``` 14 | 15 | ## Quickstart 16 | 17 | **Note:** Complete documentation available in the sails-permissions wiki: https://github.com/langateam/sails-permissions/wiki 18 | 19 | ### 1. configure sailsrc 20 | 21 | ```json 22 | { 23 | "generators": { 24 | "modules": { 25 | "permissions-api": "sails-permissions/generator" 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ### 2. run generator 32 | 33 | ```sh 34 | $ sails generate permissions-api 35 | ``` 36 | 37 | ### 3. Set environment variables 38 | 39 | | variable | description | default | 40 | |:---|:---|:---| 41 | | `ADMIN_USERNAME` | admin username | `admin` | 42 | | `ADMIN_EMAIL` | admin user email address | `admin@example.com` | 43 | | `ADMIN_PASSWORD` | admin user password | `admin1234` | 44 | 45 | ##### e.g in config/local.js (file is in .gitignore) 46 | ``` 47 | sails.config.permissions.adminUsername = 'admin' 48 | sails.config.permissions.adminEmail = 'admin@example.com' 49 | sails.config.permissions.adminPassword = 'admin1234' 50 | ``` 51 | #### 4. update configs 52 | 53 | #### config/policies.js 54 | ```js 55 | '*': [ 56 | 'basicAuth', 57 | 'passport', 58 | 'sessionAuth', 59 | 'ModelPolicy', 60 | 'AuditPolicy', 61 | 'OwnerPolicy', 62 | 'PermissionPolicy', 63 | 'RolePolicy', 64 | 'CriteriaPolicy' 65 | ], 66 | 67 | AuthController: { 68 | '*': [ 'passport' ] 69 | } 70 | ``` 71 | 72 | #### 5. Login 73 | You can now login using the aforementioned default login data or the admin settings you specified using the `/auth/local` endpoint. 74 | ```json 75 | { 76 | "identifier": "admin@example.com", 77 | "password": "admin1234" 78 | } 79 | ``` 80 | 81 | ## License 82 | MIT 83 | 84 | ## Maintained By 85 | [](http://langa.io) 86 | 87 | [npm-image]: https://img.shields.io/npm/v/sails-permissions.svg?style=flat-square 88 | [npm-url]: https://npmjs.org/package/sails-permissions 89 | [travis-image]: https://img.shields.io/travis/langateam/sails-permissions.svg?style=flat-square 90 | [travis-url]: https://travis-ci.org/langateam/sails-permissions 91 | [daviddm-image]: http://img.shields.io/david/langateam/sails-permissions.svg?style=flat-square 92 | [daviddm-url]: https://david-dm.org/langateam/sails-permissions 93 | [gitter-image]: http://img.shields.io/badge/+%20GITTER-JOIN%20CHAT%20%E2%86%92-1DCE73.svg?style=flat-square 94 | [gitter-url]: https://gitter.im/langateam/sails-permissions 95 | 96 | [hacktober-image]: http://i.imgur.com/FM9yVCI.png 97 | [hacktober-url]: https://twitter.com/langateam/status/782995392212369408 98 | -------------------------------------------------------------------------------- /api/controllers/ModelController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ModelController 3 | * 4 | * @description :: Server-side logic for managing models 5 | * @help :: See http://links.sailsjs.org/docs/controllers 6 | */ 7 | 8 | module.exports = { 9 | 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /api/controllers/PermissionController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PermissionController 3 | * 4 | * @description :: Server-side logic for managing permissions 5 | * @help :: See http://links.sailsjs.org/docs/controllers 6 | */ 7 | 8 | module.exports = { 9 | 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /api/controllers/RoleController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * RoleController 3 | * 4 | * @description :: Server-side logic for managing roles 5 | * @help :: See http://links.sailsjs.org/docs/controllers 6 | */ 7 | 8 | module.exports = { 9 | 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /api/hooks/permissions/index.js: -------------------------------------------------------------------------------- 1 | var permissionPolicies = [ 2 | 'passport', 3 | 'sessionAuth', 4 | 'ModelPolicy', 5 | 'OwnerPolicy', 6 | 'PermissionPolicy', 7 | 'RolePolicy' 8 | ] 9 | import path from 'path' 10 | import _ from 'lodash' 11 | import Marlinspike from 'marlinspike' 12 | 13 | class Permissions extends Marlinspike { 14 | constructor (sails) { 15 | super(sails, module) 16 | } 17 | 18 | configure () { 19 | if (!_.isObject(sails.config.permissions)) sails.config.permissions = { } 20 | 21 | /** 22 | * Local cache of Model name -> id mappings to avoid excessive database lookups. 23 | */ 24 | this.sails.config.blueprints.populate = false 25 | } 26 | 27 | initialize (next) { 28 | let config = this.sails.config.permissions 29 | 30 | this.installModelOwnership() 31 | this.sails.after(config.afterEvent, () => { 32 | if (!this.validateDependencies()) { 33 | this.sails.log.error('Cannot find sails-auth hook. Did you "npm install sails-auth --save"?') 34 | this.sails.log.error('Please see README for installation instructions: https://github.com/tjwebb/sails-permissions') 35 | return this.sails.lower() 36 | } 37 | 38 | if (!this.validatePolicyConfig()) { 39 | this.sails.log.warn('One or more required policies are missing.') 40 | this.sails.log.warn('Please see README for installation instructions: https://github.com/tjwebb/sails-permissions') 41 | } 42 | 43 | }) 44 | 45 | this.sails.after('hook:orm:loaded', () => { 46 | sails.models.model.count() 47 | .then(count => { 48 | if (count === _.keys(this.sails.models).length) return next() 49 | 50 | return this.initializeFixtures() 51 | .then(() => { 52 | next() 53 | }) 54 | }) 55 | .catch(error => { 56 | this.sails.log.error(error) 57 | next(error) 58 | }) 59 | }) 60 | } 61 | 62 | validatePolicyConfig () { 63 | var policies = this.sails.config.policies 64 | return _.all([ 65 | _.isArray(policies['*']), 66 | _.intersection(permissionPolicies, policies['*']).length === permissionPolicies.length, 67 | policies.AuthController && _.contains(policies.AuthController['*'], 'passport') 68 | ]) 69 | } 70 | 71 | installModelOwnership () { 72 | var models = this.sails.models 73 | if (this.sails.config.models.autoCreatedBy === false) return 74 | 75 | _.each(models, model => { 76 | if (model.autoCreatedBy === false) return 77 | 78 | _.defaults(model.attributes, { 79 | createdBy: { 80 | model: 'User', 81 | index: true 82 | }, 83 | owner: { 84 | model: 'User', 85 | index: true 86 | } 87 | }) 88 | }) 89 | } 90 | 91 | /** 92 | * Install the application. Sets up default Roles, Users, Models, and 93 | * Permissions, and creates an admin user. 94 | */ 95 | initializeFixtures () { 96 | let fixturesPath = path.resolve(__dirname, '../../../config/fixtures/') 97 | return require(path.resolve(fixturesPath, 'model')).createModels() 98 | .then(models => { 99 | this.models = models 100 | this.sails.hooks.permissions._modelCache = _.indexBy(models, 'identity') 101 | 102 | return require(path.resolve(fixturesPath, 'role')).create() 103 | }) 104 | .then(roles => { 105 | this.roles = roles 106 | var userModel = _.find(this.models, { name: 'User' }) 107 | return require(path.resolve(fixturesPath, 'user')).create(this.roles, userModel) 108 | }) 109 | .then(() => { 110 | return sails.models.user.findOne({ email: this.sails.config.permissions.adminEmail }) 111 | }) 112 | .then(user => { 113 | this.sails.log('sails-permissions: created admin user:', user) 114 | user.createdBy = user.id 115 | user.owner = user.id 116 | return user.save() 117 | }) 118 | .then(admin => { 119 | return require(path.resolve(fixturesPath, 'permission')).create(this.roles, this.models, admin, this.sails.config.permissions); 120 | }) 121 | .catch(error => { 122 | this.sails.log.error(error) 123 | }) 124 | } 125 | 126 | validateDependencies () { 127 | return !!this.sails.hooks.auth; 128 | } 129 | } 130 | 131 | export default Marlinspike.createSailsHook(Permissions) 132 | -------------------------------------------------------------------------------- /api/models/Criteria.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Criteria 3 | * 4 | * @description 5 | * Criteria specify limits on a permission, via a 'where' clause and an attribute blacklist. 6 | * For the blacklist, if the request action is update or create, and there is a blacklisted attribute in the request, 7 | * the request will fail. If the request action is read, the blacklisted attributes will be filtered. 8 | * The blacklist is not relevant for delete requests. 9 | * A where clause uses waterline query syntax to determine if a permission is allowed, ie where: { id: { '>': 5 } } 10 | */ 11 | module.exports = { 12 | autoCreatedBy: false, 13 | 14 | description: 'Specifies more granular limits on a permission', 15 | 16 | attributes: { 17 | where: 'json', 18 | blacklist: 'array', 19 | permission: { 20 | model: 'Permission' 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /api/models/Model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Model 3 | * 4 | * @description 5 | * Abstract representation of a Waterline Model. 6 | */ 7 | module.exports = { 8 | description: 'Represents a Waterline collection that a User can create, query, etc.', 9 | 10 | autoPK: true, 11 | autoCreatedBy: false, 12 | autoCreatedAt: false, 13 | autoUpdatedAt: false, 14 | 15 | attributes: { 16 | name: { 17 | type: 'string', 18 | notNull: true, 19 | unique: true 20 | }, 21 | identity: { 22 | type: 'string', 23 | notNull: true 24 | }, 25 | attributes: { 26 | type: 'json' 27 | }, 28 | permissions: { 29 | collection: 'Permission', 30 | via: 'model' 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /api/models/Passport.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash'); 3 | var _super = require('sails-auth/api/models/Passport'); 4 | 5 | _.merge(exports, _super); 6 | _.merge(exports, { 7 | 8 | autoCreatedBy: false 9 | 10 | // Extend with custom logic here by adding additional fields, methods, etc. 11 | 12 | /** 13 | * For example: 14 | * 15 | * foo: function (bar) { 16 | * bar.x = 1; 17 | * bar.y = 2; 18 | * return _super.foo(bar); 19 | * } 20 | */ 21 | }); 22 | -------------------------------------------------------------------------------- /api/models/Permission.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Permission 3 | * 4 | * @description 5 | * The actions a Role is granted on a particular Model and its attributes 6 | */ 7 | import _ from 'lodash' 8 | module.exports = { 9 | autoCreatedBy: false, 10 | 11 | description: [ 12 | 'Defines a particular `action` that a `Role` can perform on a `Model`.', 13 | 'A `User` can perform an `action` on a `Model` by having a `Role` which', 14 | 'grants the necessary `Permission`.' 15 | ].join(' '), 16 | 17 | attributes: { 18 | 19 | /** 20 | * The Model that this Permission applies to. 21 | */ 22 | model: { 23 | model: 'Model', 24 | required: true 25 | }, 26 | 27 | action: { 28 | type: 'string', 29 | index: true, 30 | notNull: true, 31 | /** 32 | * TODO remove enum and support permissions based on all controller 33 | * actions, including custom ones 34 | */ 35 | enum: [ 36 | 'create', 37 | 'read', 38 | 'update', 39 | 'delete' 40 | ] 41 | }, 42 | 43 | relation: { 44 | type: 'string', 45 | enum: [ 46 | 'role', 47 | 'owner', 48 | 'user' 49 | ], 50 | defaultsTo: 'role', 51 | index: true 52 | }, 53 | 54 | /** 55 | * The Role to which this Permission grants create, read, update, and/or 56 | * delete privileges. 57 | */ 58 | role: { 59 | model: 'Role', 60 | // Validate manually 61 | //required: true 62 | }, 63 | 64 | /** 65 | * The User to which this Permission grants create, read, update, and/or 66 | * delete privileges. 67 | */ 68 | user: { 69 | model: 'User' 70 | // Validate manually 71 | }, 72 | 73 | /** 74 | * A list of criteria. If any of the criteria match the request, the action is allowed. 75 | * If no criteria are specified, it is ignored altogether. 76 | */ 77 | criteria: { 78 | collection: 'Criteria', 79 | via: 'permission' 80 | } 81 | }, 82 | 83 | afterValidate: [ 84 | function validateOwnerCreateTautology (permission, next) { 85 | if (permission.relation == 'owner' && permission.action == 'create') { 86 | next(new Error('Creating a Permission with relation=owner and action=create is tautological')); 87 | } 88 | 89 | if (permission.action === 'delete' && 90 | _.filter(permission.criteria, function (criteria) { return !_.isEmpty(criteria.blacklist); }).length) { 91 | next(new Error('Creating a Permission with an attribute blacklist is not allowed when action=delete')); 92 | } 93 | 94 | if (permission.relation == 'user' && permission.user === "") { 95 | next(new Error('A Permission with relation user MUST have the user attribute set')); 96 | } 97 | 98 | if (permission.relation == 'role' && permission.role === "") { 99 | next(new Error('A Permission with relation role MUST have the role attribute set')); 100 | } 101 | 102 | next(); 103 | } 104 | ] 105 | }; 106 | -------------------------------------------------------------------------------- /api/models/RequestLog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * RequestLog.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | autoPK: false, 10 | autoCreatedBy: false, 11 | autoUpdatedAt: false, 12 | 13 | attributes: { 14 | id: { 15 | type: 'string', 16 | primaryKey: true 17 | }, 18 | ipAddress: { 19 | type: 'string' 20 | }, 21 | method: { 22 | type: 'string' 23 | }, 24 | url: { 25 | type: 'string', 26 | url: true 27 | }, 28 | body: { 29 | type: 'json' 30 | }, 31 | user: { 32 | model: 'User' 33 | }, 34 | model: { 35 | type: 'string' 36 | } 37 | } 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /api/models/Role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Role 3 | * 4 | * @description 5 | * Roles endow Users with Permissions. Exposes Postgres-like API for 6 | * resolving granted Permissions for a User. 7 | * 8 | * @see 9 | */ 10 | module.exports = { 11 | autoCreatedBy: false, 12 | 13 | description: 'Confers `Permission` to `User`', 14 | 15 | attributes: { 16 | name: { 17 | type: 'string', 18 | index: true, 19 | notNull: true, 20 | unique: true 21 | }, 22 | users: { 23 | collection: 'User', 24 | via: 'roles' 25 | }, 26 | active: { 27 | type: 'boolean', 28 | defaultsTo: true, 29 | index: true 30 | }, 31 | permissions: { 32 | collection: 'Permission', 33 | via: 'role' 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /api/models/SecurityLog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SecurityLog.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | autoPK: false, 10 | autoUpdatedAt: false, 11 | autoCreatedAt: false, 12 | 13 | attributes: { 14 | request: { 15 | model: 'RequestLog', 16 | primaryKey: true 17 | } 18 | } 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var _super = require('sails-auth/api/models/User'); 3 | 4 | _.merge(exports, _super); 5 | _.merge(exports, { 6 | attributes: { 7 | roles: { 8 | collection: 'Role', 9 | via: 'users', 10 | dominant: true 11 | }, 12 | permissions: { 13 | collection: "Permission", 14 | via: "user" 15 | } 16 | }, 17 | 18 | /** 19 | * Attach default Role to a new User 20 | */ 21 | afterCreate: [ 22 | function setOwner (user, next) { 23 | sails.log.verbose('User.afterCreate.setOwner', user); 24 | User 25 | .update({ id: user.id }, { owner: user.id }) 26 | .then(function (user) { 27 | next(); 28 | }) 29 | .catch(function (e) { 30 | sails.log.error(e); 31 | next(e); 32 | }); 33 | }, 34 | function attachDefaultRole (user, next) { 35 | sails.log('User.afterCreate.attachDefaultRole', user); 36 | User.findOne(user.id) 37 | .populate('roles') 38 | .then(function (_user) { 39 | user = _user; 40 | return Role.findOne({ name: 'registered' }); 41 | }) 42 | .then(function (role) { 43 | user.roles.add(role.id); 44 | return user.save(); 45 | }) 46 | .then(function (updatedUser) { 47 | sails.log.silly('role "registered" attached to user', user.username); 48 | next(); 49 | }) 50 | .catch(function (e) { 51 | sails.log.error(e); 52 | next(e); 53 | }) 54 | } 55 | ] 56 | }); 57 | -------------------------------------------------------------------------------- /api/policies/AuditPolicy.js: -------------------------------------------------------------------------------- 1 | var fnv = require('fnv-plus'); 2 | var _ = require('lodash'); 3 | var url = require('url'); 4 | 5 | module.exports = function (req, res, next) { 6 | var ipAddress = req.headers['x-forwarded-for'] || (req.connection && req.connection.remoteAddress); 7 | req.requestId = fnv.hash(new Date().valueOf() + ipAddress, 128).str(); 8 | 9 | sails.models.requestlog.create({ 10 | id: req.requestId, 11 | ipAddress: ipAddress, 12 | url: sanitizeRequestUrl(req), 13 | method: req.method, 14 | body: _.omit(req.body, 'password'), 15 | model: req.options.modelIdentity, 16 | user: (req.user || {}).id 17 | }).exec(_.identity); 18 | 19 | // persist RequestLog entry in the background; continue immediately 20 | next(); 21 | }; 22 | 23 | function sanitizeRequestUrl (req) { 24 | var requestUrl = url.format({ 25 | protocol: req.protocol, 26 | host: req.host || sails.getHost(), 27 | pathname: req.originalUrl || req.url, 28 | query: req.query 29 | }); 30 | 31 | return requestUrl.replace(/(password=).*?(&|$)/ig, '$1$2'); 32 | } 33 | -------------------------------------------------------------------------------- /api/policies/CriteriaPolicy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CriteriaPolicy 3 | * @depends PermissionPolicy 4 | * 5 | * Verify that the User fulfills permission 'where' conditions and attribute blacklist restrictions 6 | */ 7 | var wlFilter = require('waterline-criteria'); 8 | import _ from 'lodash' 9 | 10 | module.exports = function(req, res, next) { 11 | var permissions = req.permissions; 12 | 13 | if (_.isEmpty(permissions)) { 14 | return next(); 15 | } 16 | 17 | var action = PermissionService.getMethod(req.method); 18 | 19 | var body = req.body || req.query; 20 | 21 | // if we are creating, we don't need to query the db, just check the where clause vs the passed in data 22 | if (action === 'create') { 23 | if (!PermissionService.hasPassingCriteria(body, permissions, body)) { 24 | return res.send(403, { 25 | error: 'Can\'t create this object, because of failing where clause' 26 | }); 27 | } 28 | return next(); 29 | } 30 | 31 | 32 | // set up response filters if we are not mutating an existing object 33 | if (!_.contains(['update', 'delete'], action)) { 34 | 35 | // get all of the where clauses and blacklists into one flat array 36 | // if a permission has no criteria then it is always true 37 | var criteria = _.compact(_.flatten( 38 | _.map( 39 | _.pluck(permissions, 'criteria'), 40 | function(c) { 41 | if (c.length == 0) { 42 | return [{where: {}}]; 43 | } 44 | return c; 45 | } 46 | ) 47 | )); 48 | 49 | if (criteria.length) { 50 | bindResponsePolicy(req, res, criteria); 51 | } 52 | return next(); 53 | } 54 | 55 | PermissionService.findTargetObjects(req) 56 | .then(function(objects) { 57 | 58 | // attributes are not important for a delete request 59 | if (action === 'delete') { 60 | body = undefined; 61 | } 62 | 63 | if (!PermissionService.hasPassingCriteria(objects, permissions, body, req.user.id)) { 64 | return res.send(403, { 65 | error: 'Can\'t ' + action + ', because of failing where clause or attribute permissions' 66 | }); 67 | } 68 | 69 | next(); 70 | }) 71 | .catch(next); 72 | }; 73 | 74 | function bindResponsePolicy(req, res, criteria) { 75 | res._ok = res.ok; 76 | 77 | res.ok = _.bind(responsePolicy, { 78 | req: req, 79 | res: res 80 | }, criteria); 81 | } 82 | 83 | function responsePolicy(criteria, _data, options) { 84 | var req = this.req; 85 | var res = this.res; 86 | var user = req.owner; 87 | var method = PermissionService.getMethod(req); 88 | var isResponseArray = _.isArray(_data); 89 | 90 | var data = isResponseArray ? _data : [_data]; 91 | 92 | // remove undefined, since that is invalid input for waterline-criteria 93 | data = data.filter(item => { return item !== undefined }) 94 | 95 | var permitted = data.reduce(function(memo, item) { 96 | criteria.some(function(crit) { 97 | var filtered = wlFilter([item], { 98 | where: { 99 | or: [crit.where || {}] 100 | } 101 | }).results; 102 | 103 | if (filtered.length) { 104 | 105 | if (crit.blacklist && crit.blacklist.length) { 106 | crit.blacklist.forEach(function (term) { 107 | delete item[term]; 108 | }); 109 | } 110 | memo.push(item); 111 | return true; 112 | } 113 | }); 114 | return memo; 115 | }, []); 116 | 117 | if (isResponseArray) { 118 | return res._ok(permitted, options); 119 | } else if (permitted.length === 0) { 120 | sails.log.silly('permitted.length === 0'); 121 | return res.send(404); 122 | } else { 123 | res._ok(permitted[0], options); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /api/policies/ModelPolicy.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | /** 4 | * Simplified version of sails/lib/hooks/blueprints/actionUtil 5 | * see: https://github.com/balderdashy/sails/blob/b4eed1775d01f436b263362180eb3f8447af1b87/lib/hooks/blueprints/actionUtil.js#L302 6 | */ 7 | function parseModel (req) { 8 | return req.options.model || req.options.controller 9 | } 10 | 11 | /** 12 | * Query the Model that is being acted upon, and set it on the req object. 13 | */ 14 | module.exports = function ModelPolicy (req, res, next) { 15 | var modelCache = sails.hooks.permissions._modelCache; 16 | req.options.modelIdentity = parseModel(req); 17 | 18 | if (_.isEmpty(req.options.modelIdentity)) { 19 | return next(); 20 | } 21 | 22 | req.options.modelDefinition = sails.models[req.options.modelIdentity]; 23 | req.model = modelCache[req.options.modelIdentity]; 24 | 25 | if (_.isObject(req.model) && !_.isNull(req.model.id)) { 26 | return next(); 27 | } 28 | 29 | sails.log.warn('Model [', req.options.modelIdentity, '] not found in model cache'); 30 | 31 | // if the model is not found in the cache for some reason, get it from the database 32 | Model.findOne({ identity: req.options.modelIdentity }) 33 | .then(function (model) { 34 | if (!_.isObject(model)) { 35 | req.options.unknownModel = true; 36 | 37 | model = sails.models[req.options.modelIdentity]; 38 | } 39 | 40 | req.model = model; 41 | next(); 42 | }) 43 | .catch(next); 44 | }; 45 | -------------------------------------------------------------------------------- /api/policies/OwnerPolicy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO - this is setting createdBy, not owner. 3 | * The comment below, and the name of this file/function is confusing to me 4 | * Ensure that the 'owner' property of an Object is set upon creation. 5 | */ 6 | module.exports = function OwnerPolicy (req, res, next) { 7 | //sails.log('OwnerPolicy()'); 8 | if (!req.user || !req.user.id) { 9 | req.logout(); 10 | return res.send(500, new Error('req.user is not set')); 11 | } 12 | 13 | /* 14 | sails.log.verbose('OwnerPolicy user', req.user); 15 | sails.log.verbose('OwnerPolicy method', req.method); 16 | sails.log.verbose('OwnerPolicy req.body', req.body); 17 | */ 18 | 19 | if (req.options.modelDefinition.autoCreatedBy === false) { 20 | // sails.log.verbose('OwnerPolicy hasOwnershipPolicy: false'); 21 | return next(); 22 | } 23 | 24 | if ('POST' == req.method) { 25 | //req.body || (req.body = { }); 26 | req.body.createdBy = req.user.id; 27 | req.body.owner = req.user.id; 28 | } 29 | 30 | //sails.log.verbose('OwnerPolicy req.model', req.model); 31 | next(); 32 | }; 33 | -------------------------------------------------------------------------------- /api/policies/PermissionPolicy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PermissionPolicy 3 | * @depends OwnerPolicy 4 | * @depends ModelPolicy 5 | * 6 | * In order to proceed to the controller, the following verifications 7 | * must pass: 8 | * 1. User is logged in (handled previously by sails-auth sessionAuth policy) 9 | * 2. User has Permission to perform action on Model 10 | * 3. User has Permission to perform action on Attribute (if applicable) [TODO] 11 | * 4. User is satisfactorily related to the Object's owner (if applicable) 12 | * 13 | * This policy verifies #1-2 here, before any controller is invoked. However 14 | * it is not generally possible to determine ownership relationship until after 15 | * the object has been queried. Verification of #4 occurs in RolePolicy. 16 | * 17 | * @param {Object} req 18 | * @param {Object} res 19 | * @param {Function} next 20 | */ 21 | module.exports = function (req, res, next) { 22 | var options = { 23 | model: req.model, 24 | method: req.method, 25 | user: req.user 26 | }; 27 | 28 | if (req.options.unknownModel) { 29 | return next(); 30 | } 31 | 32 | PermissionService 33 | .findModelPermissions(options) 34 | .then(function (permissions) { 35 | sails.log.silly('PermissionPolicy:', permissions.length, 'permissions grant', 36 | req.method, 'on', req.model.name, 'for', req.user.username); 37 | 38 | if (!permissions || permissions.length === 0) { 39 | return res.send(403, { error: PermissionService.getErrorMessage(options) }); 40 | } 41 | 42 | req.permissions = permissions; 43 | 44 | next(); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /api/policies/RolePolicy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * RolePolicy 3 | * @depends PermissionPolicy 4 | * @depends OwnerPolicy 5 | * @depends ModelPolicy 6 | * 7 | * Verify that User is satisfactorily related to the Object's owner. 8 | * By this point, we know we have some permissions related to the action and object 9 | * If they are 'owner' permissions, verify that the objects that are being accessed are owned by the current user 10 | */ 11 | import _ from 'lodash' 12 | 13 | module.exports = function(req, res, next) { 14 | var permissions = req.permissions; 15 | var relations = _.groupBy(permissions, 'relation'); 16 | var action = PermissionService.getMethod(req.method); 17 | 18 | // continue if there exist role Permissions which grant the asserted privilege 19 | if (!_.isEmpty(relations.role)) { 20 | return next(); 21 | } 22 | if (req.options.unknownModel) { 23 | return next(); 24 | } 25 | 26 | /* 27 | * This block allows us to filter reads by the owner attribute, rather than failing an entire request 28 | * if some of the results are not owned by the user. 29 | * 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. 30 | * There is no notion of 'create' for an owner permission, so it is not relevant here. 31 | */ 32 | if (!_.contains(['update', 'delete'], action) && req.options.modelDefinition.attributes.owner) { 33 | // Some parsing must happen on the query down the line, 34 | // as req.query has no impact on the results from PermissionService.findTargetObjects. 35 | // I had to look at the actionUtil parseCriteria method to see where to augment the criteria 36 | req.params.all().where = req.params.all().where || {}; 37 | req.params.all().where.owner = req.user.id; 38 | req.query.owner = req.user.id; 39 | _.isObject(req.body) && (req.body.owner = req.user.id); 40 | } 41 | 42 | PermissionService.findTargetObjects(req) 43 | .then(function(objects) { 44 | // PermissionService.isAllowedToPerformAction checks if the user has 'user' based permissions (vs role or owner based permissions) 45 | return PermissionService.isAllowedToPerformAction(objects, req.user, action, ModelService.getTargetModelName(req), req.body) 46 | .then(function(hasUserPermissions) { 47 | if (hasUserPermissions) { 48 | return next(); 49 | } 50 | if (PermissionService.hasForeignObjects(objects, req.user)) { 51 | return res.send(403, { 52 | error: 'Cannot perform action [' + action + '] on foreign object' 53 | }); 54 | } 55 | next(); 56 | }); 57 | 58 | }) 59 | .catch(next); 60 | }; 61 | -------------------------------------------------------------------------------- /api/services/ModelService.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | var pluralize = require('pluralize'); 3 | 4 | module.exports = { 5 | /** 6 | * Return the type of model acted upon by this request. 7 | */ 8 | getTargetModelName: function (req) { 9 | // TODO there has to be a more sails-y way to do this without including 10 | // external modules 11 | if (_.isString(req.options.alias)) { 12 | sails.log.silly('singularizing', req.options.alias, 'to use as target model'); 13 | return pluralize.singular(req.options.alias); 14 | } else if (_.isString(req.options.model)) { 15 | return req.options.model; 16 | } else { 17 | return req.model && req.model.identity; 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /api/services/PermissionService.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var methodMap = { 4 | POST: 'create', 5 | GET: 'read', 6 | PUT: 'update', 7 | DELETE: 'delete' 8 | }; 9 | 10 | var wlFilter = require('waterline-criteria'); 11 | 12 | module.exports = { 13 | 14 | /** 15 | * Given an object, or a list of objects, return true if the list contains 16 | * objects not owned by the specified user. 17 | */ 18 | hasForeignObjects: function(objects, user) { 19 | if (!_.isArray(objects)) { 20 | return PermissionService.isForeignObject(user.id)(objects); 21 | } 22 | return _.any(objects, PermissionService.isForeignObject(user.id)); 23 | }, 24 | 25 | /** 26 | * Return whether the specified object is NOT owned by the specified user. 27 | */ 28 | isForeignObject: function(owner) { 29 | return function(object) { 30 | //sails.log.verbose('object', object); 31 | //sails.log.verbose('object.owner: ', object.owner, ', owner:', owner); 32 | return object.owner !== owner; 33 | }; 34 | }, 35 | 36 | /** 37 | * Find objects that some arbitrary action would be performed on, given the 38 | * same request. 39 | * 40 | * @param options.model 41 | * @param options.query 42 | * 43 | * TODO this will be less expensive when waterline supports a caching layer 44 | */ 45 | findTargetObjects: function(req) { 46 | 47 | 48 | // handle add/remove routes that have :parentid as the primary key field 49 | var originalId; 50 | if (req.params.parentid) { 51 | originalId = req.params.id; 52 | req.params.id = req.params.parentid; 53 | } 54 | 55 | return new Promise(function(resolve, reject) { 56 | sails.hooks.blueprints.middleware.find(req, { 57 | ok: resolve, 58 | serverError: reject, 59 | // this isn't perfect, since it returns a 500 error instead of a 404 error 60 | // but it is better than crashing the app when a record doesn't exist 61 | notFound: reject 62 | }); 63 | }) 64 | .then(function(result) { 65 | if (originalId !== undefined) { 66 | req.params.id = originalId; 67 | } 68 | return result; 69 | }); 70 | }, 71 | 72 | /** 73 | * Query Permissions that grant privileges to a role/user on an action for a 74 | * model. 75 | * 76 | * @param options.method 77 | * @param options.model 78 | * @param options.user 79 | */ 80 | findModelPermissions: function(options) { 81 | var action = PermissionService.getMethod(options.method); 82 | 83 | //console.log('findModelPermissions options', options) 84 | //console.log('findModelPermissions action', action) 85 | 86 | return User.findOne(options.user.id) 87 | .populate('roles') 88 | .then(function(user) { 89 | var permissionCriteria = { 90 | model: options.model.id, 91 | action: action, 92 | or: [ 93 | { role: _.pluck(user.roles, 'id') }, 94 | { user: user.id } 95 | ] 96 | }; 97 | 98 | return Permission.find(permissionCriteria).populate('criteria') 99 | }); 100 | }, 101 | 102 | /** 103 | * Given a list of objects, determine if they all satisfy at least one permission's 104 | * where clause/attribute blacklist combination 105 | * 106 | * @param {Array of objects} objects - The result of the query, or if the action is create, 107 | * the body of the object to be created 108 | * @param {Array of Permission objects} permissions - An array of permission objects 109 | * that are relevant to this particular user query 110 | * @param {Object} attributes - The body of the request, in an update or create request. 111 | * The keys of this object are checked against the permissions blacklist 112 | * @returns boolean - True if there is at least one granted permission that allows the requested action, 113 | * otherwise false 114 | */ 115 | hasPassingCriteria: function(objects, permissions, attributes, user) { 116 | // return success if there are no permissions or objects 117 | if (_.isEmpty(permissions) || _.isEmpty(objects)) return true; 118 | 119 | if (!_.isArray(objects)) { 120 | objects = [objects]; 121 | } 122 | 123 | var criteria = permissions.reduce(function (memo, perm) { 124 | if (perm) { 125 | if (!perm.criteria || perm.criteria.length==0) { 126 | // If a permission has no criteria then it passes for all cases 127 | // (like the admin role) 128 | memo = memo.concat([{where:{}}]); 129 | } 130 | else { 131 | memo = memo.concat(perm.criteria); 132 | } 133 | if (perm.relation === 'owner') { 134 | perm.criteria.forEach(function (criteria) { 135 | criteria.owner = true; 136 | }); 137 | } 138 | return memo; 139 | } 140 | }, []); 141 | 142 | 143 | if (!_.isArray(criteria)) { 144 | criteria = [criteria]; 145 | } 146 | 147 | if (_.isEmpty(criteria)) { 148 | return true; 149 | } 150 | 151 | // every object must have at least one permission that has a passing criteria and a passing attribute check 152 | return objects.every(function(obj) { 153 | return criteria.some(function(criteria) { 154 | var match = wlFilter([obj], { 155 | where: criteria.where 156 | }).results; 157 | var hasUnpermittedAttributes = PermissionService.hasUnpermittedAttributes(attributes, criteria.blacklist); 158 | var hasOwnership = true; // edge case for scenario where a user has some permissions that are owner based and some that are role based 159 | if (criteria.owner) { 160 | hasOwnership = !PermissionService.isForeignObject(user)(obj); 161 | } 162 | return match.length === 1 && !hasUnpermittedAttributes && hasOwnership; 163 | }); 164 | }); 165 | 166 | }, 167 | 168 | hasUnpermittedAttributes: function(attributes, blacklist) { 169 | if (_.isEmpty(attributes) || _.isEmpty(blacklist)) { 170 | return false; 171 | } 172 | return _.intersection(Object.keys(attributes), blacklist).length ? true : false; 173 | }, 174 | 175 | /** 176 | * Return true if the specified model supports the ownership policy; false 177 | * otherwise. 178 | */ 179 | hasOwnershipPolicy: function(model) { 180 | return model.autoCreatedBy; 181 | }, 182 | 183 | /** 184 | * Build an error message 185 | */ 186 | getErrorMessage: function(options) { 187 | var user = options.user.email || options.user.username 188 | return [ 189 | 'User', user, 'is not permitted to', options.method, options.model.name 190 | ].join(' '); 191 | }, 192 | 193 | /** 194 | * Given an action, return the CRUD method it maps to. 195 | */ 196 | getMethod: function(method) { 197 | return methodMap[method]; 198 | }, 199 | 200 | /** 201 | * create a new role 202 | * @param options 203 | * @param options.name {string} - role name 204 | * @param options.permissions {permission object, or array of permissions objects} 205 | * @param options.permissions.model {string} - the name of the model that the permission is associated with 206 | * @param options.permissions.criteria - optional criteria object 207 | * @param options.permissions.criteria.where - optional waterline query syntax object for specifying permissions 208 | * @param options.permissions.criteria.blacklist {string array} - optional attribute blacklist 209 | * @param options.users {array of user names} - optional array of user ids that have this role 210 | */ 211 | createRole: function(options) { 212 | 213 | var ok = Promise.resolve(); 214 | var permissions = options.permissions; 215 | 216 | if (!_.isArray(permissions)) { 217 | permissions = [permissions]; 218 | } 219 | 220 | 221 | // look up the model id based on the model name for each permission, and change it to an id 222 | ok = ok.then(function() { 223 | return Promise.all(permissions.map(function(permission) { 224 | return Model.findOne({ 225 | name: permission.model 226 | }) 227 | .then(function(model) { 228 | permission.model = model.id; 229 | return permission; 230 | }); 231 | })); 232 | }); 233 | 234 | // look up user ids based on usernames, and replace the names with ids 235 | ok = ok.then(function(permissions) { 236 | if (options.users) { 237 | return User.find({ 238 | username: options.users 239 | }) 240 | .then(function(users) { 241 | options.users = users; 242 | }); 243 | } 244 | }); 245 | 246 | ok = ok.then(function(users) { 247 | return Role.create(options); 248 | }); 249 | 250 | return ok; 251 | }, 252 | 253 | /** 254 | * 255 | * @param options {permission object, or array of permissions objects} 256 | * @param options.role {string} - the role name that the permission is associated with, 257 | * either this or user should be supplied, but not both 258 | * @param options.user {string} - the user than that the permission is associated with, 259 | * either this or role should be supplied, but not both 260 | * @param options.model {string} - the model name that the permission is associated with 261 | * @param options.action {string} - the http action that the permission allows 262 | * @param options.criteria - optional criteria object 263 | * @param options.criteria.where - optional waterline query syntax object for specifying permissions 264 | * @param options.criteria.blacklist {string array} - optional attribute blacklist 265 | */ 266 | grant: function(permissions) { 267 | if (!_.isArray(permissions)) { 268 | permissions = [permissions]; 269 | } 270 | 271 | // look up the models based on name, and replace them with ids 272 | var ok = Promise.all(permissions.map(function(permission) { 273 | var findRole = permission.role ? Role.findOne({ 274 | name: permission.role 275 | }) : null; 276 | var findUser = permission.user ? User.findOne({ 277 | username: permission.user 278 | }) : null; 279 | return Promise.all([findRole, findUser, Model.findOne({ 280 | name: permission.model 281 | })]) 282 | .then(([ role, user, model]) => { 283 | permission.model = model.id; 284 | if (role && role.id) { 285 | permission.role = role.id; 286 | } else if (user && user.id) { 287 | permission.user = user.id; 288 | } else { 289 | return Promise.reject(new Error('no role or user specified')); 290 | } 291 | }); 292 | })); 293 | 294 | ok = ok.then(function() { 295 | return Permission.create(permissions); 296 | }); 297 | 298 | return ok; 299 | }, 300 | 301 | /** 302 | * add one or more users to a particular role 303 | * TODO should this work with multiple roles? 304 | * @param usernames {string or string array} - list of names of users 305 | * @param rolename {string} - the name of the role that the users should be added to 306 | */ 307 | addUsersToRole: function(usernames, rolename) { 308 | if (_.isEmpty(usernames)) { 309 | return Promise.reject(new Error('One or more usernames must be provided')); 310 | } 311 | 312 | if (!_.isArray(usernames)) { 313 | usernames = [usernames]; 314 | } 315 | 316 | return Role.findOne({ 317 | name: rolename 318 | }).populate('users').then(function(role) { 319 | return User.find({ 320 | username: usernames 321 | }).then(function(users) { 322 | role.users.add(_.pluck(users, 'id')); 323 | return role.save(); 324 | }); 325 | }); 326 | }, 327 | 328 | /** 329 | * remove one or more users from a particular role 330 | * TODO should this work with multiple roles 331 | * @params usernames {string or string array} - name or list of names of users 332 | * @params rolename {string} - the name of the role that the users should be removed from 333 | */ 334 | removeUsersFromRole: function(usernames, rolename) { 335 | if (_.isEmpty(usernames)) { 336 | return Promise.reject(new Error('One or more usernames must be provided')); 337 | } 338 | 339 | if (!_.isArray(usernames)) { 340 | usernames = [usernames]; 341 | } 342 | 343 | return Role.findOne({ 344 | name: rolename 345 | }) 346 | .populate('users') 347 | .then(function(role) { 348 | return User.find({ 349 | username: usernames 350 | }, { 351 | select: ['id'] 352 | }).then(function(users) { 353 | users.map(function(user) { 354 | role.users.remove(user.id); 355 | }); 356 | return role.save(); 357 | }); 358 | }); 359 | }, 360 | 361 | /** 362 | * revoke permission from role 363 | * @param options 364 | * @param options.role {string} - the name of the role related to the permission. This, or options.user should be set, but not both. 365 | * @param options.user {string} - the name of the user related to the permission. This, or options.role should be set, but not both. 366 | * @param options.model {string} - the name of the model for the permission 367 | * @param options.action {string} - the name of the action for the permission 368 | * @param options.relation {string} - the type of the relation (owner or role) 369 | */ 370 | revoke: function(options) { 371 | var findRole = options.role ? Role.findOne({ 372 | name: options.role 373 | }) : null; 374 | var findUser = options.user ? User.findOne({ 375 | username: options.user 376 | }) : null; 377 | var ok = Promise.all([findRole, findUser, Model.findOne({ 378 | name: options.model 379 | })]); 380 | 381 | ok = ok.then(([ role, user, model ]) => { 382 | 383 | var query = { 384 | model: model.id, 385 | action: options.action, 386 | relation: options.relation 387 | }; 388 | 389 | if (role && role.id) { 390 | query.role = role.id; 391 | } else if (user && user.id) { 392 | query.user = user.id; 393 | } else { 394 | return Promise.reject(new Error('You must provide either a user or role to revoke the permission from')); 395 | } 396 | 397 | return Permission.destroy(query); 398 | }); 399 | 400 | return ok; 401 | }, 402 | 403 | /** 404 | * Check if the user (out of role) is granted to perform action on given objects 405 | * @param objects 406 | * @param user 407 | * @param action 408 | * @param model 409 | * @param body 410 | * @returns {*} 411 | */ 412 | isAllowedToPerformAction: function(objects, user, action, model, body) { 413 | if (!_.isArray(objects)) { 414 | return PermissionService.isAllowedToPerformSingle(user.id, action, model, body)(objects); 415 | } 416 | return Promise.all(objects.map(PermissionService.isAllowedToPerformSingle(user.id, action, model, body))) 417 | .then(function (allowedArray) { 418 | return allowedArray.every(function (allowed) { 419 | return allowed === true; 420 | }); 421 | }); 422 | }, 423 | 424 | /** 425 | * Resolve if the user have the permission to perform this action 426 | * @param user 427 | * @param action 428 | * @param model 429 | * @param body 430 | * @returns {Function} 431 | */ 432 | isAllowedToPerformSingle: function(user, action, model, body) { 433 | return function(obj) { 434 | return new Promise(function(resolve, reject) { 435 | Model.findOne({ 436 | identity: model 437 | }).then(function(model) { 438 | return Permission.find({ 439 | model: model.id, 440 | action: action, 441 | relation: 'user', 442 | user: user 443 | }).populate('criteria'); 444 | }).then(function(permission) { 445 | if (permission.length > 0 && PermissionService.hasPassingCriteria(obj, permission, body)) { 446 | resolve(true); 447 | } else { 448 | resolve(false); 449 | } 450 | }).catch(reject); 451 | }); 452 | }; 453 | } 454 | }; 455 | -------------------------------------------------------------------------------- /config/env/testing.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var path = require('path'); 3 | 4 | /** 5 | * Testing environment settings 6 | * 7 | * This file can include shared settings for a development team, 8 | * such as API keys or remote database passwords. If you're using 9 | * a version control solution for your Sails app, this file will 10 | * be committed to your repository unless you add it to your .gitignore 11 | * file. If your repository will be publicly viewable, don't add 12 | * any private information to this file! 13 | * 14 | */ 15 | module.exports = { 16 | log: { level: 'debug' }, 17 | models: { 18 | migrate: 'drop', 19 | connection: 'testing' 20 | }, 21 | connections: { 22 | testing: { 23 | adapter: 'waterline-postgresql' 24 | } 25 | }, 26 | hooks: { grunt: false }, 27 | port: 1336, 28 | routes: { 29 | "DELETE /role/:parentid/users/:id": { 30 | controller: 'RoleController', 31 | action: 'remove', 32 | alias: 'users' 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /config/fixtures/model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates database representations of the Model types. 3 | * 4 | * @public 5 | */ 6 | import _ from 'lodash' 7 | exports.createModels = function () { 8 | sails.log.verbose('sails-permissions: syncing waterline models'); 9 | 10 | var models = _.compact(_.map(sails.models, function (model, name) { 11 | return model && model.globalId && model.identity && { 12 | name: model.globalId, 13 | identity: model.identity, 14 | attributes: _.omit(model.attributes, _.functions(model.attributes)) 15 | }; 16 | })); 17 | 18 | return Promise.all(_.map(models, function (model) { 19 | return sails.models.model.findOrCreate({ name: model.name }, model); 20 | })); 21 | }; 22 | -------------------------------------------------------------------------------- /config/fixtures/permission.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var grants = { 4 | admin: [ 5 | { action: 'create' }, 6 | { action: 'read' }, 7 | { action: 'update' }, 8 | { action: 'delete' } 9 | ], 10 | registered: [ 11 | { action: 'create' }, 12 | { action: 'read' } 13 | ], 14 | public: [ 15 | { action: 'read' } 16 | ] 17 | }; 18 | 19 | var modelRestrictions = { 20 | registered: [ 21 | 'Role', 22 | 'Permission', 23 | 'User', 24 | 'Passport' 25 | ], 26 | public: [ 27 | 'Role', 28 | 'Permission', 29 | 'User', 30 | 'Model', 31 | 'Passport' 32 | ] 33 | }; 34 | 35 | // TODO let users override this in the actual model definition 36 | 37 | /** 38 | * Create default Role permissions 39 | */ 40 | exports.create = function (roles, models, admin, config) { 41 | return Promise.all([ 42 | grantAdminPermissions(roles, models, admin, config), 43 | grantRegisteredPermissions(roles, models, admin, config) 44 | ]) 45 | .then(function (permissions) { 46 | //sails.log.verbose('created', permissions.length, 'permissions'); 47 | return permissions; 48 | }); 49 | }; 50 | 51 | function grantAdminPermissions (roles, models, admin, config) { 52 | var adminRole = _.find(roles, { name: 'admin' }); 53 | var permissions = _.flatten(_.map(models, function (modelEntity) { 54 | //var model = sails.models[modelEntity.identity]; 55 | grants.admin = _.get(config, 'grants.admin') || grants.admin; 56 | 57 | return _.map(grants.admin, function (permission) { 58 | var newPermission = { 59 | model: modelEntity.id, 60 | action: permission.action, 61 | role: adminRole.id, 62 | }; 63 | return sails.models.permission.findOrCreate(newPermission, newPermission); 64 | }); 65 | })); 66 | 67 | return Promise.all(permissions); 68 | } 69 | 70 | function grantRegisteredPermissions (roles, models, admin, config) { 71 | var registeredRole = _.find(roles, { name: 'registered' }); 72 | var basePermissions = [ 73 | { 74 | model: _.find(models, { name: 'Permission' }).id, 75 | action: 'read', 76 | role: registeredRole.id 77 | }, 78 | { 79 | model: _.find(models, { name: 'Model' }).id, 80 | action: 'read', 81 | role: registeredRole.id 82 | }, 83 | { 84 | model: _.find(models, { name: 'User' }).id, 85 | action: 'update', 86 | role: registeredRole.id, 87 | relation: 'owner' 88 | }, 89 | { 90 | model: _.find(models, { name: 'User' }).id, 91 | action: 'read', 92 | role: registeredRole.id, 93 | relation: 'owner' 94 | } 95 | ]; 96 | 97 | // XXX copy/paste from above. terrible. improve. 98 | var permittedModels = _.filter(models, function (model) { 99 | return !_.contains(modelRestrictions.registered, model.name); 100 | }); 101 | var grantPermissions = _.flatten(_.map(permittedModels, function (modelEntity) { 102 | 103 | grants.registered = _.get(config, 'grants.registered') || grants.registered; 104 | 105 | return _.map(grants.registered, function (permission) { 106 | return { 107 | model: modelEntity.id, 108 | action: permission.action, 109 | role: registeredRole.id, 110 | }; 111 | }); 112 | })); 113 | 114 | 115 | return Promise.all( 116 | [ ...basePermissions, ...grantPermissions ].map(permission => { 117 | return sails.models.permission.findOrCreate(permission, permission); 118 | }) 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /config/fixtures/role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates default Roles 3 | * 4 | * @public 5 | */ 6 | exports.create = function () { 7 | return Promise.all([ 8 | sails.models.role.findOrCreate({ name: 'admin' }, { name: 'admin' }), 9 | sails.models.role.findOrCreate({ name: 'registered' }, { name: 'registered' }), 10 | sails.models.role.findOrCreate({ name: 'public' }, { name: 'public' }) 11 | ]); 12 | }; 13 | -------------------------------------------------------------------------------- /config/fixtures/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create admin user. 3 | * @param adminRole - the admin role which grants all permissions 4 | */ 5 | import _ from 'lodash' 6 | exports.create = function (roles, userModel) { 7 | if (_.isEmpty(sails.config.permissions.adminUsername)) { 8 | throw new Error('sails.config.permissions.adminUsername is not set'); 9 | } 10 | if (_.isEmpty(sails.config.permissions.adminPassword)) { 11 | throw new Error('sails.config.permissions.adminPassword is not set'); 12 | } 13 | if (_.isEmpty(sails.config.permissions.adminEmail)) { 14 | throw new Error('sails.config.permissions.adminEmail is not set'); 15 | } 16 | return sails.models.user.findOne({ username: sails.config.permissions.adminUsername }) 17 | .then(function (user) { 18 | if (user) return user; 19 | 20 | sails.log.info('sails-permissions: admin user does not exist; creating...'); 21 | return sails.models.user.register({ 22 | username: sails.config.permissions.adminUsername, 23 | password: sails.config.permissions.adminPassword, 24 | email: sails.config.permissions.adminEmail, 25 | roles: [ _.find(roles, { name: 'admin' }).id ], 26 | createdBy: 1, 27 | owner: 1, 28 | model: userModel.id 29 | }); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /config/permissions.js: -------------------------------------------------------------------------------- 1 | module.exports.permissions = { 2 | name: 'permissions', 3 | 4 | adminEmail: process.env.ADMIN_EMAIL || 'admin@example.com', 5 | adminUsername: process.env.ADMIN_USERNAME || 'admin', 6 | adminPassword: process.env.ADMIN_PASSWORD || 'admin1234', 7 | 8 | afterEvents: [ 9 | 'hook:auth:initialized' 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /config/policies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Policy Mappings 3 | * (sails.config.policies) 4 | * 5 | * Policies are simple functions which run **before** your controllers. 6 | * You can apply one or more policies to a given controller, or protect 7 | * its actions individually. 8 | * 9 | * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed 10 | * below by its filename, minus the extension, (e.g. "authenticated") 11 | * 12 | * For more information on how policies work, see: 13 | * http://sailsjs.org/#/documentation/concepts/Policies 14 | * 15 | * For more information on configuring policies, check out: 16 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.policies.html 17 | */ 18 | 19 | module.exports.policies = { 20 | 21 | '*': [ 22 | 'basicAuth', 23 | 'passport', 24 | 'sessionAuth', 25 | 'ModelPolicy', 26 | 'AuditPolicy', 27 | 'OwnerPolicy', 28 | 'PermissionPolicy', 29 | 'RolePolicy', 30 | 'CriteriaPolicy' 31 | ], 32 | 33 | AuthController: { 34 | '*': [ 'passport' ] 35 | }, 36 | 37 | /*************************************************************************** 38 | * * 39 | * Default policy for all controllers and actions (`true` allows public * 40 | * access) * 41 | * * 42 | ***************************************************************************/ 43 | 44 | // '*': true, 45 | 46 | /*************************************************************************** 47 | * * 48 | * Here's an example of mapping some policies to run before a controller * 49 | * and its actions * 50 | * * 51 | ***************************************************************************/ 52 | // RabbitController: { 53 | 54 | // Apply the `false` policy as the default for all of RabbitController's actions 55 | // (`false` prevents all access, which ensures that nothing bad happens to our rabbits) 56 | // '*': false, 57 | 58 | // For the action `nurture`, apply the 'isRabbitMother' policy 59 | // (this overrides `false` above) 60 | // nurture : 'isRabbitMother', 61 | 62 | // Apply the `isNiceToAnimals` AND `hasRabbitFood` policies 63 | // before letting any users feed our rabbits 64 | // feed : ['isNiceToAnimals', 'hasRabbitFood'] 65 | // } 66 | }; 67 | -------------------------------------------------------------------------------- /config/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session Configuration 3 | * (sails.config.session) 4 | * 5 | * Sails session integration leans heavily on the great work already done by 6 | * Express, but also unifies Socket.io with the Connect session store. It uses 7 | * Connect's cookie parser to normalize configuration differences between Express 8 | * and Socket.io and hooks into Sails' middleware interpreter to allow you to access 9 | * and auto-save to `req.session` with Socket.io the same way you would with Express. 10 | * 11 | * For more information on configuring the session, check out: 12 | * http://links.sailsjs.org/docs/config/session 13 | */ 14 | 15 | module.exports.session = { 16 | secret: '00000000000000000000000000000000' 17 | }; 18 | -------------------------------------------------------------------------------- /generator.js: -------------------------------------------------------------------------------- 1 | module.exports = require('sails-generate-entities')({ 2 | module: 'sails-permissions', 3 | id: 'permissions-api', 4 | statics: [ 5 | 'config/permissions.js' 6 | ], 7 | }); 8 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var babel = require("gulp-babel"); 3 | 4 | gulp.task("default", function () { 5 | gulp.src("api/**") 6 | .pipe(babel()) 7 | .pipe(gulp.dest("dist/api")); 8 | 9 | gulp.src("config/**") 10 | .pipe(babel()) 11 | .pipe(gulp.dest("dist/config")); 12 | }); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-permissions", 3 | "version": "2.2.0", 4 | "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.", 5 | "main": "dist/api/hooks/permissions/index.js", 6 | "scripts": { 7 | "test": "gulp && mocha --reporter spec --compilers js:babel/register", 8 | "prepublish": "gulp" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/tjwebb/sails-permissions.git" 13 | }, 14 | "keywords": [ 15 | "sails", 16 | "sails.js", 17 | "permissions", 18 | "privileges", 19 | "entitlements", 20 | "access", 21 | "restriction", 22 | "passport", 23 | "grant", 24 | "roles", 25 | "security", 26 | "rbac", 27 | "acl", 28 | "enterprise", 29 | "audit", 30 | "trail", 31 | "tracking" 32 | ], 33 | "author": "Travis Webb ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/tjwebb/sails-permissions/issues" 37 | }, 38 | "homepage": "https://github.com/tjwebb/sails-permissions", 39 | "devDependencies": { 40 | "babel": "^5.8.21", 41 | "gulp": "^3.9.0", 42 | "gulp-babel": "^5.2.1", 43 | "jshint": "^2.8.0", 44 | "mocha": "^2.x.x", 45 | "request": "^2.58.0", 46 | "sails": "balderdashy/sails", 47 | "sails-memory": "^0.10.5", 48 | "supertest": "^0.15.0", 49 | "waterline-postgresql": "^0.12.0" 50 | }, 51 | "dependencies": { 52 | "fnv-plus": "^1.2.10", 53 | "lodash": "^3.10.0", 54 | "marlinspike": "^1.0", 55 | "pluralize": "^1.0.1", 56 | "sails-auth": "^2.0", 57 | "sails-generate-entities": "latest", 58 | "waterline-criteria": "^0.11.1" 59 | }, 60 | "bundledDependencies": [ 61 | "fnv-plus", 62 | "lodash", 63 | "pluralize" 64 | ], 65 | "engines": { 66 | "node": ">= 0.10", 67 | "npm": ">= 2.3" 68 | }, 69 | "sails": { 70 | "isHook": true, 71 | "hookName": "permissions" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/bootstrap.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test starter - with this version of sails.js we can only start one sails server, 3 | * to solve this problem we use only one before All and after All to start and 4 | * stop the server 5 | */ 6 | var _ = require('lodash'); 7 | var Sails = require('sails'); 8 | var testingConfig = require('../config/env/testing'); 9 | var path = require('path'); 10 | var sails; 11 | 12 | before(function(done) { 13 | this.timeout(30000); 14 | 15 | var config = _.extend(testingConfig, { 16 | appPath: path.resolve(__dirname, '..') 17 | }) 18 | 19 | Sails.lift(config, function(err, server) { 20 | global.sails = server; 21 | 22 | if (err) { 23 | return done(err); 24 | } 25 | // here you can load fixtures, etc. 26 | done(err, sails); 27 | }); 28 | 29 | }); 30 | 31 | after(function(done) { 32 | global.sails.lower(done); 33 | }); 34 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --colors 3 | --timeout 30000 4 | -------------------------------------------------------------------------------- /test/unit/controllers/ModelController.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var request = require('supertest'); 3 | 4 | var adminAuth = { 5 | Authorization: 'Basic YWRtaW5AZXhhbXBsZS5jb206YWRtaW4xMjM0' 6 | }; 7 | 8 | var registeredAuth = { 9 | Authorization: 'Basic bmV3dXNlcjp1c2VyMTIzNA==' 10 | }; 11 | 12 | describe('Model Controller', function () { 13 | 14 | describe('User with Admin Role', function () { 15 | 16 | describe('#find()', function () { 17 | 18 | it('should be able to read models', function (done) { 19 | 20 | request(sails.hooks.http.app) 21 | .get('/model') 22 | .set('Authorization', adminAuth.Authorization) 23 | .expect(200) 24 | .end(function (err, res) { 25 | 26 | var models = res.body; 27 | 28 | assert.equal(models.length, 8); 29 | assert.equal(_.intersection(_.pluck(models, 'name'), [ 30 | 'Model', 31 | 'Permission', 32 | 'Role', 33 | 'User' 34 | ]).length, 4); 35 | 36 | done(err || models.error); 37 | 38 | }); 39 | }); 40 | 41 | }); 42 | 43 | }); 44 | 45 | describe('User with Public Role', function () { 46 | 47 | describe('#find()', function () { 48 | 49 | it('should not be able to read models', function (done) { 50 | 51 | request(sails.hooks.http.app) 52 | .get('/model') 53 | .expect(403) 54 | .end(function (err, res) { 55 | 56 | assert(_.isString(res.body.error)); 57 | done(err); 58 | 59 | }); 60 | }); 61 | 62 | }); 63 | 64 | }); 65 | 66 | }); 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/unit/controllers/PermissionController.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var request = require('supertest'); 3 | 4 | var adminAuth = { 5 | Authorization: 'Basic YWRtaW5AZXhhbXBsZS5jb206YWRtaW4xMjM0' 6 | }; 7 | 8 | describe('PermissionController', function () { 9 | 10 | var agent; 11 | before(function(done) { 12 | 13 | agent = request.agent(sails.hooks.http.app); 14 | 15 | agent 16 | .post('/user') 17 | .set('Authorization', adminAuth.Authorization) 18 | .send({ 19 | username: 'newuser1', 20 | email: 'newuser1@example.com', 21 | password: 'lalalal1234' 22 | }) 23 | .expect(200, function (err) { 24 | 25 | if (err) 26 | return done(err); 27 | 28 | agent 29 | .post("/permission") 30 | .set('Authorization', adminAuth.Authorization) 31 | .send({ 32 | model: 4, 33 | criteria: { 34 | where: { 35 | id: 1 36 | } 37 | }, 38 | action: "delete", 39 | relation: "user", 40 | user: 2 41 | }) 42 | .expect(201, function (err) { 43 | if (err) 44 | return done(err); 45 | 46 | agent 47 | .post('/auth/local') 48 | .send({ 49 | identifier: 'newuser1', 50 | password: 'lalalal1234' 51 | }) 52 | .expect(200) 53 | .end(function (err, res) { 54 | agent.saveCookies(res); 55 | return done(err); 56 | }); 57 | }); 58 | 59 | }); 60 | 61 | }); 62 | 63 | describe('Permission Controller', function () { 64 | 65 | describe('User with Registered Role', function () { 66 | 67 | describe('#find()', function () { 68 | 69 | it('should be able to read permissions', function (done) { 70 | 71 | agent 72 | .get('/permission') 73 | .expect(200) 74 | .end(function (err, res) { 75 | 76 | var permissions = res.body; 77 | 78 | assert.ifError(permissions.error); 79 | done(err || permissions.error); 80 | 81 | }); 82 | 83 | }); 84 | 85 | }); 86 | 87 | }); 88 | 89 | describe('User with Registered Role and granted to delete Permission 1', function () { 90 | describe("#delete()", function () { 91 | it('should be able to delete permission 1', function (done) { 92 | agent 93 | .delete("/permission/1") 94 | .expect(200) 95 | .end(function (err, res) { 96 | var permissions = res.body; 97 | 98 | assert.ifError(permissions.error); 99 | done(err || permissions.error); 100 | }); 101 | }); 102 | }); 103 | }); 104 | 105 | }); 106 | 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /test/unit/controllers/UserController.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var request = require('supertest'); 3 | var _ = require('lodash'); 4 | 5 | var adminAuth = { 6 | Authorization: 'Basic YWRtaW5AZXhhbXBsZS5jb206YWRtaW4xMjM0' 7 | }; 8 | var registeredAuth = { 9 | Authorization: 'Basic bmV3dXNlcjp1c2VyMTIzNA==' 10 | }; 11 | var newUserAuth = { 12 | Authorization: 'Basic bmV3dXNlcjp1c2VyMTIzNA==' 13 | }; 14 | 15 | describe('User Controller', function() { 16 | 17 | var adminUserId; 18 | var newUserId; 19 | var roleId; 20 | var inactiveRoleId; 21 | 22 | describe('User with Admin Role', function() { 23 | 24 | describe('#find()', function() { 25 | 26 | it('should be able to read all users', function(done) { 27 | 28 | request(sails.hooks.http.app) 29 | .get('/user') 30 | .set('Authorization', adminAuth.Authorization) 31 | .expect(200) 32 | .end(function(err, res) { 33 | 34 | var users = res.body; 35 | 36 | assert.ifError(err); 37 | assert.ifError(users.error); 38 | assert.equal(users[0].username, 'admin'); 39 | adminUserId = users[0].id; 40 | 41 | done(err); 42 | 43 | }); 44 | }); 45 | 46 | it('should be able to remove a user from a role', function(done) { 47 | var ok = Role.find({ 48 | name: 'registered' 49 | }); 50 | 51 | ok.then(function(role) { 52 | request(sails.hooks.http.app) 53 | .post('/user') 54 | .set('Authorization', adminAuth.Authorization) 55 | .send({ 56 | username: 'abouttoberemoveduser', 57 | email: 'abouttoberemoveduser@example.com', 58 | password: 'user1234' 59 | }) 60 | .expect(200) 61 | .end(function(err, res) { 62 | 63 | assert.ifError(err); 64 | var user = res.body; 65 | Role.findOne({ 66 | name: 'registered' 67 | }).populate('users', { 68 | id: user.id 69 | }) 70 | .then(function(role) { 71 | assert.equal(user.username, 'abouttoberemoveduser'); 72 | assert(_.contains(_.pluck(role.users, 'id'), user.id)); 73 | 74 | request(sails.hooks.http.app) 75 | .delete('/role/' + role.id + '/users/' + user.id) 76 | .set('Authorization', adminAuth.Authorization) 77 | .expect(200) 78 | .end(function(err, res) { 79 | // the user id should not be in the list anymore 80 | assert(!_.includes(_.pluck(res.users, 'id'), user.id)); 81 | done(); 82 | }); 83 | }); 84 | 85 | }); 86 | }) 87 | .catch(done); 88 | 89 | }); 90 | 91 | }); 92 | 93 | describe('#create()', function() { 94 | 95 | it('should be able to create a new user', function(done) { 96 | 97 | request(sails.hooks.http.app) 98 | .post('/user') 99 | .set('Authorization', adminAuth.Authorization) 100 | .send({ 101 | username: 'newuser', 102 | email: 'newuser@example.com', 103 | password: 'user1234' 104 | }) 105 | .expect(200) 106 | .end(function(err, res) { 107 | 108 | var user = res.body; 109 | 110 | assert.ifError(err); 111 | assert.ifError(user.error); 112 | assert.equal(user.username, 'newuser'); 113 | newUserId = user.id; 114 | 115 | done(err); 116 | 117 | }); 118 | 119 | }); 120 | 121 | it('should return an error if a user already exists', function(done) { 122 | 123 | request(sails.hooks.http.app) 124 | .post('/user') 125 | .set('Authorization', adminAuth.Authorization) 126 | .send({ 127 | username: 'newuser', 128 | email: 'newuser@example.com', 129 | password: 'user1234' 130 | }) 131 | .expect(400) 132 | .end(function(err) { 133 | done(err); 134 | }); 135 | 136 | }); 137 | 138 | it('should be able to create a new role, and assign a new user to it', function(done) { 139 | 140 | request(sails.hooks.http.app) 141 | .post('/role') 142 | .set('Authorization', adminAuth.Authorization) 143 | .send({ 144 | name: 'testrole', 145 | users: [newUserId] 146 | }) 147 | .expect(201) 148 | .end(function(err, res) { 149 | roleId = res.body.id; // 4 150 | done(err); 151 | }); 152 | }); 153 | 154 | it('should be able to create a new permission for updating active roles', function(done) { 155 | request(sails.hooks.http.app) 156 | .get('/model?name=role') 157 | .set('Authorization', adminAuth.Authorization) 158 | .expect(200) 159 | .end(function(err, res) { 160 | 161 | // haha roleModel 162 | var roleModel = res.body[0]; 163 | 164 | request(sails.hooks.http.app) 165 | .post('/permission') 166 | .set('Authorization', adminAuth.Authorization) 167 | .send({ 168 | model: roleModel.id, 169 | action: 'update', 170 | role: roleId, 171 | createdBy: adminUserId, 172 | criteria: { 173 | blacklist: ['id', 'stream'], 174 | where: { 175 | active: true 176 | } 177 | } 178 | }) 179 | .expect(201) 180 | .end(function(err, res) { 181 | done(err); 182 | }); 183 | 184 | }); 185 | }); 186 | 187 | it('should be able to create a new permission for updating owned roles', function(done) { 188 | request(sails.hooks.http.app) 189 | .get('/model?name=role') 190 | .set('Authorization', adminAuth.Authorization) 191 | .expect(200) 192 | .end(function(err, res) { 193 | 194 | // haha roleModel 195 | var roleModel = res.body[0]; 196 | 197 | request(sails.hooks.http.app) 198 | .post('/permission') 199 | .set('Authorization', adminAuth.Authorization) 200 | .send({ 201 | model: roleModel.id, 202 | action: 'update', 203 | role: roleId, 204 | createdBy: adminUserId, 205 | criteria: { 206 | blacklist: ['id'] 207 | }, 208 | relation: 'owner' 209 | }) 210 | .expect(201) 211 | .end(function(err, res) { 212 | done(err); 213 | }); 214 | 215 | }); 216 | }); 217 | 218 | it('should be able to create a new role and set it as inactive', function(done) { 219 | request(sails.hooks.http.app) 220 | .post('/role') 221 | .set('Authorization', adminAuth.Authorization) 222 | .send({ 223 | name: 'inactiveRole', 224 | users: [newUserId], 225 | active: false 226 | }) 227 | .expect(201) 228 | .end(function(err, res) { 229 | inactiveRoleId = res.body.id; 230 | done(err); 231 | }); 232 | }); 233 | 234 | it('should be able to create a read permission with a where clause for roles and a blacklist', function(done) { 235 | 236 | request(sails.hooks.http.app) 237 | .get('/model?name=role') 238 | .set('Authorization', adminAuth.Authorization) 239 | .expect(200) 240 | .end(function(err, res) { 241 | 242 | // haha roleModel 243 | var roleModel = res.body[0]; 244 | 245 | request(sails.hooks.http.app) 246 | .post('/permission') 247 | .set('Authorization', adminAuth.Authorization) 248 | .send({ 249 | model: roleModel.id, 250 | action: 'read', 251 | role: roleId, 252 | createdBy: adminUserId, 253 | criteria: { 254 | where: { 255 | active: true 256 | }, 257 | blacklist: ['name', 'createdAt'] 258 | } 259 | }) 260 | .expect(201) 261 | .end(function(err, res) { 262 | done(err); 263 | }); 264 | }); 265 | }); 266 | 267 | it('should be able to create a read permission with a where clause for a role that should filter out all results', function(done) { 268 | 269 | request(sails.hooks.http.app) 270 | .get('/model?name=permission') 271 | .set('Authorization', adminAuth.Authorization) 272 | .expect(200) 273 | .end(function(err, res) { 274 | 275 | var permissionModel = res.body[0]; 276 | 277 | request(sails.hooks.http.app) 278 | .post('/permission') 279 | .set('Authorization', adminAuth.Authorization) 280 | .send({ 281 | model: permissionModel.id, 282 | action: 'read', 283 | role: roleId, 284 | createdBy: adminUserId, 285 | criteria: { 286 | where: { 287 | id: { 288 | '>': 99999 289 | } 290 | } 291 | } 292 | }) 293 | .expect(201) 294 | .end(function(err, res) { 295 | done(err); 296 | }); 297 | }); 298 | }); 299 | 300 | }); 301 | 302 | 303 | }); 304 | 305 | describe('User with Registered Role', function() { 306 | 307 | describe('#create()', function() { 308 | 309 | it('should not be able to create a new user', function(done) { 310 | 311 | request(sails.hooks.http.app) 312 | .post('/user') 313 | .set('Authorization', registeredAuth.Authorization) 314 | .send({ 315 | username: 'newuser1', 316 | email: 'newuser1@example.com', 317 | password: 'lalalal1234' 318 | }) 319 | .expect(403) 320 | .end(function(err, res) { 321 | 322 | var user = res.body; 323 | 324 | assert.ifError(err); 325 | assert(_.isString(user.error), JSON.stringify(user)); 326 | 327 | done(err); 328 | 329 | }); 330 | 331 | }); 332 | 333 | }); 334 | 335 | describe('#update()', function() { 336 | 337 | it('should be able to update themselves', function(done) { 338 | 339 | request(sails.hooks.http.app) 340 | .put('/user/' + newUserId) 341 | .set('Authorization', registeredAuth.Authorization) 342 | .send({ 343 | email: 'newuserupdated@example.com' 344 | }) 345 | .expect(200) 346 | .end(function(err, res) { 347 | var user = res.body; 348 | 349 | assert.ifError(err); 350 | assert.equal(user.email, 'newuserupdated@example.com'); 351 | 352 | done(err); 353 | }); 354 | 355 | }); 356 | 357 | it('should be able to update role name', function(done) { 358 | // it should be able to do this, because an earlier test set up the role and permission for it 359 | request(sails.hooks.http.app) 360 | .put('/role/' + roleId) 361 | .set('Authorization', newUserAuth.Authorization) 362 | .send({ 363 | name: 'updatedName' 364 | }) 365 | .expect(200) 366 | .end(function(err, res) { 367 | assert.ifError(err); 368 | assert.equal(res.body.name, 'updatedName'); 369 | done(err); 370 | 371 | }); 372 | 373 | }); 374 | 375 | it('should not be able to update role id', function(done) { 376 | // it should be able to do this, because an earlier test set up the role and permission for it 377 | request(sails.hooks.http.app) 378 | .put('/role/' + roleId) 379 | .set('Authorization', newUserAuth.Authorization) 380 | .send({ 381 | id: 99 382 | }) 383 | .expect(403) 384 | .end(function(err, res) { 385 | assert(res.body.hasOwnProperty('error')); 386 | assert.ifError(err); 387 | done(err); 388 | 389 | }); 390 | 391 | }); 392 | 393 | it('should not be able to update role name when role is inactive', function(done) { 394 | // attribute is ok but where clause fails 395 | request(sails.hooks.http.app) 396 | .put('/role/' + inactiveRoleId) 397 | .set('Authorization', newUserAuth.Authorization) 398 | .send({ 399 | name: 'updatedInactiveName' 400 | }) 401 | .expect(403) 402 | .end(function(err, res) { 403 | assert(res.body.hasOwnProperty('error')); 404 | assert.ifError(err); 405 | done(err); 406 | 407 | }); 408 | }); 409 | 410 | 411 | // this test depends on a previous test that set a permission with a particular where clause/blacklist 412 | it('should read only active roles, and should not have blacklisted attributes', function(done) { 413 | 414 | request(sails.hooks.http.app) 415 | .get('/role') 416 | .set('Authorization', newUserAuth.Authorization) 417 | .send({ 418 | name: 'updatedInactiveName' 419 | }) 420 | .expect(200) 421 | .end(function(err, res) { 422 | res.body.forEach(function(role) { 423 | assert(!role.hasOwnProperty('name')); 424 | assert(!role.hasOwnProperty('createdAt')); 425 | assert(role.active); 426 | }); 427 | done(err); 428 | 429 | }); 430 | }); 431 | 432 | it.skip('should have filtered out all of the permissions results', function(done) { 433 | 434 | request(sails.hooks.http.app) 435 | .get('/permission') 436 | .set('Authorization', newUserAuth.Authorization) 437 | .expect(200) 438 | .end(function(err, res) { 439 | assert.equal(res.body.length, 0); 440 | done(err); 441 | }); 442 | }); 443 | 444 | it('should return data from /user/me', function(done) { 445 | 446 | request(sails.hooks.http.app) 447 | .get('/user/me') 448 | .set('Authorization', newUserAuth.Authorization) 449 | .expect(200) 450 | .end(function(err, res) { 451 | assert.ifError(err); 452 | assert(res.body.username == 'newuser'); 453 | done(err); 454 | }); 455 | }); 456 | 457 | it('should not be able to update another user', function(done) { 458 | 459 | request(sails.hooks.http.app) 460 | .put('/user/' + adminUserId) 461 | .set('Authorization', registeredAuth.Authorization) 462 | .send({ 463 | email: 'crapadminemail@example.com' 464 | }) 465 | .expect(403) 466 | .end(function(err, res) { 467 | 468 | var user = res.body; 469 | 470 | assert.ifError(err); 471 | assert(_.isString(user.error), JSON.stringify(user)); 472 | 473 | done(err); 474 | 475 | }); 476 | 477 | }); 478 | 479 | 480 | 481 | }); 482 | 483 | }); 484 | 485 | }); 486 | -------------------------------------------------------------------------------- /test/unit/services/PermissionService.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | describe('Permission Service', function() { 4 | 5 | it('should exist', function() { 6 | 7 | assert.ok(sails.services.permissionservice); 8 | assert.ok(global.PermissionService); 9 | 10 | }); 11 | 12 | describe('#isForeignObject()', function() { 13 | 14 | it('should return true if object is not owned by the requesting user', function(done) { 15 | 16 | var objectNotOwnedByUser = { 17 | owner: 2 18 | }; 19 | var user = 1; 20 | 21 | assert.equal(sails.services.permissionservice.isForeignObject(user)(objectNotOwnedByUser), true); 22 | 23 | done(); 24 | 25 | }); 26 | 27 | it('should return false if object is owned by the requesting user', function(done) { 28 | 29 | var objectOwnedByUser = { 30 | owner: 1 31 | }; 32 | var user = 1; 33 | 34 | assert.equal(sails.services.permissionservice.isForeignObject(user)(objectOwnedByUser), false); 35 | 36 | done(); 37 | }); 38 | 39 | }); 40 | 41 | describe('#hasForeignObjects()', function() { 42 | 43 | it('should return true if any object is not owned by the requesting user', function(done) { 44 | 45 | var objectOwnedByUser = { 46 | owner: 1 47 | }; 48 | var objectNotOwnedByUser = { 49 | owner: 2 50 | }; 51 | var user = { 52 | id: 1 53 | }; 54 | 55 | assert.equal(sails.services.permissionservice.hasForeignObjects([objectNotOwnedByUser, objectOwnedByUser], user), true); 56 | 57 | done(); 58 | }); 59 | 60 | it('should return false if all objects are owned by the requesting user', function(done) { 61 | 62 | var objectOwnedByUser = { 63 | owner: 1 64 | }; 65 | var objectOwnedByUser2 = { 66 | owner: 1 67 | }; 68 | var user = { 69 | id: 1 70 | }; 71 | 72 | assert.equal(sails.services.permissionservice.hasForeignObjects([objectOwnedByUser2, objectOwnedByUser], user), false); 73 | done(); 74 | 75 | }); 76 | 77 | }); 78 | 79 | describe('#hasOwnershipPolicy()', function() { 80 | 81 | it('should return true if object supports ownership policy', function(done) { 82 | 83 | assert.equal(sails.services.permissionservice.hasOwnershipPolicy({ 84 | autoCreatedBy: true 85 | }), true); 86 | done(); 87 | }); 88 | 89 | it('should return false if object does not support ownership policy', function(done) { 90 | 91 | assert.equal(sails.services.permissionservice.hasOwnershipPolicy({ 92 | autoCreatedBy: false 93 | }), false); 94 | done(); 95 | 96 | }); 97 | 98 | }); 99 | 100 | describe('#getMethod()', function() { 101 | 102 | it('should return \'create\' if POST request', function(done) { 103 | 104 | assert.equal(sails.services.permissionservice.getMethod('POST'), 'create'); 105 | done(); 106 | 107 | }); 108 | 109 | it('should return \'update\' if PUT request', function(done) { 110 | 111 | assert.equal(sails.services.permissionservice.getMethod('PUT'), 'update'); 112 | done(); 113 | 114 | }); 115 | 116 | it('should return \'read\' if GET request', function(done) { 117 | 118 | assert.equal(sails.services.permissionservice.getMethod('GET'), 'read'); 119 | done(); 120 | 121 | }); 122 | 123 | it('should return \'delete\' if DELETE request', function(done) { 124 | 125 | assert.equal(sails.services.permissionservice.getMethod('DELETE'), 'delete'); 126 | done(); 127 | 128 | }); 129 | 130 | }); 131 | 132 | describe('#hasPassingCriteria()', function() { 133 | 134 | it('should return an array of items that don\'t match the given criteria', function(done) { 135 | var objects = [{ 136 | x: 1 137 | }, { 138 | x: 2 139 | }, { 140 | x: 3 141 | }]; 142 | var permissions = [{ 143 | criteria: { 144 | where: { 145 | x: 2 146 | } 147 | } 148 | }]; 149 | assert.equal(sails.services.permissionservice.hasPassingCriteria(objects, permissions), false); 150 | done(); 151 | }); 152 | 153 | it('should return an array of items that don\'t match the given criteria, if the criteria has many values for the same key', function(done) { 154 | var objects = [{ 155 | x: 1 156 | }, { 157 | x: 2 158 | }, { 159 | x: 3 160 | }]; 161 | var permissions = [{ 162 | criteria: { 163 | where: { 164 | x: 2 165 | } 166 | } 167 | }, { 168 | criteria: { 169 | where: { 170 | x: 3 171 | } 172 | } 173 | }]; 174 | assert.equal(sails.services.permissionservice.hasPassingCriteria(objects, permissions), false); 175 | done(); 176 | }); 177 | 178 | it('should return an array of items that don\'t match the given criteria, if the criteria has many values for the same key', function(done) { 179 | var objects = { 180 | x: 2 181 | }; 182 | var permissions = [{ 183 | criteria: { 184 | where: { 185 | x: 2 186 | } 187 | } 188 | }, { 189 | criteria: { 190 | where: { 191 | x: 3 192 | } 193 | } 194 | }]; 195 | assert(sails.services.permissionservice.hasPassingCriteria(objects, permissions)); 196 | done(); 197 | }); 198 | 199 | it('should return an empty array if there is no criteria', function(done) { 200 | var objects = [{ 201 | x: 1 202 | }, { 203 | x: 2 204 | }, { 205 | x: 3 206 | }]; 207 | assert(sails.services.permissionservice.hasPassingCriteria(objects)); 208 | done(); 209 | }); 210 | 211 | it('should match without where clause and blacklist', function(done) { 212 | var objects = [{ 213 | x: 1 214 | }, { 215 | x: 2 216 | }, { 217 | x: 3 218 | }]; 219 | var permissions = [{ 220 | criteria: { 221 | blacklist: ['x'] 222 | } 223 | }]; 224 | assert(sails.services.permissionservice.hasPassingCriteria(objects, permissions)); 225 | done(); 226 | }); 227 | 228 | it('should match with where clause and attributes', function(done) { 229 | var objects = [{ 230 | x: 1 231 | }, { 232 | x: 2 233 | }, { 234 | x: 3 235 | }]; 236 | var permissions = [{ 237 | criteria: { 238 | where: { 239 | x: { 240 | '>': 0 241 | } 242 | }, 243 | blacklist: ['y'] 244 | } 245 | }]; 246 | assert(sails.services.permissionservice.hasPassingCriteria(objects, permissions, { 247 | x: 5 248 | })); 249 | done(); 250 | }); 251 | 252 | it('should fail with bad where clause and good blacklist', function(done) { 253 | var objects = [{ 254 | x: 1 255 | }, { 256 | x: 2 257 | }, { 258 | x: 3 259 | }]; 260 | var permissions = [{ 261 | criteria: { 262 | where: { 263 | x: { 264 | '<': 0 265 | } 266 | }, 267 | blacklist: ['y'] 268 | } 269 | }]; 270 | assert.equal(sails.services.permissionservice.hasPassingCriteria(objects, permissions, { 271 | x: 5 272 | }), false); 273 | done(); 274 | }); 275 | 276 | it('should fail with good where clause and bad blacklist', function(done) { 277 | var objects = [{ 278 | x: 1 279 | }, { 280 | x: 2 281 | }, { 282 | x: 3 283 | }]; 284 | var permissions = [{ 285 | criteria: { 286 | where: { 287 | x: { 288 | '>': 0 289 | } 290 | }, 291 | blacklist: ['x'] 292 | } 293 | }]; 294 | assert.equal(sails.services.permissionservice.hasPassingCriteria(objects, permissions, { 295 | x: 5 296 | }), false); 297 | done(); 298 | }); 299 | 300 | }); 301 | 302 | describe('#hasUnpermittedAttributes', function() { 303 | it('should return true if any of the attributes are in the blacklist', function(done) { 304 | var attributes = { 305 | ok: 1, 306 | fine: 2 307 | }; 308 | var blacklist = ["ok", "alright", "fine"]; 309 | assert(sails.services.permissionservice.hasUnpermittedAttributes(attributes, blacklist)); 310 | done(); 311 | }); 312 | 313 | it('should return true if any attributes are not permitted', function(done) { 314 | var attributes = { 315 | ok: 1, 316 | fine: 2, 317 | whatever: 3 318 | }; 319 | var blacklist = ["ok", "alright", "fine"]; 320 | assert(sails.services.permissionservice.hasUnpermittedAttributes(attributes, blacklist)); 321 | done(); 322 | }); 323 | 324 | it('should return false if none of the keys are in the blacklist', function(done) { 325 | var attributes = { 326 | ok: 1, 327 | fine: 2, 328 | whatever: 3 329 | }; 330 | var blacklist = ["notallowed"]; 331 | assert.equal(sails.services.permissionservice.hasUnpermittedAttributes(attributes, blacklist), false); 332 | done(); 333 | }); 334 | 335 | it('should return false if there are no attributes', function(done) { 336 | var attributes = {}; 337 | var blacklist = ["ok", "alright", "fine"]; 338 | assert.equal(sails.services.permissionservice.hasUnpermittedAttributes(attributes, blacklist), false); 339 | done(); 340 | }); 341 | 342 | it('should return false if blacklist is empty', function(done) { 343 | var attributes = { 344 | ok: 1, 345 | fine: 2, 346 | whatever: 3 347 | }; 348 | var blacklist = []; 349 | assert.equal(sails.services.permissionservice.hasUnpermittedAttributes(attributes, blacklist), false); 350 | done(); 351 | }); 352 | 353 | }); 354 | 355 | describe('role and permission helpers', function() { 356 | it('should create a role', function(done) { 357 | // make sure there is no existing role with this name 358 | Role.find({ 359 | name: 'fakeRole' 360 | }) 361 | .then(function(role) { 362 | assert.equal(role.length, 0); 363 | // use the helper to create a new role 364 | var newRole = { 365 | name: 'fakeRole', 366 | permissions: [{ 367 | model: 'Permission', 368 | action: 'delete', 369 | relation: 'role', 370 | }], 371 | users: ['newuser'] 372 | }; 373 | return sails.services.permissionservice.createRole(newRole); 374 | }) 375 | .then(function(result) { 376 | // make sure the role exists now that we have created it 377 | return Role.findOne({ 378 | name: 'fakeRole' 379 | }); 380 | }) 381 | .then(function(role) { 382 | assert(role && role.id); 383 | }) 384 | .done(done, done); 385 | }); 386 | 387 | it('should create a permission', function(done) { 388 | var permissionModelId; 389 | // find any existing permission for this action, and delete it 390 | Model.findOne({ 391 | name: 'Permission' 392 | }).then(function(permissionModel) { 393 | permissionModelId = permissionModel.id; 394 | return Permission.destroy({ 395 | action: 'create', 396 | model: permissionModelId, 397 | relation: 'role' 398 | }); 399 | }) 400 | .then(function(destroyed) { 401 | // make sure we actually destroyed it 402 | return Permission.find({ 403 | action: 'create', 404 | relation: 'role', 405 | model: permissionModelId 406 | }); 407 | }) 408 | .then(function(permission) { 409 | assert.equal(permission.length, 0); 410 | // create a new permission 411 | var newPermissions = [{ 412 | role: 'fakeRole', 413 | model: 'Permission', 414 | action: 'create', 415 | relation: 'role', 416 | criteria: { 417 | where: { 418 | x: 1 419 | }, 420 | blacklist: ['y'] 421 | } 422 | }, { 423 | role: 'fakeRole', 424 | model: 'Role', 425 | action: 'update', 426 | relation: 'role', 427 | criteria: { 428 | where: { 429 | x: 1 430 | }, 431 | blacklist: ['y'] 432 | } 433 | }]; 434 | return sails.services.permissionservice.grant(newPermissions); 435 | }) 436 | .then(function(perm) { 437 | // verify that it was created 438 | return Permission.findOne({ 439 | action: 'create', 440 | relation: 'role', 441 | model: permissionModelId 442 | }); 443 | }) 444 | .then(function(permission) { 445 | assert(permission && permission.id); 446 | }) 447 | .done(done, done); 448 | }); 449 | 450 | 451 | it('should grant a permission directly to a user', function(done) { 452 | var permissionModelId; 453 | // find any existing permission for this action, and delete it 454 | Model.findOne({ 455 | name: 'Permission' 456 | }).then(function(permissionModel) { 457 | permissionModelId = permissionModel.id; 458 | return Permission.destroy({ 459 | action: 'create', 460 | model: permissionModelId, 461 | relation: 'role' 462 | }); 463 | }) 464 | .then(function(destroyed) { 465 | // make sure we actually destroyed it 466 | return Permission.find({ 467 | action: 'create', 468 | relation: 'role', 469 | model: permissionModelId 470 | }); 471 | }) 472 | .then(function(permission) { 473 | assert.equal(permission.length, 0); 474 | // create a new permission 475 | var newPermissions = [{ 476 | user: 'admin', 477 | model: 'Permission', 478 | action: 'create', 479 | relation: 'role', 480 | criteria: { 481 | where: { 482 | x: 1 483 | }, 484 | blacklist: ['y'] 485 | } 486 | }, { 487 | user: 'admin', 488 | model: 'Role', 489 | action: 'update', 490 | relation: 'role', 491 | criteria: { 492 | where: { 493 | x: 1 494 | }, 495 | blacklist: ['y'] 496 | } 497 | }]; 498 | return sails.services.permissionservice.grant(newPermissions); 499 | }) 500 | .then(function(perm) { 501 | // verify that it was created 502 | return Permission.findOne({ 503 | action: 'create', 504 | relation: 'role', 505 | model: permissionModelId 506 | }); 507 | }) 508 | .then(function(permission) { 509 | assert(permission && permission.id); 510 | }) 511 | .done(done, done); 512 | }); 513 | 514 | it('should revoke a permission', function(done) { 515 | var permissionModelId; 516 | 517 | // make sure there is already an existing permission for this case 518 | Model.findOne({ 519 | name: 'Permission' 520 | }).then(function(permissionModel) { 521 | permissionModelId = permissionModel.id; 522 | return Permission.find({ 523 | action: 'create', 524 | relation: 'role', 525 | model: permissionModelId 526 | }); 527 | }) 528 | .then(function(permission) { 529 | assert.equal(permission.length, 1); 530 | return sails.services.permissionservice.revoke({ 531 | user: 'admin', 532 | model: 'Permission', 533 | relation: 'role', 534 | action: 'create' 535 | }); 536 | }) 537 | .then(function() { 538 | return Permission.find({ 539 | action: 'create', 540 | relation: 'role', 541 | model: permissionModelId 542 | }); 543 | }) 544 | .then(function(permission) { 545 | assert.equal(permission.length, 0); 546 | }) 547 | .done(done, done); 548 | }); 549 | 550 | it('should not revoke a permission if no user or role is supplied', function(done) { 551 | var permissionModelId; 552 | var newPermissions = [{ 553 | user: 'admin', 554 | model: 'Permission', 555 | action: 'create', 556 | relation: 'role', 557 | criteria: { 558 | where: { 559 | x: 1 560 | }, 561 | blacklist: ['y'] 562 | } 563 | }, { 564 | user: 'admin', 565 | model: 'Role', 566 | action: 'update', 567 | relation: 'role', 568 | criteria: { 569 | where: { 570 | x: 1 571 | }, 572 | blacklist: ['y'] 573 | } 574 | }]; 575 | 576 | return sails.services.permissionservice.grant(newPermissions) 577 | .then(function() { 578 | // make sure there is already an existing permission for this case 579 | Model.findOne({ 580 | name: 'Permission' 581 | }).then(function(permissionModel) { 582 | permissionModelId = permissionModel.id; 583 | return Permission.find({ 584 | action: 'create', 585 | relation: 'role', 586 | model: permissionModelId 587 | }); 588 | }) 589 | .then(function(permission) { 590 | assert.equal(permission.length, 1); 591 | return sails.services.permissionservice.revoke({ 592 | model: 'Permission', 593 | relation: 'role', 594 | action: 'create' 595 | }); 596 | }) 597 | .catch(function(err) { 598 | assert.equal(err.message, 'You must provide either a user or role to revoke the permission from'); 599 | }) 600 | .then(function() { 601 | return Permission.find({ 602 | action: 'create', 603 | relation: 'role', 604 | model: permissionModelId 605 | }); 606 | }) 607 | .then(function(permission) { 608 | assert.equal(permission.length, 1); 609 | }) 610 | .done(done, done); 611 | }); 612 | }); 613 | 614 | it('should remove users from a role', function(done) { 615 | var user; 616 | var ok = User.create({ 617 | username: 'test', 618 | email: 'testemail@test.test' 619 | }); 620 | 621 | ok = ok.then(function(usr) { 622 | user = usr; 623 | return PermissionService.addUsersToRole('test', 'admin'); 624 | }); 625 | 626 | ok = ok.then(function (role) { 627 | assert(_.contains(_.pluck(role.users, 'id'), user.id)); 628 | return PermissionService.removeUsersFromRole('test', 'admin'); 629 | }); 630 | 631 | ok = ok.then(function (role) { 632 | assert(!_.contains(_.pluck(role.users, 'id'), user.id)); 633 | }) 634 | .done(done, done); 635 | 636 | }); 637 | 638 | }); 639 | //TODO: add unit tests for #findTargetObjects() 640 | 641 | //TODO: add unit tests for #findModelPermissions() 642 | 643 | }); 644 | --------------------------------------------------------------------------------