From 613beebe9138152f676002556c551be267b6a6a8 Mon Sep 17 00:00:00 2001 From: gets0ul Date: Thu, 7 May 2020 04:07:23 +0700 Subject: [PATCH 1/3] - use separate scope for invites - return project with empty invites if user doesn't have enough permission --- docs/permissions.html | 48 ++++++++++++++++---------------- src/constants.js | 5 ++++ src/permissions/constants.js | 36 ++++++++++++++++-------- src/routes/projects/get.js | 31 +++++++++++++++++++-- src/routes/projects/get.spec.js | 20 +++++++++++++ src/routes/projects/list.js | 18 ++++++++++++ src/routes/projects/list.spec.js | 27 ++++++++++++++++-- 7 files changed, 145 insertions(+), 40 deletions(-) diff --git a/docs/permissions.html b/docs/permissions.html index f1ed60d8..97f5d5f5 100644 --- a/docs/permissions.html +++ b/docs/permissions.html @@ -636,8 +636,8 @@

all:connect_project - all:project-members - read:project-members + all:project-invites + read:project-invites
@@ -670,8 +670,8 @@

all:connect_project - all:project-members - read:project-members + all:project-invites + read:project-invites
@@ -704,8 +704,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -734,8 +734,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -759,8 +759,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -782,8 +782,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -806,8 +806,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -831,8 +831,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -854,8 +854,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -879,8 +879,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -909,8 +909,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
@@ -934,8 +934,8 @@

