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 caa04531..e7969646 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -117,7 +117,26 @@ 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 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 = []; + } + _.set(project, 'invites', invites); + } + } + accept(project); }).catch(reject); }); }; @@ -160,7 +179,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 3faa8a0e..ed09ffb4 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 3998e688..8264b493 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -591,6 +591,25 @@ const retrieveProjects = (req, criteria, sort, ffields) => { 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 currentUserEmail = req.authUser.email; + _.forEach(rows, (fp) => { + 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 { + // return empty invites + _.forEach(rows, (fp) => { + _.set(fp, 'invites', []); + }); + } + } _.forEach(rows, (p) => { const fp = p; if (fp.members) { diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index f57ecdb3..eb348e1e 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -404,6 +404,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 not include the project members using M2M token without "read:project-members" scope', (done) => { request(server) @@ -1153,9 +1176,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(); } });