diff --git a/src/constants.js b/src/constants.js index 1d2c0d62..de3cecc0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -30,8 +30,11 @@ export const USER_ROLE = { TOPCODER_ADMIN: 'administrator', MANAGER: 'Connect Manager', COPILOT: 'Connect Copilot', + CONNECT_ADMIN: 'Connect Admin', }; +export const ADMIN_ROLES = [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN]; + export const EVENT = { ROUTING_KEY: { diff --git a/src/permissions/project.delete.js b/src/permissions/project.delete.js index c07479d3..9fe49fc8 100644 --- a/src/permissions/project.delete.js +++ b/src/permissions/project.delete.js @@ -4,7 +4,7 @@ import _ from 'lodash'; import util from '../util'; import models from '../models'; -import { USER_ROLE, PROJECT_MEMBER_ROLE } from '../constants'; +import { PROJECT_MEMBER_ROLE } from '../constants'; /** * Super admin, Topcoder Managers are allowed to edit any project @@ -20,7 +20,7 @@ module.exports = freq => new Promise((resolve, reject) => { req.context = req.context || {}; req.context.currentProjectMembers = members; // check if auth user has acecss to this project - const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) || + const hasAccess = util.hasAdminRole(req) || !_.isUndefined(_.find(members, m => m.userId === req.authUser.userId && ((m.role === PROJECT_MEMBER_ROLE.CUSTOMER && m.isPrimary) || m.role === PROJECT_MEMBER_ROLE.MANAGER))); diff --git a/src/permissions/project.edit.js b/src/permissions/project.edit.js index 57d56847..760e672e 100644 --- a/src/permissions/project.edit.js +++ b/src/permissions/project.edit.js @@ -19,7 +19,7 @@ module.exports = freq => new Promise((resolve, reject) => { req.context = req.context || {}; req.context.currentProjectMembers = members; // check if auth user has acecss to this project - const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + const hasAccess = util.hasAdminRole(req) || util.hasRole(req, USER_ROLE.MANAGER) || !_.isUndefined(_.find(members, m => m.userId === req.authUser.userId)); diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 91776d1a..b3ed1404 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -20,7 +20,7 @@ module.exports = freq => new Promise((resolve, reject) => { req.context = req.context || {}; req.context.currentProjectMembers = members; // check if auth user has acecss to this project - const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + const hasAccess = util.hasAdminRole(req) || util.hasRole(req, USER_ROLE.MANAGER) || !_.isUndefined(_.find(members, m => m.userId === currentUserId)); diff --git a/src/permissions/projectMember.delete.js b/src/permissions/projectMember.delete.js index 634bb557..0f0ec42c 100644 --- a/src/permissions/projectMember.delete.js +++ b/src/permissions/projectMember.delete.js @@ -2,7 +2,6 @@ import _ from 'lodash'; import util from '../util'; import models from '../models'; import { - USER_ROLE, PROJECT_MEMBER_ROLE, } from '../constants'; @@ -25,7 +24,7 @@ module.exports = freq => new Promise((resolve, reject) => { const prjMemberId = _.parseInt(req.params.id); const memberToBeRemoved = _.find(members, m => m.id === prjMemberId); // check if auth user has acecss to this project - const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + const hasAccess = util.hasAdminRole(req) || (authMember && memberToBeRemoved && (authMember.role === PROJECT_MEMBER_ROLE.MANAGER || (authMember.role === PROJECT_MEMBER_ROLE.CUSTOMER && authMember.isPrimary && memberToBeRemoved.role === PROJECT_MEMBER_ROLE.CUSTOMER) || diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js index e45026ad..9497c416 100644 --- a/src/routes/projectMembers/update.spec.js +++ b/src/routes/projectMembers/update.spec.js @@ -267,7 +267,7 @@ describe('Project members update', () => { resJson.isPrimary.should.be.false; resJson.updatedBy.should.equal(40051332); deleteSpy.should.have.been.calledOnce; - server.services.pubsub.publish.calledWith('project.member.removed').should.be.true; + server.services.pubsub.publish.calledWith('project.member.updated').should.be.true; done(); } }); diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js index 1b4f1ead..9e84bf2c 100644 --- a/src/routes/projects/create.js +++ b/src/routes/projects/create.js @@ -66,7 +66,8 @@ module.exports = [ */ (req, res, next) => { const project = req.body.param; - const userRole = util.hasRole(req, USER_ROLE.MANAGER) + // by default connect admin and managers joins projects as manager + const userRole = util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.MANAGER]) ? PROJECT_MEMBER_ROLE.MANAGER : PROJECT_MEMBER_ROLE.CUSTOMER; // set defaults @@ -136,6 +137,7 @@ module.exports = [ req.log.error(err); return Promise.resolve(); }); + // return Promise.resolve(); }) .then(() => { diff --git a/src/routes/projects/delete.spec.js b/src/routes/projects/delete.spec.js index bf9ffd81..8fee3330 100644 --- a/src/routes/projects/delete.spec.js +++ b/src/routes/projects/delete.spec.js @@ -100,5 +100,39 @@ describe('Project delete test', () => { } }); }); + + it('should return 204, for connect admin, if project was successfully removed', (done) => { + request(server) + .delete(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + server.services.pubsub.publish.calledWith('project.deleted').should.be.true; + done(); + } + }); + }); + + it('should return 204, for connect admin, if project was successfully removed', (done) => { + request(server) + .delete(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + server.services.pubsub.publish.calledWith('project.deleted').should.be.true; + done(); + } + }); + }); }); }); diff --git a/src/routes/projects/get.spec.js b/src/routes/projects/get.spec.js index e1251041..9fc165ee 100644 --- a/src/routes/projects/get.spec.js +++ b/src/routes/projects/get.spec.js @@ -136,6 +136,25 @@ describe('GET Project', () => { }); }); + it('should return the project for connect admin ', (done) => { + request(server) + .get(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + done(); + } + }); + }); + it('should return attachment with downloadUrl', (done) => { models.ProjectAttachment.create({ projectId: project1.id, @@ -172,6 +191,7 @@ describe('GET Project', () => { .expect('Content-Type', /json/) .expect(200) .end((err, res) => { + stub.restore(); if (err) { done(err); } else { @@ -181,7 +201,6 @@ describe('GET Project', () => { resJson.attachments.should.have.lengthOf(1); resJson.attachments[0].filePath.should.equal(attachment.filePath); resJson.attachments[0].downloadUrl.should.exist; - stub.restore(); done(); } }); diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index 8908a602..a2ce93fa 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -123,7 +123,7 @@ module.exports = [ req.log.debug(criteria); if (!memberOnly - && (util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + && (util.hasAdminRole(req) || util.hasRole(req, USER_ROLE.MANAGER))) { // admins & topcoder managers can see all projects return retrieveProjects(req, criteria, sort, req.query.fields) diff --git a/src/routes/projects/list-db.spec.js b/src/routes/projects/list-db.spec.js index 5906d492..2c9560fb 100644 --- a/src/routes/projects/list-db.spec.js +++ b/src/routes/projects/list-db.spec.js @@ -300,5 +300,109 @@ describe('LIST Project db', () => { } }); }); + + describe('for connect admin ', () => { + it('should return the project ', (done) => { + request(server) + .get('/v4/projects/db/?fields=id%2Cmembers.id') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + } + }); + }); + + it('should return all projects that match when filtering by name', (done) => { + request(server) + .get('/v4/projects/db/?filter=keyword%3Dtest') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + } + }); + }); + + it('should return the project when filtering by keyword, which matches the name', (done) => { + request(server) + .get('/v4/projects/db/?filter=keyword%3D1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(1); + resJson[0].name.should.equal('test1'); + done(); + } + }); + }); + + it('should return the project when filtering by keyword, which matches the description', (done) => { + request(server) + .get('/v4/projects/db/?filter=keyword%3Dproject') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + } + }); + }); + + it('should return the project when filtering by keyword, which matches the details', (done) => { + request(server) + .get('/v4/projects/db/?filter=keyword%3Dcode') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(1); + resJson[0].name.should.equal('test1'); + done(); + } + }); + }); + }); }); }); diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 6801a320..f1cd2c9a 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -214,10 +214,10 @@ module.exports = [ limit: Math.min(req.query.limit || 20, 20), offset: req.query.offset || 0, }; - req.log.debug(criteria); + req.log.info(criteria); if (!memberOnly - && (util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + && (util.hasAdminRole(req) || util.hasRole(req, USER_ROLE.MANAGER))) { // admins & topcoder managers can see all projects return retrieveProjects(req, criteria, sort, req.query.fields) diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index 4ce4acbc..cdcfa1fd 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -387,5 +387,109 @@ describe('LIST Project', () => { } }); }); + + describe('GET All /projects/ for Connect Admin, ', () => { + it('should return the project ', (done) => { + request(server) + .get('/v4/projects/?fields=id%2Cmembers.id') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + } + }); + }); + + it('should return all projects, that match when filtering by name', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3Dtest') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + } + }); + }); + + it('should return the project, when filtering by keyword, which matches the name', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3D1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(1); + resJson[0].name.should.equal('test1'); + done(); + } + }); + }); + + it('should return the project, when filtering by keyword, which matches the description', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3Dproject') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + } + }); + }); + + it('should return the project, when filtering by keyword, which matches the member handle', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3Dtourist') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(1); + resJson[0].name.should.equal('test1'); + done(); + } + }); + }); + }); }); }); diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js index deb1175c..985c7c82 100644 --- a/src/routes/projects/update.js +++ b/src/routes/projects/update.js @@ -82,7 +82,7 @@ const updateProjectValdiations = { }; // NOTE- decided to disable all additional checks for now. -const validateUpdates = (existingProject, updatedProps, authUser) => { +const validateUpdates = (existingProject, updatedProps, req) => { const errors = []; switch (existingProject.status) { case PROJECT_STATUS.COMPLETED: @@ -101,7 +101,7 @@ const validateUpdates = (existingProject, updatedProps, authUser) => { // } } if (_.has(updatedProps, 'directProjectId') && - _.intersection(authUser.roles, [USER_ROLE.MANAGER, USER_ROLE.TOPCODER_ADMIN]).length === 0) { + !util.hasRoles(req, [USER_ROLE.MANAGER, USER_ROLE.TOPCODER_ADMIN])) { errors.push('Don\'t have permission to update \'directProjectId\' property'); } @@ -142,7 +142,7 @@ module.exports = [ } previousValue = _.clone(project.get({ plain: true })); // run additional validations - const validationErrors = validateUpdates(previousValue, updatedProps, req.authUser); + const validationErrors = validateUpdates(previousValue, updatedProps, req); if (validationErrors.length > 0) { const err = new Error('Unable to update project'); _.assign(err, { @@ -160,7 +160,7 @@ module.exports = [ ].map(x => x.toLowerCase()); const matchRole = role => _.indexOf(validRoles, role.toLowerCase()) >= 0; if (updatedProps.status === PROJECT_STATUS.ACTIVE && - !util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) && + !util.hasAdminRole(req) && _.isUndefined(_.find(members, m => m.userId === req.authUser.userId && matchRole(m.role))) ) { diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js index 03e27cc1..520d93db 100644 --- a/src/routes/projects/update.spec.js +++ b/src/routes/projects/update.spec.js @@ -127,7 +127,7 @@ describe('Project', () => { }); }); - it('should return 403 if invalid user will launch a project', (done) => { + it('should return 403 if invalid user will update a project', (done) => { request(server) .patch(`/v4/projects/${project1.id}`) .set({ @@ -154,7 +154,7 @@ describe('Project', () => { }); }); - it('should return 200 if topcoder manager user will launch a project', (done) => { + it('should return 200 if topcoder manager user will update a project', (done) => { request(server) .patch(`/v4/projects/${project1.id}`) .set({ @@ -723,5 +723,60 @@ describe('Project', () => { } }); }); + + xdescribe('for connect admin, ', () => { + it('should return 200, connect admin is allowed to transition project out of cancel status', (done) => { + models.Project.update({ + status: PROJECT_STATUS.CANCELLED, + }, { + where: { + id: project1.id, + }, + }) + .then(() => { + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.ACTIVE, + }, + }; + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(mbody) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051333); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + // validate that project history is updated + models.ProjectHistory.findAll({ + where: { + projectId: project1.id, + }, + }).then((histories) => { + should.exist(histories); + histories.length.should.equal(1); + const history = histories[0].get({ + plain: true, + }); + history.status.should.equal(PROJECT_STATUS.ACTIVE); + history.projectId.should.equal(project1.id); + done(); + }); + } + }); + }); + }); + }); }); }); diff --git a/src/tests/util.js b/src/tests/util.js index 77be2e78..ded1ff9f 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -22,5 +22,7 @@ export default { manager: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzQiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.J5VtOEQVph5jfe2Ji-NH7txEDcx_5gthhFeD-MzX9ck', // userId = 40051335, [ 'Topcoder User' ],handle: 'member2',email: 'test@topcoder.com' member2: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJtZW1iZXIyIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.Mh4bw3wm-cn5Kcf96gLFVlD0kySOqqk4xN3qnreAKL4', + // userId = 40051336, [ 'Connect Admin' ], handle: 'connect_admin1', email: 'connect_admin1@topcoder.com' + connectAdmin: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30', }, }; diff --git a/src/util.js b/src/util.js index b67c7041..27df14ea 100644 --- a/src/util.js +++ b/src/util.js @@ -17,6 +17,7 @@ import urlencode from 'urlencode'; import elasticsearch from 'elasticsearch'; import Promise from 'bluebird'; import AWS from 'aws-sdk'; +import { ADMIN_ROLES } from './constants'; const exec = require('child_process').exec; const models = require('./models').default; @@ -73,6 +74,27 @@ _.assignIn(util, { roles = roles.map(s => s.toLowerCase()); return _.indexOf(roles, role.toLowerCase()) >= 0; }, + /** + * Helper funtion to verify if user has specified roles + * @param {object} req Request object that should contain authUser + * @param {Array} roles specified roles + * @return {boolean} true/false + */ + hasRoles: (req, roles) => { + let authRoles = _.get(req, 'authUser.roles', []); + authRoles = authRoles.map(s => s.toLowerCase()); + return _.intersection(authRoles, roles.map(r => r.toLowerCase())).length > 0; + }, + /** + * Helper funtion to verify if user has admin roles + * @param {object} req Request object that should contain authUser + * @return {boolean} true/false + */ + hasAdminRole: (req) => { + let roles = _.get(req, 'authUser.roles', []); + roles = roles.map(s => s.toLowerCase()); + return _.intersection(roles, ADMIN_ROLES.map(r => r.toLowerCase())).length > 0; + }, /** * Parses query fields and groups them per table