From a2df4662fbfb02720c009843f7da7b8cbd9eecff Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Fri, 12 Jun 2026 14:56:19 -0400 Subject: [PATCH] Require private contacts for registry org creation --- .../create-registry-org-request.json | 4 ++- src/controller/org.controller/index.js | 14 +++++++++- .../org.controller/org.controller.js | 2 +- .../registry-org.controller.js | 2 +- src/repositories/baseOrgRepository.js | 20 +++++++++++++ .../audit/registryOrgCreatesAuditTest.js | 3 +- test/integration-tests/constants.js | 22 +++++++++++---- .../org/legacyAdminRoleRevokeTest.js | 3 +- test/integration-tests/org/registryOrg.js | 8 ++++-- .../registry-org/registryOrgCRUDTest.js | 28 ++++++++++++++++--- .../registryOrgDiscriminatorAuthorityTest.js | 9 ++++-- .../registryOrgWithJointReviewTest.js | 9 ++++-- .../registry-org/rootOrgTest.js | 3 +- .../registry-org/verifyDeepRemoveEmpty.js | 1 + .../registry-user/registryUserCRUDTest.js | 3 +- test/integration-tests/user/updateUserTest.js | 6 ++-- 16 files changed, 109 insertions(+), 28 deletions(-) diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index b1917cf18..00b448d70 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -65,6 +65,7 @@ }, "private_contacts": { "type": "array", + "minItems": 2, "items": { "type": "object", "properties": { @@ -193,6 +194,7 @@ "required": [ "short_name", "authority", - "long_name" + "long_name", + "private_contacts" ] } diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index e3ca0287a..65d803746 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -460,7 +460,19 @@ router.post('/registry/org', short_name: 'fake_company', long_name: 'Fake Company', id_quota: 1000, - authority: ['CNA'] + authority: ['CNA'], + private_contacts: [ + { + poc: 'Primary Contact', + poc_email: 'primary@example.com', + phone: '555-0100' + }, + { + poc: 'Secondary Contact', + poc_email: 'secondary@example.com', + phone: '555-0101' + } + ] } } } diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 55c41e01a..725805117 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -263,7 +263,7 @@ async function createOrg (req, res, next) { if (req.useRegistry) { // If we are creating an org via the registry flag, we can do a full validation. - const result = await repo.validateOrg(body, { session }) + const result = repo.validateOrgCreate(body) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) await session.abortTransaction() diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 2c48c4b8c..8e9e7db50 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -157,7 +157,7 @@ async function createOrg (req, res, next) { try { session.startTransaction() - const result = repo.validateOrg(body, { session }) + const result = repo.validateOrgCreate(body) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) await session.abortTransaction() diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index a0a9e92f9..803b0b87c 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -1105,6 +1105,26 @@ class BaseOrgRepository extends BaseRepository { return validateObject } + validateOrgCreate (org) { + const validateObject = this.validateOrg(org) + if (!validateObject.isValid) { + return validateObject + } + + if (!Array.isArray(org.private_contacts) || org.private_contacts.length < 2) { + return { + isValid: false, + errors: [{ + instancePath: '/private_contacts', + message: 'must contain at least 2 items', + params: { limit: 2 } + }] + } + } + + return validateObject + } + /** * @async * @function isSecretariatByShortName diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js index aaf8406a7..6fbcacee3 100644 --- a/test/integration-tests/audit/registryOrgCreatesAuditTest.js +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -16,7 +16,8 @@ async function createTestOrg (customProps = {}) { short_name: shortName, long_name: `Test Org ${shortName}`, id_quota: 1000, - authority: ['CNA'] + authority: ['CNA'], + private_contacts: constants.registryOrgPrivateContacts } const orgData = { ...defaultProps, ...customProps } diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index 0711aedd1..3d19dfcc3 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -377,6 +377,19 @@ const testOrg2 = { } } +const registryOrgPrivateContacts = [ + { + poc: 'Dave Private', + poc_email: 'daveprivate@test.org', + phone: '555-4321' + }, + { + poc: 'Dana Private', + poc_email: 'danaprivate@test.org', + phone: '555-6789' + } +] + const testRegistryOrg = { short_name: 'test_registry_org', long_name: 'Test Registry Organization', @@ -385,11 +398,7 @@ const testRegistryOrg = { emails: ['dave@test.org'], phone: '555-1234' }, - private_contacts: [{ - poc: 'Dave Private', - poc_email: 'daveprivate@test.org', - phone: '555-4321' - }], + private_contacts: registryOrgPrivateContacts, authority: ['CNA'], id_quota: 100000 } @@ -402,6 +411,7 @@ const testRegistryOrg2 = { emails: ['dave@test.org'], phone: '555-1234' }, + private_contacts: registryOrgPrivateContacts, authority: ['CNA'], id_quota: 100000 } @@ -428,6 +438,7 @@ const existingRegistryOrg = { emails: ['dave@test.org'], phone: '555-1234' }, + private_contacts: registryOrgPrivateContacts, authority: ['CNA'], id_quota: 100000 } @@ -446,6 +457,7 @@ module.exports = { testAdp2, testOrg, testOrg2, + registryOrgPrivateContacts, testRegistryOrg, testRegistryOrg2, existingOrg, diff --git a/test/integration-tests/org/legacyAdminRoleRevokeTest.js b/test/integration-tests/org/legacyAdminRoleRevokeTest.js index 770f88adc..dfeda5147 100644 --- a/test/integration-tests/org/legacyAdminRoleRevokeTest.js +++ b/test/integration-tests/org/legacyAdminRoleRevokeTest.js @@ -32,7 +32,8 @@ describe('Legacy Admin Role Grant and Revoke Test', () => { short_name: orgShortName, long_name: orgShortName, authority: ['CNA'], - id_quota: 1000 + id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts }) .then((res) => { expect(res).to.have.status(200) diff --git a/test/integration-tests/org/registryOrg.js b/test/integration-tests/org/registryOrg.js index 25022acdd..b78ef3e27 100644 --- a/test/integration-tests/org/registryOrg.js +++ b/test/integration-tests/org/registryOrg.js @@ -21,7 +21,8 @@ const postNewOrg = async (shortName, name, quota = 1000) => { short_name: shortName, long_name: name, authority: ['CNA'], - id_quota: quota + id_quota: quota, + private_contacts: constants.registryOrgPrivateContacts }) } @@ -109,7 +110,8 @@ describe('Testing Secretariat functionality for Orgs', () => { short_name: 'test_registry_org_cna', long_name: 'Testing Registry Org CNA', id_quota: 123, - authority: ['CNA'] + authority: ['CNA'], + private_contacts: constants.registryOrgPrivateContacts }).then((res) => { expect(res).to.have.status(200) }) @@ -362,7 +364,7 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app) .post('/api/registry/org') .set(secretariatHeaders) - .send({ long_name: 'MITRE Corporation', authority: ['SECRETARIAT'], short_name: 'mitre', id_quota: 1000 }) + .send({ long_name: 'MITRE Corporation', authority: ['SECRETARIAT'], short_name: 'mitre', id_quota: 1000, private_contacts: constants.registryOrgPrivateContacts }) .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('The \'mitre\' organization already exists.') diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 19b2e8737..6a7f59b65 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -17,7 +17,8 @@ const testRegistryOrg = { partner_number: 'Initial Partner Number', partner_country: 'US', advisory_locations: ['https://example.com/advisories'], - charter_or_scope: 'This is a normal string, not a URI' + charter_or_scope: 'This is a normal string, not a URI', + private_contacts: constants.registryOrgPrivateContacts } let createdOrg @@ -179,6 +180,22 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + it('Fails to create a new registry organization with fewer than two private contacts', async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + short_name: 'registry_org_missing_contacts', + private_contacts: [constants.registryOrgPrivateContacts[0]] + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].instancePath).to.equal('/private_contacts') + expect(res.body.errors[0].message).to.equal('must contain at least 2 items') + }) + }) it('Fails to create a new registry organization with an ambiguous CVE website update date', async () => { await chai.request(app) .post('/api/registry/org') @@ -561,7 +578,8 @@ describe('Testing /registryOrg endpoints', () => { short_name: 'temp_org_for_update', long_name: 'Temp Org', authority: ['CNA'], - id_quota: 10 + id_quota: 10, + private_contacts: constants.registryOrgPrivateContacts } await chai.request(app) .post('/api/registry/org') @@ -596,7 +614,8 @@ describe('Testing /registryOrg endpoints', () => { short_name: 'sub_org_test', long_name: 'Sub Org Test', authority: ['CNA'], - id_quota: 100 + id_quota: 100, + private_contacts: constants.registryOrgPrivateContacts } let createdSubOrgUUID await chai.request(app) @@ -657,7 +676,8 @@ describe('Testing /registryOrg endpoints', () => { short_name: 'temp_org_for_in_use_test', long_name: 'Temp Org In Use Test', authority: ['CNA'], - id_quota: 10 + id_quota: 10, + private_contacts: constants.registryOrgPrivateContacts } await chai.request(app) .post('/api/registry/org') diff --git a/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js b/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js index 7fa072f43..748abfde1 100644 --- a/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js +++ b/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js @@ -29,7 +29,8 @@ describe('Testing Registry Org Discriminator Authority inheritance', () => { short_name: 'test_cna_discriminator', long_name: 'Test CNA Discriminator', authority: ['CNA'], - id_quota: 1000 + id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts } orgsToCleanup.push(cnaOrgData.short_name) @@ -60,7 +61,8 @@ describe('Testing Registry Org Discriminator Authority inheritance', () => { short_name: 'test_sec_discriminator', long_name: 'Test Secretariat Discriminator', authority: ['SECRETARIAT'], - id_quota: 0 + id_quota: 0, + private_contacts: constants.registryOrgPrivateContacts } orgsToCleanup.push(secretariatOrgData.short_name) @@ -90,7 +92,8 @@ describe('Testing Registry Org Discriminator Authority inheritance', () => { const rootOrgData = { short_name: 'test_root_discriminator', long_name: 'Test Root Discriminator', - authority: ['ROOT'] + authority: ['ROOT'], + private_contacts: constants.registryOrgPrivateContacts } orgsToCleanup.push(rootOrgData.short_name) diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index 7aa683ea9..32336b18e 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -32,14 +32,16 @@ const testRegistryOrgForReview = { short_name: 'non_secretariat_org', long_name: 'Non Secretariat Org', authority: ['CNA'], - id_quota: 1000 + id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts } const testRegistryOrgForReviewWithComments = { short_name: 'non_with_comments', long_name: 'Non Secretariat Org', authority: ['CNA'], - id_quota: 1000 + id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts } const testRegistryOrgForAdvisoryReview = { @@ -47,7 +49,8 @@ const testRegistryOrgForAdvisoryReview = { long_name: 'Non Advisory Review Org', authority: ['CNA'], id_quota: 1000, - advisory_locations: ['https://example.com/advisories'] + advisory_locations: ['https://example.com/advisories'], + private_contacts: constants.registryOrgPrivateContacts } const testRegistryOrgAdminUser = { diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index 685248129..79b9fe136 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -13,7 +13,8 @@ let rootAdminHeaders const testRootOrg = { short_name: 'root_org_test_4', long_name: 'Root Org Test', - authority: ['ROOT'] + authority: ['ROOT'], + private_contacts: constants.registryOrgPrivateContacts } let createdOrg diff --git a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js index c6abde3a3..d955c0c13 100644 --- a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js +++ b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js @@ -13,6 +13,7 @@ const testNullRemovalOrg = { long_name: 'Test Null Removal Org', authority: ['CNA'], id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts, contact_info: { phone: null // Should be removed } diff --git a/test/integration-tests/registry-user/registryUserCRUDTest.js b/test/integration-tests/registry-user/registryUserCRUDTest.js index 048f4bd43..4c538e586 100644 --- a/test/integration-tests/registry-user/registryUserCRUDTest.js +++ b/test/integration-tests/registry-user/registryUserCRUDTest.js @@ -32,7 +32,8 @@ const postNewOrg = async (shortName) => { short_name: shortName, long_name: shortName, authority: ['CNA'], - id_quota: 1000 + id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts }) } diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 24cd8cb19..93d03f2e9 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -20,7 +20,8 @@ describe('Testing Edit user endpoint', () => { short_name: orgA, long_name: 'Migration Org A', authority: ['CNA'], - id_quota: 1000 + id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts }) .then(res => { expect(res.status).to.equal(200, JSON.stringify(res.body)) @@ -34,7 +35,8 @@ describe('Testing Edit user endpoint', () => { short_name: orgB, long_name: 'Migration Org B', authority: ['CNA'], - id_quota: 1000 + id_quota: 1000, + private_contacts: constants.registryOrgPrivateContacts }) .then(res => { expect(res.status).to.equal(200, JSON.stringify(res.body))