diff --git a/src/constants.js b/src/constants.js
index fb358a05..1babbe14 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -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',
diff --git a/src/permissions/constants.js b/src/permissions/constants.js
index d989b725..d89d8277 100644
--- a/src/permissions/constants.js
+++ b/src/permissions/constants.js
@@ -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,
@@ -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,
@@ -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',
diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js
index 716de4d7..cfeaede6 100644
--- a/src/routes/projects/create.js
+++ b/src/routes/projects/create.js
@@ -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
diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js
index ea8df85b..07fb0670 100644
--- a/src/routes/projects/create.spec.js
+++ b/src/routes/projects/create.spec.js
@@ -265,7 +265,6 @@ describe('Project create', () => {
type: 'generic',
description: 'test project',
details: {},
- billingAccountId: 1,
name: 'test project1',
bookmarks: [{
title: 'title1',
@@ -277,7 +276,6 @@ describe('Project create', () => {
type: 'generic',
description: 'test project',
details: {},
- billingAccountId: 1,
name: 'test project1',
attachments: [
{
@@ -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;
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js
index 5dcfa46b..00b76c32 100644
--- a/src/routes/projects/update.js
+++ b/src/routes/projects/update.js
@@ -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');
diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js
index da3d605a..da91f27d 100644
--- a/src/routes/projects/update.spec.js
+++ b/src/routes/projects/update.spec.js
@@ -14,7 +14,6 @@ import {
PROJECT_STATUS,
BUS_API_EVENT,
CONNECT_NOTIFICATION_EVENT,
- M2M_SCOPES,
} from '../../constants';
const should = chai.should();
@@ -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',
@@ -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,
@@ -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();
}
@@ -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,
@@ -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}`)