From 0d8910f351b5160269e87e0d4502da95677c05d7 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2020 11:27:33 +0530 Subject: [PATCH 01/24] Temp change for fixing missing email in token --- src/middlewares/jwtDecodePatcher.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/middlewares/jwtDecodePatcher.js diff --git a/src/middlewares/jwtDecodePatcher.js b/src/middlewares/jwtDecodePatcher.js new file mode 100644 index 00000000..575a99c5 --- /dev/null +++ b/src/middlewares/jwtDecodePatcher.js @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016 TopCoder Inc., All Rights Reserved. + */ +/** + * This is the middleware to check role. + * @author TCDEVELOPER + * @version 1.0 + */ +import _ from 'lodash'; + +module.exports = function patchAuthUser(roleName) { + return function patch(req, res, next) { + if (req.authUser) { + if (!req.authUser.email) { + req.authUser.email = _.find(req.authUser, (value, key) => { + return (key.indexOf('email') !== -1) + }) + } + } + return next(); + }; +}; From fda803e69e2055246b8f318ae0d2e44f60bc2006 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2020 11:31:05 +0530 Subject: [PATCH 02/24] added logging --- src/middlewares/jwtDecodePatcher.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/middlewares/jwtDecodePatcher.js b/src/middlewares/jwtDecodePatcher.js index 575a99c5..6bfa62c5 100644 --- a/src/middlewares/jwtDecodePatcher.js +++ b/src/middlewares/jwtDecodePatcher.js @@ -8,10 +8,11 @@ */ import _ from 'lodash'; -module.exports = function patchAuthUser(roleName) { +module.exports = function patchAuthUser(logger) { return function patch(req, res, next) { if (req.authUser) { if (!req.authUser.email) { + logger.debug(`Email not found for user with id ${req.authUser.userId}`); req.authUser.email = _.find(req.authUser, (value, key) => { return (key.indexOf('email') !== -1) }) From efe9414170758c0660af8b8705d98351de163804 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2020 11:31:46 +0530 Subject: [PATCH 03/24] Using temporary middleware to parse missing info in token --- src/app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app.js b/src/app.js index 9c970d07..668eab55 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,7 @@ import expressRequestId from 'express-request-id'; import swaggerUi from 'swagger-ui-express'; import YAML from 'yamljs'; import performanceRequestLogger from './middlewares/performanceRequestLogger'; +import jwtDecodePatcher from './middlewares/jwtDecodePatcher'; import router from './routes'; import permissions from './permissions'; import models from './models'; @@ -69,6 +70,8 @@ const logger = coreLib.logger({ app.use(performanceRequestLogger(logger)); app.logger = logger; +app.use(jwtDecodePatcher(logger)); + // ======================= // CORS ================ // ======================= From b1584de1c353d30db1cf6d99af2b4aabf03c6647 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2020 11:37:45 +0530 Subject: [PATCH 04/24] lint fix --- src/middlewares/jwtDecodePatcher.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/middlewares/jwtDecodePatcher.js b/src/middlewares/jwtDecodePatcher.js index 6bfa62c5..9531bb21 100644 --- a/src/middlewares/jwtDecodePatcher.js +++ b/src/middlewares/jwtDecodePatcher.js @@ -13,9 +13,7 @@ module.exports = function patchAuthUser(logger) { if (req.authUser) { if (!req.authUser.email) { logger.debug(`Email not found for user with id ${req.authUser.userId}`); - req.authUser.email = _.find(req.authUser, (value, key) => { - return (key.indexOf('email') !== -1) - }) + req.authUser.email = _.find(req.authUser, (value, key) => (key.indexOf('email') !== -1)); } } return next(); From 3f1437b896062453ae18432e8e7fb9c8ecdb39e5 Mon Sep 17 00:00:00 2001 From: vikasrohit Date: Mon, 16 Nov 2020 12:55:42 +0530 Subject: [PATCH 05/24] Revert "Hotfix/fixing jwt decoding new auth0" --- src/app.js | 3 --- src/middlewares/jwtDecodePatcher.js | 21 --------------------- 2 files changed, 24 deletions(-) delete mode 100644 src/middlewares/jwtDecodePatcher.js diff --git a/src/app.js b/src/app.js index 668eab55..9c970d07 100644 --- a/src/app.js +++ b/src/app.js @@ -10,7 +10,6 @@ import expressRequestId from 'express-request-id'; import swaggerUi from 'swagger-ui-express'; import YAML from 'yamljs'; import performanceRequestLogger from './middlewares/performanceRequestLogger'; -import jwtDecodePatcher from './middlewares/jwtDecodePatcher'; import router from './routes'; import permissions from './permissions'; import models from './models'; @@ -70,8 +69,6 @@ const logger = coreLib.logger({ app.use(performanceRequestLogger(logger)); app.logger = logger; -app.use(jwtDecodePatcher(logger)); - // ======================= // CORS ================ // ======================= diff --git a/src/middlewares/jwtDecodePatcher.js b/src/middlewares/jwtDecodePatcher.js deleted file mode 100644 index 9531bb21..00000000 --- a/src/middlewares/jwtDecodePatcher.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2016 TopCoder Inc., All Rights Reserved. - */ -/** - * This is the middleware to check role. - * @author TCDEVELOPER - * @version 1.0 - */ -import _ from 'lodash'; - -module.exports = function patchAuthUser(logger) { - return function patch(req, res, next) { - if (req.authUser) { - if (!req.authUser.email) { - logger.debug(`Email not found for user with id ${req.authUser.userId}`); - req.authUser.email = _.find(req.authUser, (value, key) => (key.indexOf('email') !== -1)); - } - } - return next(); - }; -}; From 40f59f8948957b943783c8b36e2637975b1af745 Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 20 Nov 2020 13:15:54 +0200 Subject: [PATCH 06/24] docs: update README about Kafka consumer --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 084ebdbc..cfc290cc 100644 --- a/README.md +++ b/README.md @@ -174,9 +174,7 @@ Local setup should work good on **Linux**, **macOS** and **Windows**. Runs the Project Service using nodemon, so it would be restarted after any of the files is updated. The project service will be served on `http://localhost:8001`. -4. *(Optional)* Start Project Service Kafka Consumer - - *Run this only if you want to test or modify logic of `lastActivityAt` or `lastActivityBy`.* +4. Start Project Service Kafka Consumer In another terminal window run: @@ -184,6 +182,18 @@ Local setup should work good on **Linux**, **macOS** and **Windows**. npm run startKafkaConsumers:dev ``` +
Click to learn what this service does +
+ + This service run Kafka Consumer which listens to some events and handles the next things: + - updates `lastActivityAt` and `lastActivityUserId` for projects when we make any updates related to the project like updating project, project attachments, project plan, project members create/delete topics and so on + - create/update/delete topics for phases when we create/update/delete phases + - update phase progress and duration when milestone is completed + - see all the Kafka handlers in file [src/events/kafkaHandlers.js](./src/events/kafkaHandlers.js) + +
+ + ## Run Connect App with Project Service locally To be able to run [Connect App](https://github.com/appirio-tech/connect-app) with the local setup of Project Service we have to do two things: From 8d2b09a6c71f3efc4c98f11d7967aabf7adf526e Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 20 Nov 2020 13:17:58 +0200 Subject: [PATCH 07/24] docs: fix README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfc290cc..9302d11a 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ Local setup should work good on **Linux**, **macOS** and **Windows**. - update phase progress and duration when milestone is completed - see all the Kafka handlers in file [src/events/kafkaHandlers.js](./src/events/kafkaHandlers.js) -
+
## Run Connect App with Project Service locally From c44dd435d2f89abfba04f97be762db36b5d8f180 Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 20 Nov 2020 17:28:58 +0200 Subject: [PATCH 08/24] feat: remove copilot members and invites (#594) * feat: remove copilot members and invites * fix: unit test * fix: invite create unit test and debug * fix: debug unit tests * fix: more debug * fix: more debug * fix: more debug * fix: more debug * fix: Copilot Manager token for unit tests * fix: remove debugging --- docs/permissions.html | 91 ++++++++++++------- src/permissions/constants.js | 63 ++++++++----- src/permissions/index.js | 9 +- src/routes/projectMemberInvites/create.js | 12 ++- .../projectMemberInvites/create.spec.js | 30 +++++- src/routes/projectMemberInvites/delete.js | 9 +- .../projectMemberInvites/delete.spec.js | 25 ++++- src/routes/projectMembers/delete.js | 27 +++++- src/tests/util.js | 3 + src/util.js | 4 +- 10 files changed, 200 insertions(+), 73 deletions(-) diff --git a/docs/permissions.html b/docs/permissions.html index 43823aa0..b827955d 100644 --- a/docs/permissions.html +++ b/docs/permissions.html @@ -523,19 +523,19 @@

- Update Project Member (to copilot) + Delete Project Member (customer)
-
UPDATE_PROJECT_MEMBER_TO_COPILOT
-
Who can update project member role to "copilot".
+
DELETE_PROJECT_MEMBER_CUSTOMER
+
Who can delete project members with "customer" role.
+ Any Project Member
Connect Admin administrator - Connect Copilot Manager
@@ -548,14 +548,19 @@

- Delete Project Member (customer) + Delete Project Member (topcoder)
-
DELETE_PROJECT_MEMBER_CUSTOMER
-
Who can delete project members with "customer" role.
+
DELETE_PROJECT_MEMBER_TOPCODER
+
Who can delete project members with some topcoder role like "manager" etc.
- Any Project Member + manager + account_manager + program_manager + account_executive + solution_architect + project_manager
@@ -573,24 +578,20 @@

- Delete Project Member (non-customer) + Delete Project Member (copilot)
-
DELETE_PROJECT_MEMBER_NON_CUSTOMER
-
Who can delete project members with non "customer" role.
+
DELETE_PROJECT_MEMBER_COPILOT
+
Who can delete project members with "copilot" role.
- manager - account_manager - program_manager - account_executive - solution_architect - project_manager + Any Project Member
Connect Admin administrator + Connect Copilot Manager
@@ -680,15 +681,6 @@

Connect Admin administrator - Connect Manager - Connect Account Manager - Connect Copilot Manager - Business Development Representative - Presales - Account Executive - Program Manager - Solution Architect - Project Manager
@@ -701,10 +693,10 @@

- Create Project Invite (non-customer) + Create Project Invite (topcoder)
-
CREATE_PROJECT_INVITE_NON_CUSTOMER
-
Who can invite project members with non "customer" role.
+
CREATE_PROJECT_INVITE_TOPCODER
+
Who can invite project members with topcoder role like "manager" etc.
@@ -731,9 +723,9 @@

- Create Project Invite (copilot) + Create Project Invite (copilot)
-
CREATE_PROJECT_INVITE_COPILOT_DIRECTLY
+
CREATE_PROJECT_INVITE_COPILOT
Who can invite user with "copilot" role directly without requesting.
@@ -876,10 +868,40 @@

- Delete Project Invite (not own, non-customer) + Delete Project Invite (not own, topcoder) +
+
DELETE_PROJECT_INVITE_NOT_OWN_TOPCODER
+
Who can delete project invites for other members with some topcoder role like "manager" etc.
+
+
+
+ manager + account_manager + program_manager + account_executive + solution_architect + project_manager +
+ +
+ Connect Admin + administrator +
+ +
+ all:connect_project + all:project-invites + write:project-invites +
+
+
+
+
+
+ Delete Project Invite (not own, copilot)
-
DELETE_PROJECT_INVITE_NOT_OWN_NON_CUSTOMER
-
Who can delete project invites for other members with non "customer" role.
+
DELETE_PROJECT_INVITE_NOT_OWN_COPILOT
+
Who can delete invites for other members with "copilot" role.
@@ -894,6 +916,7 @@

Connect Admin administrator + Connect Copilot Manager
diff --git a/src/permissions/constants.js b/src/permissions/constants.js index c69cf36f..874ea45a 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -306,19 +306,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 +317,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 +372,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 +455,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/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/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; }, From b75a4355ec83f1be86ec46ea2953ca41b39c33e4 Mon Sep 17 00:00:00 2001 From: Caizheng Peng Date: Fri, 4 Dec 2020 21:37:18 +0800 Subject: [PATCH 09/24] create job after project was created (#595) --- config/default.json | 3 +- config/development.json | 3 +- src/events/projects/index.js | 57 ++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/config/default.json b/config/default.json index 87fcaa52..b302a52f 100644 --- a/config/default.json +++ b/config/default.json @@ -75,5 +75,6 @@ "EMBED_REPORTS_MAPPING": "{\"mock\": \"/embed/looks/2\"}", "ALLOWED_USERS": "[]" }, - "DEFAULT_M2M_USERID": -101 + "DEFAULT_M2M_USERID": -101, + "taasJobApiUrl": "https://api.topcoder.com/v5/jobs" } diff --git a/config/development.json b/config/development.json index 3f3e909b..d20cddd9 100644 --- a/config/development.json +++ b/config/development.json @@ -6,5 +6,6 @@ "fileServiceEndpoint": "https://api.topcoder-dev.com/v3/files/", "connectProjectsUrl": "https://connect.topcoder-dev.com/projects/", "memberServiceEndpoint": "https://api.topcoder-dev.com/v3/members", - "identityServiceEndpoint": "https://api.topcoder-dev.com/v3/" + "identityServiceEndpoint": "https://api.topcoder-dev.com/v3/", + "taasJobApiUrl": "https://api.topcoder-dev.com/v5/jobs" } diff --git a/src/events/projects/index.js b/src/events/projects/index.js index 0f8dce34..b211cd85 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,40 @@ async function projectCreatedKafkaHandler(app, topic, payload) { await Promise.all(topicPromises); app.logger.debug('Topics for phases are successfully created.'); } + if (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 + const skills = specialist.skills.filter(skill => skill.id).map(skill => skill.id); + return createTaasJob({ + projectId: project.id, + externalId: _.get(project, 'external.id') || String(project.id), + description: specialist.roleTitle, + startDate, + endDate, + skills, + numPositions: Number(specialist.people), + resourceType: specialist.role, + rateType: 'hourly', + workload: 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 = { From 4116ef169407630e0aed22671c73e7dca034851b Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 16:35:39 +0200 Subject: [PATCH 10/24] fix: Kafka topics handlers config - As most of the topics have the same name, we cannot simply use them as objects keys. So we have to use a register method which would merge the config smartly. --- src/events/kafkaHandlers.js | 94 +++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 24 deletions(-) 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; From f8e3b8caa906c8259241bbf8696d22e6b745c337 Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 16:46:26 +0200 Subject: [PATCH 11/24] fix: improvements to job creation - use token from ENV variable instead of M2M until TaaS API supports M2M --- src/events/projects/index.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/events/projects/index.js b/src/events/projects/index.js index b211cd85..1643b960 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -22,7 +22,10 @@ const eClient = util.getElasticSearchClient(); * @return {Object} the job created */ const createTaasJob = async (data) => { - const token = await util.getM2MToken(); + // TODO uncomment when TaaS API supports M2M tokens + // see https://github.com/topcoder-platform/taas-apis/issues/40 + // const token = await util.getM2MToken(); + const token = process.env.TAAS_API_TOKEN; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, @@ -200,18 +203,21 @@ async function projectCreatedKafkaHandler(app, topic, payload) { (specialist) => { const startDate = new Date(); const endDate = moment(startDate).add(Number(specialist.duration), 'M'); // the unit of duration is month - const skills = specialist.skills.filter(skill => skill.id).map(skill => skill.id); + // use both, required and additional skills for jobs + const skills = specialist.skills.concat(specialist.additionalSkills) + // only include skills with `id` and ignore custom skills in jobs + .filter(skill => skill.id).map(skill => skill.id); return createTaasJob({ projectId: project.id, - externalId: _.get(project, 'external.id') || String(project.id), + externalId: '0', // hardcode for now description: specialist.roleTitle, startDate, endDate, skills, numPositions: Number(specialist.people), resourceType: specialist.role, - rateType: 'hourly', - workload: specialist.workLoad.title.toLowerCase(), + 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) => { From 26c30c3290ea9e624280be445d474851c12146d0 Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 16:35:39 +0200 Subject: [PATCH 12/24] fix: Kafka topics handlers config - As most of the topics have the same name, we cannot simply use them as objects keys. So we have to use a register method which would merge the config smartly. --- src/events/kafkaHandlers.js | 94 +++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 24 deletions(-) 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; From cccb677d626697b4650753ad7d5deec08965a227 Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 17:04:58 +0200 Subject: [PATCH 13/24] chore: deploy branch "feature/create-taas-jobs" --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d9851d0d..8a7b9cac 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,7 +114,7 @@ workflows: - test filters: branches: - only: ['develop'] + only: ['develop', 'feature/create-taas-jobs'] - deployProd: context : org-global requires: From 55a667b5435fe42bf960c6979e3553e502791146 Mon Sep 17 00:00:00 2001 From: maxceem Date: Sat, 5 Dec 2020 13:38:38 +0200 Subject: [PATCH 14/24] Revert "[DEV] [HOTFIX] Kafka Topics Handlers Config" --- package.json | 2 +- src/events/kafkaHandlers.js | 94 ++++++++++--------------------------- 2 files changed, 25 insertions(+), 71 deletions(-) diff --git a/package.json b/package.json index f7b398d3..b903f117 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "pg-native": "^3.0.0", "sequelize": "^5.8.7", "swagger-ui-express": "^4.0.6", - "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.6", + "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", "traverse": "^0.6.6", "urlencode": "^1.1.0", "yamljs": "^0.3.0" diff --git a/src/events/kafkaHandlers.js b/src/events/kafkaHandlers.js index c1c76611..b6bf90cf 100644 --- a/src/events/kafkaHandlers.js +++ b/src/events/kafkaHandlers.js @@ -1,22 +1,18 @@ /** * BUS Event Handlers */ -import { - CONNECT_NOTIFICATION_EVENT, - BUS_API_EVENT, - RESOURCES, -} from '../constants'; +import { CONNECT_NOTIFICATION_EVENT, BUS_API_EVENT, RESOURCES } from '../constants'; import { projectCreatedKafkaHandler, - projectUpdatedKafkaHandler, -} from './projects'; + projectUpdatedKafkaHandler } from './projects'; +import { projectPhaseAddedKafkaHandler, projectPhaseRemovedKafkaHandler, + projectPhaseUpdatedKafkaHandler } from './projectPhases'; +import { + timelineAdjustedKafkaHandler, +} from './timelines'; import { - projectPhaseAddedKafkaHandler, - projectPhaseRemovedKafkaHandler, - projectPhaseUpdatedKafkaHandler, -} from './projectPhases'; -import { timelineAdjustedKafkaHandler } from './timelines'; -import { milestoneUpdatedKafkaHandler } from './milestones'; + milestoneUpdatedKafkaHandler, +} from './milestones'; const kafkaHandlers = { /** @@ -37,64 +33,22 @@ 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, -}; - -/** - * 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; + /** + * 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, + }, }; -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; From 4baf15567f471902b6d58b56ee76825031edcc0d Mon Sep 17 00:00:00 2001 From: maxceem Date: Mon, 7 Dec 2020 14:44:28 +0200 Subject: [PATCH 15/24] feat: use "skilliD" for job creation --- src/events/projects/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/events/projects/index.js b/src/events/projects/index.js index 1643b960..3b77a32f 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -203,10 +203,13 @@ async function projectCreatedKafkaHandler(app, topic, payload) { (specialist) => { const startDate = new Date(); const endDate = moment(startDate).add(Number(specialist.duration), 'M'); // the unit of duration is month - // use both, required and additional skills for jobs - const skills = specialist.skills.concat(specialist.additionalSkills) - // only include skills with `id` and ignore custom skills in jobs - .filter(skill => skill.id).map(skill => skill.id); + // 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 From 6a9a216ca8ba6cbb49c26167d34a3df7566c3c9c Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 8 Dec 2020 10:19:35 +0200 Subject: [PATCH 16/24] Revert "Revert "[DEV] [HOTFIX] Kafka Topics Handlers Config"" --- package.json | 2 +- src/events/kafkaHandlers.js | 94 +++++++++++++++++++++++++++---------- 2 files changed, 71 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index b903f117..f7b398d3 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "pg-native": "^3.0.0", "sequelize": "^5.8.7", "swagger-ui-express": "^4.0.6", - "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", + "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.6", "traverse": "^0.6.6", "urlencode": "^1.1.0", "yamljs": "^0.3.0" 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; From 0dbe5cc799ecfb19addb1bfb27ea5cebdffa7e2b Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 8 Dec 2020 10:25:11 +0200 Subject: [PATCH 17/24] chore: don't deploy feature branch --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a7b9cac..d9851d0d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,7 +114,7 @@ workflows: - test filters: branches: - only: ['develop', 'feature/create-taas-jobs'] + only: ['develop'] - deployProd: context : org-global requires: From 7a2341452a81396a9d24d99bf5726681c41dced3 Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 8 Dec 2020 12:44:29 +0200 Subject: [PATCH 18/24] fix: remove temporary user token for TaaS API --- src/events/projects/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/events/projects/index.js b/src/events/projects/index.js index 3b77a32f..7810e361 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -22,10 +22,7 @@ const eClient = util.getElasticSearchClient(); * @return {Object} the job created */ const createTaasJob = async (data) => { - // TODO uncomment when TaaS API supports M2M tokens - // see https://github.com/topcoder-platform/taas-apis/issues/40 - // const token = await util.getM2MToken(); - const token = process.env.TAAS_API_TOKEN; + const token = await util.getM2MToken(); const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, From 64a6e10bb8dbb3dbfa7296c1786b14074d3e433c Mon Sep 17 00:00:00 2001 From: maxceem Date: Sat, 12 Dec 2020 00:02:01 +0200 Subject: [PATCH 19/24] fix: allow update milestone start/end dates --- src/routes/milestones/update.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/milestones/update.js b/src/routes/milestones/update.js index feffabd0..61cbc62c 100644 --- a/src/routes/milestones/update.js +++ b/src/routes/milestones/update.js @@ -22,9 +22,9 @@ const schema = { name: Joi.string().max(255).optional(), description: Joi.string().max(255), duration: Joi.number().integer().min(1).optional(), - startDate: Joi.any().forbidden(), + startDate: Joi.date().allow(null), actualStartDate: Joi.date().allow(null), - endDate: Joi.any().forbidden(), + endDate: Joi.date().min(Joi.ref('startDate')).allow(null), completionDate: Joi.date().allow(null), status: Joi.string().max(45).optional(), type: Joi.string().max(45).optional(), From 4a7073054b2a7c11297d775b5723a74ae75fe196 Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 17 Dec 2020 13:26:18 +0200 Subject: [PATCH 20/24] Revert "fix: allow update milestone start/end dates" This reverts commit 64a6e10bb8dbb3dbfa7296c1786b14074d3e433c. --- src/routes/milestones/update.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/milestones/update.js b/src/routes/milestones/update.js index 61cbc62c..feffabd0 100644 --- a/src/routes/milestones/update.js +++ b/src/routes/milestones/update.js @@ -22,9 +22,9 @@ const schema = { name: Joi.string().max(255).optional(), description: Joi.string().max(255), duration: Joi.number().integer().min(1).optional(), - startDate: Joi.date().allow(null), + startDate: Joi.any().forbidden(), actualStartDate: Joi.date().allow(null), - endDate: Joi.date().min(Joi.ref('startDate')).allow(null), + endDate: Joi.any().forbidden(), completionDate: Joi.date().allow(null), status: Joi.string().max(45).optional(), type: Joi.string().max(45).optional(), From 85752111b54e03a7c8d8ad632e4e7d405fe9a036 Mon Sep 17 00:00:00 2001 From: maxceem Date: Sat, 26 Dec 2020 12:38:13 +0200 Subject: [PATCH 21/24] fix: don't require milestone dates match timeline --- src/routes/milestones/commonHelper.js | 6 ------ 1 file changed, 6 deletions(-) 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) From c1107f8bedb7372a76920e4ed18831b4be0e5f23 Mon Sep 17 00:00:00 2001 From: maxceem Date: Sat, 26 Dec 2020 13:02:06 +0200 Subject: [PATCH 22/24] fix: unit test --- src/routes/milestones/create.spec.js | 15 --------------- 1 file changed, 15 deletions(-) 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') From 819d4479f0defedd05198a5f78a63b70ce5424d2 Mon Sep 17 00:00:00 2001 From: maxceem Date: Mon, 28 Dec 2020 11:29:54 +0200 Subject: [PATCH 23/24] fix: copilot is allowed to update project status --- docs/permissions.html | 31 ++++++++++++++++ src/permissions/constants.js | 13 +++++++ src/routes/projects/update.js | 24 ++++-------- src/routes/projects/update.spec.js | 59 ------------------------------ 4 files changed, 51 insertions(+), 76 deletions(-) diff --git a/docs/permissions.html b/docs/permissions.html index b827955d..b494f485 100644 --- a/docs/permissions.html +++ b/docs/permissions.html @@ -259,6 +259,37 @@

+
+
+
+ Update Project Status +
+
UPDATE_PROJECT_STATUS
+
+
+
+
+ manager + account_manager + program_manager + account_executive + solution_architect + project_manager + copilot +
+ +
+ Connect Admin + administrator +
+ +
+ all:connect_project + all:projects + write:projects +
+
+
diff --git a/src/permissions/constants.js b/src/permissions/constants.js index 874ea45a..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"', 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}`) From 7269344ed09dcd199bafa573136919f6bb8438d7 Mon Sep 17 00:00:00 2001 From: maxceem Date: Mon, 28 Dec 2020 16:55:54 +0200 Subject: [PATCH 24/24] chore: temporary disable jobs creation --- src/events/projects/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/events/projects/index.js b/src/events/projects/index.js index 7810e361..cc109d6b 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -187,7 +187,8 @@ async function projectCreatedKafkaHandler(app, topic, payload) { await Promise.all(topicPromises); app.logger.debug('Topics for phases are successfully created.'); } - if (project.type === 'talent-as-a-service') { + // 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}`);