diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index c852f1e79..8e32aa932 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -89,6 +89,9 @@ "minimum": 0, "maximum": 100000 }, + "is_last_resort": { + "type": "boolean" + }, "oversees": { "type": "array", "uniqueItems": true, diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index a6e8a9d7e..a60691ee5 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -107,6 +107,15 @@ }, "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" + }, + "is_top_level_root": { + "type": "boolean" + }, + "charter_or_scope": { + "type": "string" + }, + "disclosure_policy": { + "type": "string" } }, "required": [ diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index b1917cf18..2e583abc4 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -44,6 +44,14 @@ "type": "string", "description": "Indicates if the organization is a root or top-level root" }, + "is_top_level_root": { + "type": "boolean", + "description": "Indicates whether a ROOT organization is top-level" + }, + "is_last_resort": { + "type": "boolean", + "description": "Indicates whether a CNA organization is a CNA of last resort." + }, "users": { "type": "array", "items": { diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 2071119a1..5161b509d 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -77,6 +77,14 @@ "type": "string", "description": "Indicates if the organization is a root or top-level root" }, + "is_top_level_root": { + "type": "boolean", + "description": "Indicates whether a ROOT organization is top-level" + }, + "is_last_resort": { + "type": "boolean", + "description": "Indicates whether a CNA organization is a CNA of last resort" + }, "users": { "type": "array", "items": { diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 86fc75326..a1ba1fc1a 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -41,6 +41,14 @@ "type": "string", "description": "Indicates if the organization is a root or top-level root" }, + "is_top_level_root": { + "type": "boolean", + "description": "Indicates whether a ROOT organization is top-level" + }, + "is_last_resort": { + "type": "boolean", + "description": "Indicates whether a CNA organization is a CNA of last resort" + }, "reports_to": { "type": [ "string", diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index e4956e545..40bc74add 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -70,6 +70,14 @@ "type": "string", "description": "Indicates if the organization is a root or top-level root" }, + "is_top_level_root": { + "type": "boolean", + "description": "Indicates whether a ROOT organization is top-level" + }, + "is_last_resort": { + "type": "boolean", + "description": "Indicates whether a CNA organization is a CNA of last resort" + }, "reports_to": { "type": [ "string", diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index f388a40f7..60cbe400a 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -60,6 +60,14 @@ "type": "string", "description": "Indicates if the organization is a root or top-level root" }, + "is_top_level_root": { + "type": "boolean", + "description": "Indicates whether a ROOT organization is top-level" + }, + "is_last_resort": { + "type": "boolean", + "description": "Indicates whether a CNA organization is a CNA of last resort" + }, "users": { "type": "array", "items": { diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 4662e4872..387ef9f87 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -66,6 +66,14 @@ "type": "string", "description": "Indicates if the organization is a root or top-level root" }, + "is_top_level_root": { + "type": "boolean", + "description": "Indicates whether a ROOT organization is top-level" + }, + "is_last_resort": { + "type": "boolean", + "description": "Indicates whether a CNA organization is a CNA of last resort" + }, "users": { "type": "array", "items": { diff --git a/src/constants/index.js b/src/constants/index.js index 4a6c1d38b..c4db3affc 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -55,6 +55,8 @@ function getConstants () { 'program_data.cve_website_update_needed', 'program_data.status', 'top_level_root', + 'is_top_level_root', + 'is_last_resort', 'oversees' ], USER_ROLE_ENUM: { diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js index 2e7519c1e..20a7e083a 100644 --- a/src/model/cnaorg.js +++ b/src/model/cnaorg.js @@ -14,6 +14,7 @@ const validate = ajv.compile(CnaOrgSchema) const schema = { oversees: [String], id_quota: Number, + is_last_resort: Boolean, charter_or_scope: String, disclosure_policy: String, product_list: String diff --git a/src/model/rootorg.js b/src/model/rootorg.js index 1517ec65c..ad4dbeea9 100644 --- a/src/model/rootorg.js +++ b/src/model/rootorg.js @@ -12,7 +12,10 @@ ajv.addSchema(BaseOrgSchema) const validate = ajv.compile(RootOrgSchema) const schema = { - oversees: [String] + oversees: [String], + is_top_level_root: Boolean, + charter_or_scope: String, + disclosure_policy: String } const options = { discriminatorKey: 'kind' } diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 8b0cf5320..c8d6b21db 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -19,7 +19,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', + is_last_resort: true } let createdOrg @@ -82,6 +83,9 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.created).to.haveOwnProperty('charter_or_scope') expect(res.body.created.charter_or_scope).to.equal(testRegistryOrg.charter_or_scope) + expect(res.body.created).to.haveOwnProperty('is_last_resort') + expect(res.body.created.is_last_resort).to.equal(true) + expect(res.body.created).to.haveOwnProperty('program_data') expect(res.body.created.program_data.status).to.equal('inactive') expect(res.body.created.program_data).to.haveOwnProperty('partner_inactive_date') @@ -671,7 +675,8 @@ describe('Testing /registry/org endpoints', () => { partner_role_type: ['Researcher', 'Vendor'], partner_number: 'Updated Partner Number', partner_country: 'UK', - advisory_locations: ['https://example.com/updated_advisories'] + advisory_locations: ['https://example.com/updated_advisories'], + is_last_resort: false }) .then((res, err) => { expect(err).to.be.undefined @@ -708,6 +713,16 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.updated).to.haveOwnProperty('advisory_locations') expect(res.body.updated.advisory_locations).to.deep.equal(['https://example.com/updated_advisories']) + expect(res.body.updated.is_last_resort).to.equal(false) + }) + }) + it('Retrieves the CNA last-resort flag', async () => { + await chai.request(app) + .get('/api/registry/org/registry_org_test') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.is_last_resort).to.equal(false) }) }) it('Allows Secretariat to update program_data', async () => { diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index 18c4bd7e1..05f9d1fba 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -13,7 +13,10 @@ let rootAdminHeaders const testRootOrg = { short_name: 'root_org_test_4', long_name: 'Root Org Test', - authority: ['ROOT'] + authority: ['ROOT'], + is_top_level_root: true, + charter_or_scope: 'Coordinates CVE Numbering Authorities.', + disclosure_policy: 'Discloses vulnerabilities after coordinated remediation.' } let createdOrg let reportingOrg @@ -31,6 +34,9 @@ describe('Testing ROOT Organization Type', () => { expect(res).to.have.status(200) expect(res.body.message).to.equal(testRootOrg.short_name + ' organization was successfully created.') expect(res.body.created.authority).to.deep.equal(['ROOT']) + expect(res.body.created.is_top_level_root).to.equal(true) + expect(res.body.created.charter_or_scope).to.equal(testRootOrg.charter_or_scope) + expect(res.body.created.disclosure_policy).to.equal(testRootOrg.disclosure_policy) createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated @@ -55,6 +61,34 @@ describe('Testing ROOT Organization Type', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + + it('Secretariat can update and retrieve ROOT-specific fields', async () => { + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...createdOrg, + is_top_level_root: false, + charter_or_scope: 'Updated ROOT charter.', + disclosure_policy: 'Updated ROOT disclosure policy.' + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.updated.is_top_level_root).to.equal(false) + expect(res.body.updated.charter_or_scope).to.equal('Updated ROOT charter.') + expect(res.body.updated.disclosure_policy).to.equal('Updated ROOT disclosure policy.') + }) + + await chai.request(app) + .get(`/api/registry/org/${testRootOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.is_top_level_root).to.equal(false) + expect(res.body.charter_or_scope).to.equal('Updated ROOT charter.') + expect(res.body.disclosure_policy).to.equal('Updated ROOT disclosure policy.') + }) + }) }) context('Managing reports-to relationships', () => { @@ -211,11 +245,13 @@ describe('Testing ROOT Organization Type', () => { }) it('ROOT admin can update their own org', async () => { + const rootAdminPayload = { ...createdOrg } + delete rootAdminPayload.is_top_level_root await chai.request(app) .put(`/api/registry/org/${testRootOrg.short_name}`) .set(rootAdminHeaders) .send({ - ...createdOrg, + ...rootAdminPayload, long_name: 'Updated Root Org Test' }) .then((res) => { @@ -238,12 +274,28 @@ describe('Testing ROOT Organization Type', () => { }) }) - it('ROOT admin cannot set oversees through the standard PUT endpoint', async () => { + it('ROOT admin cannot edit is_top_level_root', async () => { await chai.request(app) .put(`/api/registry/org/${testRootOrg.short_name}`) .set(rootAdminHeaders) .send({ ...createdOrg, + is_top_level_root: true + }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + + it('ROOT admin cannot set oversees through the standard PUT endpoint', async () => { + const rootAdminPayload = { ...createdOrg } + delete rootAdminPayload.is_top_level_root + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(rootAdminHeaders) + .send({ + ...rootAdminPayload, oversees: [reportingOrg.UUID] }) .then((res) => {