From 8313e2e2fe05a6f12739f2206badf17f2b89fe42 Mon Sep 17 00:00:00 2001 From: yoution Date: Wed, 4 Dec 2019 00:09:13 +0800 Subject: [PATCH] Mask invitation emails --- package.json | 1 + src/routes/projectMemberInvites/create.js | 7 +- src/routes/projectMemberInvites/list.js | 2 +- src/routes/projectMemberInvites/update.js | 6 +- src/routes/projects/get.js | 2 +- src/routes/projects/list-db.js | 6 +- src/routes/projects/list.js | 7 +- src/util.js | 67 ++++++++++- src/util.spec.js | 134 ++++++++++++++++++++++ 9 files changed, 219 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 0b26a174..c42182d3 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "pg": "^4.5.5", "pg-native": "^1.10.1", "sequelize": "^3.23.0", + "jsonpath": "^1.0.2", "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6", "traverse": "^0.6.6", "urlencode": "^1.1.0" diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 6df85147..7e752e60 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -1,5 +1,4 @@ - import validate from 'express-validation'; import _ from 'lodash'; import Joi from 'joi'; @@ -347,9 +346,11 @@ module.exports = [ .then((values) => { const success = _.assign({}, { success: values }); if (failed.length) { - res.status(403).json(util.wrapResponse(req.id, _.assign({}, success, { failed }), null, 403)); + res.status(403).json(util.wrapResponse(req.id, + util.maskInviteEmails('$..email', _.assign({}, success, { failed }), req), null, 403)); } else { - res.status(201).json(util.wrapResponse(req.id, success, null, 201)); + res.status(201).json(util.wrapResponse(req.id, + util.maskInviteEmails('$.success[?(@.email)]', success, req), null, 201)); } }) .catch(err => next(err)); diff --git a/src/routes/projectMemberInvites/list.js b/src/routes/projectMemberInvites/list.js index 2f23cac9..9d0f4fef 100644 --- a/src/routes/projectMemberInvites/list.js +++ b/src/routes/projectMemberInvites/list.js @@ -29,7 +29,7 @@ module.exports = [ const projectId = _.parseInt(req.params.projectId); const invites = await models.ProjectMemberInvite.getPendingInvitesForProject(projectId); const invitesWithDetails = await util.getObjectsWithMemberDetails(invites, fields, req); - return res.json(util.wrapResponse(req.id, invitesWithDetails)); + return res.json(util.wrapResponse(req.id, util.maskInviteEmails('$..email', invitesWithDetails, req))); } catch (err) { return next(err); } diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index e71ddeda..fe046f06 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -135,11 +135,13 @@ module.exports = [ }; return util .addUserToProject(req, member) - .then(() => res.json(util.wrapResponse(req.id, updatedInvite))) + .then(() => res.json(util.wrapResponse(req.id, + util.maskInviteEmails('$..email', updatedInvite, req)))) .catch(err => next(err)); }); } - return res.json(util.wrapResponse(req.id, updatedInvite)); + + return res.json(util.wrapResponse(req.id, util.maskInviteEmails('$..email', updatedInvite, req))); }); }); }, diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js index b31a5fa4..91027fce 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -72,7 +72,7 @@ module.exports = [ }) .then((scopeChangeRequests) => { project.scopeChangeRequests = scopeChangeRequests; - res.status(200).json(util.wrapResponse(req.id, project)); + res.status(200).json(util.wrapResponse(req.id, util.maskInviteEmails('$..invites[?(@.email)]', project, req))); }) .catch(err => next(err)); }, diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index 187eb6b8..fbb405ab 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -128,7 +128,8 @@ module.exports = [ || util.hasRoles(req, MANAGER_ROLES))) { // admins & topcoder managers can see all projects return retrieveProjects(req, criteria, sort, req.query.fields) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .then(result => res.json(util.wrapResponse(req.id, + util.maskInviteEmails('$..invites[?(@.email)]', result.rows, req), result.count))) .catch(err => next(err)); } @@ -136,7 +137,8 @@ module.exports = [ criteria.filters.userId = req.authUser.userId; criteria.filters.email = req.authUser.email; return retrieveProjects(req, criteria, sort, req.query.fields) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .then(result => res.json(util.wrapResponse(req.id, + util.maskInviteEmails('$..invites[?(@.email)]', result.rows, req), result.count))) .catch(err => next(err)); }, ]; diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 52bf3b25..3d3435c1 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -1,4 +1,3 @@ - /* globals Promise */ import _ from 'lodash'; @@ -502,7 +501,8 @@ module.exports = [ || util.hasRoles(req, MANAGER_ROLES))) { // admins & topcoder managers can see all projects return retrieveProjects(req, criteria, sort, req.query.fields) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .then(result => res.json(util.wrapResponse(req.id, + util.maskInviteEmails('$..invites[?(@.email)]', result.rows, req), result.count))) .catch(err => next(err)); } @@ -510,7 +510,8 @@ module.exports = [ criteria.filters.email = req.authUser.email; criteria.filters.userId = req.authUser.userId; return retrieveProjects(req, criteria, sort, req.query.fields) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .then(result => res.json(util.wrapResponse(req.id, + util.maskInviteEmails('$..invites[?(@.email)]', result.rows, req), result.count))) .catch(err => next(err)); }, ]; diff --git a/src/util.js b/src/util.js index c5402fac..f049a4d8 100644 --- a/src/util.js +++ b/src/util.js @@ -1,4 +1,3 @@ - /* globals Promise */ /* * Copyright (C) 2016 TopCoder Inc., All Rights Reserved. @@ -15,8 +14,10 @@ import querystring from 'querystring'; import config from 'config'; import urlencode from 'urlencode'; import elasticsearch from 'elasticsearch'; +import jp from 'jsonpath'; import Promise from 'bluebird'; import models from './models'; + // import AWS from 'aws-sdk'; import { ADMIN_ROLES, TOKEN_SCOPES, EVENT, PROJECT_MEMBER_ROLE, VALUE_TYPE, ESTIMATION_TYPE } from './constants'; @@ -440,6 +441,70 @@ _.assignIn(util, { } }), + /** + * maksEmail + * + * @param {String} email emailstring + * + * @return {String} email has been masked + */ + maskEmail: (email) => { + // common function for formating + const addMask = (str) => { + let newStr; + const len = str.length; + if (len <= 3) { + newStr = _.repeat('*', len); + } else { + newStr = str.substr(0, 2) + _.repeat('*', len - 3) + str.substr(-1); + } + return newStr; + }; + + try { + const mailParts = email.split('@'); + const domainParts = mailParts[1].split('.'); + + let userName = mailParts[0]; + userName = addMask(userName); + mailParts[0] = userName; + + let domainName = domainParts[0]; + domainName = addMask(domainName); + domainParts[0] = domainName; + + mailParts[1] = domainParts.join('.'); + return mailParts.join('@'); + } catch (e) { + return email; + } + }, + /** + * Filter member details by input fields + * + * @param {String} jsonPath jsonpath string + * @param {Object} data the data which need to process + * @param {Object} req The request object + * + * @return {Object} data has been processed + */ + maskInviteEmails: (jsonPath, data, req) => { + const isAdmin = util.hasPermission({ topcoderRoles: ADMIN_ROLES }, req.authUser); + if (isAdmin) { + return data; + } + + jp.apply(data, jsonPath, (value) => { + if (_.isObject(value)) { + _.assign(value, { email: util.maskEmail(value.email) }); + return value; + } + // isString or null + return util.maskEmail(value); + }); + return data; + }, + /** * Filter member details by input fields * diff --git a/src/util.spec.js b/src/util.spec.js index 99722697..1fb8cd44 100644 --- a/src/util.spec.js +++ b/src/util.spec.js @@ -7,6 +7,140 @@ import util from './util'; chai.should(); describe('Util method', () => { + describe('maskEmail', () => { + it('should return the original value if the email is non-string', () => { + chai.should().not.exist(util.maskEmail(null)); + }); + it('should return the original value if the email is non-email string', () => { + util.maskEmail('aa.com').should.equal('aa.com'); + }); + it('should return "*@*.com" if the email is "a@a.com"', () => { + util.maskEmail('a@a.com').should.equal('*@*.com'); + }); + it('should return "**@**.com" if the email is "ab@aa.com"', () => { + util.maskEmail('ab@aa.com').should.equal('**@**.com'); + }); + it('should return "***@***.com" if the email is "abc@aaa.com"', () => { + util.maskEmail('abc@aaa.com').should.equal('***@***.com'); + }); + it('should return "ab*d@aa*a.com" if the email is "abcd@aaaa.com"', () => { + util.maskEmail('abcd@aaaa.com').should.equal('ab*d@aa*a.com'); + }); + it('should return "ab**e@aa**a.com" if the email is "abcde@aaaaa.com"', () => { + util.maskEmail('abcde@aaaaa.com').should.equal('ab**e@aa**a.com'); + }); + it('should return "ab***f@aa***a.com" if the email is "abcdef@aaaaaa.com"', () => { + util.maskEmail('abcdef@aaaaaa.com').should.equal('ab***f@aa***a.com'); + }); + it('should return "ab****g@aa****a.com" if the email is "abcdefg@aaaaaaa.com"', () => { + util.maskEmail('abcdefg@aaaaaaa.com').should.equal('ab****g@aa****a.com'); + }); + it('should return "ab*****h@aa****a.com" if the email is "abcdefgh@aaaaaaaa.com"', () => { + util.maskEmail('abcdefgh@aaaaaaaa.com').should.equal('ab*****h@aa*****a.com'); + }); + it('should return "ab******i@aa*****a.com" if the email is "abcdefghi@aaaaaaaaa.com"', () => { + util.maskEmail('abcdefghi@aaaaaaaaa.com').should.equal('ab******i@aa******a.com'); + }); + }); + + describe('maskInviteEmails', () => { + it('should mask emails when passing data like for a project list endpoint for non-admin user', () => { + const list = [ + { + id: 1, + invites: [{ + id: 2, + email: 'abcd@aaaa.com', + }, + ], + }, + ]; + const list2 = [ + { + id: 1, + invites: [{ + id: 2, + email: 'ab*d@aa*a.com', + }, + ], + }, + ]; + const res = { + authUser: { userId: 2 }, + }; + util.maskInviteEmails('$..invites[?(@.email)]', list, res).should.deep.equal(list2); + }); + it('should mask emails when passing data like for a project details endpoint for non-admin user', () => { + const detail = { + id: 1, + invites: [{ + id: 2, + email: 'abcd@aaaa.com', + }, + ], + }; + const detail2 = { + id: 1, + invites: [{ + id: 2, + email: 'ab*d@aa*a.com', + }, + ], + }; + const res = { + authUser: { userId: 2 }, + }; + util.maskInviteEmails('$..invites[?(@.email)]', detail, res).should.deep.equal(detail2); + }); + + it('should mask emails when passing data like for a single invite endpoint for non-admin user', () => { + const detail = { + success: [ + { + id: 1, + email: 'abcd@aaaa.com', + }, + ], + }; + const detail2 = { + success: [ + { + id: 1, + email: 'ab*d@aa*a.com', + }, + ], + }; + const res = { + authUser: { userId: 2 }, + }; + util.maskInviteEmails('$.success[?(@.email)]', detail, res).should.deep.equal(detail2); + }); + + it('should NOT mask emails when passing data like for a single invite endpoint for admin user', () => { + const detail = { + success: [ + { + id: 1, + email: 'abcd@aaaa.com', + }, + ], + }; + const detail2 = { + success: [ + { + id: 1, + email: 'abcd@aaaa.com', + }, + ], + }; + const res = { + authUser: { userId: 2, roles: ['administrator'] }, + }; + util.maskInviteEmails('$..email', detail, res).should.deep.equal(detail2); + }); + }); + + describe('isProjectSettingForEstimation', () => { it('should return "true" if key is correct: "markup_fee"', () => { util.isProjectSettingForEstimation('markup_fee').should.equal(true);