diff --git a/.github/workflows/test-http.yml b/.github/workflows/test-http.yml index 1a3d66788..5f56c1b09 100644 --- a/.github/workflows/test-http.yml +++ b/.github/workflows/test-http.yml @@ -14,22 +14,22 @@ jobs: - name: Build and Run Services and Mongo Containers run: | cp docker/.docker-env.example docker/.docker-env - docker-compose --file docker/docker-compose.yml build - docker-compose --file docker/docker-compose.yml up -d + docker compose --file docker/docker-compose.yml build + docker compose --file docker/docker-compose.yml up -d - name: Build Test Container run: | cp test-http/docker/.docker-env.example test-http/docker/.docker-env - docker-compose --file test-http/docker/docker-compose.yml build + docker compose --file test-http/docker/docker-compose.yml build - name: Run Test Container run: | - docker-compose --file test-http/docker/docker-compose.yml up -d + docker compose --file test-http/docker/docker-compose.yml up -d - name: Sleep - run: bash -c "while ! docker-compose --file docker/docker-compose.yml logs --tail=10 cveawg | grep -q 'Serving on port'; do sleep 1; done" + run: bash -c "while ! docker compose --file docker/docker-compose.yml logs --tail=10 cveawg | grep -q 'Serving on port'; do sleep 1; done" - name: Load Data into MongoDb - run: docker-compose -f docker/docker-compose.yml exec -T cveawg npm run populate:dev y + run: docker compose -f docker/docker-compose.yml exec -T cveawg npm run populate:dev y - name: Run Black Box Tests run: | - docker-compose --file test-http/docker/docker-compose.yml exec -T demon pytest src/ | tee test-http/src/testOutput.txt + docker compose --file test-http/docker/docker-compose.yml exec -T demon pytest src/ | tee test-http/src/testOutput.txt - name: Archive Test Results uses: actions/upload-artifact@v2 with: @@ -37,7 +37,7 @@ jobs: path: test-http/src/testOutput.txt retention-days: 1 - name: Extract Tests Results - run: docker-compose --file test-http/docker/docker-compose.yml exec -T demon /bin/bash src/parse_tests_output.sh | (read foo; echo "##[set-output name=result;]$(echo $foo)") + run: docker compose --file test-http/docker/docker-compose.yml exec -T demon /bin/bash src/parse_tests_output.sh | (read foo; echo "##[set-output name=result;]$(echo $foo)") id: tests_result - name: Evaluate Tests Results if: ${{ steps.tests_result.outputs.result == 'Tests failed' }} diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index b7569a79c..1f5efbd64 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -14,10 +14,10 @@ jobs: - name: Build and Run Services and Mongo Containers run: | cp docker/.docker-env.test-example docker/.docker-env - docker-compose --file docker/docker-compose.yml build - docker-compose --file docker/docker-compose.yml up -d + docker compose --file docker/docker-compose.yml build + docker compose --file docker/docker-compose.yml up -d - name: Sleep - run: bash -c "while ! docker-compose --file docker/docker-compose.yml logs --tail=10 cveawg | grep -q 'Serving on port'; do sleep 1; done" + run: bash -c "while ! docker compose --file docker/docker-compose.yml logs --tail=10 cveawg | grep -q 'Serving on port'; do sleep 1; done" - name: Run Tests - run: docker-compose -f docker/docker-compose.yml exec -T cveawg npm run test:integration + run: docker compose -f docker/docker-compose.yml exec -T cveawg npm run test:integration continue-on-error: false \ No newline at end of file diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 1a7ebad46..1fb6cb8fe 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,9 +1,9 @@ { "openapi": "3.0.2", "info": { - "version": "2.3.3", + "version": "2.4.0", "title": "CVE Services API", - "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:
CVE data is to be in the JSON 5.1 CVE Record format. Details of the JSON 5.1 schema are located here.
Contact the CVE Services team", + "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:CVE data is to be in the JSON 5.1 CVE Record format. Details of the JSON 5.1 schema are located here.
Contact the CVE Services team", "contact": { "name": "CVE Services Overview", "url": "https://cveproject.github.io/automation-cve-services#services-overview" @@ -2099,7 +2099,7 @@ "Organization" ], "summary": "Updates information about the organization specified by short name (accessible to Secretariat)", - "description": "User must belong to an organization with the Secretariat role
Secretariat: Updates any organization's information
", + "description": "User must belong to an organization with the Secretariat role, or user must belong to the organization specified by short name
Secretariat: Updates any organization's information
Non-secretariat: Updates 'last_active' timestamp to show that an org is still active
", "operationId": "orgUpdateSingle", "parameters": [ { @@ -2142,7 +2142,14 @@ "content": { "application/json": { "schema": { - "$ref": "../schemas/org/update-org-response.json" + "oneOf": [ + { + "$ref": "../schemas/org/update-org-response.json" + }, + { + "$ref": "../schemas/org/am-i-alive-response.json" + } + ] } } } diff --git a/package-lock.json b/package-lock.json index ac792daf1..f061baaba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.3.3", + "version": "2.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.3.3", + "version": "2.4.0", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", diff --git a/schemas/org/am-i-alive-response.json b/schemas/org/am-i-alive-response.json new file mode 100644 index 000000000..b9b3d55b0 --- /dev/null +++ b/schemas/org/am-i-alive-response.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Success description" + }, + "updated": { + "type": "object", + "properties": { + "last_active": { + "type": "string", + "format": "date-time", + "description": "The time the organization was last active." + } + } + } + } + } \ No newline at end of file diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 15a160a62..2c9279835 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -245,9 +245,10 @@ router.put('/org/:shortname', #swagger.summary = "Updates information about the organization specified by short name (accessible to Secretariat)" #swagger.description = "User must belong to an organization with the Secretariat role
+User must belong to an organization with the Secretariat role, or user must belong to the organization specified by short name
Secretariat: Updates any organization's information
" +Secretariat: Updates any organization's information
+Non-secretariat: Updates 'last_active' timestamp to show that an org is still active
" #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } #swagger.parameters['$ref'] = [ '#/components/parameters/id_quota', @@ -263,7 +264,12 @@ router.put('/org/:shortname', description: 'Returns information about the organization updated', content: { "application/json": { - schema: { $ref: '../schemas/org/update-org-response.json' } + schema: { + oneOf: [ + { $ref: '../schemas/org/update-org-response.json' }, + { $ref: '../schemas/org/am-i-alive-response.json' } + ] + } } } } @@ -309,10 +315,10 @@ router.put('/org/:shortname', } */ mw.validateUser, - mw.onlySecretariat, + param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + mw.validateOrg, query().custom((query) => { return mw.validateQueryParameterNames(query, ['new_short_name', 'id_quota', 'name', 'active_roles.add', 'active_roles.remove']) }), query(['new_short_name', 'id_quota', 'name', 'active_roles.add', 'active_roles.remove']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), query(['new_short_name']).optional().isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), query(['id_quota']).optional().not().isArray().isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }).withMessage(errorMsgs.ID_QUOTA), query(['name']).optional().isString().trim().notEmpty(), diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index d1a094b57..7803da44c 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -329,6 +329,7 @@ async function updateOrg (req, res, next) { const addRoles = [] const orgRepo = req.ctx.repositories.getOrgRepository() const org = await orgRepo.findOneByShortName(shortName) + const orgMakingChanges = req.ctx.org let agt = setAggregateOrgObj({ short_name: shortName }) // org doesn't exist @@ -337,30 +338,38 @@ async function updateOrg (req, res, next) { return res.status(404).json(error.orgDnePathParam(shortName)) } - Object.keys(req.ctx.query).forEach(k => { - const key = k.toLowerCase() - - if (key === 'new_short_name') { - newOrg.short_name = req.ctx.query.new_short_name - agt = setAggregateOrgObj({ short_name: newOrg.short_name }) - } else if (key === 'name') { - newOrg.name = req.ctx.query.name - } else if (key === 'id_quota') { - newOrg.policies.id_quota = req.ctx.query.id_quota - } else if (key === 'active_roles.add') { - if (Array.isArray(req.ctx.query['active_roles.add'])) { - req.ctx.query['active_roles.add'].forEach(r => { - addRoles.push(r) - }) - } - } else if (key === 'active_roles.remove') { - if (Array.isArray(req.ctx.query['active_roles.remove'])) { - req.ctx.query['active_roles.remove'].forEach(r => { - removeRoles.push(r) - }) + const isSec = await orgRepo.isSecretariat(orgMakingChanges) + + if (isSec) { + Object.keys(req.ctx.query).forEach(k => { + const key = k.toLowerCase() + + if (key === 'new_short_name') { + newOrg.short_name = req.ctx.query.new_short_name + agt = setAggregateOrgObj({ short_name: newOrg.short_name }) + } else if (key === 'name') { + newOrg.name = req.ctx.query.name + } else if (key === 'id_quota') { + newOrg.policies.id_quota = req.ctx.query.id_quota + } else if (key === 'active_roles.add') { + if (Array.isArray(req.ctx.query['active_roles.add'])) { + req.ctx.query['active_roles.add'].forEach(r => { + addRoles.push(r) + }) + } + } else if (key === 'active_roles.remove') { + if (Array.isArray(req.ctx.query['active_roles.remove'])) { + req.ctx.query['active_roles.remove'].forEach(r => { + removeRoles.push(r) + }) + } } - } - }) + }) + } + + if (shortName === orgMakingChanges) { + newOrg.last_active = Date.now() + } // updating the org's roles if (org) { @@ -403,6 +412,13 @@ async function updateOrg (req, res, next) { result = await orgRepo.aggregate(agt) result = result.length > 0 ? result[0] : null + if (!isSec) { + if (!result || !result.last_active) { + return res.status(500).json(error.serverError()) + } + result = { last_active: result.last_active } + } + const responseMessage = { message: shortName + ' organization was successfully updated.', updated: result @@ -819,7 +835,8 @@ function setAggregateOrgObj (query) { name: true, 'authority.active_roles': true, 'policies.id_quota': true, - time: true + time: true, + last_active: true } } ] diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index ed4972195..35e505103 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -135,6 +135,32 @@ async function validateUser (req, res, next) { } } +async function validateOrg (req, res, next) { + const org = req.ctx.org + const reqOrg = req.params.shortname + const orgRepo = req.ctx.repositories.getOrgRepository() + const CONSTANTS = getConstants() + + try { + logger.info({ uuid: req.ctx.uuid, message: 'Authenticating org: ' + org }) + + const isSec = await orgRepo.isSecretariat(org) + if (!isSec) { + if (org !== reqOrg) { + logger.info({ uuid: req.ctx.uuid, message: org + ' is not a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT + ' or the same as ' + reqOrg + ' and is not allowed to make these changes.' }) + return res.status(403).json(error.secretariatOnly()) + } else if (Object.keys(req.query).length > 0) { + return res.status(403).json(error.secretariatOnly()) + } + } + + logger.info({ uuid: req.ctx.uuid, message: 'Confirmed ' + org + ' has the authority to make changes to ' + reqOrg }) + next() + } catch (err) { + next(err) + } +} + // Checks that the requester belongs to an org that has the 'BULK_DOWNLOAD' role async function onlySecretariatOrBulkDownload (req, res, next) { const org = req.ctx.org @@ -483,6 +509,7 @@ module.exports = { setCacheControl, optionallyValidateUser, validateUser, + validateOrg, onlySecretariat, onlySecretariatOrBulkDownload, onlySecretariatOrAdmin, diff --git a/src/model/org.js b/src/model/org.js index 48f3b226c..2c0964dc1 100644 --- a/src/model/org.js +++ b/src/model/org.js @@ -24,7 +24,8 @@ const schema = { created: Date, modified: Date }, - inUse: Boolean + inUse: Boolean, + last_active: Date } const OrgSchema = new mongoose.Schema(schema, { collection: 'Org', timestamps: { createdAt: 'time.created', updatedAt: 'time.modified' } }) diff --git a/src/swagger.js b/src/swagger.js index 4f77c388f..c9ea656a1 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -18,7 +18,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re /* eslint-disable no-multi-str */ const doc = { info: { - version: '2.3.3', + version: '2.4.0', title: 'CVE Services API', description: "The CVE Services API supports automation tooling for the CVE Program. Credentials are \ required for most service endpoints. Representatives of \ @@ -34,7 +34,7 @@ const doc = { or MITRE) to request credentials \ \CVE data is to be in the JSON 5.1 CVE Record format. Details of the JSON 5.1 schema are \ - located here.
\ + located here.\ Contact the CVE Services team", contact: { name: 'CVE Services Overview', diff --git a/test/integration-tests/org/putOrgTest.js b/test/integration-tests/org/putOrgTest.js new file mode 100644 index 000000000..28c3a0625 --- /dev/null +++ b/test/integration-tests/org/putOrgTest.js @@ -0,0 +1,201 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +chai.use(require('chai-http')) +const expect = chai.expect + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const params = { name: 'Test Organization', id_quota: 100 } +const secretariatParams = { name: 'MITRE Corporation', id_quota: 100000 } +const cnaParams = { name: 'Adams, Nielsen and Hensley', id_quota: 1309 } + +describe('Testing org put endpoint', () => { + context('Positive Tests', () => { + it('Allows update made by a secretariat to itself', async () => { + await chai.request(app) + .put('/api/org/mitre') + .set({ ...constants.headers }) + .query(params) + .send() + .then((res, err) => { + expect(res).to.have.status(200) + expect(res.body.updated.name).to.equal(params.name) + expect(res.body.updated.policies.id_quota).to.equal(params.id_quota) + expect(err).to.be.undefined + }) + await chai.request(app) + .put('/api/org/mitre') + .set({ ...constants.headers }) + .query(secretariatParams) + .send() + .then((res, err) => { + expect(res).to.have.status(200) + expect(res.body.updated.name).to.equal(secretariatParams.name) + expect(res.body.updated.policies.id_quota).to.equal(secretariatParams.id_quota) + expect(err).to.be.undefined + }) + }) + it('Allows update made by a secretariat to another org', async () => { + await chai.request(app) + .put('/api/org/win_5') + .set({ ...constants.headers }) + .query(params) + .send() + .then((res, err) => { + expect(res).to.have.status(200) + expect(res.body.updated.name).to.equal(params.name) + expect(res.body.updated.policies.id_quota).to.equal(params.id_quota) + expect(err).to.be.undefined + }) + await chai.request(app) + .put('/api/org/win_5') + .set({ ...constants.headers }) + .query(cnaParams) + .send() + .then((res, err) => { + expect(res).to.have.status(200) + expect(res.body.updated.name).to.equal(cnaParams.name) + expect(res.body.updated.policies.id_quota).to.equal(cnaParams.id_quota) + expect(err).to.be.undefined + }) + }) + it('Update made by a secretariat to another org does NOT update last_active field', async () => { + await chai.request(app) + .put('/api/org/win_5') + .set({ ...constants.headers }) + .query(params) + .send() + .then((res, err) => { + expect(res.body.updated.last_active).to.be.undefined + expect(res).to.have.status(200) + expect(err).to.be.undefined + }) + }) + it('Update made by a secretariat to itself DOES update last_active field', async () => { + const now = Date.now() + await chai.request(app) + .put('/api/org/mitre') + .set({ ...constants.headers }) + .query(params) + .send() + .then((res, err) => { + expect(res.body.updated.last_active).to.not.be.null + // Assert that that the last_active field was updated under 2 seconds ago + const lastActive = Date.parse(res.body.updated.last_active) + const diff = Math.abs(now - lastActive) + const withinTwoSeconds = diff < 2000 + expect(withinTwoSeconds).to.be.true + expect(res).to.have.status(200) + expect(err).to.be.undefined + }) + }) + it('Update made by non-secretariat org to itself ONLY updates last_active field', async () => { + const now = Date.now() + await chai.request(app) + .put('/api/org/win_5') + .set({ ...constants.nonSecretariatUserHeaders }) + .send() + .then((res, err) => { + // Assert that that the last_active field was updated under 2 seconds ago + const lastActive = Date.parse(res.body.updated.last_active) + const diff = Math.abs(now - lastActive) + const withinTwoSeconds = diff < 2000 + expect(withinTwoSeconds).to.be.true + // Assert no other fields were changed + expect(res).to.have.status(200) + expect(res.body.updated.active_roles).to.be.undefined + expect(res.body.updated.name).to.be.undefined + expect(res.body.updated.policies).to.be.undefined + expect(err).to.be.undefined + }) + }) + it('Request body ignored in update made by non-secretariat org to itself', async () => { + const requestBody = { + key1: 'value1', + key2: 'value2', + key3: 'value3', + key4: 'value4', + key5: 'value5', + key6: 'value6', + key7: 'value7', + key8: 'value8' + } + await chai.request(app) + .put('/api/org/win_5') + .set({ ...constants.nonSecretariatUserHeaders }) + .send(requestBody) + .then((res, err) => { + expect(res).to.have.status(200) + expect(res.body.updated.last_active).to.not.be.null + expect(res.body.updated.active_roles).to.be.undefined + expect(res.body.updated.name).to.be.undefined + expect(res.body.updated.policies).to.be.undefined + expect(err).to.be.undefined + }) + }) + it('Request body ignored in update made by secretariat to itself', async () => { + const requestBody = { + key1: 'value1', + key2: 'value2', + key3: 'value3', + key4: 'value4', + key5: 'value5', + key6: 'value6', + key7: 'value7', + key8: 'value8' + } + await chai.request(app) + .put('/api/org/mitre') + .set({ ...constants.headers }) + .query(params) + .send(requestBody) + .then((res, err) => { + expect(res).to.have.status(200) + expect(res.body.updated.last_active).to.not.be.null + expect(res.body.updated.name).to.equal(params.name) + expect(res.body.updated.policies.id_quota).to.equal(params.id_quota) + expect(err).to.be.undefined + }) + }) + }) + context('Negative Tests', () => { + it('Fails update made by a non-secretariat org to a different org', async () => { + await chai.request(app) + .put('/api/org/cause_8') + .set({ ...constants.nonSecretariatUserHeaders }) + .send() + .then((res, err) => { + expect(res).to.have.status(403) + expect(err).to.be.undefined + expect(res.body).to.haveOwnProperty('error') + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + it('Fails update to fields made by a non-secretariat org to itself', async () => { + await chai.request(app) + .put('/api/org/win_5') + .set({ ...constants.nonSecretariatUserHeaders }) + .query(params) + .send() + .then((res, err) => { + expect(res).to.have.status(403) + expect(err).to.be.undefined + expect(res.body).to.haveOwnProperty('error') + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + it('Fails update made by a non-secretariat org to a secretariat', async () => { + await chai.request(app) + .put('/api/org/mitre') + .set({ ...constants.nonSecretariatUserHeaders }) + .send() + .then((res, err) => { + expect(res).to.have.status(403) + expect(err).to.be.undefined + expect(res.body).to.haveOwnProperty('error') + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + }) +}) diff --git a/test/unit-tests/middleware/validateOrgTest.js b/test/unit-tests/middleware/validateOrgTest.js new file mode 100644 index 000000000..af239880f --- /dev/null +++ b/test/unit-tests/middleware/validateOrgTest.js @@ -0,0 +1,172 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const sinon = require('sinon') +const { validateOrg } = require('../../../src/middleware/middleware.js') +const OrgRepository = require('../../../src/repositories/orgRepository.js') +const expect = chai.expect + +const secretariat = { + short_name: 'mitre', + name: 'MITRE Corporation', + authority: { + active_roles: [ + 'SECRETARIAT', + 'CNA' + ] + }, + policies: { + id_quota: 1248 + } +} + +const nonSecretariat = { + short_name: 'win_5', + name: 'test_org', + authority: { + active_roles: [ + 'CNA' + ] + }, + policies: { + id_quota: 200 + } +} + +const nonSecretariat2 = { + short_name: 'cause_8', + name: 'test_org2', + authority: { + active_roles: [ + 'CNA' + ] + }, + policies: { + id_quota: 888 + } +} + +describe('Testing the validateOrg function', () => { + let status, json, res, next, getOrgRepository, orgRepo + beforeEach(() => { + status = sinon.stub() + json = sinon.spy() + res = { json, status } + next = sinon.stub() + status.returns(res) + + orgRepo = new OrgRepository() + getOrgRepository = sinon.stub() + getOrgRepository.returns(orgRepo) + }) + context('Positive Tests', () => { + it('Secretariat can update itself', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(true) + + const req = { + ctx: { + org: secretariat.short_name, + repositories: { + getOrgRepository + } + }, + params: { + shortname: secretariat.short_name + }, + query: { + id_quota: 111 + } + } + await validateOrg(req, res, next) + + expect(next.calledOnce).to.be.true + expect(next.firstCall.args).to.be.empty + }) + it('Secretariat can update another org', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(true) + + const req = { + ctx: { + org: secretariat.short_name, + repositories: { + getOrgRepository + } + }, + params: { + shortname: nonSecretariat.short_name + }, + query: { + id_quota: 999 + } + } + await validateOrg(req, res, next) + + expect(next.calledOnce).to.be.true + expect(next.firstCall.args).to.be.empty + }) + it('Non-secretariat can update itself', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(true) + + const req = { + ctx: { + org: nonSecretariat.short_name, + repositories: { + getOrgRepository + } + }, + params: { + shortname: nonSecretariat.short_name + } + } + await validateOrg(req, res, next) + + expect(next.calledOnce).to.be.true + expect(next.firstCall.args).to.be.empty + }) + }) + context('Negative Tests', () => { + it('Non-secretariat cannot update its fields other than last_active', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(false) + + const req = { + ctx: { + org: nonSecretariat.short_name, + repositories: { + getOrgRepository + } + }, + params: { + shortname: nonSecretariat.short_name + }, + query: { + id_quota: 999 + } + } + await validateOrg(req, res, next) + + expect(status.calledWith(403)).to.be.true + expect(next.calledOnce).to.be.false + }) + it('Non-secretariat cannot update another org', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(false) + + const req = { + ctx: { + org: nonSecretariat.short_name, + repositories: { + getOrgRepository + } + }, + params: { + shortname: nonSecretariat2.short_name + }, + query: { + id_quota: 999 + } + } + await validateOrg(req, res, next) + + expect(status.calledWith(403)).to.be.true + expect(next.calledOnce).to.be.false + }) + }) +}) diff --git a/test/unit-tests/org/orgUpdateLastActiveTest.js b/test/unit-tests/org/orgUpdateLastActiveTest.js new file mode 100644 index 000000000..33fa5bc0c --- /dev/null +++ b/test/unit-tests/org/orgUpdateLastActiveTest.js @@ -0,0 +1,138 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const sinon = require('sinon') +const { ORG_UPDATE_SINGLE } = require('../../../src/controller/org.controller/org.controller.js') +const OrgRepository = require('../../../src/repositories/orgRepository.js') +const UserRepository = require('../../../src/repositories/userRepository.js') +const expect = chai.expect + +const secretariat = { + short_name: 'mitre', + name: 'MITRE Corporation', + authority: { + active_roles: [ + 'SECRETARIAT', + 'CNA' + ] + }, + policies: { + id_quota: 1248 + } +} + +const nonSecretariat = { + short_name: 'win_5', + name: 'test_org', + authority: { + active_roles: [ + 'CNA' + ] + }, + policies: { + id_quota: 200 + } +} + +describe('Testing the updateOrg function', () => { + let status, json, res, next, getOrgRepository, orgRepo, getUserRepository, + userRepo, updateOrg + beforeEach(() => { + status = sinon.stub() + json = sinon.spy() + res = { json, status } + next = sinon.spy() + status.returns(res) + + orgRepo = new OrgRepository() + getOrgRepository = sinon.stub() + getOrgRepository.returns(orgRepo) + + userRepo = new UserRepository() + getUserRepository = sinon.stub() + getUserRepository.returns(userRepo) + + updateOrg = sinon.stub(orgRepo, 'updateByOrgUUID').returns(true) + sinon.stub(orgRepo, 'getOrgUUID').returns(true) + sinon.stub(userRepo, 'getUserUUID').returns(true) + }) + context('Positive Tests', () => { + it('Secretariat updates itself', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(true) + sinon.stub(orgRepo, 'findOneByShortName').returns(secretariat) + sinon.stub(orgRepo, 'aggregate').returns([secretariat]) + + const req = { + ctx: { + org: secretariat.short_name, + repositories: { + getOrgRepository, + getUserRepository + }, + params: { + shortname: secretariat.short_name + }, + query: { + id_quota: 111 + } + } + } + await ORG_UPDATE_SINGLE(req, res, next) + + expect(status.args[0][0]).to.equal(200) + expect(updateOrg.args[0][1].policies.id_quota).to.equal(req.ctx.query.id_quota) + }) + it('Secretariat updates a different org', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(true) + sinon.stub(orgRepo, 'findOneByShortName').returns(nonSecretariat) + sinon.stub(orgRepo, 'aggregate').returns([nonSecretariat]) + + const req = { + ctx: { + org: secretariat.short_name, + repositories: { + getOrgRepository, + getUserRepository + }, + params: { + shortname: nonSecretariat.short_name + }, + query: { + id_quota: 999 + } + } + } + await ORG_UPDATE_SINGLE(req, res, next) + + expect(status.args[0][0]).to.equal(200) + expect(updateOrg.args[0][1].policies.id_quota).to.equal(req.ctx.query.id_quota) + }) + it('Non-secretariat no params only updates last_active field', async () => { + sinon.stub(orgRepo, 'isSecretariat').returns(false) + sinon.stub(orgRepo, 'findOneByShortName').returns(nonSecretariat) + const nonSecretariatAgt = nonSecretariat + nonSecretariatAgt.last_active = Date.now() + sinon.stub(orgRepo, 'aggregate').returns([nonSecretariatAgt]) + + const req = { + ctx: { + org: nonSecretariat.short_name, + repositories: { + getOrgRepository, + getUserRepository + }, + params: { + shortname: nonSecretariat.short_name + } + } + } + await ORG_UPDATE_SINGLE(req, res, next) + + expect(status.args[0][0]).to.equal(200) + const now = Date.now() + const lastActive = updateOrg.args[0][1].last_active + const diff = Math.abs(now - lastActive) + const withinHalfASecond = diff < 500 + expect(withinHalfASecond).to.be.true + }) + }) +}) diff --git a/test/unit-tests/org/orgUpdateTest.js b/test/unit-tests/org/orgUpdateTest.js index f978d6f96..4e5f00b76 100644 --- a/test/unit-tests/org/orgUpdateTest.js +++ b/test/unit-tests/org/orgUpdateTest.js @@ -48,6 +48,10 @@ class OrgUpdatedAddingRole { async getOrgUUID () { return null } + + async isSecretariat () { + return true + } } class OrgUpdatedRemovingRole { @@ -66,6 +70,10 @@ class OrgUpdatedRemovingRole { async getOrgUUID () { return null } + + async isSecretariat () { + return true + } } describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { @@ -102,6 +110,10 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { async findOneByShortName () { return orgFixtures.existentOrg } + + async isSecretariat () { + return true + } } app.route('/org-not-updated-shortname-exists/:shortname') @@ -288,6 +300,10 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { async aggregate () { return [orgFixtures.existentOrg] } + + async isSecretariat () { + return true + } } app.route('/org-not-updated-no-query-parameters/:shortname')