diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 6f78b384c..ee800fde2 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3833,7 +3833,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

", "operationId": "registryOrgUpdateSingle", "parameters": [ { @@ -8120,4 +8120,4 @@ } } } -} +} \ No newline at end of file diff --git a/datadump/pre-population/orgs.json b/datadump/pre-population/orgs.json index fcee2199c..b85d8f4ea 100644 --- a/datadump/pre-population/orgs.json +++ b/datadump/pre-population/orgs.json @@ -323,11 +323,6 @@ } }, { - "authority": { - "active_roles": [ - "CNA" - ] - }, "name": "Chase, May and Jones", "short_name": "sister_20", "time": { @@ -338,4 +333,4 @@ "id_quota": 1408 } } -] \ No newline at end of file +] diff --git a/docker/README.md b/docker/README.md index 310e181d1..e56229d02 100644 --- a/docker/README.md +++ b/docker/README.md @@ -89,9 +89,9 @@ You should see the following: > NODE_ENV=development node-dev src/scripts/populate.js 2022-06-07 19:58:32 [info]: "Successfully connected to database!" -Are you sure you wish to pre-populate the database for the development environment? Doing so will drop and rebuild the database, run migrations, and populate the Org, BaseOrg, User, BaseUser, Glossary collection(s) in the cve_dev database. (y/n) y +Are you sure you wish to pre-populate the database for the development environment? Doing so will drop and rebuild the database, populate the Org, BaseOrg, User, BaseUser, Glossary collection(s), and run migrations in the cve_dev database. (y/n) y -2022-06-07 19:58:37 [info]: "Dropping cve_dev database before running migrations..." +2022-06-07 19:58:37 [info]: "Dropping cve_dev database before population..." 2022-06-07 19:58:37 [info]: "Successfully dropped cve_dev database." 2022-06-07 19:58:37 [info]: "Populating Org collection..." 2022-06-07 19:58:37 [info]: "Org populated!" @@ -103,6 +103,7 @@ Are you sure you wish to pre-populate the database for the development environme 2022-06-07 19:58:38 [info]: "BaseUser populated!" 2022-06-07 19:58:38 [info]: "Populating Glossary collection..." 2022-06-07 19:58:39 [info]: "Glossary populated!" +2022-06-07 19:58:39 [info]: "Successfully ran database migrations." 2022-06-07 19:58:39 [info]: "Successfully populated the database!" ``` diff --git a/migrations/20260903-01-set-registry-org-disabled.js b/migrations/20260903-01-set-registry-org-disabled.js new file mode 100644 index 000000000..a51f28984 --- /dev/null +++ b/migrations/20260903-01-set-registry-org-disabled.js @@ -0,0 +1,29 @@ +const ACTIVE_ROLE_PATH = 'authority.active_roles.0' + +module.exports = { + async up (db) { + const baseOrgCollection = db.collection('BaseOrg') + const orgCollection = db.collection('Org') + + await baseOrgCollection.updateMany( + {}, + { $set: { disabled: true } } + ) + + const enabledOrgUUIDs = (await orgCollection.distinct( + 'UUID', + { [ACTIVE_ROLE_PATH]: { $exists: true } } + )).filter(UUID => typeof UUID === 'string' && UUID.length > 0) + + if (enabledOrgUUIDs.length > 0) { + await baseOrgCollection.updateMany( + { UUID: { $in: enabledOrgUUIDs } }, + { $set: { disabled: false } } + ) + } + }, + + async down () { + // Existing migration values cannot be distinguished from later Secretariat updates. + } +} diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 3b2801d62..01956a1ef 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -77,6 +77,11 @@ "long_name": { "$ref": "#/definitions/longName" }, + "disabled": { + "type": "boolean", + "default": true, + "description": "Indicates whether the organization is disabled." + }, "new_short_name": { "$ref": "#/definitions/shortName" }, diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 5f0033637..7c366abec 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -15,6 +15,9 @@ "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "disabled": { + "$ref": "/BaseOrg#/properties/disabled" + }, "new_short_name": { "description": "Used to rename an organization's short name during an update.", "type": "string", diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 8f1b48cd7..4f03b97d5 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -15,6 +15,9 @@ "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "disabled": { + "$ref": "/BaseOrg#/properties/disabled" + }, "new_short_name": { "description": "Used to rename an organization's short name during an update.", "type": "string", diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 4fdb7f4bd..9278be8fa 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -9,6 +9,11 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "default": true, + "description": "Indicates whether the organization is disabled. This field can only be modified by the Secretariat." + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 609298060..115c2a7ca 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -20,6 +20,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 83c1077f3..ae40b965c 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -17,6 +17,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "aliases": { "type": "array", "items": { diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index ce5c5ba43..84f3f4b80 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -46,6 +46,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "aliases": { "type": "array", "items": { diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 71ee3f92a..80d4eed99 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -9,6 +9,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled. This field can only be modified by the Secretariat." + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index eb6a10f75..a8d0e39e8 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -20,6 +20,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/src/constants/index.js b/src/constants/index.js index 034b570dc..756611e8e 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -49,6 +49,7 @@ function getConstants () { ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['program_data'], SECRETARIAT_ONLY_FIELDS: [ + 'disabled', 'partner_number', 'program_data', 'program_data.cve_website_update_date', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 0b5fa666e..0a720f8f3 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -10,6 +10,7 @@ const schema = { UUID: String, long_name: String, short_name: String, + disabled: { type: Boolean, default: true }, aliases: [String], authority: [String], top_level_root: String, diff --git a/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js index 0f9ae264e..3556fd770 100644 --- a/src/repositories/baseOrgRepositoryHelpers.js +++ b/src/repositories/baseOrgRepositoryHelpers.js @@ -123,7 +123,9 @@ async function processJointApprovalAndMerge (registryOrg, legacyOrg, registryObj const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins', 'inUse', 'in_use'] let registryProtectedFields = [...protectedFields] if (!isSecretariat) { - registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] + // disabled is publicly visible but can only be supplied by the Secretariat. + // Preserve its stored value when a non-Secretariat full update omits it. + registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS, 'disabled'] } let updatedRegistryOrg = null diff --git a/src/scripts/populate.js b/src/scripts/populate.js index fd69a07cd..8a71d552a 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -211,11 +211,10 @@ async function populateBaseUserCollection () { logger.info('BaseUser populated!') } -async function resetDatabaseAndRunMigrations () { - logger.info(`Dropping ${migrationConfig.mongodb.databaseName} database before running migrations...`) +async function resetDatabase () { + logger.info(`Dropping ${migrationConfig.mongodb.databaseName} database before population...`) await db.db.dropDatabase() logger.info(`Successfully dropped ${migrationConfig.mongodb.databaseName} database.`) - await runDatabaseMigrations() } async function populateSeedCollections () { @@ -273,13 +272,14 @@ async function populateDatabase () { } else { userInput = dataUtils.getUserPopulateInput( Object.keys(populateTheseCollections), - `drop and rebuild the database, run migrations, and populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s)` + `drop and rebuild the database, populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s), and run migrations` ) } if (userInput.toLowerCase() === 'y') { - await resetDatabaseAndRunMigrations() + await resetDatabase() await populateSeedCollections() + await runDatabaseMigrations() logger.info('Successfully populated the database!') console.log('Successfully populated the database!') } diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 9697a2173..5ce64bf8d 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -16,6 +16,7 @@ const orgAdminHeaders = { describe('Testing Conversation edit by index endpoint', () => { let org + let disabled before(async () => { await chai @@ -26,6 +27,7 @@ describe('Testing Conversation edit by index endpoint', () => { expect(err).to.be.undefined expect(res).to.have.status(200) org = res.body + disabled = org.disabled delete org.created delete org.last_updated delete org.admins @@ -33,6 +35,7 @@ describe('Testing Conversation edit by index endpoint', () => { delete org.top_level_root delete org.oversees delete org.program_data + delete org.disabled }) await chai @@ -86,6 +89,17 @@ describe('Testing Conversation edit by index endpoint', () => { }) context('Positive Tests', () => { + it('Preserves disabled when omitted from an org admin update', async () => { + await chai.request(app) + .get('/api/registry/org/activity_6') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.disabled).to.equal(disabled) + }) + }) + it('Should update own conversation as org admin', async () => { await chai.request(app) .put('/api/registry/org/activity_6/conversation/0') diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index c8d6b21db..3dfb2e3f9 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -65,6 +65,9 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.created).to.haveOwnProperty('authority') expect(res.body.created.authority).to.deep.equal(['CNA']) + expect(res.body.created).to.haveOwnProperty('disabled') + expect(res.body.created.disabled).to.equal(true) + expect(res.body.created).to.haveOwnProperty('id_quota') expect(res.body.created.id_quota).to.equal(testRegistryOrg.id_quota) @@ -676,6 +679,7 @@ describe('Testing /registry/org endpoints', () => { partner_number: 'Updated Partner Number', partner_country: 'UK', advisory_locations: ['https://example.com/updated_advisories'], + disabled: false, is_last_resort: false }) .then((res, err) => { @@ -699,6 +703,10 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.updated).to.haveOwnProperty('authority') expect(res.body.updated.authority).to.deep.equal(['CNA']) + expect(res.body.updated).to.haveOwnProperty('disabled') + expect(res.body.updated.disabled).to.equal(false) + createdOrg.disabled = false + expect(res.body.updated).to.haveOwnProperty('id_quota') expect(res.body.updated.id_quota).to.equal(createdOrg.id_quota) @@ -723,6 +731,7 @@ describe('Testing /registry/org endpoints', () => { .then((res) => { expect(res).to.have.status(200) expect(res.body.is_last_resort).to.equal(false) + expect(res.body.disabled).to.equal(false) }) }) it('Allows Secretariat to update program_data', async () => { @@ -1135,6 +1144,35 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: program_data, program_data.status.') }) }) + it('Fails to allow an admin to change the disabled flag', async () => { + await chai.request(app) + .put('/api/registry/org/win_5') + .set(constants.nonSecretariatUserHeaders2) + .send({ disabled: true }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: disabled.') + }) + }) + it('Fails to allow an admin to supply the unchanged disabled flag', async () => { + let disabled + await chai.request(app) + .get('/api/registry/org/win_5') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + disabled = res.body.disabled + }) + + await chai.request(app) + .put('/api/registry/org/win_5') + .set(constants.nonSecretariatUserHeaders2) + .send({ disabled }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: disabled.') + }) + }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { await chai.request(app) .put('/api/registry/org/registry_org_test') diff --git a/test/integration-tests/registry-org/registryOrgDisabledMigrationTest.js b/test/integration-tests/registry-org/registryOrgDisabledMigrationTest.js new file mode 100644 index 000000000..75d9c5c4d --- /dev/null +++ b/test/integration-tests/registry-org/registryOrgDisabledMigrationTest.js @@ -0,0 +1,29 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +describe('Registry organization disabled migration', () => { + it('enables a seeded registry organization whose legacy organization has an active authority', async () => { + const res = await chai.request(app) + .get('/api/registry/org/interesting_19') + .set(secretariatHeaders) + + expect(res).to.have.status(200) + expect(res.body.disabled).to.equal(false) + }) + + it('disables the seeded registry organization whose legacy organization has no authority', async () => { + const res = await chai.request(app) + .get('/api/registry/org/sister_20') + .set(secretariatHeaders) + + expect(res).to.have.status(200) + expect(res.body.disabled).to.equal(true) + }) +}) diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index e61254eaf..25d98d495 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -221,6 +221,9 @@ describe('Testing ROOT Organization Type', () => { context('ROOT admin permissions', () => { before(async () => { + // Non-Secretariat callers must omit Secretariat-only fields from PUT payloads. + delete createdOrg.disabled + // Create a Root Admin user await chai.request(app) .post(`/api/registry/org/${testRootOrg.short_name}/user`)