Skip to content

Protect billing account #564

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 5 commits into from
May 6, 2020
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
29 changes: 26 additions & 3 deletions docs/permissions.html
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,10 @@ <h2 class="anchor-container">
<div class="row border-top">
<div class="col py-2">
<div class="permission-title anchor-container">
<a href="#UPDATE_PROJECT_DIRECT_PROJECT_ID" name="UPDATE_PROJECT_DIRECT_PROJECT_ID" class="anchor"></a>Update Project property &quot;directProjectId&quot;
<a href="#MANAGE_PROJECT_DIRECT_PROJECT_ID" name="MANAGE_PROJECT_DIRECT_PROJECT_ID" class="anchor"></a>Manage Project property &quot;directProjectId&quot;
</div>
<div class="permission-variable"><small><code>UPDATE_PROJECT_DIRECT_PROJECT_ID</code></small></div>
<div class="text-black-50 small-text"></div>
<div class="permission-variable"><small><code>MANAGE_PROJECT_DIRECT_PROJECT_ID</code></small></div>
<div class="text-black-50 small-text">Who can set or update the &quot;directProjectId&quot; property.</div>
</div>
<div class="col-9 py-2">
<div>
Expand All @@ -294,6 +294,29 @@ <h2 class="anchor-container">
</div>
</div>
</div>
<div class="row border-top">
<div class="col py-2">
<div class="permission-title anchor-container">
<a href="#MANAGE_PROJECT_BILLING_ACCOUNT_ID" name="MANAGE_PROJECT_BILLING_ACCOUNT_ID" class="anchor"></a>Manage Project property &quot;billingAccountId&quot;
</div>
<div class="permission-variable"><small><code>MANAGE_PROJECT_BILLING_ACCOUNT_ID</code></small></div>
<div class="text-black-50 small-text">Who can set or update the &quot;billingAccountId&quot; property.</div>
</div>
<div class="col-9 py-2">
<div>
</div>

<div>
<span class="badge badge-success" title="Allowed Topcoder Role">Connect Manager</span>
<span class="badge badge-success" title="Allowed Topcoder Role">administrator</span>
</div>

<div>
<span class="badge badge-dark" title="Allowed Topcoder Role">all:connect_project</span>
<span class="badge badge-dark" title="Allowed Topcoder Role">write:projects-billing-accounts</span>
</div>
</div>
</div>
<div class="row border-top">
<div class="col py-2">
<div class="permission-title anchor-container">
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,13 @@ export const REGEX = {
};

