Skip to content

Commit 10c39a2

Browse files
authored
Merge pull request #516 from gets0ul/post-process-invite
Post-Processing Invites
2 parents 73a7773 + 96a3855 commit 10c39a2

File tree

13 files changed

+176
-28
lines changed

13 files changed

+176
-28
lines changed

src/routes/projectMemberInvites/create.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ module.exports = [
395395
})
396396
))
397397
.then((values) => {
398-
const response = _.assign({}, { success: values });
398+
const response = _.assign({}, { success: util.postProcessInvites('$[*]', values, req) });
399399
if (failed.length) {
400400
res.status(403).json(_.assign({}, response, { failed }));
401401
} else {

src/routes/projectMemberInvites/create.spec.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,8 @@ describe('Project Member Invite create', () => {
446446
resJson.role.should.equal('customer');
447447
resJson.projectId.should.equal(project2.id);
448448
resJson.userId.should.equal(12345);
449-
resJson.email.should.equal('hello@world.com');
450-
resJson.hashEmail.should.equal(md5('hello@world.com'));
449+
should.not.exist(resJson.email);
450+
should.not.exist(resJson.hashEmail);
451451
server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true;
452452
done();
453453
}
@@ -495,6 +495,8 @@ describe('Project Member Invite create', () => {
495495
resJson.role.should.equal('customer');
496496
resJson.projectId.should.equal(project2.id);
497497
resJson.userId.should.equal(40051331);
498+
should.not.exist(resJson.email);
499+
should.not.exist(resJson.hashEmail);
498500
server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true;
499501
done();
500502
}
@@ -828,7 +830,7 @@ describe('Project Member Invite create', () => {
828830
} else {
829831
const resJson = res.body.failed;
830832
should.exist(resJson);
831-
resJson[0].email.should.equal('DUPLICATE_UPPERCASE@test.com'); // email is masked
833+
resJson[0].email.should.equal('DUPLICATE_UPPERCASE@test.com');
832834
resJson[0].message.should.equal('User with such email is already invited to this project.');
833835
resJson.length.should.equal(1);
834836
done();

src/routes/projectMemberInvites/get.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ module.exports = [
114114
return invite;
115115
})
116116
))
117-
.then(invite => res.json(util.maskInviteEmails('$[*].email', invite, req)))
117+
.then(invite => res.json(util.postProcessInvites('$.email', invite, req)))
118118
.catch(next);
119119
},
120120
];

src/routes/projectMemberInvites/get.spec.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('GET Project Member Invite', () => {
5555
const invite2 = models.ProjectMemberInvite.create({
5656
id: 2,
5757
userId: testUtil.userIds.copilot,
58-
email: null,
58+
email: 'test@topcoder.com',
5959
projectId: project1.id,
6060
role: 'copilot',
6161
createdBy: 1,
@@ -206,6 +206,8 @@ describe('GET Project Member Invite', () => {
206206
const resJson = res.body;
207207
should.exist(resJson);
208208
should.exist(resJson.projectId);
209+
should.not.exist(resJson.email);
210+
should.not.exist(resJson.hashEmail);
209211
resJson.id.should.be.eql(2);
210212
resJson.userId.should.be.eql(testUtil.userIds.copilot);
211213
resJson.status.should.be.eql(INVITE_STATUS.PENDING);
@@ -230,7 +232,7 @@ describe('GET Project Member Invite', () => {
230232
should.exist(resJson);
231233
should.exist(resJson.projectId);
232234
resJson.id.should.be.eql(3);
233-
resJson.email.should.be.eql('test@topcoder.com');
235+
resJson.email.should.be.eql('t***t@t***r.com'); // masked
234236
resJson.hashEmail.should.be.eql(md5('test@topcoder.com'));
235237
resJson.status.should.be.eql(INVITE_STATUS.PENDING);
236238
done();

src/routes/projectMemberInvites/list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ module.exports = [
103103
return invites;
104104
})
105105
))
106-
.then(invites => res.json(util.maskInviteEmails('$[*].email', invites, req)))
106+
.then(invites => res.json(util.postProcessInvites('$[*]', invites, req)))
107107
.catch(next);
108108
},
109109
];

src/routes/projectMemberInvites/list.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ describe('GET Project Member Invites', () => {
209209
resJson.length.should.be.eql(1);
210210
// check invitations
211211
_.filter(resJson, inv => inv.id === 2).length.should.be.eql(1);
212+
should.not.exist(resJson[0].email);
212213
done();
213214
}
214215
});
@@ -253,6 +254,7 @@ describe('GET Project Member Invites', () => {
253254
resJson.length.should.be.eql(1);
254255
// check invitations
255256
_.filter(resJson, inv => inv.id === 3).length.should.be.eql(1);
257+
resJson[0].email.should.be.eql('t***t@t***r.com'); // masked
256258
done();
257259
}
258260
});

src/routes/projectMemberInvites/update.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,11 @@ module.exports = [
122122
};
123123
return util
124124
.addUserToProject(req, member)
125-
.then(() => res.json(util.maskInviteEmails('$.email', updatedInvite, req)))
125+
.then(() => res.json(util.postProcessInvites('$.email', updatedInvite, req)))
126126
.catch(err => next(err));
127127
});
128128
}
129-
return res.json(util.maskInviteEmails('$.email', updatedInvite, req));
129+
return res.json(util.postProcessInvites('$.email', updatedInvite, req));
130130
});
131131
})
132132
.catch(next);