all:connect_project - all:project-members - write:project-members + all:project-invites + write:project-invites
diff --git a/src/constants.js b/src/constants.js index 1babbe14..180bf7ea 100644 --- a/src/constants.js +++ b/src/constants.js @@ -281,6 +281,11 @@ export const M2M_SCOPES = { READ: 'read:project-members', WRITE: 'write:project-members', }, + PROJECT_INVITES: { + ALL: 'all:project-invites', + READ: 'read:project-invites', + WRITE: 'write:project-invites', + }, }; export const TIMELINE_REFERENCES = { diff --git a/src/permissions/constants.js b/src/permissions/constants.js index d89d8277..77b55b46 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -91,6 +91,18 @@ const SCOPES_PROJECT_MEMBERS_WRITE = [ M2M_SCOPES.PROJECT_MEMBERS.WRITE, ]; +const SCOPES_PROJECT_INVITES_READ = [ + M2M_SCOPES.CONNECT_PROJECT_ADMIN, + M2M_SCOPES.PROJECT_INVITES.ALL, + M2M_SCOPES.PROJECT_INVITES.READ, +]; + +const SCOPES_PROJECT_INVITES_WRITE = [ + M2M_SCOPES.CONNECT_PROJECT_ADMIN, + M2M_SCOPES.PROJECT_INVITES.ALL, + M2M_SCOPES.PROJECT_INVITES.WRITE, +]; + export const PERMISSION = { // eslint-disable-line import/prefer-default-export /* * Project @@ -303,7 +315,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export description: 'Who can view own invite.', }, topcoderRoles: ALL, - scopes: SCOPES_PROJECT_MEMBERS_READ, + scopes: SCOPES_PROJECT_INVITES_READ, }, READ_PROJECT_INVITE_NOT_OWN: { @@ -314,7 +326,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export }, topcoderRoles: TOPCODER_ROLES_MANAGERS_AND_ADMINS, projectRoles: ALL, - scopes: SCOPES_PROJECT_MEMBERS_READ, + scopes: SCOPES_PROJECT_INVITES_READ, }, CREATE_PROJECT_INVITE_CUSTOMER: { @@ -325,7 +337,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export }, topcoderRoles: TOPCODER_ROLES_MANAGERS_AND_ADMINS, projectRoles: ALL, - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, CREATE_PROJECT_INVITE_NON_CUSTOMER: { @@ -336,7 +348,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export }, topcoderRoles: TOPCODER_ROLES_ADMINS, projectRoles: PROJECT_ROLES_MANAGEMENT, - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, CREATE_PROJECT_INVITE_COPILOT_DIRECTLY: { @@ -349,7 +361,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export ...TOPCODER_ROLES_ADMINS, USER_ROLE.COPILOT_MANAGER, ], - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, UPDATE_PROJECT_INVITE_OWN: { @@ -359,7 +371,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export description: 'Who can update own invite.', }, topcoderRoles: ALL, - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, UPDATE_PROJECT_INVITE_NOT_OWN: { @@ -369,7 +381,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export description: 'Who can update invites for other members.', }, topcoderRoles: TOPCODER_ROLES_ADMINS, - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, UPDATE_PROJECT_INVITE_REQUESTED: { @@ -382,7 +394,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export ...TOPCODER_ROLES_ADMINS, USER_ROLE.COPILOT_MANAGER, ], - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, DELETE_PROJECT_INVITE_OWN: { @@ -392,7 +404,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export description: 'Who can delete own invite.', }, topcoderRoles: ALL, - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, DELETE_PROJECT_INVITE_NOT_OWN_CUSTOMER: { @@ -403,7 +415,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export }, topcoderRoles: TOPCODER_ROLES_ADMINS, projectRoles: ALL, - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, DELETE_PROJECT_INVITE_NOT_OWN_NON_CUSTOMER: { @@ -414,7 +426,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export }, topcoderRoles: TOPCODER_ROLES_ADMINS, projectRoles: PROJECT_ROLES_MANAGEMENT, - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, DELETE_PROJECT_INVITE_REQUESTED: { @@ -427,7 +439,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export ...TOPCODER_ROLES_ADMINS, USER_ROLE.COPILOT_MANAGER, ], - scopes: SCOPES_PROJECT_MEMBERS_WRITE, + scopes: SCOPES_PROJECT_INVITES_WRITE, }, /** diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js index 148601b2..a3b220fa 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -3,6 +3,7 @@ import config from 'config'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; +import { PERMISSION } from '../../permissions/constants'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); @@ -115,7 +116,23 @@ const retrieveProjectFromES = (projectId, req) => { const es = util.getElasticSearchClient(); es.search(searchCriteria).then((docs) => { const rows = _.map(docs.hits.hits, single => single._source); // eslint-disable-line no-underscore-dangle - accept(rows[0]); + const project = rows[0]; + if (project && project.invites) { + if (!util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_NOT_OWN, req)) { + let invites; + if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_OWN, req)) { + // only include own invites + const currentUserId = req.authUser.userId; + const email = req.authUser.email; + invites = _.filter(project.invites, invite => invite.userId === currentUserId || invite.email === email); + } else { + // return empty invites + invites = []; + } + _.set(project, 'invites', invites); + } + } + accept(project); }).catch(reject); }); }; @@ -156,7 +173,17 @@ const retrieveProjectFromDB = (projectId, req) => { if (attachments) { project.attachments = attachments; } - return models.ProjectMemberInvite.getPendingAndReguestedInvitesForProject(projectId); + if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_NOT_OWN, req)) { + // include all invites + return models.ProjectMemberInvite.getPendingAndReguestedInvitesForProject(projectId); + } else if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_OWN, req)) { + // include only own invites + const currentUserId = req.authUser.userId; + const email = req.authUser.email; + return models.ProjectMemberInvite.getPendingOrRequestedProjectInvitesForUser(projectId, email, currentUserId); + } + // empty + return Promise.resolve([]); }) .then((invites) => { project.invites = invites; diff --git a/src/routes/projects/get.spec.js b/src/routes/projects/get.spec.js index a12e46f6..732f65d5 100644 --- a/src/routes/projects/get.spec.js +++ b/src/routes/projects/get.spec.js @@ -274,6 +274,26 @@ describe('GET Project', () => { }); }); + it('should return the project with empty invites using M2M token without "read:project-invites" scope', (done) => { + request(server) + .get(`/v5/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.m2m['read:projects']}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.invites.should.be.empty; + done(); + } + }); + }); + it('should return project with "members", "invites", and "attachments" by default when data comes from ES', (done) => { request(server) .get(`/v5/projects/${data[0].id}`) diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index a9ea8445..9d1c6afe 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -568,6 +568,24 @@ const retrieveProjects = (req, criteria, sort, ffields) => { const es = util.getElasticSearchClient(); es.search(searchCriteria).then((docs) => { const rows = _.map(docs.hits.hits, single => single._source); // eslint-disable-line no-underscore-dangle + if (rows) { + if (!util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_NOT_OWN, req)) { + if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_OWN, req)) { + // only include own invites + const currentUserId = req.authUser.userId; + const email = req.authUser.email; + _.forEach(rows, (fp) => { + const invites = _.filter(fp.invites, invite => invite.userId === currentUserId || invite.email === email); + _.set(fp, 'invites', invites); + }); + } else { + // return empty invites + _.forEach(rows, (fp) => { + _.set(fp, 'invites', []); + }); + } + } + } accept({ rows, count: docs.hits.total, pageSize: criteria.limit, page: criteria.page }); }).catch(reject); }); diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index e454a181..bc9867c7 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -405,6 +405,29 @@ describe('LIST Project', () => { }); }); + it('should return the project with empty invites using M2M token without "read:project-invites" scope', (done) => { + request(server) + .get('/v5/projects') + .set({ + Authorization: `Bearer ${testUtil.m2m['read:projects']}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.lengthOf(3); + resJson.forEach((project) => { + project.invites.should.be.empty; + }); + done(); + } + }); + }); + it('should return the project when project that is in reviewed state in which the copilot is its member or has been invited', (done) => { request(server) .get('/v5/projects') @@ -1130,9 +1153,9 @@ describe('LIST Project', () => { should.exist(resJson); resJson.should.have.lengthOf(1); resJson[0].name.should.equal('test1'); - resJson[0].invites.should.have.lengthOf(2); + resJson[0].invites.should.have.lengthOf(1); resJson[0].invites[0].should.have.property('email'); - resJson[0].invites[1].email.should.equal('h***o@w***d.com'); + resJson[0].invites[0].userId.should.equal(40051335); done(); } }); From cb1517e78cb7f4a0a7146927506bdb218a2455db Mon Sep 17 00:00:00 2001 From: gets0ul Date: Fri, 8 May 2020 17:46:15 +0700 Subject: [PATCH 2/3] filter invites by email in case-insensitive way --- src/routes/projects/get.js | 7 +++++-- src/routes/projects/list.js | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js index a3b220fa..5594594c 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -123,8 +123,11 @@ const retrieveProjectFromES = (projectId, req) => { if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_OWN, req)) { // only include own invites const currentUserId = req.authUser.userId; - const email = req.authUser.email; - invites = _.filter(project.invites, invite => invite.userId === currentUserId || invite.email === email); + const currentUserEmail = req.authUser.email; + invites = _.filter(project.invites, invite => ( + (invite.userId !== null && invite.userId === currentUserId) || + (invite.email && currentUserEmail && invite.email.toLowerCase() === currentUserEmail.toLowerCase()) + )); } else { // return empty invites invites = []; diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 9d1c6afe..fce01062 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -575,7 +575,10 @@ const retrieveProjects = (req, criteria, sort, ffields) => { const currentUserId = req.authUser.userId; const email = req.authUser.email; _.forEach(rows, (fp) => { - const invites = _.filter(fp.invites, invite => invite.userId === currentUserId || invite.email === email); + const invites = _.filter(fp.invites, invite => ( + (invite.userId !== null && invite.userId === currentUserId) || + (invite.email && currentUserEmail && invite.email.toLowerCase() === currentUserEmail.toLowerCase()) + )); _.set(fp, 'invites', invites); }); } else { From b49c0a3147a040865a9b417096e30d3ca811ff22 Mon Sep 17 00:00:00 2001 From: gets0ul Date: Mon, 11 May 2020 13:36:05 +0700 Subject: [PATCH 3/3] fix typo and lint error --- src/routes/projects/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index fce01062..3fc9010c 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -573,7 +573,7 @@ const retrieveProjects = (req, criteria, sort, ffields) => { if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_OWN, req)) { // only include own invites const currentUserId = req.authUser.userId; - const email = req.authUser.email; + const currentUserEmail = req.authUser.email; _.forEach(rows, (fp) => { const invites = _.filter(fp.invites, invite => ( (invite.userId !== null && invite.userId === currentUserId) ||