From 596917b83d9bb5e5fe894bf075e35f6e50182555 Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Wed, 16 Jan 2019 16:37:26 +0530 Subject: [PATCH 01/22] Added debug log. --- src/routes/projectMemberInvites/create.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 7717bdc9..2f2b75e2 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -207,6 +207,7 @@ module.exports = [ const forbidUserList = []; _.zip(invite.userIds, rolesList).forEach((data) => { const [userId, roles] = data; + req.log.debug(roles); if (!util.hasIntersection(MANAGER_ROLES, roles)) { forbidUserList.push(userId); From 74a87017eda5672aa7873d2d3ec12b04286a8d6d Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Wed, 16 Jan 2019 17:26:50 +0530 Subject: [PATCH 02/22] updated check for adding user as observer --- src/routes/projectMemberInvites/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 2f2b75e2..3214c493 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -181,7 +181,7 @@ module.exports = [ // permission: // user has to have constants.MANAGER_ROLES role // to be invited as PROJECT_MEMBER_ROLE.MANAGER - if (invite.role === PROJECT_MEMBER_ROLE.MANAGER) { + if (_.includes(PROJECT_MEMBER_MANAGER_ROLES, invite.role)) { _.forEach(invite.userIds, (userId) => { req.log.info(userId); promises.push(util.getUserRoles(userId, req.log, req.id)); From 98256130fa233d1c59febe5921339dafbe5d447b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 22 Jan 2019 11:55:27 +0800 Subject: [PATCH 03/22] Temporary add some details the error message to quickly debug #235 --- src/routes/projectMemberInvites/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 300ba0ad..6f74881f 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -68,7 +68,7 @@ module.exports = [ } } else if ((!!putInvite.userId && putInvite.userId !== req.authUser.userId) || (!!putInvite.email && putInvite.email !== req.authUser.email)) { - error = 'Project members can only update invites for themselves'; + error = 'Project members can only update invites for themselves putInvite: ' + JSON.stringify(putInvite) + ', req.authUser: ' + JSON.stringify(req.authUser); // eslint-disable-line } if (error) { From c05e71fb7cc4b1b95e7bf188f8bfd5bd73cecd6a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 22 Jan 2019 12:49:35 +0800 Subject: [PATCH 04/22] Revert "Temporary add some details the error message to quickly debug #235" This reverts commit 98256130fa233d1c59febe5921339dafbe5d447b. --- src/routes/projectMemberInvites/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 6f74881f..300ba0ad 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -68,7 +68,7 @@ module.exports = [ } } else if ((!!putInvite.userId && putInvite.userId !== req.authUser.userId) || (!!putInvite.email && putInvite.email !== req.authUser.email)) { - error = 'Project members can only update invites for themselves putInvite: ' + JSON.stringify(putInvite) + ', req.authUser: ' + JSON.stringify(req.authUser); // eslint-disable-line + error = 'Project members can only update invites for themselves'; } if (error) { From a3875400961aa484201e241818ce66a6f295f5aa Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 30 Jan 2019 14:25:42 +0800 Subject: [PATCH 05/22] winning submission from challenge 30081357 - Topcoder Connect - Organization configs --- config/default.json | 1 + migrations/20190129_organization_config.sql | 28 +++ postman.json | 176 ++++++++++++++- src/models/orgConfig.js | 28 +++ src/permissions/index.js | 5 + src/routes/index.js | 15 +- src/routes/orgConfig/create.js | 58 +++++ src/routes/orgConfig/create.spec.js | 161 ++++++++++++++ src/routes/orgConfig/delete.js | 37 ++++ src/routes/orgConfig/delete.spec.js | 126 +++++++++++ src/routes/orgConfig/get.js | 39 ++++ src/routes/orgConfig/get.spec.js | 121 +++++++++++ src/routes/orgConfig/list.js | 31 +++ src/routes/orgConfig/list.spec.js | 137 ++++++++++++ src/routes/orgConfig/update.js | 63 ++++++ src/routes/orgConfig/update.spec.js | 229 ++++++++++++++++++++ 16 files changed, 1253 insertions(+), 2 deletions(-) create mode 100644 migrations/20190129_organization_config.sql create mode 100644 src/models/orgConfig.js create mode 100644 src/routes/orgConfig/create.js create mode 100644 src/routes/orgConfig/create.spec.js create mode 100644 src/routes/orgConfig/delete.js create mode 100644 src/routes/orgConfig/delete.spec.js create mode 100644 src/routes/orgConfig/get.js create mode 100644 src/routes/orgConfig/get.spec.js create mode 100644 src/routes/orgConfig/list.js create mode 100644 src/routes/orgConfig/list.spec.js create mode 100644 src/routes/orgConfig/update.js create mode 100644 src/routes/orgConfig/update.spec.js diff --git a/config/default.json b/config/default.json index 2b0ac247..4da2970e 100644 --- a/config/default.json +++ b/config/default.json @@ -35,6 +35,7 @@ "idleTimeout": 1000 }, "kafkaConfig": { + "hosts": "localhost:9092" }, "analyticsKey": "", "VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", diff --git a/migrations/20190129_organization_config.sql b/migrations/20190129_organization_config.sql new file mode 100644 index 00000000..3e9e041e --- /dev/null +++ b/migrations/20190129_organization_config.sql @@ -0,0 +1,28 @@ +-- +-- CREATE NEW TABLE: +-- org_config +-- +CREATE TABLE org_config ( + id bigint NOT NULL, + orgId character varying(45) NOT NULL, + configName character varying(45) NOT NULL, + configValue character varying(512), + "deletedAt" timestamp with time zone, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone, + "deletedBy" bigint, + "createdBy" bigint NOT NULL, + "updatedBy" bigint NOT NULL +); + +CREATE SEQUENCE org_config_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE org_config_id_seq OWNED BY org_config.id; + +ALTER TABLE org_config + ALTER COLUMN id SET DEFAULT nextval('org_config_id_seq'); diff --git a/postman.json b/postman.json index ecd5c408..38312bf2 100644 --- a/postman.json +++ b/postman.json @@ -3301,6 +3301,180 @@ } ] }, + { + "name": "Organization Config", + "description": "", + "item": [ + { + "name": "Create organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \"orgId\": \"20000013\",\r\n \"configName\": \"project_catalog_url\",\r\n \"configValue\": \"/projects/1\"\r\n }\r\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "List organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig", + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "List organization config - filter", + "request": { + "url": { + "raw": "{{api-url}}/v4/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "orgConfig" + ], + "query": [ + { + "key": "filter", + "value": "orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Get organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig/1", + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Update organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig/1", + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \"configName\": \"project_catalog_url\"\r\n }\r\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Delete organization config", + "request": { + "url": "{{api-url}}/v4/orgConfig/1", + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + } + ] + }, { "name": "Product Category", "item": [ @@ -5010,4 +5184,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/models/orgConfig.js b/src/models/orgConfig.js new file mode 100644 index 00000000..79c2ae9b --- /dev/null +++ b/src/models/orgConfig.js @@ -0,0 +1,28 @@ +/* eslint-disable valid-jsdoc */ + +/** + * The Organization config model + */ +module.exports = (sequelize, DataTypes) => { + const OrgConfig = sequelize.define('OrgConfig', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + orgId: { type: DataTypes.STRING(45), allowNull: false }, + configName: { type: DataTypes.STRING(45), allowNull: false }, + configValue: { type: DataTypes.STRING(512) }, + deletedAt: DataTypes.DATE, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: DataTypes.BIGINT, + createdBy: { type: DataTypes.BIGINT, allowNull: false }, + updatedBy: { type: DataTypes.BIGINT, allowNull: false }, + }, { + tableName: 'org_config', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + }); + + return OrgConfig; +}; diff --git a/src/permissions/index.js b/src/permissions/index.js index 0b9880e3..e2bce5ca 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -54,6 +54,11 @@ module.exports = () => { Authorizer.setPolicy('projectType.delete', projectAdmin); Authorizer.setPolicy('projectType.view', true); // anyone can view project types + Authorizer.setPolicy('orgConfig.create', projectAdmin); + Authorizer.setPolicy('orgConfig.edit', projectAdmin); + Authorizer.setPolicy('orgConfig.delete', projectAdmin); + Authorizer.setPolicy('orgConfig.view', true); // anyone can view project types + Authorizer.setPolicy('productCategory.create', projectAdmin); Authorizer.setPolicy('productCategory.edit', projectAdmin); Authorizer.setPolicy('productCategory.delete', projectAdmin); diff --git a/src/routes/index.js b/src/routes/index.js index dabe7454..716cdd2e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -42,6 +42,12 @@ router.route('/v4/projects/metadata/projectTypes') router.route('/v4/projects/metadata/projectTypes/:key') .get(require('./projectTypes/get')); +router.route('/v4/orgConfig') + .get(require('./orgConfig/list')); + +router.route('/v4/orgConfig/:id(\\d+)') + .get(require('./orgConfig/get')); + router.route('/v4/projects/metadata/productCategories') .get(require('./productCategories/list')); router.route('/v4/projects/metadata/productCategories/:key') @@ -53,7 +59,7 @@ router.route('/v4/projects/metadata') .get(require('./metadata/list')); router.all( - RegExp(`\\/${apiVersion}\\/(projects|timelines)(?!\\/health).*`), (req, res, next) => ( + RegExp(`\\/${apiVersion}\\/(projects|timelines|orgConfig)(?!\\/health).*`), (req, res, next) => ( // JWT authentication jwtAuth(config)(req, res, next) ), @@ -182,6 +188,13 @@ router.route('/v4/projects/:projectId(\\d+)/members/invite') .put(require('./projectMemberInvites/update')) .get(require('./projectMemberInvites/get')); +router.route('/v4/orgConfig') + .post(require('./orgConfig/create')); + +router.route('/v4/orgConfig/:id(\\d+)') + .patch(require('./orgConfig/update')) + .delete(require('./orgConfig/delete')); + // register error handler router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars // DO NOT REMOVE next arg.. even though eslint diff --git a/src/routes/orgConfig/create.js b/src/routes/orgConfig/create.js new file mode 100644 index 00000000..6b53d6d2 --- /dev/null +++ b/src/routes/orgConfig/create.js @@ -0,0 +1,58 @@ +/** + * API to add a organization config + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + body: { + param: Joi.object().keys({ + id: Joi.any().strip(), + orgId: Joi.string().max(45).required(), + configName: Joi.string().max(45).required(), + configValue: Joi.string().max(512), + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.create'), + (req, res, next) => { + const entity = _.assign(req.body.param, { + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }); + + // Check if duplicated key + return models.OrgConfig.findOne({ where: { orgId: req.body.param.orgId, configName: req.body.param.configName } }) + .then((existing) => { + if (existing) { + const apiErr = new Error(`Organization config exists for orgId ${req.body.param.orgId} + and configName ${req.body.param.configName}`); + apiErr.status = 422; + return Promise.reject(apiErr); + } + + // Create + return models.OrgConfig.create(entity); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next); + }, +]; diff --git a/src/routes/orgConfig/create.spec.js b/src/routes/orgConfig/create.spec.js new file mode 100644 index 00000000..4f4ef848 --- /dev/null +++ b/src/routes/orgConfig/create.spec.js @@ -0,0 +1,161 @@ +/** + * Tests for create.js + */ +import _ from 'lodash'; +import chai from 'chai'; +import request from 'supertest'; + +import server from '../../app'; +import testUtil from '../../tests/util'; +import models from '../../models'; + +const should = chai.should(); + +describe('CREATE organization config', () => { + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create({ + orgId: 'ORG1', + configName: 'project_catefory_url', + configValue: 'http://localhost/url', + createdBy: 1, + updatedBy: 1, + })).then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('POST /orgConfig', () => { + const body = { + param: { + orgId: 'ORG2', + configName: 'project_catefory_url', + configValue: 'http://localhost/url', + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/orgConfig') + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 422 for missing orgId', (done) => { + const invalidBody = _.cloneDeep(body); + delete invalidBody.param.orgId; + + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 422 for missing configName', (done) => { + const invalidBody = _.cloneDeep(body); + delete invalidBody.param.configName; + + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 422 for duplicated orgId and configName', (done) => { + const invalidBody = _.cloneDeep(body); + invalidBody.param.orgId = 'ORG1'; + invalidBody.param.configName = 'project_catefory_url'; + + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 201 for connect admin', (done) => { + request(server) + .post('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + resJson.createdBy.should.be.eql(40051336); // connect admin + resJson.updatedBy.should.be.eql(40051336); // connect admin + done(); + }); + }); + }); +}); diff --git a/src/routes/orgConfig/delete.js b/src/routes/orgConfig/delete.js new file mode 100644 index 00000000..d33e7608 --- /dev/null +++ b/src/routes/orgConfig/delete.js @@ -0,0 +1,37 @@ +/** + * API to delete a organization config + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + id: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.delete'), + (req, res, next) => + models.sequelize.transaction(() => + models.OrgConfig.findById(req.params.id) + .then((entity) => { + if (!entity) { + const apiErr = new Error(`Organization config not found for id ${req.params.id}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + // Update the deletedBy, then delete + return entity.update({ deletedBy: req.authUser.userId }); + }) + .then(entity => entity.destroy())) + .then(() => { + res.status(204).end(); + }) + .catch(next), +]; diff --git a/src/routes/orgConfig/delete.spec.js b/src/routes/orgConfig/delete.spec.js new file mode 100644 index 00000000..85f39033 --- /dev/null +++ b/src/routes/orgConfig/delete.spec.js @@ -0,0 +1,126 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const expectAfterDelete = (id, err, next) => { + if (err) throw err; + setTimeout(() => + models.OrgConfig.findOne({ + where: { + id, + }, + paranoid: false, + }) + .then((res) => { + if (!res) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(res.deletedAt); + chai.assert.isNotNull(res.deletedBy); + + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, next); + } + }), 500); +}; + +describe('DELETE organization config', () => { + const id = 1; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create({ + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + })).then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('DELETE /orgConfig/{id}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed config', (done) => { + request(server) + .delete('/v4/orgConfig/not_existed') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted config', (done) => { + models.OrgConfig.destroy({ where: { id } }) + .then(() => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 204, for admin, if config was successfully removed', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(id, err, done)); + }); + + it('should return 204, for connect admin, if config was successfully removed', (done) => { + request(server) + .delete(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(id, err, done)); + }); + }); +}); diff --git a/src/routes/orgConfig/get.js b/src/routes/orgConfig/get.js new file mode 100644 index 00000000..5c14779b --- /dev/null +++ b/src/routes/orgConfig/get.js @@ -0,0 +1,39 @@ +/** + * API to get a organization config + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + id: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.view'), + (req, res, next) => models.OrgConfig.findOne({ + where: { + id: req.params.id, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((orgConfig) => { + // Not found + if (!orgConfig) { + const apiErr = new Error(`Organization config not found for id ${req.params.id}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, orgConfig)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/orgConfig/get.spec.js b/src/routes/orgConfig/get.spec.js new file mode 100644 index 00000000..34a4d337 --- /dev/null +++ b/src/routes/orgConfig/get.spec.js @@ -0,0 +1,121 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('GET organization config', () => { + const config = { + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + }; + + const id = config.id; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create(config)) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /orgConfig/{id}', () => { + it('should return 404 for non-existed config', (done) => { + request(server) + .get('/v4/orgConfig/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted config', (done) => { + models.OrgConfig.destroy({ where: { id } }) + .then(() => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(config.id); + resJson.orgId.should.be.eql(config.orgId); + resJson.configName.should.be.eql(config.configName); + resJson.configValue.should.be.eql(config.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(config.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 even if user is not authenticated', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .expect(200, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/orgConfig/list.js b/src/routes/orgConfig/list.js new file mode 100644 index 00000000..55f05f1d --- /dev/null +++ b/src/routes/orgConfig/list.js @@ -0,0 +1,31 @@ +/** + * API to list organization config + */ +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; + +const permissions = tcMiddleware.permissions; + +module.exports = [ + permissions('orgConfig.view'), + (req, res, next) => { + // handle filters + const filters = util.parseQueryFilter(req.query.filter); + if (!util.isValidFilter(filters, ['orgId', 'configName'])) { + return util.handleError('Invalid filters', null, req, next); + } + req.log.debug(filters); + // Get all organization config + const where = filters || {}; + return models.OrgConfig.findAll({ + where, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }) + .then((orgConfigs) => { + res.json(util.wrapResponse(req.id, orgConfigs)); + }) + .catch(next); + }, +]; diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js new file mode 100644 index 00000000..91ac0cdb --- /dev/null +++ b/src/routes/orgConfig/list.spec.js @@ -0,0 +1,137 @@ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('LIST organization config', () => { + const configs = [ + { + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + }, + { + id: 2, + orgId: 'ORG1', + configName: 'project_catalog_url', + configValue: '/projects/2', + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create(configs[0])) + .then(() => models.OrgConfig.create(configs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /orgConfig', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const config = configs[0]; + + const resJson = res.body.result.content; + resJson.should.have.length(2); + resJson[0].id.should.be.eql(config.id); + resJson[0].orgId.should.be.eql(config.orgId); + resJson[0].configName.should.be.eql(config.configName); + resJson[0].configValue.should.be.eql(config.configValue); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(config.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + + done(); + }); + }); + + it('should return 200 with filters', (done) => { + request(server) + .get(`/v4/orgConfig?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const config = configs[0]; + + const resJson = res.body.result.content; + resJson.should.have.length(1); + resJson[0].id.should.be.eql(config.id); + resJson[0].orgId.should.be.eql(config.orgId); + resJson[0].configName.should.be.eql(config.configName); + resJson[0].configValue.should.be.eql(config.configValue); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(config.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + + done(); + }); + }); + + it('should return 200 even if user is not authenticated', (done) => { + request(server) + .get('/v4/orgConfig') + .expect(200, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/orgConfig') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/orgConfig/update.js b/src/routes/orgConfig/update.js new file mode 100644 index 00000000..7bc2b52d --- /dev/null +++ b/src/routes/orgConfig/update.js @@ -0,0 +1,63 @@ +/** + * API to update a organization config + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + id: Joi.number().integer().positive().required(), + }, + body: { + param: Joi.object().keys({ + id: Joi.any().strip(), + orgId: Joi.string().max(45).optional(), + configName: Joi.string().max(45).optional(), + configValue: Joi.string().max(512).optional(), + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('orgConfig.edit'), + (req, res, next) => { + const entityToUpdate = _.assign(req.body.param, { + updatedBy: req.authUser.userId, + }); + + return models.OrgConfig.findOne({ + where: { + id: req.params.id, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((orgConfig) => { + // Not found + if (!orgConfig) { + const apiErr = new Error(`Organization config not found for id ${req.params.id}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + return orgConfig.update(entityToUpdate); + }) + .then((orgConfig) => { + res.json(util.wrapResponse(req.id, orgConfig)); + return Promise.resolve(); + }) + .catch(next); + }, +]; diff --git a/src/routes/orgConfig/update.spec.js b/src/routes/orgConfig/update.spec.js new file mode 100644 index 00000000..69ff3319 --- /dev/null +++ b/src/routes/orgConfig/update.spec.js @@ -0,0 +1,229 @@ +/** + * Tests for get.js + */ +import _ from 'lodash'; +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('UPDATE organization config', () => { + const config = { + id: 1, + orgId: 'ORG1', + configName: 'project_category_url', + configValue: '/projects/1', + createdBy: 1, + updatedBy: 1, + }; + const id = config.id; + + beforeEach(() => testUtil.clearDb() + .then(() => models.OrgConfig.create(config)) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('PATCH /orgConfig/{id}', () => { + const body = { + param: { + id: 1, + orgId: 'ORG2', + configName: 'project_category_url_update', + configValue: '/projects/2', + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed config', (done) => { + request(server) + .patch('/v4/orgConfig/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + + it('should return 404 for deleted config', (done) => { + models.OrgConfig.destroy({ where: { id } }) + .then(() => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + }); + + it('should return 200 for admin configValue updated', (done) => { + const partialBody = _.cloneDeep(body); + delete partialBody.param.orgId; + delete partialBody.param.configName; + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(partialBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(config.orgId); + resJson.configName.should.be.eql(config.configName); + resJson.configValue.should.be.eql(partialBody.param.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for admin orgId updated', (done) => { + const partialBody = _.cloneDeep(body); + delete partialBody.param.configName; + delete partialBody.param.configValue; + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(partialBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(partialBody.param.orgId); + resJson.configName.should.be.eql(config.configName); + resJson.configValue.should.be.eql(config.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for admin configName updated', (done) => { + const partialBody = _.cloneDeep(body); + delete partialBody.param.orgId; + delete partialBody.param.configValue; + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(partialBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(config.orgId); + resJson.configName.should.be.eql(partialBody.param.configName); + resJson.configValue.should.be.eql(config.configValue); + resJson.createdBy.should.be.eql(config.createdBy); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for admin all fields updated', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .patch(`/v4/orgConfig/${id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(id); + resJson.orgId.should.be.eql(body.param.orgId); + resJson.configName.should.be.eql(body.param.configName); + resJson.configValue.should.be.eql(body.param.configValue); + resJson.createdBy.should.be.eql(config.createdBy); // should not update createdAt + resJson.updatedBy.should.be.eql(40051336); // connect admin + done(); + }); + }); + }); +}); From 86032fb066ca08917d91c50cf43a4c31a83a47b4 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 30 Jan 2019 15:21:30 +0530 Subject: [PATCH 06/22] Move orgConfig path to metadata resource --- postman.json | 12 ++++++------ src/routes/index.js | 8 ++++---- src/routes/orgConfig/create.spec.js | 18 +++++++++--------- src/routes/orgConfig/delete.spec.js | 18 +++++++++--------- src/routes/orgConfig/get.spec.js | 16 ++++++++-------- src/routes/orgConfig/list.spec.js | 14 +++++++------- src/routes/orgConfig/update.spec.js | 22 +++++++++++----------- 7 files changed, 54 insertions(+), 54 deletions(-) diff --git a/postman.json b/postman.json index 38312bf2..56087004 100644 --- a/postman.json +++ b/postman.json @@ -3308,7 +3308,7 @@ { "name": "Create organization config", "request": { - "url": "{{api-url}}/v4/orgConfig", + "url": "{{api-url}}/v4/projects/metadata/orgConfig", "method": "POST", "header": [ { @@ -3333,7 +3333,7 @@ { "name": "List organization config", "request": { - "url": "{{api-url}}/v4/orgConfig", + "url": "{{api-url}}/v4/projects/metadata/orgConfig", "method": "GET", "header": [ { @@ -3359,7 +3359,7 @@ "name": "List organization config - filter", "request": { "url": { - "raw": "{{api-url}}/v4/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "raw": "{{api-url}}/v4/projects/metadata/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", "host": [ "{{api-url}}" ], @@ -3401,7 +3401,7 @@ { "name": "Get organization config", "request": { - "url": "{{api-url}}/v4/orgConfig/1", + "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "GET", "header": [ { @@ -3426,7 +3426,7 @@ { "name": "Update organization config", "request": { - "url": "{{api-url}}/v4/orgConfig/1", + "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "PATCH", "header": [ { @@ -3451,7 +3451,7 @@ { "name": "Delete organization config", "request": { - "url": "{{api-url}}/v4/orgConfig/1", + "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "DELETE", "header": [ { diff --git a/src/routes/index.js b/src/routes/index.js index 716cdd2e..110c5ca3 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -42,10 +42,10 @@ router.route('/v4/projects/metadata/projectTypes') router.route('/v4/projects/metadata/projectTypes/:key') .get(require('./projectTypes/get')); -router.route('/v4/orgConfig') +router.route('/v4/projects/metadata/orgConfig') .get(require('./orgConfig/list')); -router.route('/v4/orgConfig/:id(\\d+)') +router.route('/v4/projects/metadata/orgConfig/:id(\\d+)') .get(require('./orgConfig/get')); router.route('/v4/projects/metadata/productCategories') @@ -188,10 +188,10 @@ router.route('/v4/projects/:projectId(\\d+)/members/invite') .put(require('./projectMemberInvites/update')) .get(require('./projectMemberInvites/get')); -router.route('/v4/orgConfig') +router.route('/v4/projects/metadata/orgConfig') .post(require('./orgConfig/create')); -router.route('/v4/orgConfig/:id(\\d+)') +router.route('/v4/projects/metadata/orgConfig/:id(\\d+)') .patch(require('./orgConfig/update')) .delete(require('./orgConfig/delete')); diff --git a/src/routes/orgConfig/create.spec.js b/src/routes/orgConfig/create.spec.js index 4f4ef848..c13a7521 100644 --- a/src/routes/orgConfig/create.spec.js +++ b/src/routes/orgConfig/create.spec.js @@ -34,14 +34,14 @@ describe('CREATE organization config', () => { it('should return 403 if user is not authenticated', (done) => { request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .send(body) .expect(403, done); }); it('should return 403 for member', (done) => { request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) @@ -51,7 +51,7 @@ describe('CREATE organization config', () => { it('should return 403 for copilot', (done) => { request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) @@ -61,7 +61,7 @@ describe('CREATE organization config', () => { it('should return 403 for manager', (done) => { request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) @@ -74,7 +74,7 @@ describe('CREATE organization config', () => { delete invalidBody.param.orgId; request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -88,7 +88,7 @@ describe('CREATE organization config', () => { delete invalidBody.param.configName; request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -103,7 +103,7 @@ describe('CREATE organization config', () => { invalidBody.param.configName = 'project_catefory_url'; request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -114,7 +114,7 @@ describe('CREATE organization config', () => { it('should return 201 for admin', (done) => { request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -140,7 +140,7 @@ describe('CREATE organization config', () => { it('should return 201 for connect admin', (done) => { request(server) - .post('/v4/orgConfig') + .post('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) diff --git a/src/routes/orgConfig/delete.spec.js b/src/routes/orgConfig/delete.spec.js index 85f39033..0bc6aefc 100644 --- a/src/routes/orgConfig/delete.spec.js +++ b/src/routes/orgConfig/delete.spec.js @@ -24,7 +24,7 @@ const expectAfterDelete = (id, err, next) => { chai.assert.isNotNull(res.deletedBy); request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -51,13 +51,13 @@ describe('DELETE organization config', () => { describe('DELETE /orgConfig/{id}', () => { it('should return 403 if user is not authenticated', (done) => { request(server) - .delete(`/v4/orgConfig/${id}`) + .delete(`/v4/projects/metadata/orgConfig/${id}`) .expect(403, done); }); it('should return 403 for member', (done) => { request(server) - .delete(`/v4/orgConfig/${id}`) + .delete(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) @@ -66,7 +66,7 @@ describe('DELETE organization config', () => { it('should return 403 for copilot', (done) => { request(server) - .delete(`/v4/orgConfig/${id}`) + .delete(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) @@ -75,7 +75,7 @@ describe('DELETE organization config', () => { it('should return 403 for manager', (done) => { request(server) - .delete(`/v4/orgConfig/${id}`) + .delete(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) @@ -84,7 +84,7 @@ describe('DELETE organization config', () => { it('should return 404 for non-existed config', (done) => { request(server) - .delete('/v4/orgConfig/not_existed') + .delete('/v4/projects/metadata/orgConfig/not_existed') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -95,7 +95,7 @@ describe('DELETE organization config', () => { models.OrgConfig.destroy({ where: { id } }) .then(() => { request(server) - .delete(`/v4/orgConfig/${id}`) + .delete(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -105,7 +105,7 @@ describe('DELETE organization config', () => { it('should return 204, for admin, if config was successfully removed', (done) => { request(server) - .delete(`/v4/orgConfig/${id}`) + .delete(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -115,7 +115,7 @@ describe('DELETE organization config', () => { it('should return 204, for connect admin, if config was successfully removed', (done) => { request(server) - .delete(`/v4/orgConfig/${id}`) + .delete(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) diff --git a/src/routes/orgConfig/get.spec.js b/src/routes/orgConfig/get.spec.js index 34a4d337..e3f64325 100644 --- a/src/routes/orgConfig/get.spec.js +++ b/src/routes/orgConfig/get.spec.js @@ -31,7 +31,7 @@ describe('GET organization config', () => { describe('GET /orgConfig/{id}', () => { it('should return 404 for non-existed config', (done) => { request(server) - .get('/v4/orgConfig/1234') + .get('/v4/projects/metadata/orgConfig/1234') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -42,7 +42,7 @@ describe('GET organization config', () => { models.OrgConfig.destroy({ where: { id } }) .then(() => { request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -52,7 +52,7 @@ describe('GET organization config', () => { it('should return 200 for admin', (done) => { request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -76,13 +76,13 @@ describe('GET organization config', () => { it('should return 200 even if user is not authenticated', (done) => { request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .expect(200, done); }); it('should return 200 for connect admin', (done) => { request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) @@ -92,7 +92,7 @@ describe('GET organization config', () => { it('should return 200 for connect manager', (done) => { request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) @@ -102,7 +102,7 @@ describe('GET organization config', () => { it('should return 200 for member', (done) => { request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) @@ -111,7 +111,7 @@ describe('GET organization config', () => { it('should return 200 for copilot', (done) => { request(server) - .get(`/v4/orgConfig/${id}`) + .get(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js index 91ac0cdb..feb1a85f 100644 --- a/src/routes/orgConfig/list.spec.js +++ b/src/routes/orgConfig/list.spec.js @@ -40,7 +40,7 @@ describe('LIST organization config', () => { describe('GET /orgConfig', () => { it('should return 200 for admin', (done) => { request(server) - .get('/v4/orgConfig') + .get('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -66,7 +66,7 @@ describe('LIST organization config', () => { it('should return 200 with filters', (done) => { request(server) - .get(`/v4/orgConfig?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) + .get(`/v4/projects/metadata/orgConfig?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -92,13 +92,13 @@ describe('LIST organization config', () => { it('should return 200 even if user is not authenticated', (done) => { request(server) - .get('/v4/orgConfig') + .get('/v4/projects/metadata/orgConfig') .expect(200, done); }); it('should return 200 for connect admin', (done) => { request(server) - .get('/v4/orgConfig') + .get('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) @@ -108,7 +108,7 @@ describe('LIST organization config', () => { it('should return 200 for connect manager', (done) => { request(server) - .get('/v4/orgConfig') + .get('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) @@ -118,7 +118,7 @@ describe('LIST organization config', () => { it('should return 200 for member', (done) => { request(server) - .get('/v4/orgConfig') + .get('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) @@ -127,7 +127,7 @@ describe('LIST organization config', () => { it('should return 200 for copilot', (done) => { request(server) - .get('/v4/orgConfig') + .get('/v4/projects/metadata/orgConfig') .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) diff --git a/src/routes/orgConfig/update.spec.js b/src/routes/orgConfig/update.spec.js index 69ff3319..e1f18672 100644 --- a/src/routes/orgConfig/update.spec.js +++ b/src/routes/orgConfig/update.spec.js @@ -40,14 +40,14 @@ describe('UPDATE organization config', () => { it('should return 403 if user is not authenticated', (done) => { request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .send(body) .expect(403, done); }); it('should return 403 for member', (done) => { request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) @@ -57,7 +57,7 @@ describe('UPDATE organization config', () => { it('should return 403 for copilot', (done) => { request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .send(body) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, @@ -67,7 +67,7 @@ describe('UPDATE organization config', () => { it('should return 403 for manager', (done) => { request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .send(body) .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, @@ -77,7 +77,7 @@ describe('UPDATE organization config', () => { it('should return 404 for non-existed config', (done) => { request(server) - .patch('/v4/orgConfig/1234') + .patch('/v4/projects/metadata/orgConfig/1234') .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -89,7 +89,7 @@ describe('UPDATE organization config', () => { models.OrgConfig.destroy({ where: { id } }) .then(() => { request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -103,7 +103,7 @@ describe('UPDATE organization config', () => { delete partialBody.param.orgId; delete partialBody.param.configName; request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -131,7 +131,7 @@ describe('UPDATE organization config', () => { delete partialBody.param.configName; delete partialBody.param.configValue; request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -159,7 +159,7 @@ describe('UPDATE organization config', () => { delete partialBody.param.orgId; delete partialBody.param.configValue; request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -184,7 +184,7 @@ describe('UPDATE organization config', () => { it('should return 200 for admin all fields updated', (done) => { request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -208,7 +208,7 @@ describe('UPDATE organization config', () => { it('should return 200 for connect admin', (done) => { request(server) - .patch(`/v4/orgConfig/${id}`) + .patch(`/v4/projects/metadata/orgConfig/${id}`) .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) From b29a6c69c8302f5a7adf58de1dc8a1cb2dbc7c3a Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 30 Jan 2019 15:24:43 +0530 Subject: [PATCH 07/22] lint fix --- src/routes/orgConfig/list.spec.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js index feb1a85f..bd3c77a8 100644 --- a/src/routes/orgConfig/list.spec.js +++ b/src/routes/orgConfig/list.spec.js @@ -11,6 +11,7 @@ import testUtil from '../../tests/util'; const should = chai.should(); describe('LIST organization config', () => { + const orgConfigPath = '/v4/projects/metadata/orgConfig' const configs = [ { id: 1, @@ -40,7 +41,7 @@ describe('LIST organization config', () => { describe('GET /orgConfig', () => { it('should return 200 for admin', (done) => { request(server) - .get('/v4/projects/metadata/orgConfig') + .get(`${orgConfigPath}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -66,7 +67,7 @@ describe('LIST organization config', () => { it('should return 200 with filters', (done) => { request(server) - .get(`/v4/projects/metadata/orgConfig?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) @@ -92,13 +93,13 @@ describe('LIST organization config', () => { it('should return 200 even if user is not authenticated', (done) => { request(server) - .get('/v4/projects/metadata/orgConfig') + .get(`${orgConfigPath}`) .expect(200, done); }); it('should return 200 for connect admin', (done) => { request(server) - .get('/v4/projects/metadata/orgConfig') + .get(`${orgConfigPath}`) .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) @@ -108,7 +109,7 @@ describe('LIST organization config', () => { it('should return 200 for connect manager', (done) => { request(server) - .get('/v4/projects/metadata/orgConfig') + .get(`${orgConfigPath}`) .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) @@ -118,7 +119,7 @@ describe('LIST organization config', () => { it('should return 200 for member', (done) => { request(server) - .get('/v4/projects/metadata/orgConfig') + .get(`${orgConfigPath}`) .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) @@ -127,7 +128,7 @@ describe('LIST organization config', () => { it('should return 200 for copilot', (done) => { request(server) - .get('/v4/projects/metadata/orgConfig') + .get(`${orgConfigPath}`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) From a466252af7fe063b70ea588c3461f476c28dbf98 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 30 Jan 2019 15:25:30 +0530 Subject: [PATCH 08/22] lint fix --- src/routes/orgConfig/list.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js index bd3c77a8..e7f2ac1e 100644 --- a/src/routes/orgConfig/list.spec.js +++ b/src/routes/orgConfig/list.spec.js @@ -11,7 +11,7 @@ import testUtil from '../../tests/util'; const should = chai.should(); describe('LIST organization config', () => { - const orgConfigPath = '/v4/projects/metadata/orgConfig' + const orgConfigPath = '/v4/projects/metadata/orgConfig'; const configs = [ { id: 1, From faf9578020f9f7e875d9ee7ffa50e3de467c1dee Mon Sep 17 00:00:00 2001 From: phoenix303 Date: Thu, 31 Jan 2019 00:55:06 -0800 Subject: [PATCH 09/22] Organization config final fixes --- migrations/20190129_organization_config.sql | 6 +- package-lock.json | 9 +- src/routes/orgConfig/list.js | 13 + swagger.yaml | 281 +++++++++++++++++++- 4 files changed, 292 insertions(+), 17 deletions(-) diff --git a/migrations/20190129_organization_config.sql b/migrations/20190129_organization_config.sql index 3e9e041e..3e218c71 100644 --- a/migrations/20190129_organization_config.sql +++ b/migrations/20190129_organization_config.sql @@ -4,9 +4,9 @@ -- CREATE TABLE org_config ( id bigint NOT NULL, - orgId character varying(45) NOT NULL, - configName character varying(45) NOT NULL, - configValue character varying(512), + "orgId" character varying(45) NOT NULL, + "configName" character varying(45) NOT NULL, + "configValue" character varying(512), "deletedAt" timestamp with time zone, "createdAt" timestamp with time zone, "updatedAt" timestamp with time zone, diff --git a/package-lock.json b/package-lock.json index 20e07864..10c0fce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7602,7 +7602,7 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#02350d46d3b8d89ee4686d5c1a5d0086943cbfe8", + "version": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab", "requires": { "auth0-js": "9.8.2", "axios": "0.12.0", @@ -7611,12 +7611,13 @@ "jwks-rsa": "1.3.0", "le_node": "1.8.0", "lodash": "4.17.11", - "millisecond": "0.1.2" + "millisecond": "0.1.2", + "request": "2.88.0" }, "dependencies": { "axios": { "version": "0.12.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.12.0.tgz", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.12.0.tgz", "integrity": "sha1-uQewIhzDTsHJ+sGOx/B935V4W6Q=", "requires": { "follow-redirects": "0.0.7" @@ -7624,7 +7625,7 @@ }, "follow-redirects": { "version": "0.0.7", - "resolved": "http://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", "requires": { "debug": "2.6.9", diff --git a/src/routes/orgConfig/list.js b/src/routes/orgConfig/list.js index 55f05f1d..0c8222f0 100644 --- a/src/routes/orgConfig/list.js +++ b/src/routes/orgConfig/list.js @@ -1,17 +1,30 @@ /** * API to list organization config */ +import validate from 'express-validation'; +import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; const permissions = tcMiddleware.permissions; +const schema = { + query: { + filter: Joi.string().required(), + }, +}; + module.exports = [ + validate(schema), permissions('orgConfig.view'), (req, res, next) => { // handle filters const filters = util.parseQueryFilter(req.query.filter); + // Throw error if orgId is not present in filter + if (!filters.orgId) { + return next(util.buildApiError('Missing filter orgId', 422)); + } if (!util.isValidFilter(filters, ['orgId', 'configName'])) { return util.handleError('Invalid filters', null, req, next); } diff --git a/swagger.yaml b/swagger.yaml index b547e16e..cf670a7a 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -659,7 +659,7 @@ paths: description: Project migrated successfully schema: $ref: "#/definitions/ProjectUpgradeResponse" - + /projects/metadata: get: tags: @@ -1741,7 +1741,136 @@ paths: description: Invalid server state or unknown error schema: $ref: "#/definitions/ErrorModel" - + /orgConfig: + get: + tags: + - orgConfig + operationId: findOrgConfigs + security: + - Bearer: [] + description: Retrieve all organization configs. All user roles can access this endpoint. + parameters: + - name: filter + required: true + type: string + in: query + description: | + Url encoded list of Supported filters + - orgId (required) + - configName + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: A list of organization configs + schema: + $ref: "#/definitions/OrgConfigListResponse" + post: + tags: + - orgConfig + operationId: addOrgConfig + security: + - Bearer: [] + description: Create a organization config. Only admin or connect admin can access this endpoint. + parameters: + - in: body + name: body + required: true + schema: + $ref: '#/definitions/OrgConfigCreateBodyParam' + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '201': + description: Returns the newly created organization config + schema: + $ref: "#/definitions/OrgConfigResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + /orgConfig/{id}: + get: + tags: + - orgConfig + description: Retrieve project type by id. All user roles can access this endpoint. + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a project type + schema: + $ref: "#/definitions/OrgConfigResponse" + parameters: + - $ref: "#/parameters/idParam" + operationId: getOrgConfig + patch: + tags: + - orgConfig + operationId: updateOrgConfig + security: + - Bearer: [] + description: Update a organization config. Only admin or connect admin can access this endpoint. + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: Successfully updated organization config. + schema: + $ref: "#/definitions/OrgConfigResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: "#/parameters/idParam" + - name: body + in: body + required: true + schema: + $ref: "#/definitions/OrgConfigCreateBodyParam" + + delete: + tags: + - orgConfig + description: Remove an existing organization config. Only admin or connect admin can access this endpoint. + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/idParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If organization config is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Organization config successfully removed parameters: projectIdParam: name: projectId @@ -1812,6 +1941,13 @@ parameters: type: integer format: int64 minimum: 1 + idParam: + name: id + in: path + description: organization config id + required: true + type: integer + format: int64 offsetParam: name: offset description: "number of items to skip. Defaults to 0" @@ -3153,6 +3289,132 @@ definitions: readOnly: true - $ref: "#/definitions/ProjectTypeCreateRequest" + OrgConfigRequest: + title: Organization config request object + type: object + required: + - orgId + - configName + properties: + orgId: + type: string + description: the org id + configName: + type: string + description: the organization config name + + OrgConfigCreateRequest: + title: Organization config creation request object + type: object + allOf: + - type: object + properties: + configValue: + type: string + description: the organization config id + - $ref: "#/definitions/OrgConfigRequest" + + OrgConfigCreateBodyParam: + title: Organization config creation body param + type: object + required: + - param + properties: + param: + $ref: "#/definitions/OrgConfigCreateRequest" + + OrgConfig: + title: Organization config object + allOf: + - type: object + required: + - id + - orgId + - configName + - createdAt + - createdBy + - updatedAt + - updatedBy + properties: + id: + type: number + format: int64 + description: the id + orgId: + type: string + description: the org id + configName: + type: string + description: the config name + configValue: + type: string + description: the config value + createdAt: + type: string + description: Datetime (GMT) when object was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this object + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when object was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this object + readOnly: true + - $ref: "#/definitions/OrgConfigCreateRequest" + + OrgConfigResponse: + title: Single organization config response object + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/OrgConfig" + + OrgConfigListResponse: + title: Organization confige list response object + type: object + properties: + id: + type: string + readOnly: true + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + type: array + items: + $ref: "#/definitions/OrgConfig" ProjectTypeResponse: title: Single project type response object @@ -3584,7 +3846,7 @@ definitions: format: long minimum: 1 description: the milestone template reference id - metadata: + metadata: type: object description: the milestone template metadata @@ -3708,7 +3970,7 @@ definitions: type: array items: $ref: "#/definitions/MilestoneTemplate" - + AllMetadataResponse: title: All metadata response object type: object @@ -3752,7 +4014,7 @@ definitions: type: array items: $ref: "#/definitions/ProductCategory" - + ProjectMemberInvite: type: object properties: @@ -3797,7 +4059,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this task readOnly: true - + AddProjectMemberInvitesRequest: title: Add project member invites request object type: object @@ -3819,8 +4081,8 @@ definitions: role: description: The target role in the project type: string - enum: ["manager", "customer", "copilot"] - + enum: ["manager", "customer", "copilot"] + UpdateProjectMemberInviteRequest: title: Update project member invite request object type: object @@ -3839,7 +4101,7 @@ definitions: description: The invite status type: string enum: ["pending", "accepted", "refused", "canceled"] - + ProjectMemberInviteResponse: title: Project member invite response object type: object @@ -3861,4 +4123,3 @@ definitions: $ref: "#/definitions/ResponseMetadata" content: $ref: "#/definitions/ProjectMemberInvite" - From 6cb4b87ab6033b9bf0823c71c6f0c041aadf6f64 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 1 Feb 2019 10:03:49 +0800 Subject: [PATCH 10/22] fix URL for orgConfig in postman and swagger --- postman.json | 150 ++++++++++++++++++----------- swagger.yaml | 261 ++++++++++++++++++++++++++------------------------- 2 files changed, 224 insertions(+), 187 deletions(-) diff --git a/postman.json b/postman.json index 56087004..8dd0d400 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "e810fc27-5518-4cc5-8f90-6b1423c6b0b4", + "_postman_id": "d9f6cae1-30c2-4c85-96c3-428b1f2fc08d", "name": "tc-project-service", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -3303,173 +3303,209 @@ }, { "name": "Organization Config", - "description": "", "item": [ { "name": "Create organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig", "method": "POST", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "{\r\n \"param\":{\r\n \"orgId\": \"20000013\",\r\n \"configName\": \"project_catalog_url\",\r\n \"configValue\": \"/projects/1\"\r\n }\r\n}" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig" + ] + } }, "response": [] }, { - "name": "List organization config", + "name": "List organization config - error without filter", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig", "method": "GET", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" - }, - "response": [] - }, - { - "name": "List organization config - filter", - "request": { "url": { - "raw": "{{api-url}}/v4/projects/metadata/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "raw": "{{api-url}}/v4/projects/metadata/orgConfig", "host": [ "{{api-url}}" ], "path": [ "v4", + "projects", + "metadata", "orgConfig" - ], - "query": [ - { - "key": "filter", - "value": "orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", - "equals": true, - "description": "" - } - ], - "variable": [] - }, + ] + } + }, + "response": [] + }, + { + "name": "List organization config - filter", + "request": { "method": "GET", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "orgConfig" + ], + "query": [ + { + "key": "filter", + "value": "orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url" + } + ] + } }, "response": [] }, { "name": "Get organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "GET", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig", + "1" + ] + } }, "response": [] }, { "name": "Update organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "PATCH", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "{\r\n \"param\":{\r\n \"configName\": \"project_catalog_url\"\r\n }\r\n}" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig", + "1" + ] + } }, "response": [] }, { "name": "Delete organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "DELETE", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig", + "1" + ] + } }, "response": [] } @@ -5184,4 +5220,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/swagger.yaml b/swagger.yaml index cf670a7a..b87eb9e1 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1177,6 +1177,137 @@ paths: '204': description: Project type successfully removed + /projects/metadata/orgConfig: + get: + tags: + - orgConfig + operationId: findOrgConfigs + security: + - Bearer: [] + description: Retrieve all organization configs. All user roles can access this endpoint. + parameters: + - name: filter + required: true + type: string + in: query + description: | + Url encoded list of Supported filters + - orgId (required) + - configName + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: A list of organization configs + schema: + $ref: "#/definitions/OrgConfigListResponse" + post: + tags: + - orgConfig + operationId: addOrgConfig + security: + - Bearer: [] + description: Create a organization config. Only admin or connect admin can access this endpoint. + parameters: + - in: body + name: body + required: true + schema: + $ref: '#/definitions/OrgConfigCreateBodyParam' + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '201': + description: Returns the newly created organization config + schema: + $ref: "#/definitions/OrgConfigResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projects/metadata/orgConfig/{id}: + get: + tags: + - orgConfig + description: Retrieve project type by id. All user roles can access this endpoint. + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a project type + schema: + $ref: "#/definitions/OrgConfigResponse" + parameters: + - $ref: "#/parameters/idParam" + operationId: getOrgConfig + patch: + tags: + - orgConfig + operationId: updateOrgConfig + security: + - Bearer: [] + description: Update a organization config. Only admin or connect admin can access this endpoint. + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: Successfully updated organization config. + schema: + $ref: "#/definitions/OrgConfigResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: "#/parameters/idParam" + - name: body + in: body + required: true + schema: + $ref: "#/definitions/OrgConfigCreateBodyParam" + + delete: + tags: + - orgConfig + description: Remove an existing organization config. Only admin or connect admin can access this endpoint. + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/idParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If organization config is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Organization config successfully removed /timelines: get: @@ -1741,136 +1872,6 @@ paths: description: Invalid server state or unknown error schema: $ref: "#/definitions/ErrorModel" - /orgConfig: - get: - tags: - - orgConfig - operationId: findOrgConfigs - security: - - Bearer: [] - description: Retrieve all organization configs. All user roles can access this endpoint. - parameters: - - name: filter - required: true - type: string - in: query - description: | - Url encoded list of Supported filters - - orgId (required) - - configName - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of organization configs - schema: - $ref: "#/definitions/OrgConfigListResponse" - post: - tags: - - orgConfig - operationId: addOrgConfig - security: - - Bearer: [] - description: Create a organization config. Only admin or connect admin can access this endpoint. - parameters: - - in: body - name: body - required: true - schema: - $ref: '#/definitions/OrgConfigCreateBodyParam' - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '201': - description: Returns the newly created organization config - schema: - $ref: "#/definitions/OrgConfigResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - /orgConfig/{id}: - get: - tags: - - orgConfig - description: Retrieve project type by id. All user roles can access this endpoint. - security: - - Bearer: [] - responses: - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project type - schema: - $ref: "#/definitions/OrgConfigResponse" - parameters: - - $ref: "#/parameters/idParam" - operationId: getOrgConfig - patch: - tags: - - orgConfig - operationId: updateOrgConfig - security: - - Bearer: [] - description: Update a organization config. Only admin or connect admin can access this endpoint. - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated organization config. - schema: - $ref: "#/definitions/OrgConfigResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - $ref: "#/parameters/idParam" - - name: body - in: body - required: true - schema: - $ref: "#/definitions/OrgConfigCreateBodyParam" - - delete: - tags: - - orgConfig - description: Remove an existing organization config. Only admin or connect admin can access this endpoint. - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/idParam" - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: If organization config is not found - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Organization config successfully removed parameters: projectIdParam: name: projectId From 3a0db80c1dd4a438cdb605653763e4bd53fbba01 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 1 Feb 2019 10:30:57 +0800 Subject: [PATCH 11/22] fixed tests to reflect that filter with orgId is now required --- src/routes/orgConfig/list.spec.js | 64 ++++++++++++------------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js index e7f2ac1e..44f42cc7 100644 --- a/src/routes/orgConfig/list.spec.js +++ b/src/routes/orgConfig/list.spec.js @@ -32,40 +32,12 @@ describe('LIST organization config', () => { ]; beforeEach(() => testUtil.clearDb() - .then(() => models.OrgConfig.create(configs[0])) - .then(() => models.OrgConfig.create(configs[1])) - .then(() => Promise.resolve()), + .then(() => models.OrgConfig.bulkCreate(configs)), ); after(testUtil.clearDb); describe('GET /orgConfig', () => { - it('should return 200 for admin', (done) => { - request(server) - .get(`${orgConfigPath}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const config = configs[0]; - - const resJson = res.body.result.content; - resJson.should.have.length(2); - resJson[0].id.should.be.eql(config.id); - resJson[0].orgId.should.be.eql(config.orgId); - resJson[0].configName.should.be.eql(config.configName); - resJson[0].configValue.should.be.eql(config.configValue); - should.exist(resJson[0].createdAt); - resJson[0].updatedBy.should.be.eql(config.updatedBy); - should.exist(resJson[0].updatedAt); - should.not.exist(resJson[0].deletedBy); - should.not.exist(resJson[0].deletedAt); - - done(); - }); - }); - - it('should return 200 with filters', (done) => { + it('should return 200 for admin with filter', (done) => { request(server) .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ @@ -91,15 +63,15 @@ describe('LIST organization config', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 200 even if user is not authenticated with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .expect(200, done); }); - it('should return 200 for connect admin', (done) => { + it('should return 200 for connect admin with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) @@ -107,9 +79,9 @@ describe('LIST organization config', () => { .end(done); }); - it('should return 200 for connect manager', (done) => { + it('should return 200 for connect manager with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) @@ -117,22 +89,34 @@ describe('LIST organization config', () => { .end(done); }); - it('should return 200 for member', (done) => { + it('should return 200 for member with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) .expect(200, done); }); - it('should return 200 for copilot', (done) => { + it('should return 200 for copilot with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .expect(200, done); }); + + it('should return 422 without filter query param', (done) => { + request(server) + .get(`${orgConfigPath}`) + .expect(422, done); + }); + + it('should return 422 with filter query param but without orgId defined', (done) => { + request(server) + .get(`${orgConfigPath}?filter=configName=${configs[0].configName}`) + .expect(422, done); + }); }); }); From 989646204fff72ccac130eeda19a80644bb43f5f Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 7 Feb 2019 16:11:22 +0530 Subject: [PATCH 12/22] Trying to fix the merge of priceConfig. It should just override the complete JSON. --- src/routes/projectTemplates/update.js | 2 +- src/util.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/projectTemplates/update.js b/src/routes/projectTemplates/update.js index 0e048afe..acc7e741 100644 --- a/src/routes/projectTemplates/update.js +++ b/src/routes/projectTemplates/update.js @@ -64,7 +64,7 @@ module.exports = [ } // Merge JSON fields - entityToUpdate.scope = util.mergeJsonObjects(projectTemplate.scope, entityToUpdate.scope); + entityToUpdate.scope = util.mergeJsonObjects(projectTemplate.scope, entityToUpdate.scope, ['priceConfig']); entityToUpdate.phases = util.mergeJsonObjects(projectTemplate.phases, entityToUpdate.phases); // removes null phase templates entityToUpdate.phases = _.omitBy(entityToUpdate.phases, _.isNull); diff --git a/src/util.js b/src/util.js index c5e999c3..05f504b2 100644 --- a/src/util.js +++ b/src/util.js @@ -379,12 +379,13 @@ _.assignIn(util, { * Merge two JSON objects. For array fields, the target will be replaced by source. * @param {Object} targetObj the target object * @param {Object} sourceObj the source object + * @param {Object} mergeExceptions list of keys which should be exempted from merge * @returns {Object} the merged object */ // eslint-disable-next-line consistent-return - mergeJsonObjects: (targetObj, sourceObj) => _.mergeWith(targetObj, sourceObj, (target, source) => { - // Overwrite the array - if (_.isArray(source)) { + mergeJsonObjects: (targetObj, sourceObj, mergeExceptions) => _.mergeWith(targetObj, sourceObj, (target, source, key) => { + // Overwrite the array or merge exception keys + if (_.isArray(source) || (mergeExceptions && mergeExceptions.indexOf(key) !== -1)) { return source; } }), From 2f57df8da06964a4a95169176b2296c71d36cfe5 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 7 Feb 2019 16:27:35 +0530 Subject: [PATCH 13/22] lint fix --- src/util.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/util.js b/src/util.js index 05f504b2..e8d61ef8 100644 --- a/src/util.js +++ b/src/util.js @@ -382,13 +382,14 @@ _.assignIn(util, { * @param {Object} mergeExceptions list of keys which should be exempted from merge * @returns {Object} the merged object */ - // eslint-disable-next-line consistent-return - mergeJsonObjects: (targetObj, sourceObj, mergeExceptions) => _.mergeWith(targetObj, sourceObj, (target, source, key) => { - // Overwrite the array or merge exception keys - if (_.isArray(source) || (mergeExceptions && mergeExceptions.indexOf(key) !== -1)) { - return source; - } - }), + mergeJsonObjects: (targetObj, sourceObj, mergeExceptions) => + // eslint-disable-next-line consistent-return + _.mergeWith(targetObj, sourceObj, (target, source, key) => { + // Overwrite the array or merge exception keys + if (_.isArray(source) || (mergeExceptions && mergeExceptions.indexOf(key) !== -1)) { + return source; + } + }), /** * Add userId to project From b246a4d3f8e92eb23a5c44ca2c1d07fc36593f40 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 15:53:53 +0100 Subject: [PATCH 14/22] attachment persmissions challenge --- .../20190201_userIds_project_attachment.sql | 4 + postman.json | 209 ++++++++++++++++-- src/events/index.js | 4 +- src/models/projectAttachment.js | 28 +++ src/permissions/index.js | 8 +- src/permissions/project.downloadAttachment.js | 36 +++ src/permissions/project.updateAttachment.js | 42 ++++ src/routes/attachments/create.js | 3 + src/routes/attachments/delete.spec.js | 26 ++- src/routes/attachments/download.js | 5 +- src/routes/attachments/download.spec.js | 137 ++++++++++++ src/routes/attachments/update.js | 1 + src/routes/attachments/update.spec.js | 24 +- src/tests/util.js | 8 + src/util.js | 8 +- swagger.yaml | 6 + 16 files changed, 522 insertions(+), 27 deletions(-) create mode 100644 migrations/20190201_userIds_project_attachment.sql create mode 100644 src/permissions/project.downloadAttachment.js create mode 100644 src/permissions/project.updateAttachment.js create mode 100644 src/routes/attachments/download.spec.js diff --git a/migrations/20190201_userIds_project_attachment.sql b/migrations/20190201_userIds_project_attachment.sql new file mode 100644 index 00000000..75b02976 --- /dev/null +++ b/migrations/20190201_userIds_project_attachment.sql @@ -0,0 +1,4 @@ +-- +-- project_attachments +-- +ALTER TABLE project_attachments ADD COLUMN "userIds" integer[]; diff --git a/postman.json b/postman.json index ecd5c408..1df3a669 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "e810fc27-5518-4cc5-8f90-6b1423c6b0b4", + "_postman_id": "97085cd7-b298-4f1c-9629-24af14ff5f13", "name": "tc-project-service", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -278,17 +278,17 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission\",\n\t\t\"filePath\": \"asdjshdasdas/asdsadj/asdasd.png\",\n\t\t\"s3Bucket\": \"topcoder-project-service\",\n\t\t\"contentType\": \"application/png\"\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission\",\n\t\t\"filePath\": \"asdjshdasdas/asdsadj/asdasd.png\",\n\t\t\"s3Bucket\": \"topcoder-project-service\",\n\t\t\"contentType\": \"application/png\",\n\t\t\"userIds\": [40051331]\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/7/attachments", + "raw": "{{api-url}}/v4/projects/1/attachments", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7", + "1", "attachments" ] }, @@ -296,6 +296,111 @@ }, "response": [] }, + { + "name": "Download attachment", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/1/attachments/2", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "attachments", + "2" + ] + }, + "description": "Create an project attachment" + }, + "response": [] + }, + { + "name": "Download attachment admin", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-admin-40051333}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/1/attachments/2", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "attachments", + "2" + ] + }, + "description": "Create an project attachment" + }, + "response": [] + }, + { + "name": "Download attachment - No access", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-copilot-40051332}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/1/attachments/2", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "attachments", + "2" + ] + }, + "description": "Create an project attachment" + }, + "response": [] + }, { "name": "Update attachment", "request": { @@ -315,14 +420,49 @@ "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission updated\",\n\t\t\"description\": \"updated project attachment\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/7/attachments/2", + "raw": "{{api-url}}/v4/projects/1/attachments/2", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7", + "1", + "attachments", + "2" + ] + }, + "description": "Update project attachment" + }, + "response": [] + }, + { + "name": "Update attachment - No access", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-copilot-40051332}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission updated\",\n\t\t\"description\": \"updated project attachment\",\n\t\t\"userIds\": null\n\t}\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/1/attachments/2", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", "attachments", "2" ] @@ -365,6 +505,41 @@ "description": "Delete a project attachment" }, "response": [] + }, + { + "name": "Delete attachment - No access", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-copilot-40051332}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/1/attachments/2", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "attachments", + "2" + ] + }, + "description": "Delete a project attachment" + }, + "response": [] } ] }, @@ -1098,7 +1273,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n\t}\n}" + "raw": "{\n \"param\": {\n \"name\": \"Test 3\",\n \"details\": {\n \"utm\": {\n \"code\": \"\"\n },\n \"appDefinition\": {\n \"primaryTarget\": \"phone\",\n \"goal\": {\n \"value\": \"Nothing\"\n },\n \"users\": {\n \"value\": \"No one\"\n },\n \"notes\": \"\"\n },\n \"hideDiscussions\": true\n },\n \"description\": \"Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message\",\n \"templateId\": 3,\n \"type\": \"app\"\n }\n}" }, "url": { "raw": "{{api-url}}/v4/projects", @@ -2240,17 +2415,17 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t}\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2019-02-15T00:00:00\",\n\t\t\"endDate\": \"2019-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t}\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases", + "raw": "{{api-url}}/v4/projects/2/phases", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", + "2", "phases" ] } @@ -2594,7 +2769,7 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases/3", + "raw": "{{api-url}}/v4/projects/1/phases/2", "host": [ "{{api-url}}" ], @@ -2603,7 +2778,7 @@ "projects", "1", "phases", - "3" + "2" ] } }, @@ -2630,7 +2805,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test phase product\",\n\t\t\"type\": \"type 1\",\n\t\t\"estimatedPrice\": 10\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test phase product\",\n\t\t\"type\": \"application_development\",\n\t\t\"estimatedPrice\": 10000\n\t}\n}" }, "url": { "raw": "{{api-url}}/v4/projects/1/phases/1/products", @@ -3147,7 +3322,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"key\": \"generic\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"key\": \"app\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTypes", @@ -3320,7 +3495,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"key\": \"generic\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"icon\",\r\n \"question\": \"question\",\r\n \"info\": \"info\",\r\n \"aliases\": [\"key-1\", \"key-2\"]\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"key\": \"app\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"icon\",\r\n \"question\": \"question\",\r\n \"info\": \"info\",\r\n \"aliases\": [\"key-1\", \"key-2\"]\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/productCategories", @@ -3663,7 +3838,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2019-02-29T00:00:00.000Z\",\r\n \"endDate\": \"2019-04-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/timelines", @@ -3694,7 +3869,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1,\r\n \"templateId\": 1\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2019-02-29T00:00:00.000Z\",\r\n \"endDate\": \"2019-04-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1,\r\n \"templateId\": 1\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/timelines", diff --git a/src/events/index.js b/src/events/index.js index 806b5b1c..a4d5945f 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -7,7 +7,7 @@ import { projectMemberAddedHandler, projectMemberRemovedHandler, import { projectMemberInviteCreatedHandler, projectMemberInviteUpdatedHandler } from './projectMemberInvites'; import { projectAttachmentRemovedHandler, - projectAttachmentUpdatedHandler } from './projectAttachments'; + projectAttachmentUpdatedHandler, projectAttachmentAddedHandler } from './projectAttachments'; import { projectPhaseAddedHandler, projectPhaseRemovedHandler, projectPhaseUpdatedHandler } from './projectPhases'; import { phaseProductAddedHandler, phaseProductRemovedHandler, @@ -35,7 +35,7 @@ export const rabbitHandlers = { [EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED]: projectMemberUpdatedHandler, [EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED]: projectMemberInviteCreatedHandler, [EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED]: projectMemberInviteUpdatedHandler, - [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED]: projectMemberInviteUpdatedHandler, + [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED]: projectAttachmentAddedHandler, [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_REMOVED]: projectAttachmentRemovedHandler, [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_UPDATED]: projectAttachmentUpdatedHandler, [EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED]: projectPhaseAddedHandler, diff --git a/src/models/projectAttachment.js b/src/models/projectAttachment.js index 9e917b25..4288a9e8 100644 --- a/src/models/projectAttachment.js +++ b/src/models/projectAttachment.js @@ -9,6 +9,7 @@ module.exports = function defineProjectAttachment(sequelize, DataTypes) { description: { type: DataTypes.STRING, allowNull: true }, filePath: { type: DataTypes.STRING, allowNull: false }, contentType: { type: DataTypes.STRING, allowNull: false }, + userIds: DataTypes.ARRAY({ type: DataTypes.INTEGER, allowNull: true }), deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, @@ -33,6 +34,33 @@ module.exports = function defineProjectAttachment(sequelize, DataTypes) { raw: true, }); }, + + getAttachmentById(projectId, attachmentId) { + return this.findOne({ + where: { + projectId, + id: attachmentId, + }, + }); + }, + + getAttachmentsForUser(projectId, userId) { + return this.findAll({ + where: { + projectId, + $or: [{ + createdBy: { $eq: userId }, + }, { + userIds: { + $or: [ + { $contains: [userId] }, + { $eq: null }, + ], + }, + }], + }, + }); + }, }, }); diff --git a/src/permissions/index.js b/src/permissions/index.js index 0b9880e3..7d1b1e86 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -6,6 +6,8 @@ const projectEdit = require('./project.edit'); const projectDelete = require('./project.delete'); const projectMemberDelete = require('./projectMember.delete'); const projectAdmin = require('./admin.ops'); +const projectAttachmentUpdate = require('./project.updateAttachment'); +const projectAttachmentDownload = require('./project.downloadAttachment'); // const connectManagerOrAdmin = require('./connectManagerOrAdmin.ops'); const copilotAndAbove = require('./copilotAndAbove'); @@ -20,9 +22,9 @@ module.exports = () => { Authorizer.setPolicy('project.addMember', projectView); Authorizer.setPolicy('project.removeMember', projectMemberDelete); Authorizer.setPolicy('project.addAttachment', projectEdit); - Authorizer.setPolicy('project.updateAttachment', projectEdit); - Authorizer.setPolicy('project.removeAttachment', projectEdit); - Authorizer.setPolicy('project.downloadAttachment', projectView); + Authorizer.setPolicy('project.updateAttachment', projectAttachmentUpdate); + Authorizer.setPolicy('project.removeAttachment', projectAttachmentUpdate); + Authorizer.setPolicy('project.downloadAttachment', projectAttachmentDownload); Authorizer.setPolicy('project.updateMember', projectEdit); Authorizer.setPolicy('project.admin', projectAdmin); diff --git a/src/permissions/project.downloadAttachment.js b/src/permissions/project.downloadAttachment.js new file mode 100644 index 00000000..b8f4ebcb --- /dev/null +++ b/src/permissions/project.downloadAttachment.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import util from '../util'; +import models from '../models'; + +/** + * Connect admin and Topcoder admins are allowed to download any project attachments + * Rest can update attachments that they created or given access + * @param {Object} freq the express request instance + * @return {Promise} Returns a promise + */ +module.exports = freq => new Promise((resolve, reject) => { + const projectId = _.parseInt(freq.params.projectId); + const attachmentId = _.parseInt(freq.params.id); + const userId = freq.authUser.userId; + + if (util.hasAdminRole(freq)) { + return resolve(true); + } + return models.ProjectAttachment.getAttachmentById(projectId, attachmentId) + .then((attachment) => { + const req = freq; + req.context = req.context || {}; + req.context.existingAttachment = attachment; + + // deligate not found to the actual handler + if (!attachment) { + return resolve(true); + } + + if (attachment.createdBy === userId || attachment.userIds === null || attachment.userIds.indexOf(userId) >= 0) { + return resolve(true); + } + + return reject(new Error('You\'re not allowed to download')); + }); +}); diff --git a/src/permissions/project.updateAttachment.js b/src/permissions/project.updateAttachment.js new file mode 100644 index 00000000..5441a9f5 --- /dev/null +++ b/src/permissions/project.updateAttachment.js @@ -0,0 +1,42 @@ +import _ from 'lodash'; +import util from '../util'; +import models from '../models'; + +/** + * Connect admin and Topcoder admins are allowed to update any project attachments + * Rest can update attachments that they created + * @param {Object} freq the express request instance + * @return {Promise} Returns a promise + */ +module.exports = freq => new Promise((resolve, reject) => { + const projectId = _.parseInt(freq.params.projectId); + const attachmentId = _.parseInt(freq.params.id); + + freq.log.debug('Hello'); + + if (util.hasAdminRole(freq)) { + freq.log.debug('Has Admin Role!'); + return resolve(true); + } + + freq.log.debug('Hello Hello Hello'); + + return models.ProjectAttachment.getAttachmentById(projectId, attachmentId) + .then((attachment) => { + freq.log.debug('Hello Hello'); + const req = freq; + req.context = req.context || {}; + req.context.existingAttachment = attachment; + + // deligate not found to the actual handler + if (!attachment) { + return resolve(true); + } + + if (attachment.createdBy === req.authUser.userId) { + return resolve(true); + } + + return reject(new Error('Only admins and the user that uploaded the docs can modify')); + }); +}); diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js index 5aa5ed2f..7a2401e4 100644 --- a/src/routes/attachments/create.js +++ b/src/routes/attachments/create.js @@ -25,6 +25,7 @@ const addAttachmentValidations = { filePath: Joi.string().required(), s3Bucket: Joi.string().required(), contentType: Joi.string().required(), + userIds: Joi.array(Joi.number().integer().positive()).allow(null).default(null), }).required(), }, }; @@ -41,6 +42,7 @@ module.exports = [ const data = req.body.param; // default values const projectId = req.params.projectId; + const userIds = data.userIds; _.assign(data, { projectId, createdBy: req.authUser.userId, @@ -98,6 +100,7 @@ module.exports = [ req.log.debug('creating db record'); return models.ProjectAttachment.create({ projectId, + userIds, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, title: data.title, diff --git a/src/routes/attachments/delete.spec.js b/src/routes/attachments/delete.spec.js index b9366d2d..bf73d585 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -48,7 +48,7 @@ describe('Project Attachments delete', () => { size: 12312, category: null, filePath: 'https://media.topcoder.com/projects/1/test.txt', - createdBy: 1, + createdBy: testUtil.userIds.copilot, updatedBy: 1, }).then((a1) => { attachment = a1; @@ -91,7 +91,7 @@ describe('Project Attachments delete', () => { .expect(404, done); }); - it('should return 204 if attachment was successfully removed', (done) => { + it('should return 204 if the CREATOR removes the attachment successfully', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { delete: () => Promise.resolve({ status: 200, @@ -147,6 +147,28 @@ describe('Project Attachments delete', () => { }); }); + it('should return 204 if ADMIN deletes the attachment successfully', (done) => { + request(server) + .delete(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: { userId: 1, projectId: project1.id, role: 'customer' } }) + .expect(204, done) + .end((err) => { + if (err) { + done(err); + } else { + request(server) + .get(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + } + }); + }); + describe('Bus api', () => { let createEventSpy; diff --git a/src/routes/attachments/download.js b/src/routes/attachments/download.js index f226d520..ddbe4595 100644 --- a/src/routes/attachments/download.js +++ b/src/routes/attachments/download.js @@ -1,5 +1,5 @@ - import _ from 'lodash'; +import config from 'config'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; @@ -30,6 +30,9 @@ module.exports = [ err.status = 404; return Promise.reject(err); } + if (process.env.NODE_ENV === 'development' || config.get('enableFileUpload') === false) { + return ['dummy://url']; + } return util.getFileDownloadUrl(req, attachment.filePath); }) .then((result) => { diff --git a/src/routes/attachments/download.spec.js b/src/routes/attachments/download.spec.js new file mode 100644 index 00000000..7119de1e --- /dev/null +++ b/src/routes/attachments/download.spec.js @@ -0,0 +1,137 @@ +/* eslint-disable no-unused-expressions */ +import sinon from 'sinon'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; +import util from '../../util'; + +describe('Project Attachments download', () => { + let project1; + let attachment; + let getFileDownloadUrlStub; + + before(() => { + getFileDownloadUrlStub = sinon.stub(util, 'getFileDownloadUrl'); + getFileDownloadUrlStub.returns(['dummy://url']); + }); + + beforeEach((done) => { + testUtil.clearDb() + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + project1 = p; + // create members + return models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }).then(() => models.ProjectAttachment.create({ + projectId: project1.id, + title: 'test.txt', + description: 'blah', + contentType: 'application/unknown', + size: 12312, + category: null, + filePath: 'https://media.topcoder.com/projects/1/test.txt', + createdBy: testUtil.userIds.copilot, + updatedBy: 1, + userIds: [testUtil.userIds.member], + }).then((a1) => { + attachment = a1; + done(); + })); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('Download /projects/{id}/attachments/{id}', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return 403 if USER does not have permissions', (done) => { + request(server) + .get(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .send() + .expect(403, done); + }); + + it('should return 403 if MANAGER does not have permissions', (done) => { + request(server) + .get(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send() + .expect(403, done); + }); + + it('should return 404 if attachment was not found', (done) => { + request(server) + .get(`/v4/projects/${project1.id}/attachments/8888888`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send() + .expect(404, done); + }); + + it('should return 200 when the CREATOR can download attachment successfully', (done) => { + request(server) + .get(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send() + .expect(200, done); + }); + + it('should return 200 when the USER with permission can download attachment successfully', (done) => { + request(server) + .get(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send() + .expect(200, done); + }); + + it('should return 200 when ADMIN can download attachment successfully', (done) => { + request(server) + .get(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send() + .expect(200, done); + }); + }); +}); diff --git a/src/routes/attachments/update.js b/src/routes/attachments/update.js index 9a67a86a..fea494ec 100644 --- a/src/routes/attachments/update.js +++ b/src/routes/attachments/update.js @@ -19,6 +19,7 @@ const updateProjectAttachmentValidation = { param: Joi.object().keys({ title: Joi.string().required(), description: Joi.string().optional().allow(null).allow(''), + userIds: Joi.array(Joi.number().integer().positive()).allow(null).default(null), }), }, }; diff --git a/src/routes/attachments/update.spec.js b/src/routes/attachments/update.spec.js index 99dd0b15..e8f5b8da 100644 --- a/src/routes/attachments/update.spec.js +++ b/src/routes/attachments/update.spec.js @@ -47,8 +47,9 @@ describe('Project Attachments update', () => { size: 12312, category: null, filePath: 'https://media.topcoder.com/projects/1/test.txt', - createdBy: 1, + createdBy: testUtil.userIds.copilot, updatedBy: 1, + userIds: [], }).then((a1) => { attachment = a1; done(); @@ -111,6 +112,27 @@ describe('Project Attachments update', () => { }); }); + it('should return 200 if admin updates the attachment', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: { title: 'updated title 1', description: 'updated description 1' } }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.title.should.equal('updated title 1'); + resJson.description.should.equal('updated description 1'); + done(); + } + }); + }); + describe('Bus api', () => { let createEventSpy; diff --git a/src/tests/util.js b/src/tests/util.js index 232c553d..3031dd1b 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -27,6 +27,14 @@ export default { // userId = 40051336, [ 'Connect Admin' ], handle: 'connect_admin1', email: 'connect_admin1@topcoder.com' connectAdmin: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30', }, + userIds: { + member: 40051331, + copilot: 40051332, + admin: 40051333, + manager: 40051334, + member2: 40051335, + connectAdmin: 40051336, + }, getDecodedToken: token => jwt.decode(token), // Waits for 500ms and executes cb function diff --git a/src/util.js b/src/util.js index c5e999c3..dab342d6 100644 --- a/src/util.js +++ b/src/util.js @@ -245,7 +245,13 @@ _.assignIn(util, { }, getProjectAttachments: (req, projectId) => { let attachments = []; - return models.ProjectAttachment.getActiveProjectAttachments(projectId) + let attachmentsPromise; + if (util.hasAdminRole(req)) { + attachmentsPromise = models.ProjectAttachment.getActiveProjectAttachments(projectId); + } else { + attachmentsPromise = models.ProjectAttachment.getAttachmentsForUser(projectId, req.authUser.userId); + } + return attachmentsPromise .then((_attachments) => { // if attachments were requested if (_attachments) { diff --git a/swagger.yaml b/swagger.yaml index b547e16e..ee012c74 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -2209,6 +2209,12 @@ definitions: type: number format: float description: The size of attachment + userIds: + type: array + items: + type: integer + format: int64 + description: Users allowed to access the attachment NewProjectAttachmentBodyParam: type: object From 3f88ca1ed63e79d0a158dbef76a89d466d5d3da0 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 15:57:12 +0100 Subject: [PATCH 15/22] remove debug code --- src/permissions/project.updateAttachment.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/permissions/project.updateAttachment.js b/src/permissions/project.updateAttachment.js index 5441a9f5..0fe4f182 100644 --- a/src/permissions/project.updateAttachment.js +++ b/src/permissions/project.updateAttachment.js @@ -12,18 +12,12 @@ module.exports = freq => new Promise((resolve, reject) => { const projectId = _.parseInt(freq.params.projectId); const attachmentId = _.parseInt(freq.params.id); - freq.log.debug('Hello'); - if (util.hasAdminRole(freq)) { - freq.log.debug('Has Admin Role!'); return resolve(true); } - freq.log.debug('Hello Hello Hello'); - return models.ProjectAttachment.getAttachmentById(projectId, attachmentId) .then((attachment) => { - freq.log.debug('Hello Hello'); const req = freq; req.context = req.context || {}; req.context.existingAttachment = attachment; From c01c0c489f391d16a57c70f541059b82881ecb22 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 17:47:13 +0100 Subject: [PATCH 16/22] rename userIds field to allowedUsers --- migrations/20190201_userIds_project_attachment.sql | 2 +- src/models/projectAttachment.js | 4 ++-- src/permissions/project.downloadAttachment.js | 3 ++- src/routes/attachments/create.js | 6 +++--- src/routes/attachments/delete.spec.js | 2 +- src/routes/attachments/download.spec.js | 2 +- src/routes/attachments/update.js | 2 +- src/routes/attachments/update.spec.js | 2 +- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/migrations/20190201_userIds_project_attachment.sql b/migrations/20190201_userIds_project_attachment.sql index 75b02976..657b09a3 100644 --- a/migrations/20190201_userIds_project_attachment.sql +++ b/migrations/20190201_userIds_project_attachment.sql @@ -1,4 +1,4 @@ -- -- project_attachments -- -ALTER TABLE project_attachments ADD COLUMN "userIds" integer[]; +ALTER TABLE project_attachments ADD COLUMN "allowedUsers" integer[]; diff --git a/src/models/projectAttachment.js b/src/models/projectAttachment.js index 4288a9e8..53043b32 100644 --- a/src/models/projectAttachment.js +++ b/src/models/projectAttachment.js @@ -9,7 +9,7 @@ module.exports = function defineProjectAttachment(sequelize, DataTypes) { description: { type: DataTypes.STRING, allowNull: true }, filePath: { type: DataTypes.STRING, allowNull: false }, contentType: { type: DataTypes.STRING, allowNull: false }, - userIds: DataTypes.ARRAY({ type: DataTypes.INTEGER, allowNull: true }), + allowedUsers: DataTypes.ARRAY({ type: DataTypes.INTEGER, allowNull: true }), deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, @@ -51,7 +51,7 @@ module.exports = function defineProjectAttachment(sequelize, DataTypes) { $or: [{ createdBy: { $eq: userId }, }, { - userIds: { + allowedUsers: { $or: [ { $contains: [userId] }, { $eq: null }, diff --git a/src/permissions/project.downloadAttachment.js b/src/permissions/project.downloadAttachment.js index b8f4ebcb..8c986d19 100644 --- a/src/permissions/project.downloadAttachment.js +++ b/src/permissions/project.downloadAttachment.js @@ -27,7 +27,8 @@ module.exports = freq => new Promise((resolve, reject) => { return resolve(true); } - if (attachment.createdBy === userId || attachment.userIds === null || attachment.userIds.indexOf(userId) >= 0) { + if (attachment.createdBy === userId || attachment.allowedUsers === null || + attachment.allowedUsers.indexOf(userId) >= 0) { return resolve(true); } diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js index 7a2401e4..23a3ad9b 100644 --- a/src/routes/attachments/create.js +++ b/src/routes/attachments/create.js @@ -25,7 +25,7 @@ const addAttachmentValidations = { filePath: Joi.string().required(), s3Bucket: Joi.string().required(), contentType: Joi.string().required(), - userIds: Joi.array(Joi.number().integer().positive()).allow(null).default(null), + allowedUsers: Joi.array(Joi.number().integer().positive()).allow(null).default(null), }).required(), }, }; @@ -42,7 +42,7 @@ module.exports = [ const data = req.body.param; // default values const projectId = req.params.projectId; - const userIds = data.userIds; + const allowedUsers = data.allowedUsers; _.assign(data, { projectId, createdBy: req.authUser.userId, @@ -100,7 +100,7 @@ module.exports = [ req.log.debug('creating db record'); return models.ProjectAttachment.create({ projectId, - userIds, + allowedUsers, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, title: data.title, diff --git a/src/routes/attachments/delete.spec.js b/src/routes/attachments/delete.spec.js index bf73d585..a3045ed0 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -48,7 +48,7 @@ describe('Project Attachments delete', () => { size: 12312, category: null, filePath: 'https://media.topcoder.com/projects/1/test.txt', - createdBy: testUtil.userIds.copilot, + createdBy: testUtil.allowedUsers.copilot, updatedBy: 1, }).then((a1) => { attachment = a1; diff --git a/src/routes/attachments/download.spec.js b/src/routes/attachments/download.spec.js index 7119de1e..8cfc80cb 100644 --- a/src/routes/attachments/download.spec.js +++ b/src/routes/attachments/download.spec.js @@ -52,7 +52,7 @@ describe('Project Attachments download', () => { filePath: 'https://media.topcoder.com/projects/1/test.txt', createdBy: testUtil.userIds.copilot, updatedBy: 1, - userIds: [testUtil.userIds.member], + allowedUsers: [testUtil.userIds.member], }).then((a1) => { attachment = a1; done(); diff --git a/src/routes/attachments/update.js b/src/routes/attachments/update.js index fea494ec..0bf90738 100644 --- a/src/routes/attachments/update.js +++ b/src/routes/attachments/update.js @@ -19,7 +19,7 @@ const updateProjectAttachmentValidation = { param: Joi.object().keys({ title: Joi.string().required(), description: Joi.string().optional().allow(null).allow(''), - userIds: Joi.array(Joi.number().integer().positive()).allow(null).default(null), + allowedUsers: Joi.array(Joi.number().integer().positive()).allow(null).default(null), }), }, }; diff --git a/src/routes/attachments/update.spec.js b/src/routes/attachments/update.spec.js index e8f5b8da..0796e22f 100644 --- a/src/routes/attachments/update.spec.js +++ b/src/routes/attachments/update.spec.js @@ -49,7 +49,7 @@ describe('Project Attachments update', () => { filePath: 'https://media.topcoder.com/projects/1/test.txt', createdBy: testUtil.userIds.copilot, updatedBy: 1, - userIds: [], + allowedUsers: [], }).then((a1) => { attachment = a1; done(); From a42212f3f7b793353a5fb616ae3cb42fbdd16c92 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 17:48:16 +0100 Subject: [PATCH 17/22] deploy feature branch --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 459ec723..a0c8e104 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,7 +76,7 @@ workflows: - test filters: branches: - only: ['dev'] + only: ['dev', 'feature/attachmentPermissions'] - deployProd: requires: - test From 4b5f7fedc787949e0d4b76868afe7ff9602adb66 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 18:19:34 +0100 Subject: [PATCH 18/22] fix test --- src/routes/attachments/delete.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/attachments/delete.spec.js b/src/routes/attachments/delete.spec.js index a3045ed0..bf73d585 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -48,7 +48,7 @@ describe('Project Attachments delete', () => { size: 12312, category: null, filePath: 'https://media.topcoder.com/projects/1/test.txt', - createdBy: testUtil.allowedUsers.copilot, + createdBy: testUtil.userIds.copilot, updatedBy: 1, }).then((a1) => { attachment = a1; From ffe159d1e2e5354f8da4ff9f032e21ac58d0448c Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 18:22:50 +0100 Subject: [PATCH 19/22] update swagger --- swagger.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swagger.yaml b/swagger.yaml index f122463c..2d7e7c8b 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -2346,7 +2346,7 @@ definitions: type: number format: float description: The size of attachment - userIds: + allowedUsers: type: array items: type: integer From 17a8bb8b32c062bf773f171320bcbf0ba801aec3 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 18:24:33 +0100 Subject: [PATCH 20/22] update postman --- postman.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman.json b/postman.json index d9bc9163..999689af 100644 --- a/postman.json +++ b/postman.json @@ -278,7 +278,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission\",\n\t\t\"filePath\": \"asdjshdasdas/asdsadj/asdasd.png\",\n\t\t\"s3Bucket\": \"topcoder-project-service\",\n\t\t\"contentType\": \"application/png\",\n\t\t\"userIds\": [40051331]\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission\",\n\t\t\"filePath\": \"asdjshdasdas/asdsadj/asdasd.png\",\n\t\t\"s3Bucket\": \"topcoder-project-service\",\n\t\t\"contentType\": \"application/png\",\n\t\t\"allowedUsers\": [40051331]\n\t}\n}" }, "url": { "raw": "{{api-url}}/v4/projects/1/attachments", @@ -452,7 +452,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission updated\",\n\t\t\"description\": \"updated project attachment\",\n\t\t\"userIds\": null\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission updated\",\n\t\t\"description\": \"updated project attachment\",\n\t\t\"allowedUsers\": null\n\t}\n}" }, "url": { "raw": "{{api-url}}/v4/projects/1/attachments/2", From 0782c7c715bd3c8ef57a1bf1403ade162e474094 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 20:01:22 +0100 Subject: [PATCH 21/22] update bus api events --- src/events/busApi.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/events/busApi.js b/src/events/busApi.js index 495656a9..b3282b32 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -252,6 +252,7 @@ module.exports = (app, logger) => { projectUrl: connectProjectUrl(projectId), fileName: attachment.filePath.replace(/^.*[\\\/]/, ''), // eslint-disable-line fileUrl: connectProjectAttachmentUrl(projectId, attachment.id), + allowedUsers: attachment.allowedUsers, userId: req.authUser.userId, initiatorUserId: req.authUser.userId, }, logger); From dfc63f19ebaeeb69753e687b3084df5fa42a515e Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 21:01:53 +0100 Subject: [PATCH 22/22] limit draft phase update events to topcoder project members --- src/events/busApi.js | 9 +++++++++ src/util.js | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/events/busApi.js b/src/events/busApi.js index 495656a9..98af2a0c 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -4,6 +4,7 @@ import { EVENT, BUS_API_EVENT, PROJECT_STATUS, PROJECT_PHASE_STATUS, PROJECT_MEM from '../constants'; import { createEvent } from '../services/busApi'; import models from '../models'; +import getTopcoderProjectMembers from '../util'; /** * Map of project status and event name sent to bus api @@ -363,6 +364,8 @@ module.exports = (app, logger) => { projectUrl: connectProjectUrl(projectId), userId: req.authUser.userId, initiatorUserId: req.authUser.userId, + allowedUsers: created.status === PROJECT_PHASE_STATUS.DRAFT ? + getTopcoderProjectMembers(project.members) : null, }, logger); return sendPlanReadyEventIfNeeded(req, project, created); }).catch(err => null); // eslint-disable-line no-unused-vars @@ -387,6 +390,8 @@ module.exports = (app, logger) => { projectUrl: connectProjectUrl(projectId), userId: req.authUser.userId, initiatorUserId: req.authUser.userId, + allowedUsers: deleted.status === PROJECT_PHASE_STATUS.DRAFT ? + getTopcoderProjectMembers(project.members) : null, }, logger); }).catch(err => null); // eslint-disable-line no-unused-vars }); @@ -438,6 +443,8 @@ module.exports = (app, logger) => { projectName: project.name, userId: req.authUser.userId, initiatorUserId: req.authUser.userId, + allowedUsers: updated.status === PROJECT_PHASE_STATUS.DRAFT ? + getTopcoderProjectMembers(project.members) : null, }, logger)); events.forEach((event) => { eventsMap[event] = true; }); } @@ -483,6 +490,8 @@ module.exports = (app, logger) => { projectUrl: connectProjectUrl(projectId), userId: req.authUser.userId, initiatorUserId: req.authUser.userId, + allowedUsers: updated.status === PROJECT_PHASE_STATUS.DRAFT ? + getTopcoderProjectMembers(project.members) : null, }, logger); } }).catch(err => null); // eslint-disable-line no-unused-vars diff --git a/src/util.js b/src/util.js index e8d61ef8..af4f5202 100644 --- a/src/util.js +++ b/src/util.js @@ -18,7 +18,7 @@ import elasticsearch from 'elasticsearch'; import Promise from 'bluebird'; // import AWS from 'aws-sdk'; -import { ADMIN_ROLES, TOKEN_SCOPES, EVENT } from './constants'; +import { ADMIN_ROLES, TOKEN_SCOPES, EVENT, PROJECT_MEMBER_ROLE } from './constants'; const exec = require('child_process').exec; const models = require('./models').default; @@ -468,6 +468,13 @@ _.assignIn(util, { }); }); }, + + /** + * Filter only members of topcoder team + * @param {Array} members project members + * @return {Array} tpcoder project members + */ + getTopcoderProjectMembers: members => _(members).filter(m => m.role !== PROJECT_MEMBER_ROLE.CUSTOMER), }); export default util;