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
- short_name
- long_name
- authority
- aliases
- oversees
- top_level_root
- charter_or_scope
- product_list
- disclosure_policy
- contact_info.websites
- contact_info.emails
- partner_role_type
- partner_country
- advisory_locations
- advisory_location_require_credentials
- vulnerability_advisory_location_for_web_scraping
- industry
- tl_root_start_date
- is_cna_discussion_list
",
+ "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
- short_name
- long_name
- authority
- aliases
- oversees
- top_level_root
- is_top_level_root
- is_last_resort
- charter_or_scope
- product_list
- disclosure_policy
- contact_info.websites
- contact_info.emails
- partner_role_type
- partner_country
- advisory_locations
- advisory_location_require_credentials
- vulnerability_advisory_location_for_web_scraping
- industry
- tl_root_start_date
- is_cna_discussion_list
",
"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`)