src/routes/projects/get.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ module.exports = [
186186
req.log.debug('Project found in ES');
187187
return result;
188188
}).then((project) => {
189-
res.status(200).json(util.maskInviteEmails('$.invites[?(@.email)]', project, req));
189+
res.status(200).json(util.postProcessInvites('$.invites[?(@.email)]', project, req));
190190
})
191191
.catch(err => next(err));
192192
},

src/routes/projects/get.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,28 @@ describe('GET Project', () => {
526526
});
527527
});
528528

529+
it('should not return "email" for any invite which has userId field', (done) => {
530+
request(server)
531+
.get(`/v5/projects/${project1.id}`)
532+
.set({
533+
Authorization: `Bearer ${testUtil.jwts.member}`,
534+
})
535+
.expect('Content-Type', /json/)
536+
.expect(200)
537+
.end((err, res) => {
538+
if (err) {
539+
done(err);
540+
} else {
541+
const resJson = res.body;
542+
should.exist(resJson);
543+
resJson.invites.length.should.be.eql(1);
544+
resJson.invites[0].should.have.property('userId');
545+
should.not.exist(resJson.invites[0].email);
546+
done();
547+
}
548+
});
549+
});
550+
529551
it('should only return "members.role" field, when it\'s the only field listed in "fields" query param', (done) => {
530552
request(server)
531553
.get(`/v5/projects/${project1.id}?fields=members.role`)

src/routes/projects/list.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -629,15 +629,18 @@ module.exports = [
629629
// so we don't want DB to return unrelated data, ref issue #450
630630
if (_.intersection(_.keys(filters), SUPPORTED_FILTERS).length > 0) {
631631
req.log.debug('Don\'t fallback to DB because some filters are defined.');
632-
return util.setPaginationHeaders(req, res, util.maskInviteEmails('$[*].invites[?(@.email)]', result, req));
632+
return util.setPaginationHeaders(req, res,
633+
util.postProcessInvites('$.rows[*].invites[?(@.email)]', result, req));
633634
}
634635

635636
return retrieveProjectsFromDB(req, criteria, sort, req.query.fields)
636-
.then(r => util.setPaginationHeaders(req, res, util.maskInviteEmails('$[*].invites[?(@.email)]', r, req)));
637+
.then(r => util.setPaginationHeaders(req, res,
638+
util.postProcessInvites('$.rows[*].invites[?(@.email)]', r, req)));
637639
}
638640
req.log.debug('Projects found in ES');
639641
// set header
640-
return util.setPaginationHeaders(req, res, util.maskInviteEmails('$[*].invites[?(@.email)]', result, req));
642+
return util.setPaginationHeaders(req, res,
643+
util.postProcessInvites('$.rows[*].invites[?(@.email)]', result, req));
641644
})
642645
.catch(err => next(err));
643646
}
@@ -655,14 +658,17 @@ module.exports = [
655658
// so we don't want DB to return unrelated data, ref issue #450
656659
if (_.intersection(_.keys(filters), SUPPORTED_FILTERS).length > 0) {
657660
req.log.debug('Don\'t fallback to DB because some filters are defined.');
658-
return util.setPaginationHeaders(req, res, util.maskInviteEmails('$[*].invites[?(@.email)]', result, req));
661+
return util.setPaginationHeaders(req, res,
662+
util.postProcessInvites('$.rows[*].invites[?(@.email)]', result, req));
659663
}
660664

661665
return retrieveProjectsFromDB(req, criteria, sort, req.query.fields)
662-
.then(r => util.setPaginationHeaders(req, res, util.maskInviteEmails('$[*].invites[?(@.email)]', r, req)));
666+
.then(r => util.setPaginationHeaders(req, res,
667+
util.postProcessInvites('$.rows[*].invites[?(@.email)]', r, req)));
663668
}
664669
req.log.debug('Projects found in ES');
665-
return util.setPaginationHeaders(req, res, util.maskInviteEmails('$[*].invites[?(@.email)]', result, req));
670+
return util.setPaginationHeaders(req, res,
671+
util.postProcessInvites('$.rows[*].invites[?(@.email)]', result, req));
666672
})
667673
.catch(err => next(err));
668674
},