export const M2M_SCOPES = {
// for backward compatibility we should allow ALL M2M operations with `CONNECT_PROJECT_ADMIN`
CONNECT_PROJECT_ADMIN: 'all:connect_project',
PROJECTS: {
ALL: 'all:projects',
READ: 'read:projects',
WRITE: 'write:projects',
WRITE_BILLING_ACCOUNTS: 'write:projects-billing-accounts',
},
PROJECT_MEMBERS: {
ALL: 'all:project-members',
Expand Down
23 changes: 21 additions & 2 deletions src/permissions/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ const SCOPES_PROJECTS_WRITE = [
M2M_SCOPES.PROJECTS.WRITE,
];

const SCOPES_PROJECTS_WRITE_BILLING_ACCOUNTS = [
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
M2M_SCOPES.PROJECTS.WRITE_BILLING_ACCOUNTS,
];

const SCOPES_PROJECT_MEMBERS_READ = [
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
M2M_SCOPES.PROJECT_MEMBERS.ALL,
Expand Down Expand Up @@ -142,10 +147,11 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECTS_WRITE,
},

UPDATE_PROJECT_DIRECT_PROJECT_ID: {
MANAGE_PROJECT_DIRECT_PROJECT_ID: {
meta: {
title: 'Update Project property "directProjectId"',
title: 'Manage Project property "directProjectId"',
group: 'Project',
description: 'Who can set or update the "directProjectId" property.',
},
topcoderRoles: [
USER_ROLE.MANAGER,
Expand All @@ -154,6 +160,19 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECTS_WRITE,
},

MANAGE_PROJECT_BILLING_ACCOUNT_ID: {
meta: {
title: 'Manage Project property "billingAccountId"',
group: 'Project',
description: 'Who can set or update the "billingAccountId" property.',
},
topcoderRoles: [
USER_ROLE.MANAGER,
USER_ROLE.TOPCODER_ADMIN,
],
scopes: SCOPES_PROJECTS_WRITE_BILLING_ACCOUNTS,
},

DELETE_PROJECT: {
meta: {
title: 'Delete Project',
Expand Down
12 changes: 12 additions & 0 deletions src/routes/projects/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,18 @@ module.exports = [
*/
(req, res, next) => {
const project = req.body;
if (_.has(project, 'directProjectId') &&
!util.hasPermissionByReq(PERMISSION.MANAGE_PROJECT_DIRECT_PROJECT_ID, req)) {
const err = new Error('You do not have permission to set \'directProjectId\' property');
err.status = 400;
throw err;
}
if (_.has(project, 'billingAccountId') &&
!util.hasPermissionByReq(PERMISSION.MANAGE_PROJECT_BILLING_ACCOUNT_ID, req)) {
const err = new Error('You do not have permission to set \'billingAccountId\' property');
err.status = 400;
throw err;
}
// by default connect admin and managers joins projects as manager
const userRole = util.hasPermissionByReq(PERMISSION.CREATE_PROJECT_AS_MANAGER, req)
? PROJECT_MEMBER_ROLE.MANAGER
Expand Down
44 changes: 35 additions & 9 deletions src/routes/projects/create.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ describe('Project create', () => {
type: 'generic',
description: 'test project',
details: {},
billingAccountId: 1,
name: 'test project1',
bookmarks: [{
title: 'title1',
Expand All @@ -277,7 +276,6 @@ describe('Project create', () => {
type: 'generic',
description: 'test project',
details: {},
billingAccountId: 1,
name: 'test project1',
attachments: [
{
Expand Down Expand Up @@ -399,6 +397,34 @@ describe('Project create', () => {
.expect(400, done);
});

it(`should return 400 when creating project with billingAccountId
without "write:projects-billing-accounts" scope in M2M token`, (done) => {
const validBody = _.cloneDeep(body);
validBody.billingAccountId = 1;
request(server)
.post('/v5/projects')
.set({
Authorization: `Bearer ${testUtil.m2m['write:projects']}`,
})
.send(validBody)
.expect('Content-Type', /json/)
.expect(400, done);
});

it(`should return 400 when creating project with directProjectId
without "write:projects" scope in M2M token`, (done) => {
const validBody = _.cloneDeep(body);
validBody.directProjectId = 1;
request(server)
.post('/v5/projects')
.set({
Authorization: `Bearer ${testUtil.m2m['write:project-members']}`,
})
.send(validBody)
.expect('Content-Type', /json/)
.expect(400, done);
});

it('should return 201 if valid user and data', (done) => {
const validBody = _.cloneDeep(body);
validBody.templateId = 3;
Expand Down Expand Up @@ -433,7 +459,7 @@ describe('Project create', () => {
} else {
const resJson = res.body;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.not.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.status.should.be.eql('in_review');
resJson.type.should.be.eql(body.type);
Expand Down Expand Up @@ -489,7 +515,7 @@ describe('Project create', () => {
} else {
const resJson = res.body;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.not.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.status.should.be.eql('in_review');
resJson.type.should.be.eql(body.type);
Expand Down Expand Up @@ -544,7 +570,7 @@ describe('Project create', () => {
} else {
const resJson = res.body;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.not.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.status.should.be.eql('in_review');
resJson.type.should.be.eql(body.type);
Expand Down Expand Up @@ -598,7 +624,7 @@ describe('Project create', () => {
} else {
const resJson = res.body;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.not.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.status.should.be.eql('in_review');
resJson.type.should.be.eql(bodyWithAttachments.type);
Expand Down Expand Up @@ -679,7 +705,7 @@ describe('Project create', () => {
} else {
const resJson = res.body;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.not.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.status.should.be.eql('in_review');
resJson.type.should.be.eql(body.type);
Expand Down Expand Up @@ -752,7 +778,7 @@ describe('Project create', () => {
} else {
const resJson = res.body;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.not.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.status.should.be.eql('in_review');
resJson.type.should.be.eql(body.type);
Expand Down Expand Up @@ -885,7 +911,7 @@ describe('Project create', () => {
} else {
const resJson = res.body;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.not.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.status.should.be.eql('in_review');
resJson.type.should.be.eql(body.type);
Expand Down
8 changes: 6 additions & 2 deletions src/routes/projects/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,12 @@ const validateUpdates = (existingProject, updatedProps, req) => {
// }
}
if (_.has(updatedProps, 'directProjectId') &&
!util.hasPermissionByReq(PERMISSION.UPDATE_PROJECT_DIRECT_PROJECT_ID, req)) {
errors.push('Don\'t have permission to update \'directProjectId\' property');
!util.hasPermissionByReq(PERMISSION.MANAGE_PROJECT_DIRECT_PROJECT_ID, req)) {
errors.push('You do not have permission to update \'directProjectId\' property');
}
if (_.has(updatedProps, 'billingAccountId') &&
!util.hasPermissionByReq(PERMISSION.MANAGE_PROJECT_BILLING_ACCOUNT_ID, req)) {
errors.push('You do not have permission to update \'billingAccountId\' property');
}
if ((existingProject.status !== PROJECT_STATUS.DRAFT) && (updatedProps.status === PROJECT_STATUS.DRAFT)) {
errors.push('cannot update a project status to draft');
Expand Down
25 changes: 19 additions & 6 deletions src/routes/projects/update.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
PROJECT_STATUS,
BUS_API_EVENT,
CONNECT_NOTIFICATION_EVENT,
M2M_SCOPES,
} from '../../constants';

const should = chai.should();
Expand Down Expand Up @@ -192,11 +191,11 @@ describe('Project', () => {
});
});

it(`should return the project using M2M token with "${M2M_SCOPES.PROJECTS.WRITE}" scope`, (done) => {
it('should return the project using M2M token with "write:projects" scope', (done) => {
request(server)
.patch(`/v5/projects/${project1.id}`)
.set({
Authorization: `Bearer ${testUtil.m2m[M2M_SCOPES.PROJECTS.WRITE]}`,
Authorization: `Bearer ${testUtil.m2m['write:projects']}`,
})
.send({
name: 'updateProject name by M2M',
Expand Down Expand Up @@ -584,7 +583,7 @@ describe('Project', () => {
request(server)
.patch(`/v5/projects/${project1.id}`)
.set({
Authorization: `Bearer ${testUtil.jwts.copilot}`,
Authorization: `Bearer ${testUtil.jwts.manager}`,
})
.send({
billingAccountId: 123,
Expand All @@ -600,7 +599,7 @@ describe('Project', () => {
should.exist(resJson);
resJson.billingAccountId.should.equal(123);
resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00');
resJson.updatedBy.should.equal(40051332);
resJson.updatedBy.should.equal(40051334);
server.services.pubsub.publish.calledWith('project.updated').should.be.true;
done();
}
Expand All @@ -611,7 +610,7 @@ describe('Project', () => {
request(server)
.patch(`/v5/projects/${project1.id}`)
.set({
Authorization: `Bearer ${testUtil.jwts.copilot}`,
Authorization: `Bearer ${testUtil.jwts.manager}`,
})
.send({
billingAccountId: 1,
Expand Down Expand Up @@ -659,6 +658,20 @@ describe('Project', () => {
});
});

it('should return 400 when updating billingAccountId without "write:projects-billing-accounts" scope in M2M token',
(done) => {
request(server)
.patch(`/v5/projects/${project1.id}`)
.set({
Authorization: `Bearer ${testUtil.m2m['write:projects']}`,
})
.send({
billingAccountId: 123,
})
.expect('Content-Type', /json/)
.expect(400, done);
});

it.skip('should return 200 and update bookmarks', (done) => {
request(server)
.patch(`/v5/projects/${project1.id}`)
Expand Down