Skip to content

Commit 276abd3

Browse files
authored
Merge pull request #573 from topcoder-platform/feature/members-invites-permission-fixes
Members & invites permission fixes
2 parents 61aa6a2 + 2f82d14 commit 276abd3

File tree

8 files changed

+224
-47
lines changed

8 files changed

+224
-47
lines changed

docs/permissions.html

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,8 @@ <h2 class="anchor-container">
636636

637637
<div>
638638
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
639-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
640-
<span class="badge badge-dark" title="Allowed Topcoder Role">read:project-members</span>
639+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
640+
<span class="badge badge-dark" title="Allowed Topcoder Role">read:project-invites</span>
641641
</div>
642642
</div>
643643
</div>
@@ -670,8 +670,8 @@ <h2 class="anchor-container">
670670

671671
<div>
672672
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
673-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
674-
<span class="badge badge-dark" title="Allowed Topcoder Role">read:project-members</span>
673+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
674+
<span class="badge badge-dark" title="Allowed Topcoder Role">read:project-invites</span>
675675
</div>
676676
</div>
677677
</div>
@@ -704,8 +704,8 @@ <h2 class="anchor-container">
704704

705705
<div>
706706
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
707-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
708-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
707+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
708+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
709709
</div>
710710
</div>
711711
</div>
@@ -734,8 +734,8 @@ <h2 class="anchor-container">
734734

735735
<div>
736736
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
737-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
738-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
737+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
738+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
739739
</div>
740740
</div>
741741
</div>
@@ -759,8 +759,8 @@ <h2 class="anchor-container">
759759

760760
<div>
761761
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
762-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
763-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
762+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
763+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
764764
</div>
765765
</div>
766766
</div>
@@ -782,8 +782,8 @@ <h2 class="anchor-container">
782782

783783
<div>
784784
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
785-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
786-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
785+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
786+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
787787
</div>
788788
</div>
789789
</div>
@@ -806,8 +806,8 @@ <h2 class="anchor-container">
806806

807807
<div>
808808
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
809-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
810-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
809+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
810+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
811811
</div>
812812
</div>
813813
</div>
@@ -831,8 +831,8 @@ <h2 class="anchor-container">
831831

832832
<div>
833833
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
834-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
835-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
834+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
835+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
836836
</div>
837837
</div>
838838
</div>
@@ -854,8 +854,8 @@ <h2 class="anchor-container">
854854

855855
<div>
856856
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
857-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
858-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
857+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
858+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
859859
</div>
860860
</div>
861861
</div>
@@ -879,8 +879,8 @@ <h2 class="anchor-container">
879879

880880
<div>
881881
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
882-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
883-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
882+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
883+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
884884
</div>
885885
</div>
886886
</div>
@@ -909,8 +909,8 @@ <h2 class="anchor-container">
909909

910910
<div>
911911
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
912-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
913-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
912+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
913+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
914914
</div>
915915
</div>
916916
</div>
@@ -934,8 +934,8 @@ <h2 class="anchor-container">
934934

935935
<div>
936936
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
937-
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-members</span>
938-
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-members</span>
937+
<span class="badge badge-dark" title="Allowed Topcoder Role">all:project-invites</span>
938+
<span class="badge badge-dark" title="Allowed Topcoder Role">write:project-invites</span>
939939
</div>
940940
</div>
941941
</div>

src/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ export const M2M_SCOPES = {
281281
READ: 'read:project-members',
282282
WRITE: 'write:project-members',
283283
},
284+
PROJECT_INVITES: {
285+
ALL: 'all:project-invites',
286+
READ: 'read:project-invites',
287+
WRITE: 'write:project-invites',
288+
},
284289
};
285290

286291
export const TIMELINE_REFERENCES = {

src/permissions/constants.js

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ const SCOPES_PROJECT_MEMBERS_WRITE = [
117117
M2M_SCOPES.PROJECT_MEMBERS.WRITE,
118118
];
119119

120+
const SCOPES_PROJECT_INVITES_READ = [
121+
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
122+
M2M_SCOPES.PROJECT_INVITES.ALL,
123+
M2M_SCOPES.PROJECT_INVITES.READ,
124+
];
125+
126+
const SCOPES_PROJECT_INVITES_WRITE = [
127+
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
128+
M2M_SCOPES.PROJECT_INVITES.ALL,
129+
M2M_SCOPES.PROJECT_INVITES.WRITE,
130+
];
131+
120132
/**
121133
* The full list of possible permission rules in Project Service
122134
*/
@@ -332,7 +344,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
332344
description: 'Who can view own invite.',
333345
},
334346
topcoderRoles: ALL,
335-
scopes: SCOPES_PROJECT_MEMBERS_READ,
347+
scopes: SCOPES_PROJECT_INVITES_READ,
336348
},
337349

