diff --git a/docs/permissions.html b/docs/permissions.html index 16be0bc6..f1ed60d8 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.
@@ -294,6 +294,29 @@

+
+
+
+ Manage Project property "billingAccountId" +
+
MANAGE_PROJECT_BILLING_ACCOUNT_ID
+
Who can set or update the "billingAccountId" property.
+
+
+
+
+ +
+ Connect Manager + administrator +
+ +
+ all:connect_project + write:projects-billing-accounts +
+
+
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}`)