Connect Admin
administrator
+ Connect Copilot Manager
diff --git a/src/events/kafkaHandlers.js b/src/events/kafkaHandlers.js
index b6bf90cf..c1c76611 100644
--- a/src/events/kafkaHandlers.js
+++ b/src/events/kafkaHandlers.js
@@ -1,18 +1,22 @@
/**
* BUS Event Handlers
*/
-import { CONNECT_NOTIFICATION_EVENT, BUS_API_EVENT, RESOURCES } from '../constants';
import {
- projectCreatedKafkaHandler,
- projectUpdatedKafkaHandler } from './projects';
-import { projectPhaseAddedKafkaHandler, projectPhaseRemovedKafkaHandler,
- projectPhaseUpdatedKafkaHandler } from './projectPhases';
+ CONNECT_NOTIFICATION_EVENT,
+ BUS_API_EVENT,
+ RESOURCES,
+} from '../constants';
import {
- timelineAdjustedKafkaHandler,
-} from './timelines';
+ projectCreatedKafkaHandler,
+ projectUpdatedKafkaHandler,
+} from './projects';
import {
- milestoneUpdatedKafkaHandler,
-} from './milestones';
+ projectPhaseAddedKafkaHandler,
+ projectPhaseRemovedKafkaHandler,
+ projectPhaseUpdatedKafkaHandler,
+} from './projectPhases';
+import { timelineAdjustedKafkaHandler } from './timelines';
+import { milestoneUpdatedKafkaHandler } from './milestones';
const kafkaHandlers = {
/**
@@ -33,22 +37,64 @@ const kafkaHandlers = {
// Events coming from timeline/milestones (considering it as a separate module/service in future)
[CONNECT_NOTIFICATION_EVENT.MILESTONE_TRANSITION_COMPLETED]: milestoneUpdatedKafkaHandler,
[CONNECT_NOTIFICATION_EVENT.TIMELINE_ADJUSTED]: timelineAdjustedKafkaHandler,
+};
- /**
- * New Unified Bus Events
- */
- [BUS_API_EVENT.PROJECT_CREATED]: {
- [RESOURCES.PROJECT]: projectCreatedKafkaHandler,
- },
- [BUS_API_EVENT.PROJECT_PHASE_CREATED]: {
- [RESOURCES.PHASE]: projectPhaseAddedKafkaHandler,
- },
- [BUS_API_EVENT.PROJECT_PHASE_UPDATED]: {
- [RESOURCES.PHASE]: projectPhaseUpdatedKafkaHandler,
- },
- [BUS_API_EVENT.PROJECT_PHASE_DELETED]: {
- [RESOURCES.PHASE]: projectPhaseRemovedKafkaHandler,
- },
+/**
+ * Register New Unified Bus Event Handlers
+ *
+ * We need this special method so it would properly merge topics with the same names
+ * but different resources.
+ *
+ * @param {String} topic Kafka topic name
+ * @param {String} resource resource name
+ * @param {Function} handler handler method
+ *
+ * @returns {void}
+ */
+const registerKafkaHandler = (topic, resource, handler) => {
+ let topicConfig = kafkaHandlers[topic];
+
+ // if config for topic is not yet initialized, create it
+ if (!topicConfig) {
+ topicConfig = {};
+ kafkaHandlers[topic] = topicConfig;
+ }
+
+ if (typeof topicConfig !== 'object') {
+ throw new Error(
+ `Topic "${topic}" should be defined as object with resource names as keys.`,
+ );
+ }
+
+ if (topicConfig[resource]) {
+ throw new Error(
+ `Handler for topic "${topic}" with resource ${resource} has been already registered.`,
+ );
+ }
+
+ topicConfig[resource] = handler;
};
+registerKafkaHandler(
+ BUS_API_EVENT.PROJECT_CREATED,
+ RESOURCES.PROJECT,
+ projectCreatedKafkaHandler,
+);
+registerKafkaHandler(
+ BUS_API_EVENT.PROJECT_PHASE_CREATED,
+ RESOURCES.PHASE,
+ projectPhaseAddedKafkaHandler,
+);
+registerKafkaHandler(
+ BUS_API_EVENT.PROJECT_PHASE_UPDATED,
+ RESOURCES.PHASE,
+ projectPhaseUpdatedKafkaHandler,
+);
+registerKafkaHandler(
+ BUS_API_EVENT.PROJECT_PHASE_DELETED,
+ RESOURCES.PHASE,
+ projectPhaseRemovedKafkaHandler,
+);
+
+
export default kafkaHandlers;
diff --git a/src/events/projects/index.js b/src/events/projects/index.js
index 0f8dce34..cc109d6b 100644
--- a/src/events/projects/index.js
+++ b/src/events/projects/index.js
@@ -5,6 +5,8 @@ import _ from 'lodash';
import Joi from 'joi';
import Promise from 'bluebird';
import config from 'config';
+import axios from 'axios';
+import moment from 'moment';
import util from '../../util';
import models from '../../models';
import { createPhaseTopic } from '../projectPhases';
@@ -14,6 +16,27 @@ const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName');
const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType');
const eClient = util.getElasticSearchClient();
+/**
+ * creates taas job
+ * @param {Object} data the job data
+ * @return {Object} the job created
+ */
+const createTaasJob = async (data) => {
+ const token = await util.getM2MToken();
+ const headers = {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ };
+ const res = await axios
+ .post(config.taasJobApiUrl, data, { headers })
+ .catch((err) => {
+ const error = new Error();
+ error.message = _.get(err, 'response.data.message', error.message);
+ throw error;
+ });
+ return res.data;
+};
+
/**
* Payload for deprecated BUS events like `connect.notification.project.updated`.
*/
@@ -164,6 +187,47 @@ async function projectCreatedKafkaHandler(app, topic, payload) {
await Promise.all(topicPromises);
app.logger.debug('Topics for phases are successfully created.');
}
+ // TODO: temporary disable this feature, until we release TaaS APP
+ if (false === true && project.type === 'talent-as-a-service') {
+ const specialists = _.get(project, 'details.taasDefinition.specialists');
+ if (!specialists || !specialists.length) {
+ app.logger.debug(`no specialists found in the project ${project.id}`);
+ return;
+ }
+ const targetSpecialists = _.filter(specialists, specialist => Number(specialist.people) > 0); // must be at least one people
+ await Promise.all(
+ _.map(
+ targetSpecialists,
+ (specialist) => {
+ const startDate = new Date();
+ const endDate = moment(startDate).add(Number(specialist.duration), 'M'); // the unit of duration is month
+ // make sure that skills would be unique in the list
+ const skills = _.uniq(
+ // use both, required and additional skills for jobs
+ specialist.skills.concat(specialist.additionalSkills)
+ // only include skills with `skillId` and ignore custom skills in jobs
+ .filter(skill => skill.skillId).map(skill => skill.skillId),
+ );
+ return createTaasJob({
+ projectId: project.id,
+ externalId: '0', // hardcode for now
+ description: specialist.roleTitle,
+ startDate,
+ endDate,
+ skills,
+ numPositions: Number(specialist.people),
+ resourceType: specialist.role,
+ rateType: 'hourly', // hardcode for now
+ workload: _.get(specialist, 'workLoad.title', '').toLowerCase(),
+ }).then((job) => {
+ app.logger.debug(`jobId: ${job.id} job created for roleTitle ${specialist.roleTitle}`);
+ }).catch((err) => {
+ app.logger.error(`Unable to create job for ${specialist.roleTitle}: ${err.message}`);
+ });
+ },
+ ),
+ );
+ }
}
module.exports = {
diff --git a/src/permissions/constants.js b/src/permissions/constants.js
index c69cf36f..aa9f4ee9 100644
--- a/src/permissions/constants.js
+++ b/src/permissions/constants.js
@@ -195,6 +195,19 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECTS_WRITE,
},
+ UPDATE_PROJECT_STATUS: {
+ meta: {
+ title: 'Update Project Status',
+ group: 'Project',
+ },
+ topcoderRoles: TOPCODER_ROLES_ADMINS,
+ projectRoles: [
+ ...PROJECT_ROLES_MANAGEMENT,
+ PROJECT_MEMBER_ROLE.COPILOT,
+ ],
+ scopes: SCOPES_PROJECTS_WRITE,
+ },
+
MANAGE_PROJECT_DIRECT_PROJECT_ID: {
meta: {
title: 'Manage Project property "directProjectId"',
@@ -306,19 +319,6 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
},
- UPDATE_PROJECT_MEMBER_TO_COPILOT: {
- meta: {
- title: 'Update Project Member (to copilot)',
- group: 'Project Member',
- description: 'Who can update project member role to "copilot".',
- },
- topcoderRoles: [
- ...TOPCODER_ROLES_ADMINS,
- USER_ROLE.COPILOT_MANAGER,
- ],
- scopes: SCOPES_PROJECT_MEMBERS_WRITE,
- },
-
DELETE_PROJECT_MEMBER_CUSTOMER: {
meta: {
title: 'Delete Project Member (customer)',
@@ -330,17 +330,31 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
},
- DELETE_PROJECT_MEMBER_NON_CUSTOMER: {
+ DELETE_PROJECT_MEMBER_TOPCODER: {
meta: {
- title: 'Delete Project Member (non-customer)',
+ title: 'Delete Project Member (topcoder)',
group: 'Project Member',
- description: 'Who can delete project members with non "customer" role.',
+ description: 'Who can delete project members with some topcoder role like "manager" etc.',
},
topcoderRoles: TOPCODER_ROLES_ADMINS,
projectRoles: PROJECT_ROLES_MANAGEMENT,
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
},
+ DELETE_PROJECT_MEMBER_COPILOT: {
+ meta: {
+ title: 'Delete Project Member (copilot)',
+ group: 'Project Member',
+ description: 'Who can delete project members with "copilot" role.',
+ },
+ topcoderRoles: [
+ ...TOPCODER_ROLES_ADMINS,
+ USER_ROLE.COPILOT_MANAGER,
+ ],
+ projectRoles: ALL,
+ scopes: SCOPES_PROJECT_MEMBERS_WRITE,
+ },
+
/*
* Project Invite
*/
@@ -371,23 +385,23 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
group: 'Project Invite',
description: 'Who can invite project members with "customer" role.',
},
- topcoderRoles: TOPCODER_ROLES_MANAGERS_AND_ADMINS,
+ topcoderRoles: TOPCODER_ROLES_ADMINS,
projectRoles: ALL,
scopes: SCOPES_PROJECT_INVITES_WRITE,
},
- CREATE_PROJECT_INVITE_NON_CUSTOMER: {
+ CREATE_PROJECT_INVITE_TOPCODER: {
meta: {
- title: 'Create Project Invite (non-customer)',
+ title: 'Create Project Invite (topcoder)',
group: 'Project Invite',
- description: 'Who can invite project members with non "customer" role.',
+ description: 'Who can invite project members with topcoder role like "manager" etc.',
},
topcoderRoles: TOPCODER_ROLES_ADMINS,
projectRoles: PROJECT_ROLES_MANAGEMENT,
scopes: SCOPES_PROJECT_INVITES_WRITE,
},
- CREATE_PROJECT_INVITE_COPILOT_DIRECTLY: {
+ CREATE_PROJECT_INVITE_COPILOT: {
meta: {
title: 'Create Project Invite (copilot)',
group: 'Project Invite',
@@ -454,17 +468,31 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECT_INVITES_WRITE,
},
- DELETE_PROJECT_INVITE_NOT_OWN_NON_CUSTOMER: {
+ DELETE_PROJECT_INVITE_NOT_OWN_TOPCODER: {
meta: {
- title: 'Delete Project Invite (not own, non-customer)',
+ title: 'Delete Project Invite (not own, topcoder)',
group: 'Project Invite',
- description: 'Who can delete project invites for other members with non "customer" role.',
+ description: 'Who can delete project invites for other members with some topcoder role like "manager" etc.',
},
topcoderRoles: TOPCODER_ROLES_ADMINS,
projectRoles: PROJECT_ROLES_MANAGEMENT,
scopes: SCOPES_PROJECT_INVITES_WRITE,
},
+ DELETE_PROJECT_INVITE_NOT_OWN_COPILOT: {
+ meta: {
+ title: 'Delete Project Invite (not own, copilot)',
+ group: 'Project Invite',
+ description: 'Who can delete invites for other members with "copilot" role.',
+ },
+ topcoderRoles: [
+ ...TOPCODER_ROLES_ADMINS,
+ USER_ROLE.COPILOT_MANAGER,
+ ],
+ projectRoles: PROJECT_ROLES_MANAGEMENT,
+ scopes: SCOPES_PROJECT_INVITES_WRITE,
+ },
+
DELETE_PROJECT_INVITE_REQUESTED: {
meta: {
title: 'Delete Project Invite (requested)',
diff --git a/src/permissions/index.js b/src/permissions/index.js
index 9344c0b1..a37fdb04 100644
--- a/src/permissions/index.js
+++ b/src/permissions/index.js
@@ -31,12 +31,14 @@ module.exports = () => {
]));
Authorizer.setPolicy('projectMember.delete', generalPermission([
PERMISSION.DELETE_PROJECT_MEMBER_CUSTOMER,
- PERMISSION.DELETE_PROJECT_MEMBER_NON_CUSTOMER,
+ PERMISSION.DELETE_PROJECT_MEMBER_TOPCODER,
+ PERMISSION.DELETE_PROJECT_MEMBER_COPILOT,
]));
Authorizer.setPolicy('projectMemberInvite.create', generalPermission([
PERMISSION.CREATE_PROJECT_INVITE_CUSTOMER,
- PERMISSION.CREATE_PROJECT_INVITE_NON_CUSTOMER,
+ PERMISSION.CREATE_PROJECT_INVITE_TOPCODER,
+ PERMISSION.CREATE_PROJECT_INVITE_COPILOT,
]));
Authorizer.setPolicy('projectMemberInvite.view', generalPermission([
PERMISSION.READ_PROJECT_INVITE_OWN,
@@ -49,7 +51,8 @@ module.exports = () => {
Authorizer.setPolicy('projectMemberInvite.delete', generalPermission([
PERMISSION.DELETE_PROJECT_INVITE_OWN,
PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_CUSTOMER,
- PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_NON_CUSTOMER,
+ PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_COPILOT,
+ PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_TOPCODER,
]));
Authorizer.setPolicy('projectAttachment.create', generalPermission(PERMISSION.CREATE_PROJECT_ATTACHMENT));
diff --git a/src/routes/milestones/commonHelper.js b/src/routes/milestones/commonHelper.js
index a5aed781..ad09300f 100644
--- a/src/routes/milestones/commonHelper.js
+++ b/src/routes/milestones/commonHelper.js
@@ -23,11 +23,6 @@ async function createMilestone(authUser, timeline, data, transaction) {
// eslint-disable-next-line
const userId = authUser.userId;
const entity = Object.assign({}, data, { createdBy: userId, updatedBy: userId, timelineId: timeline.id });
- if (entity.startDate < timeline.startDate) {
- const apiErr = new Error('Milestone startDate must not be before the timeline startDate');
- apiErr.status = 400;
- throw apiErr;
- }
// Circumvent postgresql duplicate key error, see https://stackoverflow.com/questions/50834623/sequelizejs-error-duplicate-key-value-violates-unique-constraint-message-pkey
await models.sequelize.query('SELECT setval(\'milestones_id_seq\', (SELECT MAX(id) FROM "milestones"))',
{ raw: true, transaction });
@@ -123,7 +118,6 @@ async function updateMilestone(authUser, timelineId, data, transaction, item) {
const isUpdatedActualStartDate = milestone.actualStartDate && entityToUpdate.actualStartDate
&& !moment(milestone.actualStartDate).isSame(entityToUpdate.actualStartDate);
-
if (
(isUpdatedCompletionDate || isUpdatedActualStartDate)
&& !util.hasPermission({ topcoderRoles: ADMIN_ROLES }, authUser)
diff --git a/src/routes/milestones/create.spec.js b/src/routes/milestones/create.spec.js
index 588d42bb..6b4966e4 100644
--- a/src/routes/milestones/create.spec.js
+++ b/src/routes/milestones/create.spec.js
@@ -344,21 +344,6 @@ describe('CREATE milestone', () => {
.expect(400, done);
});
- it('should return 400 if startDate is before the timeline startDate', (done) => {
- const invalidBody = _.assign({}, body, {
- startDate: '2018-05-01T00:00:00.000Z',
- });
-
- request(server)
- .post('/v5/timelines/1/milestones')
- .set({
- Authorization: `Bearer ${testUtil.jwts.admin}`,
- })
- .send(invalidBody)
- .expect('Content-Type', /json/)
- .expect(400, done);
- });
-
it('should return 400 if invalid timelineId param', (done) => {
request(server)
.post('/v5/timelines/0/milestones')
diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js
index 97e0cd25..1f113739 100644
--- a/src/routes/projectMemberInvites/create.js
+++ b/src/routes/projectMemberInvites/create.js
@@ -274,8 +274,14 @@ module.exports = [
}
if (
- invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER &&
- !util.hasPermissionByReq(PERMISSION.CREATE_PROJECT_INVITE_NON_CUSTOMER, req)
+ ( // if cannot invite non-customer user
+ invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER &&
+ !util.hasPermissionByReq(PERMISSION.CREATE_PROJECT_INVITE_TOPCODER, req)
+ ) && !(
+ // and if cannot invite copilot directly
+ invite.role === PROJECT_MEMBER_ROLE.COPILOT &&
+ util.hasPermissionByReq(PERMISSION.CREATE_PROJECT_INVITE_COPILOT, req)
+ )
) {
const err = new Error(`You are not allowed to invite user as ${invite.role}.`);
err.status = 403;
@@ -373,7 +379,7 @@ module.exports = [
role: invite.role,
// invite copilots directly if user has permissions
status: (invite.role !== PROJECT_MEMBER_ROLE.COPILOT ||
- util.hasPermissionByReq(PERMISSION.CREATE_PROJECT_INVITE_COPILOT_DIRECTLY, req))
+ util.hasPermissionByReq(PERMISSION.CREATE_PROJECT_INVITE_COPILOT, req))
? INVITE_STATUS.PENDING
: INVITE_STATUS.REQUESTED,
createdBy: req.authUser.userId,
diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js
index 2ed2f15c..49376e0c 100644
--- a/src/routes/projectMemberInvites/create.spec.js
+++ b/src/routes/projectMemberInvites/create.spec.js
@@ -736,7 +736,7 @@ describe('Project Member Invite create', () => {
});
});
- it('should return 201 if try to create customer with COPILOT', (done) => {
+ it('should return 201 if try to create copilot invite with COPILOT role', (done) => {
util.getUserRoles.restore();
sandbox.stub(util, 'getUserRoles', () => Promise.resolve(['Connect Copilot']));
request(server)
@@ -764,6 +764,34 @@ describe('Project Member Invite create', () => {
});
});
+ it('should return 201 if try to create copilot invite by "Connect Copilot Manager"', (done) => {
+ util.getUserRoles.restore();
+ sandbox.stub(util, 'getUserRoles', () => Promise.resolve([USER_ROLE.COPILOT]));
+ request(server)
+ .post(`/v5/projects/${project2.id}/invites`)
+ .set({
+ Authorization: `Bearer ${testUtil.jwts.copilotManager}`,
+ })
+ .send({
+ handles: ['test_customer1'],
+ role: 'copilot',
+ })
+ .expect('Content-Type', /json/)
+ .expect(201)
+ .end((err, res) => {
+ if (err) {
+ done(err);
+ } else {
+ const resJson = res.body.success[0];
+ should.exist(resJson);
+ resJson.role.should.equal('copilot');
+ resJson.projectId.should.equal(project2.id);
+ resJson.userId.should.equal(40051331);
+ done();
+ }
+ });
+ });
+
it('should return 403 and failed list when trying add already invited member by lowercase email', (done) => {
request(server)
.post(`/v5/projects/${project1.id}/invites`)
diff --git a/src/routes/projectMemberInvites/delete.js b/src/routes/projectMemberInvites/delete.js
index 2ec3536f..d8c8be91 100644
--- a/src/routes/projectMemberInvites/delete.js
+++ b/src/routes/projectMemberInvites/delete.js
@@ -44,8 +44,9 @@ module.exports = [
error = 'You don\'t have permissions to cancel requested invites.';
} else if (
invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER
+ && invite.role !== PROJECT_MEMBER_ROLE.COPILOT
&& !ownInvite
- && !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_NON_CUSTOMER, req)
+ && !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_TOPCODER, req)
) {
error = 'You don\'t have permissions to cancel invites to Topcoder Team for other users.';
} else if (
@@ -54,6 +55,12 @@ module.exports = [
&& !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_CUSTOMER, req)
) {
error = 'You don\'t have permissions to cancel invites to Customer Team for other users.';
+ } else if (
+ invite.role === PROJECT_MEMBER_ROLE.COPILOT
+ && !ownInvite
+ && !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_INVITE_NOT_OWN_COPILOT, req)
+ ) {
+ error = 'You don\'t have permissions to cancel invites to Copilot Team for other users.';
}
if (error) {
diff --git a/src/routes/projectMemberInvites/delete.spec.js b/src/routes/projectMemberInvites/delete.spec.js
index fbb601ec..d6bddd46 100644
--- a/src/routes/projectMemberInvites/delete.spec.js
+++ b/src/routes/projectMemberInvites/delete.spec.js
@@ -157,7 +157,20 @@ describe('Project member invite delete', () => {
updatedAt: '2016-06-30 00:33:07+00',
});
- return Promise.all([pm, invite4, invite5, invite6]);
+ const invite7 = models.ProjectMemberInvite.create({
+ id: 7,
+ projectId: project2.id,
+ userId: testUtil.userIds.copilot,
+ email: null,
+ role: PROJECT_MEMBER_ROLE.COPILOT,
+ status: INVITE_STATUS.ACCEPTED,
+ createdBy: 1,
+ updatedBy: 1,
+ createdAt: '2016-06-30 00:33:07+00',
+ updatedAt: '2016-06-30 00:33:07+00',
+ });
+
+ return Promise.all([pm, invite4, invite5, invite6, invite7]);
});
Promise.all([p1, p2]).then(() => done());
@@ -335,6 +348,16 @@ describe('Project member invite delete', () => {
.end(() => done());
});
+ it('should return 204 if "Connect Copilot Manager" cancels invitation for copilot', (done) => {
+ request(server)
+ .delete(`/v5/projects/${project1.id}/invites/7`)
+ .set({
+ Authorization: `Bearer ${testUtil.jwts.copilotManager}`,
+ })
+ .expect(204)
+ .end(() => done());
+ });
+
it('should return 204 if user cancels invitation', (done) => {
request(server)
.delete(`/v5/projects/${project1.id}/invites/5`)
diff --git a/src/routes/projectMembers/delete.js b/src/routes/projectMembers/delete.js
index 8c8e4d55..c654ecf6 100644
--- a/src/routes/projectMembers/delete.js
+++ b/src/routes/projectMembers/delete.js
@@ -31,12 +31,31 @@ module.exports = [
return Promise.reject(err);
}
+ const isOwnMember = member.userId === req.authUser.userId;
+
if (
- member.userId !== req.authUser.userId &&
- member.role !== PROJECT_MEMBER_ROLE.CUSTOMER &&
- !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_MEMBER_NON_CUSTOMER, req)
+ !isOwnMember &&
+ member.role !== PROJECT_MEMBER_ROLE.CUSTOMER &&
+ member.role !== PROJECT_MEMBER_ROLE.COPILOT &&
+ !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_MEMBER_TOPCODER, req)
+ ) {
+ const err = new Error('You don\'t have permissions to delete other members from Topcoder Team.');
+ err.status = 403;
+ return Promise.reject(err);
+ } else if (
+ !isOwnMember &&
+ member.role === PROJECT_MEMBER_ROLE.CUSTOMER &&
+ !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_MEMBER_CUSTOMER, req)
+ ) {
+ const err = new Error('You don\'t have permissions to delete other members with "customer" role.');
+ err.status = 403;
+ return Promise.reject(err);
+ } else if (
+ !isOwnMember &&
+ member.role === PROJECT_MEMBER_ROLE.COPILOT &&
+ !util.hasPermissionByReq(PERMISSION.DELETE_PROJECT_MEMBER_COPILOT, req)
) {
- const err = new Error('You don\'t have permissions to delete other members with non-customer role.');
+ const err = new Error('You don\'t have permissions to delete other members with "copilot" role.');
err.status = 403;
return Promise.reject(err);
}
diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js
index 3e44381d..311f1831 100644
--- a/src/routes/projects/update.js
+++ b/src/routes/projects/update.js
@@ -7,7 +7,6 @@ import {
import models from '../../models';
import {
PROJECT_STATUS,
- PROJECT_MEMBER_ROLE,
EVENT,
RESOURCES,
REGEX,
@@ -224,23 +223,14 @@ module.exports = [
});
return Promise.reject(err);
}
- // Only project manager (user with manager role assigned) or topcoder
- // admin should be allowed to transition project status to 'active'.
- const members = req.context.currentProjectMembers;
- const validRoles = [
- PROJECT_MEMBER_ROLE.MANAGER,
- PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
- PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
- PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
- ].map(x => x.toLowerCase());
- const matchRole = role => _.indexOf(validRoles, role.toLowerCase()) >= 0;
- if (updatedProps.status === PROJECT_STATUS.ACTIVE &&
- !util.hasAdminRole(req) &&
- _.isUndefined(_.find(members,
- m => m.userId === req.authUser.userId && matchRole(m.role)))
+
+ // check if user has permissions to update project status
+ if (
+ updatedProps.status &&
+ updatedProps.status !== project.status &&
+ !util.hasPermissionByReq(PERMISSION.UPDATE_PROJECT_STATUS, req)
) {
- const err = new Error('Only assigned topcoder-managers or topcoder admins should be allowed ' +
- 'to launch a project');
+ const err = new Error('You are not allowed to update project status.');
err.status = 403;
return Promise.reject(err);
}
diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js
index 1f952e09..24c9e9a5 100644
--- a/src/routes/projects/update.spec.js
+++ b/src/routes/projects/update.spec.js
@@ -148,28 +148,6 @@ describe('Project', () => {
});
});
- it('should return 403 if invalid user will update a project', (done) => {
- request(server)
- .patch(`/v5/projects/${project1.id}`)
- .set({
- Authorization: `Bearer ${testUtil.jwts.copilot}`,
- })
- .send({
- status: 'active',
- })
- .expect('Content-Type', /json/)
- .expect(403)
- .end((err, res) => {
- if (err) {
- done(err);
- } else {
- res.body.message.should.equal('Only assigned topcoder-managers or topcoder admins' +
- ' should be allowed to launch a project');
- done();
- }
- });
- });
-
it('should return 200 if topcoder manager user will update a project', (done) => {
request(server)
.patch(`/v5/projects/${project1.id}`)
@@ -495,43 +473,6 @@ describe('Project', () => {
});
});
- it('should return 403, copilot is not allowed to transition project out of cancel status', (done) => {
- models.Project.update({
- status: PROJECT_STATUS.CANCELLED,
- }, {
- where: {
- id: project1.id,
- },
- })
- .then(() => {
- const mbody = {
- name: 'updatedProject name',
- status: PROJECT_STATUS.ACTIVE,
-
- };
- request(server)
- .patch(`/v5/projects/${project1.id}`)
- .set({
- Authorization: `Bearer ${testUtil.jwts.copilot}`,
- })
- .send(mbody)
- .expect('Content-Type', /json/)
- .expect(403)
- .end((err, res) => {
- if (err) {
- done(err);
- } else {
- res
- .body
- .message
- .should
- .equal('Only assigned topcoder-managers or topcoder admins should be allowed to launch a project');
- done();
- }
- });
- });
- });
-
it('should return 200 and project history should not be updated', (done) => {
request(server)
.patch(`/v5/projects/${project1.id}`)
diff --git a/src/tests/util.js b/src/tests/util.js
index e9a64bbb..a98e7b5c 100644
--- a/src/tests/util.js
+++ b/src/tests/util.js
@@ -28,6 +28,8 @@ export default {
admin: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw',
// userId = 40051334, roles: [ 'Manager', 'Topcoder User' ],handle: 'test1',email: 'test@topcoder.com'
manager: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzQiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.J5VtOEQVph5jfe2Ji-NH7txEDcx_5gthhFeD-MzX9ck',
+ // userId = 40051337, roles: [ 'Connect Copilot Manager', 'Connect Manager', 'Topcoder User' ], handle: 'connect_copilot_manger', email: 'connect_copilot_manger@topcoder.com'
+ copilotManager: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIiwiQ29ubmVjdCBDb3BpbG90IE1hbmFnZXIiXSwiaXNzIjoiaHR0cHM6Ly9hcGkudG9wY29kZXItZGV2LmNvbSIsImhhbmRsZSI6ImNvbm5lY3RfY29waWxvdF9tYW5nZXIiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzM3IiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6ImNvbm5lY3RfY29waWxvdF9tYW5nZXJAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.j9nTufEslU5CLXqkwHixC-nNdysJSCYQC9MhacOca64',
// 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'
@@ -54,6 +56,7 @@ export default {
manager: 40051334,
member2: 40051335,
connectAdmin: 40051336,
+ copilotManager: 40051337,
romit: 40158431,
},
getDecodedToken: token => jwt.decode(token),
diff --git a/src/util.js b/src/util.js
index 2ad0aa4a..1cdd8cfd 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1277,14 +1277,14 @@ const projectServiceUtils = {
return false;
}
- // console.log('hasPermission', permission, user);
-
const allowRule = permission.allowRule ? permission.allowRule : permission;
const denyRule = permission.denyRule ? permission.denyRule : null;
const allow = util.matchPermissionRule(allowRule, user, projectMembers);
const deny = util.matchPermissionRule(denyRule, user, projectMembers);
+ // console.log('hasPermission', JSON.stringify({ permission, user, projectMembers, allow, deny }, null, 2));
+
return allow && !deny;
},