diff --git a/api/v1/controllers/UserController.js b/api/v1/controllers/UserController.js index 9b1455f..0c9d459 100644 --- a/api/v1/controllers/UserController.js +++ b/api/v1/controllers/UserController.js @@ -7,6 +7,7 @@ const middleware = require('../middleware'); const requests = require('../requests'); const scopes = require('../utils/scopes'); const roles = require('../utils/roles'); +const errors = require('../errors'); const router = require('express').Router(); @@ -117,7 +118,6 @@ function requestPasswordReset(req, res, next) { }) .catch((error) => next(error)); } - function updateContactInfo(req, res, next) { services.UserService.updateContactInfo(req.user, req.body.newEmail) .then((result) => { @@ -128,6 +128,32 @@ function updateContactInfo(req, res, next) { .catch((error) => next(error)); } +function assignNewRole(req, res, next) { + services.UserService + .findUserById(req.body.id) + .then((assignedUser) => { + if (services.UserService + .canAssign(req.user, assignedUser, req.body.role, req.originUser)) { + + services.UserService.addRole(assignedUser, req.body.role, true) + .then(() => { + services.UserService + .findUserById(assignedUser.id) + .then((updatedUser) => { + let updatedUserJson = updatedUser.toJSON(); + updatedUserJson.roles = updatedUser.related("roles").toJSON(); + res.body = updatedUserJson; + return next(); + }) + }) + + } else { + return next(new errors.UnauthorizedError()); + } + }) + .catch((error) => next(error)); +} + router.use(bodyParser.json()); router.use(middleware.auth); @@ -139,6 +165,8 @@ router.post('/reset', middleware.request(requests.ResetTokenRequest), requestPas router.get('/', middleware.permission(roles.NONE, isAuthenticated), getAuthenticatedUser); router.get('/:id(\\d+)', middleware.permission(roles.HOSTS, isRequester), getUser); router.get('/email/:email', middleware.permission(roles.HOSTS), getUserByEmail); +router.post('/assign', middleware.request(requests.RoleAssignmentRequest), + middleware.permission(roles.ORGANIZERS), assignNewRole); router.get('/github/:handle', middleware.permission(roles.HOSTS), getUserByGithubHandle); router.put('/contactinfo', middleware.request(requests.UserContactInfoRequest), middleware.permission(roles.NONE, isAuthenticated), updateContactInfo); diff --git a/api/v1/requests/RoleAssignmentRequest.js b/api/v1/requests/RoleAssignmentRequest.js new file mode 100644 index 0000000..0caa25e --- /dev/null +++ b/api/v1/requests/RoleAssignmentRequest.js @@ -0,0 +1,20 @@ +const roles = require('../utils/roles'); +const Request = require('./Request'); + +const bodyRequired = ['id', 'role']; +const bodyValidations = { + 'id': [ 'required', 'integer' ], + 'role': ['required', 'string', roles.verifyRole] +}; + +function RoleAssignmentRequest(headers, body) { + Request.call(this, headers, body); + + this.bodyRequired = bodyRequired; + this.bodyValidations = bodyValidations; +} + +RoleAssignmentRequest.prototype = Object.create(Request.prototype); +RoleAssignmentRequest.prototype.constructor = RoleAssignmentRequest; + +module.exports = RoleAssignmentRequest; diff --git a/api/v1/requests/index.js b/api/v1/requests/index.js index 94f2d05..b3b14c1 100644 --- a/api/v1/requests/index.js +++ b/api/v1/requests/index.js @@ -20,5 +20,6 @@ module.exports = { CheckInRequest: require('./CheckInRequest'), RSVPRequest: require('./RSVPRequest'), UniversalTrackingRequest: require('./UniversalTrackingRequest'), + RoleAssignmentRequest: require('./RoleAssignmentRequest'), UserContactInfoRequest: require('./UserContactInfoRequest') }; diff --git a/api/v1/services/UserService.js b/api/v1/services/UserService.js index bd53abf..ce38644 100644 --- a/api/v1/services/UserService.js +++ b/api/v1/services/UserService.js @@ -3,8 +3,22 @@ const _Promise = require('bluebird'); const _ = require('lodash'); const User = require('../models/User'); +const UserRole = require('../models/UserRole'); const errors = require('../errors'); const utils = require('../utils'); +const roles = require('../utils').roles; + +const ROLE_VALUE_MAP = {"ADMIN":2, "STAFF":1, "SPONSOR":0, "MENTOR":0, "VOLUNTEER":0, "ATTENDEE":0}; + +function maxRole(userRoles) { + let max = 0; + userRoles.forEach((roleObj) => { + if(ROLE_VALUE_MAP[roleObj.role] > max) { + max = ROLE_VALUE_MAP[roleObj.role]; + } + }); + return max; +} /** * Creates a user of the specified role. When a password is not specified, a @@ -135,6 +149,37 @@ module.exports.resetPassword = (user, password) => user .setPassword(password) .then((updated) => updated.save()); +/** + * Adds role to a user + * @param {User} user assigning new role + * @param {User} assignedUser the assigned User's model + * @param {Role} newRole the new role + * @param {User} originUser the original user to determine if impersonated + * @return {Boolean} whether user can assign new role to assignedUser + */ +module.exports.canAssign = (user, assignedUser, newRole, originUser) => { + let maxUserRole = maxRole(user.related('roles').toJSON()); + let maxAssigneeRole = maxRole(assignedUser.related('roles').toJSON()); + + return maxUserRole > ROLE_VALUE_MAP[newRole] + && maxUserRole > maxAssigneeRole + && _.isUndefined(originUser) + && (maxAssigneeRole > 0 || assignedUser.hasRole(roles.VOLUNTEER)); +}; + +/** + * Adds role to a user + * @param {User} user a User model + * @param {String} role a role to assign to the user + * @param {Boolean} active a boolean whether new role should be active + * @return {UserRole} the updatedUserRole + */ +module.exports.addRole = (user, role, active) => UserRole + .addRole(user, role, active) + .then((updatedUser) => { + return updatedUser; + }); + module.exports.updateContactInfo = (user, newEmail) => { if(!_.isNull(user.get('password'))) { const message = 'Cannot update the contact info of a Basic user'; diff --git a/test/user.js b/test/user.js index 6c38572..f5c8797 100644 --- a/test/user.js +++ b/test/user.js @@ -5,6 +5,7 @@ const sinon = require('sinon'); const _ = require('lodash'); const errors = require('../api/v1/errors'); +const utils = require('../api/v1/utils'); const UserService = require('../api/v1/services/UserService.js'); const User = require('../api/v1/models/User.js'); @@ -19,7 +20,11 @@ describe('UserService', () => { let testUser; before((done) => { - testUser = { id: 1, email: 'new@example.com', password: 'password123' }; + testUser = { + id: 1, + email: 'new@example.com', + password: 'password123' + }; _createUser = sinon.spy(User, 'create'); _findByEmail = sinon.spy(User, 'findByEmail'); @@ -29,11 +34,11 @@ describe('UserService', () => { tracker.install(); done(); }); - it('creates a new user', function (done) { + it('creates a new user', function(done) { const testUserClone = _.clone(testUser); tracker.on('query', (query) => { - query.response([ '1' ]); + query.response(['1']); }); const user = UserService.createUser(testUserClone.email, testUserClone.password, null); @@ -71,7 +76,10 @@ describe('UserService', () => { let _findByEmail; before((done) => { - const testUser = User.forge({ id: 1, email: 'valid@example.com' }); + const testUser = User.forge({ + id: 1, + email: 'valid@example.com' + }); _findByEmail = sinon.stub(User, 'findByEmail'); @@ -98,7 +106,10 @@ describe('UserService', () => { describe('findUserById', () => { let _findById; before((done) => { - const testUser = User.forge({ id: 1, email: 'new@example.com' }); + const testUser = User.forge({ + id: 1, + email: 'new@example.com' + }); testUser.setPassword('password123'); _findById = sinon.stub(User, 'findById'); @@ -111,10 +122,10 @@ describe('UserService', () => { it('finds existing user', (done) => { const user = UserService.findUserById(1); expect(user).to.eventually.have.deep.property('attributes.id', 1, 'ID should be 1, the searched for ID') - .then(() => { - expect(user).to.eventually.have.deep.property('attributes.email', - 'new@example.com', 'email should be new@example.com').notify(done); -}); + .then(() => { + expect(user).to.eventually.have.deep.property('attributes.email', + 'new@example.com', 'email should be new@example.com').notify(done); + }); }); it('throws exception after searching for non-existent user', (done) => { const user = UserService.findUserById(2); @@ -131,12 +142,15 @@ describe('UserService', () => { let testUser; before((done) => { - testUser = User.forge({ id: 1, email: 'new@example.com' }); + testUser = User.forge({ + id: 1, + email: 'new@example.com' + }); testUser.setPassword('password123') - .then((updatedUser) => { - testUser = updatedUser; - done(); -}); + .then((updatedUser) => { + testUser = updatedUser; + done(); + }); }); @@ -161,16 +175,19 @@ describe('UserService', () => { let testUser; let _save; before((done) => { - testUser = User.forge({ id: 1, email: 'new@example.com' }); + testUser = User.forge({ + id: 1, + email: 'new@example.com' + }); testUser.setPassword('password123') - .then(function(updatedUser){ - testUser = updatedUser; + .then(function(updatedUser) { + testUser = updatedUser; - _save = sinon.stub(User.prototype, 'save'); - _save.withArgs().returns(this); + _save = sinon.stub(User.prototype, 'save'); + _save.withArgs().returns(this); - done(); -}); + done(); + }); }); it('resets password', (done) => { const user = UserService.resetPassword(testUser, 'password456').then((updatedUser) => UserService.verifyPassword(updatedUser, 'password456')); @@ -181,4 +198,138 @@ describe('UserService', () => { done(); }); }); + + describe('canAssign', () => { + let testUserTemplate; + let assignedUserTemplate; + + before((done) => { + testUserTemplate = User.forge({ + id: 1, + email: 'old@example.com' + }); + assignedUserTemplate = User.forge({ + id: 2, + email: 'new@example.com' + }); + + done(); + }); + + it('valid role assignment', (done) => { + let testUser = _.clone(testUserTemplate); + let assignedUser = _.clone(assignedUserTemplate); + + testUser.related('roles').add({ + role: utils.roles.ADMIN, + active: 1 + }); + assignedUser.related('roles').add({ + role: utils.roles.VOLUNTEER, + active: 1 + }); + + const result = UserService + .canAssign(testUser, assignedUser, utils.roles.STAFF, undefined); + + expect(result).to.equal(true); + done(); + }); + + it('too high role assignment', (done) => { + let testUser = _.clone(testUserTemplate); + let assignedUser = _.clone(assignedUserTemplate); + + testUser.related('roles').add({ + role: utils.roles.ADMIN, + active: 1 + }); + assignedUser.related('roles').add({ + role: utils.roles.ADMIN, + active: 1 + }); + + const result = UserService + .canAssign(testUser, assignedUser, utils.roles.STAFF, undefined); + + expect(result).to.equal(false); + done(); + }); + + it('too low user', (done) => { + let testUser = _.clone(testUserTemplate); + let assignedUser = _.clone(assignedUserTemplate); + + testUser.related('roles').add({ + role: utils.roles.VOLUNTEER, + active: 1 + }); + assignedUser.related('roles').add({ + role: utils.roles.ADMIN, + active: 1 + }); + + const result = UserService + .canAssign(testUser, assignedUser, utils.roles.STAFF, undefined); + + expect(result).to.equal(false); + done(); + }); + + it('non volunteer level zero assignment', (done) => { + let testUser = _.clone(testUserTemplate); + let assignedUser = _.clone(assignedUserTemplate); + + testUser.related('roles').add({ + role: utils.roles.ADMIN, + active: 1 + }); + assignedUser.related('roles').add({ + role: utils.roles.ATTENDEE, + active: 1 + }); + + const result = UserService + .canAssign(testUser, assignedUser, utils.roles.STAFF, undefined); + + expect(result).to.equal(false); + done(); + }); + + it('impersonated', (done) => { + let testUser = _.clone(testUserTemplate); + let assignedUser = _.clone(assignedUserTemplate); + + testUser.related('roles').add({ + role: utils.roles.ADMIN, + active: 1 + }); + assignedUser.related('roles').add({ + role: utils.roles.VOLUNTEER, + active: 1 + }); + + const result = UserService + .canAssign(testUser, assignedUser, utils.roles.STAFF, testUser); + + expect(result).to.equal(false); + done(); + }); + + it('same user', (done) => { + let testUser = _.clone(testUserTemplate); + + testUser.related('roles').add({ + role: utils.roles.ADMIN, + active: 1 + }); + + const result = UserService + .canAssign(testUser, testUser, utils.roles.STAFF, undefined); + + expect(result).to.equal(false); + done(); + }); + + }); });