338350
READ_PROJECT_INVITE_NOT_OWN: {
@@ -343,7 +355,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
343355
},
344356
topcoderRoles: TOPCODER_ROLES_MANAGERS_AND_ADMINS,
345357
projectRoles: ALL,
346-
scopes: SCOPES_PROJECT_MEMBERS_READ,
358+
scopes: SCOPES_PROJECT_INVITES_READ,
347359
},
348360

349361
CREATE_PROJECT_INVITE_CUSTOMER: {
@@ -354,7 +366,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
354366
},
355367
topcoderRoles: TOPCODER_ROLES_MANAGERS_AND_ADMINS,
356368
projectRoles: ALL,
357-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
369+
scopes: SCOPES_PROJECT_INVITES_WRITE,
358370
},
359371

360372
CREATE_PROJECT_INVITE_NON_CUSTOMER: {
@@ -365,7 +377,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
365377
},
366378
topcoderRoles: TOPCODER_ROLES_ADMINS,
367379
projectRoles: PROJECT_ROLES_MANAGEMENT,
368-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
380+
scopes: SCOPES_PROJECT_INVITES_WRITE,
369381
},
370382

371383
CREATE_PROJECT_INVITE_COPILOT_DIRECTLY: {
@@ -378,7 +390,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
378390
...TOPCODER_ROLES_ADMINS,
379391
USER_ROLE.COPILOT_MANAGER,
380392
],
381-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
393+
scopes: SCOPES_PROJECT_INVITES_WRITE,
382394
},
383395

384396
UPDATE_PROJECT_INVITE_OWN: {
@@ -388,7 +400,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
388400
description: 'Who can update own invite.',
389401
},
390402
topcoderRoles: ALL,
391-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
403+
scopes: SCOPES_PROJECT_INVITES_WRITE,
392404
},
393405

394406
UPDATE_PROJECT_INVITE_NOT_OWN: {
@@ -398,7 +410,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
398410
description: 'Who can update invites for other members.',
399411
},
400412
topcoderRoles: TOPCODER_ROLES_ADMINS,
401-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
413+
scopes: SCOPES_PROJECT_INVITES_WRITE,
402414
},
403415

404416
UPDATE_PROJECT_INVITE_REQUESTED: {
@@ -411,7 +423,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
411423
...TOPCODER_ROLES_ADMINS,
412424
USER_ROLE.COPILOT_MANAGER,
413425
],
414-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
426+
scopes: SCOPES_PROJECT_INVITES_WRITE,
415427
},
416428

417429
DELETE_PROJECT_INVITE_OWN: {
@@ -421,7 +433,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
421433
description: 'Who can delete own invite.',
422434
},
423435
topcoderRoles: ALL,
424-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
436+
scopes: SCOPES_PROJECT_INVITES_WRITE,
425437
},
426438

427439
DELETE_PROJECT_INVITE_NOT_OWN_CUSTOMER: {
@@ -432,7 +444,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
432444
},
433445
topcoderRoles: TOPCODER_ROLES_ADMINS,
434446
projectRoles: ALL,
435-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
447+
scopes: SCOPES_PROJECT_INVITES_WRITE,
436448
},
437449

438450
DELETE_PROJECT_INVITE_NOT_OWN_NON_CUSTOMER: {
@@ -443,7 +455,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
443455
},
444456
topcoderRoles: TOPCODER_ROLES_ADMINS,
445457
projectRoles: PROJECT_ROLES_MANAGEMENT,
446-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
458+
scopes: SCOPES_PROJECT_INVITES_WRITE,
447459
},
448460

449461
DELETE_PROJECT_INVITE_REQUESTED: {
@@ -456,7 +468,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
456468
...TOPCODER_ROLES_ADMINS,
457469
USER_ROLE.COPILOT_MANAGER,
458470
],
459-
scopes: SCOPES_PROJECT_MEMBERS_WRITE,
471+
scopes: SCOPES_PROJECT_INVITES_WRITE,
460472
},
461473

