diff --git a/docs/permissions.html b/docs/permissions.html index 16be0bc6..d9e262ec 100644 --- a/docs/permissions.html +++ b/docs/permissions.html @@ -273,10 +273,10 @@

- Update Project property "directProjectId" + Manage Project property "directProjectId"
-
UPDATE_PROJECT_DIRECT_PROJECT_ID
-
+
MANAGE_PROJECT_DIRECT_PROJECT_ID
+
Who can set or update the "directProjectId" property.
@@ -332,6 +332,30 @@

+
+
+
+ Manage Project property "billingAccountId" +
+
MANAGE_PROJECT_BILLING_ACCOUNT_ID
+
Who can set or update the "billingAccountId" property.
+
+
+
+
+ +
+ Connect Manager + administrator +
+ +
+ all:connect_project + all:projects + write:projects-billing-accounts +
+
+

diff --git a/src/constants.js b/src/constants.js index fb358a05..74fdf708 100644 --- a/src/constants.js +++ b/src/constants.js @@ -273,6 +273,7 @@ export const M2M_SCOPES = { 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..845c2332 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -74,6 +74,12 @@ const SCOPES_PROJECTS_WRITE = [ M2M_SCOPES.PROJECTS.WRITE, ]; +const SCOPES_PROJECTS_WRITE_BILLING_ACCOUNTS = [ + M2M_SCOPES.CONNECT_PROJECT_ADMIN, + M2M_SCOPES.PROJECTS.ALL, + M2M_SCOPES.PROJECTS.WRITE_BILLING_ACCOUNTS, +]; + const SCOPES_PROJECT_MEMBERS_READ = [ M2M_SCOPES.CONNECT_PROJECT_ADMIN, M2M_SCOPES.PROJECT_MEMBERS.ALL, @@ -142,10 +148,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, @@ -172,6 +179,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, + }, + /* * Project Member */ 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..804ab746 100644 --- a/src/routes/projects/update.spec.js +++ b/src/routes/projects/update.spec.js @@ -584,7 +584,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 +600,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 +611,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 +659,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[M2M_SCOPES.PROJECTS.WRITE]}`, + }) + .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}`)