diff --git a/src/constants.js b/src/constants.js index e29d3fcb..50f11de6 100644 --- a/src/constants.js +++ b/src/constants.js @@ -35,6 +35,7 @@ export const USER_ROLE = { export const ADMIN_ROLES = [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN]; +export const MANAGER_ROLES = [...ADMIN_ROLES, USER_ROLE.MANAGER]; export const EVENT = { ROUTING_KEY: { diff --git a/src/routes/projectMembers/create.js b/src/routes/projectMembers/create.js index 64f2dfe7..ac809e0a 100644 --- a/src/routes/projectMembers/create.js +++ b/src/routes/projectMembers/create.js @@ -6,7 +6,7 @@ import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { PROJECT_MEMBER_ROLE, EVENT } from '../../constants'; +import { PROJECT_MEMBER_ROLE, MANAGER_ROLES, EVENT } from '../../constants'; /** * API to add a project member. @@ -53,10 +53,21 @@ module.exports = [ if (_.isUndefined(member.isPrimary)) { member.isPrimary = _.isUndefined(_.find(members, m => m.isPrimary && m.role === member.role)); } + let promise = Promise.resolve(); + if (member.role === PROJECT_MEMBER_ROLE.MANAGER) { + promise = util.getUserRoles(member.userId, req.log, req.id); + } req.log.debug('creating member', member); let newMember = null; // register member - return models.ProjectMember.create(member) + return promise.then((memberRoles) => { + if (member.role === PROJECT_MEMBER_ROLE.MANAGER + && (!memberRoles || !util.hasIntersection(MANAGER_ROLES, memberRoles))) { + const err = new Error('This user can\'t be added as a Manager to the project'); + err.status = 400; + return next(err); + } + return models.ProjectMember.create(member) .then((_newMember) => { newMember = _newMember.get({ plain: true }); // publish event @@ -72,5 +83,6 @@ module.exports = [ req.log.error('Unable to register ', err); next(err); }); + }); }, ]; diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js index 0c714775..4d0c9f2f 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -8,13 +8,14 @@ import models from '../../models'; import util from '../../util'; import server from '../../app'; import testUtil from '../../tests/util'; +import { USER_ROLE } from '../../constants'; const should = chai.should(); describe('Project Members create', () => { let project1; let project2; - before((done) => { + beforeEach((done) => { testUtil.clearDb() .then(() => { models.Project.create({ @@ -246,5 +247,232 @@ describe('Project Members create', () => { } }); }); + + it('should return 400 for trying to add customers as manager', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: 'Topcoder User', + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userId: 3, + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*can't be added as a Manager/); + done(); + } + }); + }); + + it('should return 400 for trying to add copilot as manager', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userId: 3, + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + done(); + } + }); + }); + + it('should return 201 and register Connect Manager as manager', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.MANAGER, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userId: 3, + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.isPrimary.should.be.truthy; + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(3); + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + done(); + } + }); + }); + + it('should return 201 and register Connect Admin as manager', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.CONNECT_ADMIN, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userId: 3, + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.isPrimary.should.be.truthy; + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(3); + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + done(); + } + }); + }); + + it('should return 201 and register Topcoder Admin as manager', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.TOPCODER_ADMIN, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userId: 3, + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.isPrimary.should.be.truthy; + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(3); + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + done(); + } + }); + }); }); }); diff --git a/src/util.js b/src/util.js index 27df14ea..6371f726 100644 --- a/src/util.js +++ b/src/util.js @@ -85,6 +85,16 @@ _.assignIn(util, { authRoles = authRoles.map(s => s.toLowerCase()); return _.intersection(authRoles, roles.map(r => r.toLowerCase())).length > 0; }, + /** + * Helper funtion to find intersection (case insensitive) between two arrays + * @param {Array} array1 first array of strings + * @param {Array} array2 second array of strings + * @return {boolean} true/false + */ + hasIntersection: (array1, array2) => { + const lowercased = array1.map(s => s.toLowerCase()); + return _.intersection(lowercased, array2.map(r => r.toLowerCase())).length > 0; + }, /** * Helper funtion to verify if user has admin roles * @param {object} req Request object that should contain authUser @@ -307,6 +317,27 @@ _.assignIn(util, { return Promise.reject(err); } }), + + /** + * Retrieve member details from userIds + */ + getUserRoles: Promise.coroutine(function* (userId, logger, requestId) { // eslint-disable-line func-names + try { + const token = yield this.getSystemUserToken(logger); + const httpClient = this.getHttpClient({ id: requestId, log: logger }); + return httpClient.get(`${config.identityServiceEndpoint}roles`, { + params: { + filter: `subjectID=${userId}`, + }, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }).then(res => _.get(res, 'data.result.content', []).map(r => r.roleName)); + } catch (err) { + return Promise.reject(err); + } + }), }); export default util;