462474
/*

src/permissions/generalPermission.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ module.exports = permissions => async (req) => {
4040
// if one of the `permission` requires to know Project Members, but current route doesn't belong to any project
4141
// this means such `permission` most likely has been applied by mistake, so we throw an error
4242
const permissionsRequireProjectMembers = _.isArray(permissions)
43-
? _.some(permissions, permission => util.hasPermissionByReq(permission, req))
43+
? _.some(permissions, permission => util.isPermissionRequireProjectMembers(permission))
4444
: util.isPermissionRequireProjectMembers(permissions);
4545

4646
if (_.isUndefined(req.params.projectId) && permissionsRequireProjectMembers) {
@@ -62,6 +62,7 @@ module.exports = permissions => async (req) => {
6262
// - if user has permissions to access endpoint even we don't know if he is a member or no,
6363
// then code would proceed and endpoint would decide to throw 404 if project doesn't exist
6464
// or perform endpoint operation if loading project members above failed because of some other reason
65+
req.log.error(`Cannot load project members: ${err.message}.`);
6566
}
6667
}
6768

src/routes/projects/get.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import config from 'config';
33
import { middleware as tcMiddleware } from 'tc-core-library-js';
44
import models from '../../models';
55
import util from '../../util';
6+
import { PERMISSION } from '../../permissions/constants';
67
import permissionUtils from '../../utils/permissions';
78

89
const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName');
@@ -104,7 +105,8 @@ const retrieveProjectFromES = (projectId, req) => {
104105
fields = fields ? fields.split(',') : [];
105106
fields = util.parseFields(fields, {
106107
projects: PROJECT_ATTRIBUTES,
107-
project_members: util.addUserDetailsFieldsIfAllowed(PROJECT_MEMBER_ATTRIBUTES_ES, req),
108+
project_members: util.hasPermissionByReq(PERMISSION.READ_PROJECT_MEMBER, req)
109+
? util.addUserDetailsFieldsIfAllowed(PROJECT_MEMBER_ATTRIBUTES_ES, req) : null,
108110
project_member_invites: PROJECT_MEMBER_INVITE_ATTRIBUTES,
109111
project_phases: PROJECT_PHASE_ATTRIBUTES,
110112
project_phases_products: PROJECT_PHASE_PRODUCTS_ATTRIBUTES,
@@ -116,7 +118,26 @@ const retrieveProjectFromES = (projectId, req) => {
116118
const es = util.getElasticSearchClient();
117119
es.search(searchCriteria).then((docs) => {
118120
const rows = _.map(docs.hits.hits, single => single._source); // eslint-disable-line no-underscore-dangle
119-
accept(rows[0]);
121+
const project = rows[0];
122+
if (project && project.invites) {
123+
if (!util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_NOT_OWN, req)) {
124+
let invites;
125+
if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_OWN, req)) {
126+
// only include own invites
127+
const currentUserId = req.authUser.userId;
128+
const currentUserEmail = req.authUser.email;
129+
invites = _.filter(project.invites, invite => (
130+
(invite.userId !== null && invite.userId === currentUserId) ||
131+
(invite.email && currentUserEmail && invite.email.toLowerCase() === currentUserEmail.toLowerCase())
132+
));
133+
} else {
134+
// return empty invites
135+
invites = [];
136+
}
137+
_.set(project, 'invites', invites);
138+
}
139+
}
140+
accept(project);
120141
}).catch(reject);
121142
});
122143
};
@@ -144,7 +165,9 @@ const retrieveProjectFromDB = (projectId, req) => {
144165
return Promise.reject(apiErr);
145166
}
146167
// check context for project members
147-
project.members = _.map(req.context.currentProjectMembers, m => _.pick(m, fields.project_members));
168+
if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_MEMBER, req)) {
169+
project.members = _.map(req.context.currentProjectMembers, m => _.pick(m, fields.project_members));
170+
}
148171
// check if attachments field was requested
149172
if (!req.query.fields || _.indexOf(req.query.fields, 'attachments') > -1) {
150173
return util.getProjectAttachments(req, project.id);
@@ -157,7 +180,17 @@ const retrieveProjectFromDB = (projectId, req) => {
157180
if (attachments) {
158181
project.attachments = attachments;
159182
}
160-
return models.ProjectMemberInvite.getPendingAndReguestedInvitesForProject(projectId);
183+
if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_NOT_OWN, req)) {
184+
// include all invites
185+
return models.ProjectMemberInvite.getPendingAndReguestedInvitesForProject(projectId);
186+
} else if (util.hasPermissionByReq(PERMISSION.READ_PROJECT_INVITE_OWN, req)) {
187+
// include only own invites
188+
const currentUserId = req.authUser.userId;
189+
const email = req.authUser.email;
190+
return models.ProjectMemberInvite.getPendingOrRequestedProjectInvitesForUser(projectId, email, currentUserId);
191+
}
192+
// empty
193+
return Promise.resolve([]);
161194
})
162195
.then((invites) => {
163196
project.invites = invites;

src/routes/projects/get.spec.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,27 @@ describe('GET Project', () => {
268268
should.not.exist(resJson.billingAccountId);
269269
should.exist(resJson.name);
270270
resJson.status.should.be.eql('draft');
271-
resJson.members.should.have.lengthOf(2);
271+
should.not.exist(resJson.members);
272+
done();
273+
}
274+
});
275+
});
276+
277+
it('should return the project with empty invites using M2M token without "read:project-invites" scope', (done) => {
278+
request(server)
279+
.get(`/v5/projects/${project1.id}`)
280+
.set({
281+
Authorization: `Bearer ${testUtil.m2m['read:projects']}`,
282+
})
283+
.expect('Content-Type', /json/)
284+
.expect(200)
285+
.end((err, res) => {
286+
if (err) {
287+
done(err);
288+
} else {
289+
const resJson = res.body;
290+
should.exist(resJson);
291+
resJson.invites.should.be.empty;
272292
done();
273293
}
274294
});

0 commit comments

Comments
 (0)