Skip to content

Mask invitation emails #418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions src/routes/projectMemberInvites/create.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@


import validate from 'express-validation';
import _ from 'lodash';
import Joi from 'joi';
Expand Down Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion src/routes/projectMemberInvites/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 4 additions & 2 deletions src/routes/projectMemberInvites/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
});
});
},
Expand Down
2 changes: 1 addition & 1 deletion src/routes/projects/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},
Expand Down
6 changes: 4 additions & 2 deletions src/routes/projects/list-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,17 @@ 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));
}

// regular users can only see projects they are members of (or invited, handled bellow)
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));
},
];
7 changes: 4 additions & 3 deletions src/routes/projects/list.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/* globals Promise */

import _ from 'lodash';
Expand Down Expand Up @@ -502,15 +501,17 @@ 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));
}

// regular users can only see projects they are members of (or invited, handled below)
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));
},
];
67 changes: 66 additions & 1 deletion src/util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/* globals Promise */
/*
* Copyright (C) 2016 TopCoder Inc., All Rights Reserved.
Expand All @@ -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';
Expand Down Expand Up @@ -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
*
Expand Down
134 changes: 134 additions & 0 deletions src/util.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down