src/routes/projects/list.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ const data = [
6464
email: 'test@topcoder.com',
6565
status: 'pending',
6666
},
67+
{
68+
id: 2,
69+
email: 'hello@world.com',
70+
status: 'pending',
71+
createdBy: 1,
72+
},
6773
],
6874
phases: [
6975

@@ -376,6 +382,10 @@ describe('LIST Project', () => {
376382
const resJson = res.body;
377383
should.exist(resJson);
378384
resJson.should.have.lengthOf(2);
385+
resJson[0].invites[0].should.have.property('userId');
386+
should.not.exist(resJson[0].invites[0].email);
387+
resJson[1].invites[0].should.have.property('userId');
388+
should.not.exist(resJson[1].invites[0].email);
379389
done();
380390
}
381391
});
@@ -1070,6 +1080,10 @@ describe('LIST Project', () => {
10701080
should.exist(resJson);
10711081
resJson.should.have.lengthOf(1);
10721082
resJson[0].name.should.equal('test1');
1083+
resJson[0].invites.should.have.lengthOf(2);
1084+
resJson[0].invites[0].should.have.property('userId');
1085+
should.not.exist(resJson[0].invites[0].email);
1086+
resJson[0].invites[1].email.should.equal('h***o@w***d.com');
10731087
done();
10741088
}
10751089
});

src/util.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,11 @@ _.assignIn(util, {
639639
}
640640
},
641641
/**
642-
* Mask email in the fields defined by `jsonPath` in the `data`.
642+
* Post-process given invite(s) with following constraints:
643+
* - email field will be omitted from invite if the invite has defined userId
644+
* - email field (if existed) will be masked UNLESS current user has admin permissions OR current user created this invite
645+
*
646+
* Email to be masked is found in the fields defined by `jsonPath` in the `data`.
643647
* Immutable - doesn't modify data, but creates a clone.
644648
*
645649
* @param {String} jsonPath jsonpath string
@@ -648,24 +652,45 @@ _.assignIn(util, {
648652
*
649653
* @return {Object} data has been processed
650654
*/
651-
maskInviteEmails: (jsonPath, data, req) => {
655+
postProcessInvites: (jsonPath, data, req) => {
652656
// clone data to avoid mutations
653657
const dataClone = _.cloneDeep(data);
654658

655659
const isAdmin = util.hasPermission({ topcoderRoles: [USER_ROLE.TOPCODER_ADMIN] }, req.authUser);
660+
const currentUserId = req.authUser.userId;
656661

657662
if (isAdmin) {
658663
// even though we didn't make any changes to the data, return a clone here for consistency
659664
return dataClone;
660665
}
661666

667+
const postProcessInvite = (invite) => {
668+
if (!_.has(invite, 'email')) {
669+
return invite;
670+
}
671+
let email;
672+
if (!invite.userId) {
673+
// mask email if non-admin or not own invite
674+
email = isAdmin || invite.createdBy === currentUserId ? invite.email : util.maskEmail(invite.email);
675+
} else {
676+
// userId is defined, no email field returned
677+
email = null;
678+
}
679+
_.assign(invite, { email });
680+
if (!invite.email && _.has(invite, 'hashEmail')) {
681+
_.assign(invite, { hashEmail: null });
682+
}
683+
return invite;
684+
};
685+
662686
jp.apply(dataClone, jsonPath, (value) => {
663687
if (_.isObject(value)) {
664-
_.assign(value, { email: util.maskEmail(value.email) });
665-
return value;
688+
// data contains nested invite object
689+
return postProcessInvite(value);
666690
}
667-
// isString or null
668-
return util.maskEmail(value);
691+
// data is single invite object
692+
// value is string or null
693+
return postProcessInvite(dataClone).email;
669694
});
670695

671696
return dataClone;

0 commit comments

Comments
 (0)