From a1a11fffdbfc6dd44d11df46049fd6602b8836b0 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 20 Apr 2026 16:13:16 -0400 Subject: [PATCH 01/40] Adding validation for invalid key input on Org creation/update --- schemas/registry-org/BaseOrg.json | 2 +- schemas/registry-org/CNAOrg.json | 24 +++++++++++++++++++++--- src/model/cnaorg.js | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 87f1b1e57..d3c4a45a3 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "./BaseOrg.json", + "$id": "/BaseOrg", "type": "object", "title": "CVE Base Organization", "description": "Base schema for a CVE Organization", diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 0402e8338..32a4bbcc9 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -5,9 +5,26 @@ "title": "CVE CNA Organization", "description": "Schema for a CVE CNA Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { + "UUID": {}, + "__t": {}, + "short_name": {}, + "long_name": {}, + "aliases": {}, + "root_or_tlr": {}, + "users": {}, + "admins": {}, + "contact_info": {}, + "partner_role": {}, + "partner_type": {}, + "partner_country": {}, + "vulnerability_advisory_locations": {}, + "advisory_location_require_credentials": {}, + "industry": {}, + "tl_root_start_date": {}, + "is_cna_discussion_list": {}, "authority": { "const": ["CNA"] }, @@ -15,7 +32,7 @@ "type": "array", "uniqueItems": true, "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "hard_quota": { @@ -36,7 +53,8 @@ "$ref": "/BaseOrg#/definitions/uriType" } }, + "additionalProperties": false, "required": ["hard_quota"] } ] -} +} \ No newline at end of file diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js index ab17599c9..45f2e0233 100644 --- a/src/model/cnaorg.js +++ b/src/model/cnaorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const CnaOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/CNAOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const CnaOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/CNAOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) From 35132d7f51a1afac2841690ff75a11f20bd34d4f Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 21 Apr 2026 16:55:40 -0400 Subject: [PATCH 02/40] Condensed two schema locations into one, updated references for tests, added new fields to schemas for validation, updated existing tests to handle schema changes, added new tests to handle user/org POST/PUT requests that have erroneus data previously dropped --- .../5.2.0_published_cna_container.json | 0 .../5.2.0_rejected_cna_container.json | 0 .../middleware/schemas => schemas}/Audit.json | 0 .../CVE_JSON_5.2.0_bundled.json | 0 schemas/registry-org/ADPOrg.json | 2 +- schemas/registry-org/BaseOrg.json | 23 ++- schemas/registry-org/BulkDownloadOrg.json | 4 +- schemas/registry-org/CNAOrg.json | 115 ++++++----- schemas/registry-org/SecretariatOrg.json | 4 +- schemas/registry-user/BaseUser.json | 104 ++++++++++ src/constants/index.js | 2 +- .../cve.controller/cve.middleware.js | 4 +- src/middleware/middleware.js | 2 +- src/middleware/schemas/ADPOrg.json | 17 -- src/middleware/schemas/BaseOrg.json | 157 -------------- src/middleware/schemas/BaseUser.json | 72 ------- src/middleware/schemas/BulkDownloadOrg.json | 17 -- src/middleware/schemas/CNAOrg.json | 42 ---- src/middleware/schemas/RegistryUser.json | 10 - src/middleware/schemas/SecretariatOrg.json | 33 --- src/model/adporg.js | 4 +- src/model/audit.js | 2 +- src/model/baseuser.js | 2 +- src/model/bulkdownloadorg.js | 4 +- src/model/cve.js | 2 +- src/model/secretariatorg.js | 4 +- test/integration-tests/constants.js | 15 ++ .../conversation/editConversationTest.js | 5 + test/integration-tests/helpers.js | 6 + test/integration-tests/org/postOrgTest.js | 14 ++ .../integration-tests/org/postOrgUsersTest.js | 22 ++ test/integration-tests/org/registryOrg.js | 36 ++-- .../org/registryOrgAsOrgAdmin.js | 4 + .../org/regularUsersTestRegistry.js | 1 + .../registry-org/createUserByOrgTest.js | 191 ++++++++++-------- .../registry-org/registryOrgCRUDTest.js | 30 +++ test/integration-tests/user/createUserTest.js | 137 +++++++------ .../user/regularUserUpdateTest.js | 1 + test/integration-tests/user/updateUserTest.js | 36 +++- 39 files changed, 544 insertions(+), 580 deletions(-) rename {src/middleware/schemas => schemas}/5.2.0_published_cna_container.json (100%) rename {src/middleware/schemas => schemas}/5.2.0_rejected_cna_container.json (100%) rename {src/middleware/schemas => schemas}/Audit.json (100%) rename {src/middleware/schemas => schemas}/CVE_JSON_5.2.0_bundled.json (100%) create mode 100644 schemas/registry-user/BaseUser.json delete mode 100644 src/middleware/schemas/ADPOrg.json delete mode 100644 src/middleware/schemas/BaseOrg.json delete mode 100644 src/middleware/schemas/BaseUser.json delete mode 100644 src/middleware/schemas/BulkDownloadOrg.json delete mode 100644 src/middleware/schemas/CNAOrg.json delete mode 100644 src/middleware/schemas/RegistryUser.json delete mode 100644 src/middleware/schemas/SecretariatOrg.json diff --git a/src/middleware/schemas/5.2.0_published_cna_container.json b/schemas/5.2.0_published_cna_container.json similarity index 100% rename from src/middleware/schemas/5.2.0_published_cna_container.json rename to schemas/5.2.0_published_cna_container.json diff --git a/src/middleware/schemas/5.2.0_rejected_cna_container.json b/schemas/5.2.0_rejected_cna_container.json similarity index 100% rename from src/middleware/schemas/5.2.0_rejected_cna_container.json rename to schemas/5.2.0_rejected_cna_container.json diff --git a/src/middleware/schemas/Audit.json b/schemas/Audit.json similarity index 100% rename from src/middleware/schemas/Audit.json rename to schemas/Audit.json diff --git a/src/middleware/schemas/CVE_JSON_5.2.0_bundled.json b/schemas/CVE_JSON_5.2.0_bundled.json similarity index 100% rename from src/middleware/schemas/CVE_JSON_5.2.0_bundled.json rename to schemas/CVE_JSON_5.2.0_bundled.json diff --git a/schemas/registry-org/ADPOrg.json b/schemas/registry-org/ADPOrg.json index be9829003..7979d1f55 100644 --- a/schemas/registry-org/ADPOrg.json +++ b/schemas/registry-org/ADPOrg.json @@ -5,7 +5,7 @@ "title": "CVE ADP Organization", "description": "Schema for a CVE ADP Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index d3c4a45a3..d2a42bbf5 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -4,6 +4,7 @@ "type": "object", "title": "CVE Base Organization", "description": "Base schema for a CVE Organization", + "additionalProperties": false, "definitions": { "uuidType": { "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", @@ -34,7 +35,12 @@ "authority": { "description": "The authority (role) of this organization within the CVE program", "type": "string", - "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + "enum": [ + "CNA", + "SECRETARIAT", + "BULK_DOWNLOAD", + "ADP" + ] } }, "properties": { @@ -47,6 +53,9 @@ "long_name": { "$ref": "#/definitions/longName" }, + "new_short_name": { + "$ref": "#/definitions/shortName" + }, "aliases": { "type": "array", "uniqueItems": true, @@ -81,6 +90,18 @@ "$ref": "#/definitions/uuidType" } }, + "hard_quota": { + "description": "The maximum number of CVE IDs this organization can reserve.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "description": "The threshold for notifying the organization about their remaining CVE ID count.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, "contact_info": { "type": "object", "properties": { diff --git a/schemas/registry-org/BulkDownloadOrg.json b/schemas/registry-org/BulkDownloadOrg.json index cabc0777a..526626f17 100644 --- a/schemas/registry-org/BulkDownloadOrg.json +++ b/schemas/registry-org/BulkDownloadOrg.json @@ -1,11 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "BaseOrg", + "$id": "BulkDownloadOrg", "type": "object", "title": "CVE Bulk Download Organization", "description": "Schema for a CVE Bulk Download Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 32a4bbcc9..367302530 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -1,60 +1,75 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", "$id": "CNAOrg", + "type": "object", "title": "CVE CNA Organization", "description": "Schema for a CVE CNA Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { + "additionalProperties": false, + "properties": { + "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, + "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, + "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "new_short_name": { + "description": "Used to rename an organization's short name during an update.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "contact_info": { + "type": "object", "properties": { - "UUID": {}, - "__t": {}, - "short_name": {}, - "long_name": {}, - "aliases": {}, - "root_or_tlr": {}, - "users": {}, - "admins": {}, - "contact_info": {}, - "partner_role": {}, - "partner_type": {}, - "partner_country": {}, - "vulnerability_advisory_locations": {}, - "advisory_location_require_credentials": {}, - "industry": {}, - "tl_root_start_date": {}, - "is_cna_discussion_list": {}, - "authority": { - "const": ["CNA"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "/BaseOrg#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { + "poc": { "type": "string" }, + "poc_email": { "type": "string" }, + "poc_phone": { "type": "string" }, + "org_email": { "type": "string" }, + "website": { "type": "string" } + }, + "additionalProperties": false + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/authority" + } + }, + "policies": { + "type": "object", + "properties": { + "id_quota": { "type": "integer", - "minimum": 0 - }, - "charter_or_scope": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "disclosure_policy": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "product_list": { - "$ref": "/BaseOrg#/definitions/uriType" + "minimum": 0, + "maximum": 100000 } - }, - "additionalProperties": false, - "required": ["hard_quota"] + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + }, + "partner_role": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" } - ] + }, + "required": ["short_name", "hard_quota"] } \ No newline at end of file diff --git a/schemas/registry-org/SecretariatOrg.json b/schemas/registry-org/SecretariatOrg.json index 469bd7df5..4e658b571 100644 --- a/schemas/registry-org/SecretariatOrg.json +++ b/schemas/registry-org/SecretariatOrg.json @@ -5,7 +5,7 @@ "title": "CVE Secretariat Organization", "description": "Schema for a CVE Secretariat Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { @@ -15,7 +15,7 @@ "type": "array", "uniqueItems": true, "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "hard_quota": { diff --git a/schemas/registry-user/BaseUser.json b/schemas/registry-user/BaseUser.json new file mode 100644 index 000000000..5fa85687e --- /dev/null +++ b/schemas/registry-user/BaseUser.json @@ -0,0 +1,104 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/BaseUser", + "type": "object", + "title": "CVE Base User Schema", + "additionalProperties": false, + "description": "The schema for CVE Services Users", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "name": { + "description": "User's name components", + "type": "object", + "required": [ + "first", + "last" + ], + "properties": { + "first": { + "type": "string", + "maxLength": 100 + }, + "middle": { + "type": "string", + "maxLength": 100 + }, + "last": { + "type": "string", + "maxLength": 100 + }, + "suffix": { + "type": "string", + "maxLength": 100 + } + }, + "additionalProperties": false + } + }, + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "username": { + "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.", + "type": "string", + "minLength": 3, + "maxLength": 128, + "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$" + }, + "active": { + "description": "Whether the user account is active. Supports boolean or string based on legacy test constants.", + "type": [ + "boolean", + "string" + ] + }, + "authority": { + "description": "The user's authority and roles, often used in joint review contexts.", + "type": "object", + "properties": { + "active_roles": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "secret": { + "description": "Hashed secret for user authentication", + "type": "string" + }, + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "status": { + "description": "User status: 'active' or 'inactive'", + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "role": { + "description": "The user's role in the organization", + "type": "string" + }, + "org_short_name": { + "description": "Used to update the organization association of a user", + "type": "string", + "minLength": 2, + "maxLength": 32 + } + }, + "required": [ + "username" + ] +} \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js index a4c73c910..69f295e9f 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,5 +1,5 @@ const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) /** * Return default values. diff --git a/src/controller/cve.controller/cve.middleware.js b/src/controller/cve.controller/cve.middleware.js index 90b5a64e7..576bdad3f 100644 --- a/src/controller/cve.controller/cve.middleware.js +++ b/src/controller/cve.controller/cve.middleware.js @@ -4,8 +4,8 @@ const errors = require('./error') const error = new errors.CveControllerError() const utils = require('../../utils/utils') const fs = require('fs') -const RejectedSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_rejected_cna_container.json')) -const cnaContainerSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_published_cna_container.json')) +const RejectedSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_rejected_cna_container.json')) +const cnaContainerSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_published_cna_container.json')) const logger = require('../../middleware/logger') const Ajv = require('ajv') const addFormats = require('ajv-formats') diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 67e47e7b8..6cd272531 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -1,6 +1,6 @@ const getConstants = require('../constants').getConstants const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) const argon2 = require('argon2') const logger = require('./logger') const Ajv = require('ajv') diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json deleted file mode 100644 index 7979d1f55..000000000 --- a/src/middleware/schemas/ADPOrg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "ADPOrg", - "type": "object", - "title": "CVE ADP Organization", - "description": "Schema for a CVE ADP Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["ADP"] - } - } - } - ] -} diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json deleted file mode 100644 index f7039bcca..000000000 --- a/src/middleware/schemas/BaseOrg.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/BaseOrg", - "type": "object", - "title": "CVE Base Organization", - "description": "Base schema for a CVE Organization", - "definitions": { - "uuidType": { - "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", - "type": "string", - "format": "uuid", - "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" - }, - "uriType": { - "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", - "type": "string", - "format": "uri", - "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", - "minLength": 1, - "maxLength": 2048 - }, - "shortName": { - "description": "A 2-32 character name that can be used to complement an organization's UUID.", - "type": "string", - "minLength": 2, - "maxLength": 32 - }, - "longName": { - "description": "A 1-256 character name that can be used to complement an organization's short_name.", - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "authority": { - "description": "The authority (role) of this organization within the CVE program", - "type": "string", - "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] - }, - "discriminator": { - "description": "Discriminator key used by Mongoose for type inheritance", - "type": "string" - }, - "timestamp": { - "description": "Date/time format based on RFC3339 and ISO ISO8601, with an optional timezone in the format 'yyyy-MM-ddTHH:mm:ss[+-]ZH:ZM'. If timezone offset is not given, GMT (+00:00) is assumed.", - "pattern": "^(((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)|(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))|(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))|(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30)))T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", - "type": "string" - } - }, - "properties": { - "UUID": { - "$ref": "#/definitions/uuidType" - }, - "__t": { - "$ref": "#/definitions/discriminator" - }, - "short_name": { - "$ref": "#/definitions/shortName" - }, - "long_name": { - "$ref": "#/definitions/longName" - }, - "aliases": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "authority": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/authority" - } - }, - "root_or_tlr": { - "type": "boolean" - }, - "users": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "admins": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "contact_info": { - "type": "object", - "properties": { - "additional_contact_users": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - }, - "poc_phone": { - "type": "string" - }, - "org_email": { - "type": "string", - "format": "email" - }, - "website": { - "$ref": "#/definitions/uriType", - "pattern": "^(ftp|http)s?://\\S+$" - } - }, - "additionalProperties": false - }, - "partner_role": { - "type": "string" - }, - "partner_type": { - "type": "string" - }, - "partner_country": { - "type": "string" - }, - "vulnerability_advisory_locations": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "advisory_location_require_credentials": { - "type": "boolean" - }, - "industry": { - "type": "string" - }, - "tl_root_start_date": { - "$ref": "#/definitions/timestamp" - }, - "is_cna_discussion_list": { - "type": "boolean" - } - }, - "required": [ - "short_name", - "long_name" - ] -} \ No newline at end of file diff --git a/src/middleware/schemas/BaseUser.json b/src/middleware/schemas/BaseUser.json deleted file mode 100644 index 2c1bf93de..000000000 --- a/src/middleware/schemas/BaseUser.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/BaseUser", - "type": "object", - "title": "CVE Base User Schema", - "description": "The schema for CVE Services Users", - "definitions": { - "uuidType": { - "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", - "type": "string", - "format": "uuid", - "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" - }, - "name": { - "description": "User's name components", - "type": "object", - "required": [ - "first", - "last" - ], - "properties": { - "first": { - "type": "string", - "maxLength": 100 - }, - "middle": { - "type": "string", - "maxLength": 100 - }, - "last": { - "type": "string", - "maxLength": 100 - }, - "suffix": { - "type": "string", - "maxLength": 100 - } - }, - "additionalProperties": false - } - }, - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "username": { - "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.", - "type": "string", - "minLength": 3, - "maxLength": 128, - "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$" - }, - "secret": { - "description": "Hashed secret for user authentication", - "type": "string" - }, - "UUID": { - "$ref": "#/definitions/uuidType" - }, - "status": { - "description": "User status: 'active' or 'inactive'", - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - }, - "required": [ - "username" - ] -} \ No newline at end of file diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json deleted file mode 100644 index ada140853..000000000 --- a/src/middleware/schemas/BulkDownloadOrg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "BaseOrg", - "type": "object", - "title": "CVE Bulk Download Organization", - "description": "Schema for a CVE Bulk Download Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["BULK_DOWNLOAD"] - } - } - } - ] -} diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json deleted file mode 100644 index c1188c8c4..000000000 --- a/src/middleware/schemas/CNAOrg.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "$id": "CNAOrg", - "title": "CVE CNA Organization", - "description": "Schema for a CVE CNA Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["CNA"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "/BaseOrg#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { - "type": "integer", - "minimum": 0 - }, - "charter_or_scope": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "disclosure_policy": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "product_list": { - "$ref": "/BaseOrg#/definitions/uriType" - } - }, - "required": ["hard_quota"] - } - ] -} diff --git a/src/middleware/schemas/RegistryUser.json b/src/middleware/schemas/RegistryUser.json deleted file mode 100644 index de95595ab..000000000 --- a/src/middleware/schemas/RegistryUser.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "$id": "RegistryUser", - "title": "CVE Registry User Schema", - "description": "Schema for a CVE Registry User", - "allOf": [ - { "$ref": "/BaseUser" } - ] -} diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json deleted file mode 100644 index 4e658b571..000000000 --- a/src/middleware/schemas/SecretariatOrg.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "SecretariatOrg", - "type": "object", - "title": "CVE Secretariat Organization", - "description": "Schema for a CVE Secretariat Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["SECRETARIAT"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "/BaseOrg#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["hard_quota"] - } - ] -} diff --git a/src/model/adporg.js b/src/model/adporg.js index f5efa867c..0c5a80799 100644 --- a/src/model/adporg.js +++ b/src/model/adporg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const AdpOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/ADPOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const AdpOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/ADPOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/audit.js b/src/model/audit.js index 9d346566c..d749b691d 100644 --- a/src/model/audit.js +++ b/src/model/audit.js @@ -4,7 +4,7 @@ const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const AuditSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/Audit.json')) +const AuditSchemaJSON = JSON.parse(fs.readFileSync('schemas/Audit.json')) // Initialize AJV const ajv = new Ajv({ allErrors: true }) diff --git a/src/model/baseuser.js b/src/model/baseuser.js index 260179970..fcd4b1777 100644 --- a/src/model/baseuser.js +++ b/src/model/baseuser.js @@ -6,7 +6,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') // Load BaseUser JSON schema -const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseUser.json')) +const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('schemas/registry-user/BaseUser.json')) // Initialize AJV const ajv = new Ajv({ allErrors: true }) diff --git a/src/model/bulkdownloadorg.js b/src/model/bulkdownloadorg.js index e196b5ff3..cdf06cf9d 100644 --- a/src/model/bulkdownloadorg.js +++ b/src/model/bulkdownloadorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BulkDownloadOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BulkDownloadOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/cve.js b/src/model/cve.js index 59e23aeee..81f1eee87 100644 --- a/src/model/cve.js +++ b/src/model/cve.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose') const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) const Ajv = require('ajv') const addFormats = require('ajv-formats') diff --git a/src/model/secretariatorg.js b/src/model/secretariatorg.js index 127d236a6..073f1c9d7 100644 --- a/src/model/secretariatorg.js +++ b/src/model/secretariatorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const SecretariatOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/SecretariatOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const SecretariatOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/SecretariatOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index de4946825..80700e31e 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -363,6 +363,20 @@ const testOrg = { } } +const testOrg2 = { + + short_name: 'test_org2', + name: 'Test Organization 2', + authority: { + active_roles: [ + 'CNA' + ] + }, + policies: { + id_quota: 100000 + } +} + const testRegistryOrg = { short_name: 'test_registry_org', long_name: 'Test Registry Organization', @@ -432,6 +446,7 @@ module.exports = { testAdp, testAdp2, testOrg, + testOrg2, testRegistryOrg, testRegistryOrg2, existingOrg, diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 7adff82e6..ced1fb59e 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -29,6 +29,11 @@ describe('Testing Conversation endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) org = res.body + delete org.created + delete org.last_updated + delete org.admins + delete org.users + delete org.root_or_tlr }) await chai diff --git a/test/integration-tests/helpers.js b/test/integration-tests/helpers.js index 2b8685d4e..de16ccfd8 100644 --- a/test/integration-tests/helpers.js +++ b/test/integration-tests/helpers.js @@ -122,6 +122,9 @@ async function userDeactivateAsSecHelper (userName, orgShortName) { .set(constants.headers) .then(res => res.body) + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${userName}`) .set(constants.headers) @@ -139,6 +142,9 @@ async function userReactivateAsSecHelper (userName, orgShortName) { .then(res => res.body) user.status = 'active' + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${userName}`) diff --git a/test/integration-tests/org/postOrgTest.js b/test/integration-tests/org/postOrgTest.js index fbc8b8dde..a94245fd6 100644 --- a/test/integration-tests/org/postOrgTest.js +++ b/test/integration-tests/org/postOrgTest.js @@ -97,5 +97,19 @@ describe('Testing Org post endpoint', () => { expect(res.body.error).to.equal('ORG_EXISTS') }) }) + it('Should fail to create an org with an erroneous key not found in the schema with registry enabled', async () => { + await chai.request(app) + .post('/api/registry/org') + .set({ ...constants.headers }) + .send({ + ...constants.testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res, err) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js index 098763b1b..d952c7237 100644 --- a/test/integration-tests/org/postOrgUsersTest.js +++ b/test/integration-tests/org/postOrgUsersTest.js @@ -332,5 +332,27 @@ describe('Testing user post endpoint', () => { ) }) }) + it('Fails creation of user with registry enabled and an erroneous key not found in the schema', async () => { + await chai + .request(app) + .post('/api/registry/org/mitre/user') + .set({ ...constants.headers, ...shortName }) + .send({ + username: 'fakeregistryuser1002', + name: { + first: 'FirstName', + last: 'LastName', + middle: 'MiddleName', + suffix: 'Suffix' + }, + role: 'ADMIN', + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/registryOrg.js b/test/integration-tests/org/registryOrg.js index 9328f32dc..0dec2824e 100644 --- a/test/integration-tests/org/registryOrg.js +++ b/test/integration-tests/org/registryOrg.js @@ -157,21 +157,6 @@ describe('Testing Secretariat functionality for Orgs', () => { }) }) - it('A new user is created even if extra data is in the body', async () => { - const username = uuidv4() - await chai.request(app) - .post('/api/registry/org/mitre/user') - .set(secretariatHeaders) - .send({ - username, - ubiquitous: 'mendacious' - }) - .then((res) => { - expect(res).to.have.status(200) - expect(res.body.message).to.equal(`${username} was successfully created.`) - }) - }) - it('A users username can be updated', async function () { const { orgShortName, username } = await createNewUserWithNewOrg() const newUsername = uuidv4() @@ -179,6 +164,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -209,6 +196,8 @@ describe('Testing Secretariat functionality for Orgs', () => { let user await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -244,6 +233,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -319,6 +310,21 @@ describe('Testing Secretariat functionality for Orgs', () => { }) context('Negative Tests', () => { + it('A new user is not created if extra data is in the body', async () => { + const username = uuidv4() + await chai.request(app) + .post('/api/registry/org/mitre/user') + .set(secretariatHeaders) + .send({ + username, + ubiquitous: 'mendacious' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) + it('Should not retrieve an org for a non-existent UUID', async () => { const nonExistentUUID = 'nonexistent123' await chai.request(app) diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index d248215dd..ef2356bce 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -282,6 +282,10 @@ describe('Testing Registry Org as org admin', () => { it('Registry: Services api prevents org admins from updating a users username if that user already exists', async () => { let user await chai.request(app).get('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com').set(adminHeaders).then((res) => { user = res.body }) + + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com') .set(adminHeaders) diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js index 348df2336..b67aee73f 100644 --- a/test/integration-tests/org/regularUsersTestRegistry.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -24,6 +24,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with .set(constants.nonSecretariatUserHeaders) .then((res) => { previousBody = res.body }) + delete previousBody.created_by await chai.request(app) .put(`/api/registry/org/${org}/user/${user}`) .set(constants.nonSecretariatUserHeaders) diff --git a/test/integration-tests/registry-org/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js index 9397eb8db..3cba5a152 100644 --- a/test/integration-tests/registry-org/createUserByOrgTest.js +++ b/test/integration-tests/registry-org/createUserByOrgTest.js @@ -9,98 +9,111 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { - // Positive test - it('Should create a new user in an organization', (done) => { - const orgShortName = 'mitre' - const newUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' - }, - role: 'ADMIN' - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(200) - expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) - expect(res.body).to.have.property('created') - expect(res.body.created).to.have.property('username', newUser.username) - expect(res.body.created).to.have.property('secret') - done() - }) - }) - - // Negative test: Organization does not exist - it('Should not create a user in a non-existent organization', (done) => { - const orgShortName = 'nonexistentorg' - const newUser = { - username: 'testuser2@example.com', - name: { - first: 'Test', - last: 'User' + context('Positive Tests', () => { + it('Should create a new user in an organization', (done) => { + const orgShortName = 'mitre' + const newUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + role: 'ADMIN' } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(404) - expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) + expect(res.body).to.have.property('created') + expect(res.body.created).to.have.property('username', newUser.username) + expect(res.body.created).to.have.property('secret') + done() + }) + }) }) - - // Negative test: User already exists - it('Should not create a user that already exists', (done) => { - const orgShortName = 'mitre' - const existingUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' + context('Negative Tests', () => { + it('Should not create a user in a non-existent organization', (done) => { + const orgShortName = 'nonexistentorg' + const newUser = { + username: 'testuser2@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(existingUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) - done() - }) - }) - - // Negative test: Validation error (missing username) - it('Should not create a user with a missing username', (done) => { - const orgShortName = 'mitre' - const invalidUser = { - name: { - first: 'Test', - last: 'User' + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(404) + expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) + done() + }) + }) + it('Should not create a user that already exists', (done) => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(invalidUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal('Parameters were invalid') - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) + done() + }) + }) + it('Should not create a user with a missing username', (done) => { + const orgShortName = 'mitre' + const invalidUser = { + name: { + first: 'Test', + last: 'User' + } + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(invalidUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal('Parameters were invalid') + done() + }) + }) + it('Should not create a user with an erroneous key not found in the schema', async () => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + test: 'additional key not in schema' + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index e1e7f04d3..85b218053 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -60,6 +60,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) createdOrg = res.body.created + delete createdOrg.created + delete createdOrg.last_updated }) }) }) @@ -102,6 +104,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) + it('Fails to create a new registry organization with an erroneous key not found in the schema', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) context('Testing GET /registryOrg endpoints', () => { @@ -324,6 +340,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) + it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { + await chai.request(app) + .put('/api/registryOrg/registry_org_test') + .set(secretariatHeaders) + .send({ + ...createdOrg, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) context('Testing DELETE /registryOrg endpoint', () => { diff --git a/test/integration-tests/user/createUserTest.js b/test/integration-tests/user/createUserTest.js index a2050a67e..2b2e90010 100644 --- a/test/integration-tests/user/createUserTest.js +++ b/test/integration-tests/user/createUserTest.js @@ -24,16 +24,13 @@ const body = { const registryBody = { username: 'adpUser2', - active: 'true', name: { first: 'SecondTestCnaAdmin', last: 'test', middle: 'N', suffix: 'I' }, - authority: { - active_roles: ['Admin'] - } + status: 'active' } const nonAdminBody = { @@ -51,68 +48,94 @@ const nonAdminBody = { const registryNonAdminBody = { username: 'nonAdminUser2', - active: 'true', name: { first: 'SecondTestCnaAdmin', last: 'test', middle: 'N', suffix: 'I' }, - authority: { - } + status: 'active' } describe('Testing create user endpoint', () => { - it('Should return 200 and new user', (done) => { - chai.request(app) - .post('/api/org/range_4/user') - .set(constants.headers) - .send(body) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(body.username) - expect(res).to.have.status(200) - done() - }) - }) - it('Should return 200 and new user with registry enabled', (done) => { - chai.request(app) - .post('/api/registry/org/range_4/user') - .set(constants.headers) - .send(registryBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(registryBody.username) - expect(res).to.have.status(200) - done() - }) - }) - it('Should return 200 and create a non admin user', (done) => { - chai.request(app) - .post('/api/org/range_4/user') - .set(constants.headers) - .send(nonAdminBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(nonAdminBody.username) - expect(res).to.have.status(200) - done() - }) + context('Positive Tests', () => { + it('Should return 200 and new user', (done) => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send(body) + .end((err, res) => { + expect(err).to.be.null + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(body.username) + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and new user with registry enabled', (done) => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send(registryBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and create a non admin user', (done) => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send(nonAdminBody) + .end((err, res) => { + expect(err).to.be.null + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(nonAdminBody.username) + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and create a non admin user with registry enabled', (done) => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send(registryNonAdminBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + done() + }) + }) }) - it('Should return 200 and create a non admin user with registry enabled', (done) => { - chai.request(app) - .post('/api/registry/org/range_4/user') - .set(constants.headers) - .send(registryNonAdminBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(registryNonAdminBody.username) - expect(res).to.have.status(200) - done() - }) + context('Negative Tests', () => { + it('Should return 400 creating a new user with an erroneous key not found in the schema', async () => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send({ + ...body, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) + it('Should return 400 creating a new user with registry enabled and an erroneous key not found in the schema', async () => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send({ + ...registryBody, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/user/regularUserUpdateTest.js b/test/integration-tests/user/regularUserUpdateTest.js index 7be104954..32ff85d5a 100644 --- a/test/integration-tests/user/regularUserUpdateTest.js +++ b/test/integration-tests/user/regularUserUpdateTest.js @@ -22,6 +22,7 @@ describe('Regular User Self-Update Tests', () => { expect(res).to.have.status(200) expect(res.body.username).to.equal('jasminesmith@win_5.com') user = res.body + delete user.created_by }) }) it('Should allow regular user to update their own contact info (name)', async () => { diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 610438687..51f401f90 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -9,7 +9,7 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') describe('Testing Edit user endpoint', () => { - context('User edit tests', () => { + context('Positive Tests', () => { it('Should return 200 when only name changes are done', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?name.first=NewName') @@ -22,6 +22,7 @@ describe('Testing Edit user endpoint', () => { it('Should return 200 when only name changes are done with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -37,6 +38,8 @@ describe('Testing Edit user endpoint', () => { expect(res).to.have.status(200) }) }) + }) + context('Negative Tests', () => { it('Should return an error when admin is required', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?new_username=NewUsername') @@ -75,6 +78,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a first name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -103,6 +107,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a middle name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -131,6 +136,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a last name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -159,6 +165,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a suffix of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.suffix=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567') .set(constants.nonSecretariatUserHeaders) @@ -221,7 +228,6 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('ORG_DNE_PARAM') }) }) - it('Should return 404 when target organization in body does not exist', async () => { const user = constants.headers['CVE-API-USER'] const org = constants.headers['CVE-API-ORG'] @@ -236,5 +242,31 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('ORG_DNE_PARAM') }) }) + it('Should return 400 updating a user with an erroneous key not found in the schema', async () => { + await chai.request(app) + .put('/api/org/win_5/user/jasminesmith@win_5.com?test=testing123') + .set(constants.nonSecretariatUserHeaders) + .then((res, err) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0].msg).to.equal("'test' is not a valid parameter name.") + }) + }) + it('Should return 400 updating a registry user with an erroneous key not found in the schema', async () => { + const user = constants.headers['CVE-API-USER'] + const org = constants.headers['CVE-API-ORG'] + await chai.request(app) + .put(`/api/registry/org/${org}/user/${user}`) + .set(constants.headers) + .send({ + username: 'user1', + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) From de2b7a68092ef1d333860e172dc4878b3e98f3cf Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Wed, 22 Apr 2026 09:20:45 -0400 Subject: [PATCH 03/40] Fixed int tests that broke when merging with dev --- .../registry-org/registryOrgCRUDTest.js | 38 +++++++++---------- test/integration-tests/user/updateUserTest.js | 6 +++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index b6bf8e383..a36b6a419 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -343,27 +343,6 @@ describe('Testing /registryOrg endpoints', () => { .delete(`/api/registryOrg/${subOrg.short_name}`) .set(secretariatHeaders) }) - it('Ignores protected fields such as users and admins during an update', async () => { - const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - - await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) - .set(secretariatHeaders) - .send({ - ...createdOrg, - users: maliciousUsers, - admins: maliciousAdmins - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(200) - - // Ensure the response body.updated does not contain the malicious data - expect(res.body.updated.users || []).to.not.include(maliciousUsers[0]) - expect(res.body.updated.admins || []).to.not.include(maliciousAdmins[0]) - }) - }) }) context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { @@ -405,6 +384,23 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + it('Ignores protected fields such as users and admins during an update', async () => { + const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + + await chai.request(app) + .put(`/api/registryOrg/${createdOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...createdOrg, + users: maliciousUsers, + admins: maliciousAdmins + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) it('Fails to update a registry organization with reports_to manually provided', async () => { await chai.request(app) .put(`/api/registryOrg/${createdOrg.short_name}`) diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 1fe33f07c..3871a4b2a 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -42,6 +42,9 @@ describe('Testing Edit user endpoint', () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + delete user.created + delete user.created_by + delete user.last_updated const maliciousUUID = 'd41d8cd9-8f00-3204-a980-0998ecf8427e' const originalUUID = user.UUID @@ -65,6 +68,9 @@ describe('Testing Edit user endpoint', () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + delete user.created + delete user.created_by + delete user.last_updated await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.headers) From 7c6edb3d76176c54768150db00db8f4748c153d2 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 12:46:14 -0400 Subject: [PATCH 04/40] testing readPreference --- src/utils/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/db.js b/src/utils/db.js index 6fde268f5..4b3be05a8 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 320944b318a1afbfdcf6ccd11c09bbe9cba69d9b Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 13:06:04 -0400 Subject: [PATCH 05/40] Revert "testing readPreference" This reverts commit c7414e3278fbef14fc1fff2341c3d397c8c28079. --- src/utils/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/db.js b/src/utils/db.js index 4b3be05a8..6fde268f5 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 3b1a752c0416b40cbd486ece033677fe16ef67f9 Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Thu, 23 Apr 2026 13:35:50 -0400 Subject: [PATCH 06/40] testing read preference at transaction level --- .../registry-org.controller/registry-org.controller.js | 2 +- src/utils/db.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 64da4d038..2e8ef9e1a 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -134,7 +134,7 @@ async function getOrg (req, res, next) { */ async function createOrg (req, res, next) { try { - const session = await mongoose.startSession({ causalConsistency: false }) + const session = await mongoose.startSession({ causalConsistency: false, readPreference: 'primary' }) const repo = req.ctx.repositories.getBaseOrgRepository() const body = req.ctx.body const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) diff --git a/src/utils/db.js b/src/utils/db.js index 6fde268f5..4b3be05a8 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 9da249b1714340e2e1a31653bf3b2fc0e4959338 Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Thu, 23 Apr 2026 13:45:52 -0400 Subject: [PATCH 07/40] moving read preference from session to transaction --- .../registry-org.controller/registry-org.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 2e8ef9e1a..cb37d8651 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -134,7 +134,7 @@ async function getOrg (req, res, next) { */ async function createOrg (req, res, next) { try { - const session = await mongoose.startSession({ causalConsistency: false, readPreference: 'primary' }) + const session = await mongoose.startSession({ causalConsistency: false }) const repo = req.ctx.repositories.getBaseOrgRepository() const body = req.ctx.body const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) @@ -146,7 +146,7 @@ async function createOrg (req, res, next) { } try { - session.startTransaction() + session.startTransaction({ readPreference: 'primary' }) const result = repo.validateOrg(body, { session }) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) From 2603b89bcea51a35f854f190f2c05f4a7ca3755f Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Thu, 23 Apr 2026 13:57:37 -0400 Subject: [PATCH 08/40] Reverting dev changes --- .../registry-org.controller/registry-org.controller.js | 2 +- src/utils/db.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index cb37d8651..64da4d038 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -146,7 +146,7 @@ async function createOrg (req, res, next) { } try { - session.startTransaction({ readPreference: 'primary' }) + session.startTransaction() const result = repo.validateOrg(body, { session }) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) diff --git a/src/utils/db.js b/src/utils/db.js index 4b3be05a8..6fde268f5 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 21d8a235c63159684036afafb2c37298540f468f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:21:43 +0000 Subject: [PATCH 09/40] Bump path-to-regexp from 0.1.12 to 0.1.13 Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 0.1.12 to 0.1.13. - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13) --- updated-dependencies: - dependency-name: path-to-regexp dependency-version: 0.1.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88c96fde7..227cb2a19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.0", + "version": "2.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.0", + "version": "2.7.1", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", @@ -6685,9 +6685,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { From 0dd84af6f2ca886bc9d963675c3a147ba58e853e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:21:56 +0000 Subject: [PATCH 10/40] Bump brace-expansion from 1.1.12 to 1.1.13 Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 84 +++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 227cb2a19..39c922f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,9 +431,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -512,9 +512,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1510,9 +1510,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2883,9 +2883,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2978,9 +2978,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3126,9 +3126,9 @@ "license": "Python-2.0" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5846,9 +5846,9 @@ } }, "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6116,9 +6116,9 @@ } }, "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7482,9 +7482,9 @@ } }, "node_modules/replace-in-file/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7685,9 +7685,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8359,9 +8359,9 @@ } }, "node_modules/standard/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9059,9 +9059,9 @@ } }, "node_modules/swagger-autogen/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9158,9 +9158,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9867,9 +9867,9 @@ } }, "node_modules/yamljs/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", From 2999738e35699b90d35a1eb34173edddef5adc58 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 15:15:06 -0400 Subject: [PATCH 11/40] fix high vuln --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39c922f3d..aa1aceef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5305,9 +5305,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.flattendeep": { From 3774de8b28ba9ecd1285c82ade5a4ecfc0444941 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 15:29:45 -0400 Subject: [PATCH 12/40] updating versions --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa1aceef9..f590f20c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.1", + "version": "2.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.1", + "version": "2.7.2", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", From 43329b87b77f7c6410591f98c08b532450aaa58e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:45:54 -0400 Subject: [PATCH 13/40] Fixing version number conflicts --- api-docs/openapi.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 01ab0fad9..eaf2feb02 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.7.5", + "version": "2.8.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:
  • If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials
  • Contact your Root (Google, INCIBE, JPCERT/CC, or Red Hat) or Top-Level Root (CISA ICS or MITRE) to request credentials

CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located here.

Contact the CVE Services team", "contact": { diff --git a/package-lock.json b/package-lock.json index f590f20c2..6c434b937 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.2", + "version": "2.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.2", + "version": "2.8.0", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", diff --git a/package.json b/package.json index 8e0ec46bb..2b6186b3c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cve-services", "author": "Automation Working Group", - "version": "2.7.5", + "version": "2.8.0", "license": "(CC0)", "devDependencies": { "@faker-js/faker": "^7.6.0", From b62d7ad4c65e8548327c16fb830e6a40b0ee4cf1 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:57:08 -0400 Subject: [PATCH 14/40] Conflicts --- schemas/glossary/glossary.json | 24 ++ .../list-glossary-items-response.json | 16 + .../glossary.controller.js | 128 +++++++ src/controller/glossary.controller/index.js | 341 ++++++++++++++++++ src/middleware/schemas/ADPOrg.json | 18 + src/middleware/schemas/BaseOrg.json | 158 ++++++++ src/middleware/schemas/BulkDownloadOrg.json | 18 + src/middleware/schemas/CNAOrg.json | 45 +++ src/middleware/schemas/SecretariatOrg.json | 36 ++ src/model/glossary.js | 14 + src/repositories/glossaryRepository.js | 26 ++ src/repositories/repositoryFactory.js | 6 + src/routes.config.js | 2 + src/scripts/migrate.js | 11 + src/scripts/populate.js | 8 +- .../glossary/glossaryCRUDTest.js | 156 ++++++++ 16 files changed, 1005 insertions(+), 2 deletions(-) create mode 100644 schemas/glossary/glossary.json create mode 100644 schemas/glossary/list-glossary-items-response.json create mode 100644 src/controller/glossary.controller/glossary.controller.js create mode 100644 src/controller/glossary.controller/index.js create mode 100644 src/middleware/schemas/ADPOrg.json create mode 100644 src/middleware/schemas/BaseOrg.json create mode 100644 src/middleware/schemas/BulkDownloadOrg.json create mode 100644 src/middleware/schemas/CNAOrg.json create mode 100644 src/middleware/schemas/SecretariatOrg.json create mode 100644 src/model/glossary.js create mode 100644 src/repositories/glossaryRepository.js create mode 100644 test/integration-tests/glossary/glossaryCRUDTest.js diff --git a/schemas/glossary/glossary.json b/schemas/glossary/glossary.json new file mode 100644 index 000000000..0f6bbe633 --- /dev/null +++ b/schemas/glossary/glossary.json @@ -0,0 +1,24 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/glossary.json", + "type": "object", + "properties": { + "short_name": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "def": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "short_name", + "label", + "def" + ], + "additionalProperties": false +} diff --git a/schemas/glossary/list-glossary-items-response.json b/schemas/glossary/list-glossary-items-response.json new file mode 100644 index 000000000..fee135e11 --- /dev/null +++ b/schemas/glossary/list-glossary-items-response.json @@ -0,0 +1,16 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/list-glossary-items-response.json", + "type": "object", + "properties": { + "glossary": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "glossary" + ], + "additionalProperties": false +} diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js new file mode 100644 index 000000000..18f0f2a8f --- /dev/null +++ b/src/controller/glossary.controller/glossary.controller.js @@ -0,0 +1,128 @@ +const errors = require('../../utils/error') +const error = new errors.IDRError() + +/** + * Retrieves all glossary items. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing an array of all glossary items. + */ +async function getAllGlossaryItems (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const result = await glossaryRepo.getAll() + return res.status(200).json({ glossary: result }) + } catch (err) { + next(err) + } +} + +/** + * Retrieves a single glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the requested glossary item, or a 404 if not found. + */ +async function getGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + + const result = await glossaryRepo.findOneByShortName(shortName) + if (!result) { + return res.status(404).json(error.notFound()) + } + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Creates a new glossary item. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the newly created glossary item, or a 400 if it already exists. + */ +async function createGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const glossaryData = req.body + + const existing = await glossaryRepo.findOneByShortName(glossaryData.short_name) + if (existing) { + return res.status(400).json(error.badInput(['Glossary item with this short_name already exists'])) + } + + const result = await glossaryRepo.collection.create(glossaryData) + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Updates an existing glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the updated glossary item, or a 404 if not found. + */ +async function updateGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + const glossaryData = req.body + + if (glossaryData.short_name && glossaryData.short_name !== shortName) { + return res.status(400).json(error.badInput(['Cannot change short_name through this endpoint.'])) + } + + const result = await glossaryRepo.updateByShortName(shortName, glossaryData) + if (!result) { + return res.status(404).json(error.notFound()) + } + + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Deletes an existing glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON message confirming deletion, or a 404 if not found. + */ +async function deleteGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + + const result = await glossaryRepo.deleteByShortName(shortName) + if (!result) { + return res.status(404).json(error.notFound()) + } + return res.status(200).json({ message: 'Glossary item deleted' }) + } catch (err) { + next(err) + } +} + +module.exports = { + getAllGlossaryItems, + getGlossaryItem, + createGlossaryItem, + updateGlossaryItem, + deleteGlossaryItem +} diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js new file mode 100644 index 000000000..8d83cc8ca --- /dev/null +++ b/src/controller/glossary.controller/index.js @@ -0,0 +1,341 @@ +const router = require('express').Router() +const controller = require('./glossary.controller') +const mw = require('../../middleware/middleware') + +// Get all glossary items - SEC only +router.get('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryAll' + #swagger.summary = "Retrieves all glossary items (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves all glossary items

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns a list of all glossary items', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/list-glossary-items-response.json' + } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.getAllGlossaryItems +) + +// Get glossary item by short_name - SEC only +router.get('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossarySingle' + #swagger.summary = "Retrieves a single glossary item by its short name (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves the specified glossary item

" + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the specified glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.getGlossaryItem +) + +// Create a glossary item - SEC only +router.post('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryCreate' + #swagger.summary = "Creates a new glossary item (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Creates a new glossary item

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the created glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.createGlossaryItem +) + +// Update a glossary item - SEC only +router.put('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryUpdate' + #swagger.summary = "Updates an existing glossary item (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Updates the specified glossary item

" + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the updated glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.updateGlossaryItem +) + +// Delete a glossary item - SEC only +router.delete('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryDelete' + #swagger.summary = "Deletes an existing glossary item (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Deletes the specified glossary item

" + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Confirms deletion of the glossary item' + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.deleteGlossaryItem +) + +module.exports = router diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json new file mode 100644 index 000000000..b5bea44cb --- /dev/null +++ b/src/middleware/schemas/ADPOrg.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "ADPOrg", + "type": "object", + "title": "CVE ADP Organization", + "description": "Schema for a CVE ADP Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["ADP"] + } + } + } + ] +} diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json new file mode 100644 index 000000000..a87e55fe4 --- /dev/null +++ b/src/middleware/schemas/BaseOrg.json @@ -0,0 +1,158 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/BaseOrg", + "type": "object", + "title": "CVE Base Organization", + "description": "Base schema for a CVE Organization", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "uriType": { + "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", + "type": "string", + "format": "uri", + "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", + "minLength": 1, + "maxLength": 2048 + }, + "shortName": { + "description": "A 2-32 character name that can be used to complement an organization's UUID.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "longName": { + "description": "A 1-256 character name that can be used to complement an organization's short_name.", + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authority": { + "description": "The authority (role) of this organization within the CVE program", + "type": "string", + "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + }, + "discriminator": { + "description": "Discriminator key used by Mongoose for type inheritance", + "type": "string" + }, + "timestamp": { + "description": "Date/time format based on RFC3339 and ISO ISO8601, with an optional timezone in the format 'yyyy-MM-ddTHH:mm:ss[+-]ZH:ZM'. If timezone offset is not given, GMT (+00:00) is assumed.", + "pattern": "^(((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)|(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))|(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))|(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30)))T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "type": "string" + } + }, + "properties": { + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "__t": { + "$ref": "#/definitions/discriminator" + }, + "short_name": { + "$ref": "#/definitions/shortName" + }, + "long_name": { + "$ref": "#/definitions/longName" + }, + "aliases": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/authority" + } + }, + "root_or_tlr": { + "type": "boolean" + }, + "users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "admins": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "contact_info": { + "type": "object", + "properties": { + "additional_contact_users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + }, + "poc_phone": { + "type": "string" + }, + "org_email": { + "type": "string", + "format": "email" + }, + "website": { + "$ref": "#/definitions/uriType", + "type": "string", + "pattern": "^(ftp|http)s?://\\S+$" + } + }, + "additionalProperties": false + }, + "partner_role": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" + }, + "vulnerability_advisory_locations": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "advisory_location_require_credentials": { + "type": "boolean" + }, + "industry": { + "type": "string" + }, + "tl_root_start_date": { + "$ref": "#/definitions/timestamp" + }, + "is_cna_discussion_list": { + "type": "boolean" + } + }, + "required": [ + "short_name", + "long_name" + ] +} \ No newline at end of file diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json new file mode 100644 index 000000000..768ae1123 --- /dev/null +++ b/src/middleware/schemas/BulkDownloadOrg.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "BaseOrg", + "type": "object", + "title": "CVE Bulk Download Organization", + "description": "Schema for a CVE Bulk Download Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["BULK_DOWNLOAD"] + } + } + } + ] +} diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json new file mode 100644 index 000000000..966056f75 --- /dev/null +++ b/src/middleware/schemas/CNAOrg.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "$id": "CNAOrg", + "title": "CVE CNA Organization", + "description": "Schema for a CVE CNA Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["CNA"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "charter_or_scope": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "disclosure_policy": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "product_list": { + "$ref": "/BaseOrg#/definitions/uriType" + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json new file mode 100644 index 000000000..f4e4e1637 --- /dev/null +++ b/src/middleware/schemas/SecretariatOrg.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "SecretariatOrg", + "type": "object", + "title": "CVE Secretariat Organization", + "description": "Schema for a CVE Secretariat Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["SECRETARIAT"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/src/model/glossary.js b/src/model/glossary.js new file mode 100644 index 000000000..fd9e9a2b5 --- /dev/null +++ b/src/model/glossary.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose') + +const schema = { + short_name: { type: String, required: true }, + label: { type: String, required: true }, + def: { type: String, required: true } +} + +const GlossarySchema = new mongoose.Schema(schema, { collection: 'Glossary', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) + +GlossarySchema.index({ short_name: 1 }, { unique: true }) + +const Glossary = mongoose.model('Glossary', GlossarySchema) +module.exports = Glossary diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js new file mode 100644 index 000000000..934cce610 --- /dev/null +++ b/src/repositories/glossaryRepository.js @@ -0,0 +1,26 @@ +const BaseRepository = require('./baseRepository') +const Glossary = require('../model/glossary') + +class GlossaryRepository extends BaseRepository { + constructor () { + super(Glossary) + } + + async getAll () { + return this.find({}, { multiple: true }) + } + + async findOneByShortName (shortName) { + return this.findOne({ short_name: shortName }) + } + + async updateByShortName (shortName, newGlossaryData) { + return this.findOneAndUpdate({ short_name: shortName }, newGlossaryData) + } + + async deleteByShortName (shortName) { + return this.collection.findOneAndDelete({ short_name: shortName }) + } +} + +module.exports = GlossaryRepository diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 4750fffea..7f97e1177 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -7,6 +7,7 @@ const BaseOrgRepository = require('./baseOrgRepository') const BaseUserRepository = require('./baseUserRepository') const ConversationRepository = require('./conversationRepository') const ReviewObjectRepository = require('./reviewObjectRepository') +const GlossaryRepository = require('./glossaryRepository') class RepositoryFactory { getOrgRepository () { @@ -54,6 +55,11 @@ class RepositoryFactory { return repo } + getGlossaryRepository () { + const repo = new GlossaryRepository() + return repo + } + getAuditRepository () { const AuditRepository = require('./auditRepository') const repo = new AuditRepository() diff --git a/src/routes.config.js b/src/routes.config.js index 1cf2fe159..9cf95cdc3 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -12,6 +12,7 @@ const RegistryOrgController = require('./controller/registry-org.controller') const AuditController = require('./controller/audit.controller') const ConversationController = require('./controller/conversation.controller') const ReviewObjectController = require('./controller/review-object.controller') +const GlossaryController = require('./controller/glossary.controller') var options = { swaggerOptions: { @@ -40,6 +41,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', RegistryOrgController) app.use('/api/', ConversationController) app.use('/api/', ReviewObjectController) + app.use('/api/', GlossaryController) app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)) app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions)) app.use('/schemas/', SchemasController) diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 7f4bd5879..a0097c1d6 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -65,6 +65,7 @@ async function run () { // Each helper handlers querying changes from srcDB and updating trgDB await orgHelper(db) await userHelper(db) + await glossaryHelper(db) } catch (err) { // Ensures that the client will close when you finish/error await dbClient.close() @@ -265,3 +266,13 @@ async function userHelper (db) { await trgUserCol.updateOne(trgQuery, updateDoc, options) } } + +async function glossaryHelper (db) { + console.log('Ensuring Glossary collection exists...') + // Create collection if it doesn't exist + await db.createCollection('Glossary').catch((err) => { + if (err.codeName !== 'NamespaceExists') { + console.warn('Could not create Glossary collection', err) + } + }) +} diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 28fe3d057..c70713938 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -21,6 +21,7 @@ const BaseUser = require('../model/baseuser') const ReviewObject = require('../model/reviewobject') const Conversation = require('../model/conversation') const Audit = require('../model/audit') +const Glossary = require('../model/glossary') const error = new errors.IDRError() @@ -34,14 +35,16 @@ const populateTheseCollections = { BaseUser: BaseUser, ReviewObject: ReviewObject, Conversation: Conversation, - Audit: Audit + Audit: Audit, + Glossary: Glossary } const indexesToCreate = { Cve: [{ 'cve.cveMetadata.cveId': 1 }, { 'cve.cveMetadata.dateUpdated': 1 }], 'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }], User: [{ UUID: 1 }], - Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }] + Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }], + Glossary: [{ short_name: 1 }] } // Body Parser Middleware @@ -143,6 +146,7 @@ db.once('open', async () => { await Audit.createCollection() await ReviewObject.createCollection() await Conversation.createCollection() + await Glossary.createCollection() } catch (err) { logger.error('Error creating indexes:', err) } finally { diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js new file mode 100644 index 000000000..dfe65fc87 --- /dev/null +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -0,0 +1,156 @@ +/* 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' } + +const testGlossaryItem = { + short_name: 'test_glossary_item', + label: 'Test Glossary Item', + def: 'The definition of Test Glossary Item' +} + +describe('Testing /glossary endpoints', () => { + context('Testing POST /glossary endpoint', () => { + context('Positive Tests', () => { + it('Creates a new glossary item', async () => { + await chai.request(app) + .post('/api/glossary') + .set(secretariatHeaders) + .send(testGlossaryItem) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('short_name') + expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + + expect(res.body).to.haveOwnProperty('label') + expect(res.body.label).to.equal(testGlossaryItem.label) + + expect(res.body).to.haveOwnProperty('def') + expect(res.body.def).to.equal(testGlossaryItem.def) + }) + }) + }) + context('Negative Tests', () => { + it('Fails to create a new glossary item with an existing short name', async () => { + await chai.request(app) + .post('/api/glossary') + .set(secretariatHeaders) + .send(testGlossaryItem) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0]).to.equal('Glossary item with this short_name already exists') + }) + }) + }) + }) + context('Testing GET /glossary endpoints', () => { + context('Positive Tests', () => { + it('Gets a list of all glossary items', async () => { + await chai.request(app) + .get('/api/glossary') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.glossary).to.be.an('array').that.is.not.empty + }) + }) + it('Gets a glossary item by short name', async () => { + await chai.request(app) + .get(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('label', testGlossaryItem.label) + expect(res.body).to.have.property('short_name', testGlossaryItem.short_name) + expect(res.body).to.have.property('def', testGlossaryItem.def) + }) + }) + }) + context('Negative Tests', () => { + it('Fails to get a glossary item that does not exist', async () => { + await chai.request(app) + .get('/api/glossary/short_name/nonexistent_item') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.message).to.equal('404: resource not found') + }) + }) + }) + }) + context('Testing PUT /glossary endpoint', () => { + context('Positive Tests', () => { + it('Updates a glossary item', async () => { + await chai.request(app) + .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .send({ + ...testGlossaryItem, + label: 'Updated Glossary Item', + def: 'Updated definition' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('short_name') + expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + + expect(res.body).to.haveOwnProperty('label') + expect(res.body.label).to.equal('Updated Glossary Item') + + expect(res.body).to.haveOwnProperty('def') + expect(res.body.def).to.equal('Updated definition') + }) + }) + }) + context('Negative Tests', () => { + it('Fails to update a glossary item changing its short_name', async () => { + await chai.request(app) + .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .send({ + ...testGlossaryItem, + short_name: 'new_short_name' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0]).to.equal('Cannot change short_name through this endpoint.') + }) + }) + }) + }) + context('Testing DELETE /glossary endpoint', () => { + context('Positive Tests', () => { + it('Deletes a glossary item', async () => { + await chai.request(app) + .delete(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.equal('Glossary item deleted') + }) + }) + }) + context('Negative Tests', () => { + it('Fails to delete a glossary item that does not exist', async () => { + await chai.request(app) + .delete('/api/glossary/short_name/nonexistent_item') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.message).to.equal('404: resource not found') + }) + }) + }) + }) +}) From 66dd9c609a078549340b857898b324fa0ac9fc21 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 14:46:17 -0400 Subject: [PATCH 15/40] Added Glossary capability --- datadump/pre-population/glossary.json | 7 ++ .../create-glossary-item-response.json | 17 +++++ schemas/glossary/glossary.json | 4 +- .../glossary.controller.js | 33 +++++---- src/controller/glossary.controller/index.js | 18 ++--- src/model/glossary.js | 4 +- src/repositories/glossaryRepository.js | 14 ++-- src/scripts/populate.js | 8 ++- .../glossary/glossaryCRUDTest.js | 67 +++++++++++++------ 9 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 datadump/pre-population/glossary.json create mode 100644 schemas/glossary/create-glossary-item-response.json diff --git a/datadump/pre-population/glossary.json b/datadump/pre-population/glossary.json new file mode 100644 index 000000000..ac9ccc4e7 --- /dev/null +++ b/datadump/pre-population/glossary.json @@ -0,0 +1,7 @@ +[ + { + "services_short_name": "long_name", + "label": "Long Name", + "def": "The full, official name of an organization participating in the CVE program." + } +] diff --git a/schemas/glossary/create-glossary-item-response.json b/schemas/glossary/create-glossary-item-response.json new file mode 100644 index 000000000..d4c4b2c38 --- /dev/null +++ b/schemas/glossary/create-glossary-item-response.json @@ -0,0 +1,17 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/create-glossary-item-response.json", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "glossary_item_added": { + "$ref": "glossary.json" + } + }, + "required": [ + "message", + "glossary_item_added" + ], + "additionalProperties": false +} diff --git a/schemas/glossary/glossary.json b/schemas/glossary/glossary.json index 0f6bbe633..d5e59f8b0 100644 --- a/schemas/glossary/glossary.json +++ b/schemas/glossary/glossary.json @@ -2,7 +2,7 @@ "$id": "https://cve.mitre.org/api-docs/schema/glossary/glossary.json", "type": "object", "properties": { - "short_name": { + "services_short_name": { "type": "string", "minLength": 1 }, @@ -16,7 +16,7 @@ } }, "required": [ - "short_name", + "services_short_name", "label", "def" ], diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js index 18f0f2a8f..29410395c 100644 --- a/src/controller/glossary.controller/glossary.controller.js +++ b/src/controller/glossary.controller/glossary.controller.js @@ -30,9 +30,9 @@ async function getAllGlossaryItems (req, res, next) { async function getGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name - const result = await glossaryRepo.findOneByShortName(shortName) + const result = await glossaryRepo.findOneByServicesShortName(servicesShortName) if (!result) { return res.status(404).json(error.notFound()) } @@ -55,13 +55,22 @@ async function createGlossaryItem (req, res, next) { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() const glossaryData = req.body - const existing = await glossaryRepo.findOneByShortName(glossaryData.short_name) + const existing = await glossaryRepo.findOneByServicesShortName(glossaryData.services_short_name) if (existing) { - return res.status(400).json(error.badInput(['Glossary item with this short_name already exists'])) + return res.status(400).json(error.badInput(['Glossary item with this services_short_name already exists'])) } - const result = await glossaryRepo.collection.create(glossaryData) - return res.status(200).json(result) + const createdDoc = await glossaryRepo.collection.create(glossaryData) + const result = createdDoc.toObject() + delete result._id + delete result.__v + delete result.createdAt + delete result.updatedAt + + return res.status(200).json({ + message: 'glossary item successfully added', + glossary_item_added: result + }) } catch (err) { next(err) } @@ -78,14 +87,14 @@ async function createGlossaryItem (req, res, next) { async function updateGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name const glossaryData = req.body - if (glossaryData.short_name && glossaryData.short_name !== shortName) { - return res.status(400).json(error.badInput(['Cannot change short_name through this endpoint.'])) + if (glossaryData.services_short_name && glossaryData.services_short_name !== servicesShortName) { + return res.status(400).json(error.badInput(['Cannot change services_short_name through this endpoint.'])) } - const result = await glossaryRepo.updateByShortName(shortName, glossaryData) + const result = await glossaryRepo.updateByServicesShortName(servicesShortName, glossaryData) if (!result) { return res.status(404).json(error.notFound()) } @@ -107,9 +116,9 @@ async function updateGlossaryItem (req, res, next) { async function deleteGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name - const result = await glossaryRepo.deleteByShortName(shortName) + const result = await glossaryRepo.deleteByServicesShortName(servicesShortName) if (!result) { return res.status(404).json(error.notFound()) } diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js index 8d83cc8ca..d510651de 100644 --- a/src/controller/glossary.controller/index.js +++ b/src/controller/glossary.controller/index.js @@ -58,8 +58,8 @@ router.get('/glossary', controller.getAllGlossaryItems ) -// Get glossary item by short_name - SEC only -router.get('/glossary/short_name/:short_name', +// Get glossary item by services_short_name - SEC only +router.get('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossarySingle' @@ -69,7 +69,7 @@ router.get('/glossary/short_name/:short_name',

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves the specified glossary item

" - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', @@ -150,11 +150,11 @@ router.post('/glossary', } } #swagger.responses[200] = { - description: 'Returns the created glossary item', + description: 'Returns the created glossary item wrapped in a success message', content: { "application/json": { schema: { - $ref: '../schemas/glossary/glossary.json' + $ref: '../schemas/glossary/create-glossary-item-response.json' } } } @@ -198,7 +198,7 @@ router.post('/glossary', ) // Update a glossary item - SEC only -router.put('/glossary/short_name/:short_name', +router.put('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossaryUpdate' @@ -208,7 +208,7 @@ router.put('/glossary/short_name/:short_name',

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Updates the specified glossary item

" - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', @@ -281,7 +281,7 @@ router.put('/glossary/short_name/:short_name', ) // Delete a glossary item - SEC only -router.delete('/glossary/short_name/:short_name', +router.delete('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossaryDelete' @@ -291,7 +291,7 @@ router.delete('/glossary/short_name/:short_name',

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Deletes the specified glossary item

" - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', diff --git a/src/model/glossary.js b/src/model/glossary.js index fd9e9a2b5..d06f09a61 100644 --- a/src/model/glossary.js +++ b/src/model/glossary.js @@ -1,14 +1,14 @@ const mongoose = require('mongoose') const schema = { - short_name: { type: String, required: true }, + services_short_name: { type: String, required: true }, label: { type: String, required: true }, def: { type: String, required: true } } const GlossarySchema = new mongoose.Schema(schema, { collection: 'Glossary', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) -GlossarySchema.index({ short_name: 1 }, { unique: true }) +GlossarySchema.index({ services_short_name: 1 }, { unique: true }) const Glossary = mongoose.model('Glossary', GlossarySchema) module.exports = Glossary diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js index 934cce610..3ff74abe4 100644 --- a/src/repositories/glossaryRepository.js +++ b/src/repositories/glossaryRepository.js @@ -7,19 +7,19 @@ class GlossaryRepository extends BaseRepository { } async getAll () { - return this.find({}, { multiple: true }) + return this.collection.find({}, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() } - async findOneByShortName (shortName) { - return this.findOne({ short_name: shortName }) + async findOneByServicesShortName (servicesShortName) { + return this.collection.findOne({ services_short_name: servicesShortName }, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() } - async updateByShortName (shortName, newGlossaryData) { - return this.findOneAndUpdate({ short_name: shortName }, newGlossaryData) + async updateByServicesShortName (servicesShortName, newGlossaryData) { + return this.collection.findOneAndUpdate({ services_short_name: servicesShortName }, newGlossaryData, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }, new: true }).exec() } - async deleteByShortName (shortName) { - return this.collection.findOneAndDelete({ short_name: shortName }) + async deleteByServicesShortName (servicesShortName) { + return this.collection.findOneAndDelete({ services_short_name: servicesShortName }, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 } }).exec() } } diff --git a/src/scripts/populate.js b/src/scripts/populate.js index c70713938..b92659901 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -44,7 +44,7 @@ const indexesToCreate = { 'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }], User: [{ UUID: 1 }], Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }], - Glossary: [{ short_name: 1 }] + Glossary: [{ services_short_name: 1 }] } // Body Parser Middleware @@ -125,6 +125,12 @@ db.once('open', async () => { CveId, dataUtils.newCveIdTransform )) + // Glossary + populatePromises.push(dataUtils.populateCollection( + './datadump/pre-population/glossary.json', + Glossary + )) + // don't close database connection until all remaining populate // promises are resolved Promise.all(populatePromises).then(async function () { diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js index dfe65fc87..921fc3c10 100644 --- a/test/integration-tests/glossary/glossaryCRUDTest.js +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -9,7 +9,7 @@ const app = require('../../../src/index.js') const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } const testGlossaryItem = { - short_name: 'test_glossary_item', + services_short_name: 'test_glossary_item', label: 'Test Glossary Item', def: 'The definition of Test Glossary Item' } @@ -26,14 +26,25 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('short_name') - expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('glossary item successfully added') - expect(res.body).to.haveOwnProperty('label') - expect(res.body.label).to.equal(testGlossaryItem.label) + expect(res.body).to.haveOwnProperty('glossary_item_added') + const item = res.body.glossary_item_added - expect(res.body).to.haveOwnProperty('def') - expect(res.body.def).to.equal(testGlossaryItem.def) + expect(item).to.haveOwnProperty('services_short_name') + expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) + + expect(item).to.haveOwnProperty('label') + expect(item.label).to.equal(testGlossaryItem.label) + + expect(item).to.haveOwnProperty('def') + expect(item.def).to.equal(testGlossaryItem.def) + + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') }) }) }) @@ -46,7 +57,7 @@ describe('Testing /glossary endpoints', () => { .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0]).to.equal('Glossary item with this short_name already exists') + expect(res.body.details[0]).to.equal('Glossary item with this services_short_name already exists') }) }) }) @@ -60,24 +71,35 @@ describe('Testing /glossary endpoints', () => { .then((res) => { expect(res).to.have.status(200) expect(res.body.glossary).to.be.an('array').that.is.not.empty + res.body.glossary.forEach(item => { + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') + }) }) }) it('Gets a glossary item by short name', async () => { await chai.request(app) - .get(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .get(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) expect(res.body).to.have.property('label', testGlossaryItem.label) - expect(res.body).to.have.property('short_name', testGlossaryItem.short_name) + expect(res.body).to.have.property('services_short_name', testGlossaryItem.services_short_name) expect(res.body).to.have.property('def', testGlossaryItem.def) + + expect(res.body).to.not.have.property('_id') + expect(res.body).to.not.have.property('__v') + expect(res.body).to.not.have.property('createdAt') + expect(res.body).to.not.have.property('updatedAt') }) }) }) context('Negative Tests', () => { it('Fails to get a glossary item that does not exist', async () => { await chai.request(app) - .get('/api/glossary/short_name/nonexistent_item') + .get('/api/glossary/nonexistent_item') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) @@ -90,7 +112,7 @@ describe('Testing /glossary endpoints', () => { context('Positive Tests', () => { it('Updates a glossary item', async () => { await chai.request(app) - .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .put(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .send({ ...testGlossaryItem, @@ -101,30 +123,35 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('short_name') - expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + expect(res.body).to.haveOwnProperty('services_short_name') + expect(res.body.services_short_name).to.equal(testGlossaryItem.services_short_name) expect(res.body).to.haveOwnProperty('label') expect(res.body.label).to.equal('Updated Glossary Item') expect(res.body).to.haveOwnProperty('def') expect(res.body.def).to.equal('Updated definition') + + expect(res.body).to.not.have.property('_id') + expect(res.body).to.not.have.property('__v') + expect(res.body).to.not.have.property('createdAt') + expect(res.body).to.not.have.property('updatedAt') }) }) }) context('Negative Tests', () => { - it('Fails to update a glossary item changing its short_name', async () => { + it('Fails to update a glossary item changing its services_short_name', async () => { await chai.request(app) - .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .put(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .send({ ...testGlossaryItem, - short_name: 'new_short_name' + services_short_name: 'new_services_short_name' }) .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0]).to.equal('Cannot change short_name through this endpoint.') + expect(res.body.details[0]).to.equal('Cannot change services_short_name through this endpoint.') }) }) }) @@ -133,7 +160,7 @@ describe('Testing /glossary endpoints', () => { context('Positive Tests', () => { it('Deletes a glossary item', async () => { await chai.request(app) - .delete(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .delete(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -144,7 +171,7 @@ describe('Testing /glossary endpoints', () => { context('Negative Tests', () => { it('Fails to delete a glossary item that does not exist', async () => { await chai.request(app) - .delete('/api/glossary/short_name/nonexistent_item') + .delete('/api/glossary/nonexistent_item') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) From 3a0ef426cef9b441d38d8413048b1846c4d8872e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 14:56:00 -0400 Subject: [PATCH 16/40] Make the returns a little bit better --- .../create-glossary-item-response.json | 4 +-- .../update-glossary-item-response.json | 17 +++++++++++ .../glossary.controller.js | 7 +++-- src/controller/glossary.controller/index.js | 4 +-- .../glossary/glossaryCRUDTest.js | 30 +++++++++++-------- 5 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 schemas/glossary/update-glossary-item-response.json diff --git a/schemas/glossary/create-glossary-item-response.json b/schemas/glossary/create-glossary-item-response.json index d4c4b2c38..c4d5a2394 100644 --- a/schemas/glossary/create-glossary-item-response.json +++ b/schemas/glossary/create-glossary-item-response.json @@ -5,13 +5,13 @@ "message": { "type": "string" }, - "glossary_item_added": { + "created": { "$ref": "glossary.json" } }, "required": [ "message", - "glossary_item_added" + "created" ], "additionalProperties": false } diff --git a/schemas/glossary/update-glossary-item-response.json b/schemas/glossary/update-glossary-item-response.json new file mode 100644 index 000000000..64bc27db6 --- /dev/null +++ b/schemas/glossary/update-glossary-item-response.json @@ -0,0 +1,17 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/update-glossary-item-response.json", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "updated": { + "$ref": "glossary.json" + } + }, + "required": [ + "message", + "updated" + ], + "additionalProperties": false +} diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js index 29410395c..b55d65850 100644 --- a/src/controller/glossary.controller/glossary.controller.js +++ b/src/controller/glossary.controller/glossary.controller.js @@ -69,7 +69,7 @@ async function createGlossaryItem (req, res, next) { return res.status(200).json({ message: 'glossary item successfully added', - glossary_item_added: result + created: result }) } catch (err) { next(err) @@ -99,7 +99,10 @@ async function updateGlossaryItem (req, res, next) { return res.status(404).json(error.notFound()) } - return res.status(200).json(result) + return res.status(200).json({ + message: 'glossary item successfully updated', + updated: result + }) } catch (err) { next(err) } diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js index d510651de..8c374131b 100644 --- a/src/controller/glossary.controller/index.js +++ b/src/controller/glossary.controller/index.js @@ -225,11 +225,11 @@ router.put('/glossary/:services_short_name', } } #swagger.responses[200] = { - description: 'Returns the updated glossary item', + description: 'Returns the updated glossary item wrapped in a success message', content: { "application/json": { schema: { - $ref: '../schemas/glossary/glossary.json' + $ref: '../schemas/glossary/update-glossary-item-response.json' } } } diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js index 921fc3c10..844f4b4c2 100644 --- a/test/integration-tests/glossary/glossaryCRUDTest.js +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -29,8 +29,8 @@ describe('Testing /glossary endpoints', () => { expect(res.body).to.haveOwnProperty('message') expect(res.body.message).to.equal('glossary item successfully added') - expect(res.body).to.haveOwnProperty('glossary_item_added') - const item = res.body.glossary_item_added + expect(res.body).to.haveOwnProperty('created') + const item = res.body.created expect(item).to.haveOwnProperty('services_short_name') expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) @@ -123,19 +123,25 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('services_short_name') - expect(res.body.services_short_name).to.equal(testGlossaryItem.services_short_name) + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('glossary item successfully updated') - expect(res.body).to.haveOwnProperty('label') - expect(res.body.label).to.equal('Updated Glossary Item') + expect(res.body).to.haveOwnProperty('updated') + const item = res.body.updated - expect(res.body).to.haveOwnProperty('def') - expect(res.body.def).to.equal('Updated definition') + expect(item).to.haveOwnProperty('services_short_name') + expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) - expect(res.body).to.not.have.property('_id') - expect(res.body).to.not.have.property('__v') - expect(res.body).to.not.have.property('createdAt') - expect(res.body).to.not.have.property('updatedAt') + expect(item).to.haveOwnProperty('label') + expect(item.label).to.equal('Updated Glossary Item') + + expect(item).to.haveOwnProperty('def') + expect(item.def).to.equal('Updated definition') + + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') }) }) }) From 626fd5ef8804355298cf0828198f2c16bc771202 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:46:53 -0400 Subject: [PATCH 17/40] Unlock for development --- src/controller/org.controller/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index f7805c496..1c5e6f110 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, + // mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG From dfd154069e31a1ac38e020abc151add96b9aeeb0 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 14:46:19 -0400 Subject: [PATCH 18/40] Added restricted fields, changed how data is returned to the user, secretariat only editing fields --- schemas/registry-org/BaseOrg.json | 24 ++++ schemas/registry-org/CNAOrg.json | 7 +- .../get-registry-org-response.json | 28 ++++ .../list-registry-orgs-response.json | 28 ++++ src/constants/index.js | 13 +- src/controller/org.controller/error.js | 7 + .../org.controller/org.controller.js | 11 ++ .../org.controller/org.middleware.js | 30 +++++ .../registry-org.controller/error.js | 7 + .../registry-org.controller.js | 24 +++- .../registry-org.middleware.js | 6 + src/model/baseorg.js | 8 ++ src/repositories/baseOrgRepository.js | 120 +++++++++--------- .../org/registryOrgAsOrgAdmin.js | 15 +++ .../registry-org/registryOrgCRUDTest.js | 26 ++++ 15 files changed, 288 insertions(+), 66 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index d2a42bbf5..63f81c9ac 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -133,6 +133,30 @@ } }, "additionalProperties": false + }, + "partner_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + } + }, + "additionalProperties": false } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 367302530..fc8fc5cc9 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -15,6 +15,7 @@ "minLength": 2, "maxLength": 32 }, + "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, "contact_info": { "type": "object", "properties": { @@ -64,12 +65,16 @@ "partner_role": { "type": "string" }, + "partner_number": { + "type": "string" + }, "partner_type": { "type": "string" }, "partner_country": { "type": "string" - } + }, + "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } }, "required": ["short_name", "hard_quota"] } \ No newline at end of file diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 79ac6a30d..b5390cbaa 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -109,6 +109,10 @@ "type": "string", "description": "Role of the partner" }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, "partner_type": { "type": "string", "description": "Type of the partner" @@ -117,6 +121,30 @@ "type": "string", "description": "Country of the partner" }, + "partner_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + } + }, + "description": "Additional partner metadata (restricted)" + }, "vulnerability_advisory_locations": { "type": "array", "items": { diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index c578c6f7a..93eacb702 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -138,6 +138,10 @@ "type": "string", "description": "Role of the partner" }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, "partner_type": { "type": "string", "description": "Type of the partner" @@ -146,6 +150,30 @@ "type": "string", "description": "Country of the partner" }, + "partner_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + } + }, + "description": "Additional partner metadata (restricted)" + }, "vulnerability_advisory_locations": { "type": "array", "items": { diff --git a/src/constants/index.js b/src/constants/index.js index 69f295e9f..40241631a 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,8 +44,19 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_type', 'partner_country', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], + ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], + ORG_RESTRICTED_FIELDS: ['partner_data'], + SECRETARIAT_ONLY_FIELDS: [ + 'partner_number', + 'partner_data', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status' + ], USER_ROLE_ENUM: { ADMIN: 'ADMIN' }, diff --git a/src/controller/org.controller/error.js b/src/controller/org.controller/error.js index 4c4df5fd3..cde82381f 100644 --- a/src/controller/org.controller/error.js +++ b/src/controller/org.controller/error.js @@ -91,6 +91,13 @@ class OrgControllerError extends idrErr.IDRError { err.message = 'The requested user can not be created and added to the organization because the organization has hit its limit of 100 users. Contact the Secretariat.' return err } + + secretariatOnlyEditing (fields) { + const err = {} + err.error = 'SECRETARIAT_ONLY' + err.message = `The following fields can only be modified by the Secretariat: ${fields.join(', ')}.` + return err + } } module.exports = { diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 297887707..40f58c0f9 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -5,6 +5,7 @@ const getConstants = require('../../constants').getConstants const errors = require('./error') const error = new errors.OrgControllerError() const validateUUID = require('uuid').validate +const _ = require('lodash') /** * Get the details of all orgs. @@ -353,6 +354,16 @@ async function updateOrg (req, res, next) { const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) const isSecretariat = await orgRepository.isSecretariatByShortName(req.ctx.org, { session }) const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + + if (!isSecretariat) { + const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS + const restrictedFieldsSent = secretariatOnlyFields.filter(field => _.has(queryParametersJson, field)) + if (restrictedFieldsSent.length > 0) { + logger.info({ uuid: req.ctx.uuid, message: `Non-secretariat attempted to edit restricted fields: ${restrictedFieldsSent.join(', ')}` }) + await session.abortTransaction() + return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) + } + } const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, !req.useRegistry, requestingUserUUID, isAdmin, isSecretariat) responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 204f6066b..78bdba0b5 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -60,6 +60,16 @@ function validateCreateOrgParameters () { body(['is_cna_discussion_list']) .default(false) .isBoolean(), + body([ + 'partner_data.cve_website_update_date', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date' + ]) + .optional({ nullable: true }) + .isDate(), + body(['partner_data.cve_website_update_needed']) + .optional() + .isBoolean(), body( [ 'charter_or_scope', @@ -71,8 +81,10 @@ function validateCreateOrgParameters () { 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.status', 'industry' ]) .default('') @@ -135,8 +147,14 @@ function validateCreateOrgParameters () { 'contact_info.additional_contact_users', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', @@ -220,8 +238,14 @@ function validateUpdateOrgParameters () { 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', @@ -309,8 +333,14 @@ const QUERY_PARAMETERS = { 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js index d4af5f1e6..aa54d2fc1 100644 --- a/src/controller/registry-org.controller/error.js +++ b/src/controller/registry-org.controller/error.js @@ -119,6 +119,13 @@ class RegistryOrgControllerError extends idrErr.IDRError { err.message = 'Parameters were invalid: conversation must be an object with a body.' return err } + + secretariatOnlyEditing (fields) { + const err = {} + err.error = 'SECRETARIAT_ONLY' + err.message = `The following fields can only be modified by the Secretariat: ${fields.join(', ')}.` + return err + } } module.exports = { diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 64da4d038..676a10356 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -37,7 +37,7 @@ async function getAllOrgs (req, res, next) { options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value try { - returnValue = await repo.getAllOrgs({ ...options }) + returnValue = await repo.getAllOrgs({ ...options }, false, isSecretariat) // fetch conversations for (let i = 0; i < returnValue.organizations.length; i++) { const conversation = await conversationRepo.getAllByTargetUUID(returnValue.organizations[i].UUID, isSecretariat) @@ -89,8 +89,7 @@ async function getOrg (req, res, next) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } - - returnValue = await repo.getOrg(identifier, identifierIsUUID) + returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, false, isSecretariat) if (returnValue) { // fetch conversation @@ -145,6 +144,15 @@ async function createOrg (req, res, next) { return res.status(400).json(error.uuidProvided('org')) } + if (!isSecretariat) { + const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS + const restrictedFieldsSent = secretariatOnlyFields.filter(field => _.has(body, field)) + if (restrictedFieldsSent.length > 0) { + logger.info({ uuid: req.ctx.uuid, message: `Non-secretariat attempted to edit restricted fields: ${restrictedFieldsSent.join(', ')}` }) + return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) + } + } + try { session.startTransaction() const result = repo.validateOrg(body, { session }) @@ -258,6 +266,16 @@ async function updateOrg (req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } + if (!isSecretariat) { + const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS + const restrictedFieldsSent = secretariatOnlyFields.filter(field => _.has(body, field)) + if (restrictedFieldsSent.length > 0) { + logger.info({ uuid: req.ctx.uuid, message: `Non-secretariat attempted to edit restricted fields: ${restrictedFieldsSent.join(', ')}` }) + await session.abortTransaction() + return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) + } + } + // Edge Case: if a user has requested an org, but it is not approved yet, then we need to check to see if if there is a review org for the shortname request. if (!org) { diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 3b1c51a81..e51511ffb 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -18,8 +18,14 @@ function parsePostParams (req, res, next) { 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.admins', 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index daff859fc..f73218164 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -23,8 +23,16 @@ const schema = { website: String }, partner_role: String, + partner_number: String, partner_type: String, partner_country: String, + partner_data: { + cve_website_update_date: Date, + cve_website_update_needed: Boolean, + partner_active_date: Date, + partner_inactive_date: Date, + status: String + }, vulnerability_advisory_locations: [String], advisory_location_require_credentials: Boolean, industry: String, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index a95065e7a..d296c41c5 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -49,7 +49,23 @@ function setAggregateOrgObj (query) { * @param {object} query - The query object to match. * @returns {Array} The aggregation pipeline. */ -function setAggregateRegistryOrgObj (query) { +function setAggregateRegistryOrgObj (query, isSecretariat = false) { + const CONSTANTS = getConstants() + const projection = { + _id: false, + __t: false, + __v: false, + inUse: false, + in_use: false, + parentOrg: false + } + + if (!isSecretariat) { + CONSTANTS.ORG_RESTRICTED_FIELDS.forEach(field => { + projection[field] = false + }) + } + return [ { $match: query @@ -74,17 +90,35 @@ function setAggregateRegistryOrgObj (query) { } }, { - $project: { - _id: false, - __t: false, - inUse: false, - in_use: false, - parentOrg: false - } + $project: projection } ] } +function getOrgProjection (isSecretariat = false) { + const CONSTANTS = getConstants() + const projection = {} + CONSTANTS.ORG_EXCLUDED_FIELDS.forEach(field => { + projection[field] = 0 + }) + if (!isSecretariat) { + CONSTANTS.ORG_RESTRICTED_FIELDS.forEach(field => { + projection[field] = 0 + }) + } + return projection +} + +function filterOrg (orgObj, isSecretariat = false) { + const CONSTANTS = getConstants() + const _ = require('lodash') + let fieldsToOmit = [...CONSTANTS.ORG_EXCLUDED_FIELDS] + if (!isSecretariat) { + fieldsToOmit = [...fieldsToOmit, ...CONSTANTS.ORG_RESTRICTED_FIELDS] + } + return _.omit(orgObj, fieldsToOmit) +} + class BaseOrgRepository extends BaseRepository { constructor () { super(BaseOrg) @@ -115,11 +149,11 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns the legacy format. * @returns {Promise} The organization object. */ - async findOneByShortName (shortName, options = {}, returnLegacyFormat = false) { + async findOneByShortName (shortName, options = {}, returnLegacyFormat = false, projection = {}) { const OrgRepository = require('./orgRepository') const legacyOrgRepo = new OrgRepository() if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options) - const data = await BaseOrgModel.findOne({ short_name: shortName }, null, options) + const data = await BaseOrgModel.findOne({ short_name: shortName }, projection, options) return data } @@ -132,11 +166,11 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns the legacy format. * @returns {Promise} The organization object. */ - async findOneByUUID (UUID, options = {}, returnLegacyFormat = false) { + async findOneByUUID (UUID, options = {}, returnLegacyFormat = false, projection = {}) { const OrgRepository = require('./orgRepository') const legacyOrgRepo = new OrgRepository() if (returnLegacyFormat) return await legacyOrgRepo.findOneByUUID(UUID, options) - return await BaseOrgModel.findOne({ UUID: UUID }, null, options) + return await BaseOrgModel.findOne({ UUID: UUID }, projection, options) } /** @@ -261,7 +295,7 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns data in legacy format. * @returns {Promise} Paginated result containing organizations and metadata. */ - async getAllOrgs (options = {}, returnLegacyFormat = false) { + async getAllOrgs (options = {}, returnLegacyFormat = false, isSecretariat = false) { const OrgRepository = require('./orgRepository') const orgRepo = new OrgRepository() let pg @@ -269,7 +303,7 @@ class BaseOrgRepository extends BaseRepository { const agt = setAggregateOrgObj({}) pg = await orgRepo.aggregatePaginate(agt, options) } else { - const agt = setAggregateRegistryOrgObj({}) + const agt = setAggregateRegistryOrgObj({}, isSecretariat) pg = await this.aggregatePaginate(agt, options) } @@ -322,11 +356,12 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns legacy format. * @returns {Promise} The sanitized organization object. */ - async getOrg (identifier, identifierIsUUID = false, options = {}, returnLegacyFormat = false) { + async getOrg (identifier, identifierIsUUID = false, options = {}, returnLegacyFormat = false, isSecretariat = false) { const { deepRemoveEmpty } = require('../utils/utils') + const projection = getOrgProjection(isSecretariat) const data = identifierIsUUID - ? await this.findOneByUUID(identifier, options, returnLegacyFormat) - : await this.findOneByShortName(identifier, options, returnLegacyFormat) + ? await this.findOneByUUID(identifier, options, returnLegacyFormat, projection) + : await this.findOneByShortName(identifier, options, returnLegacyFormat, projection) if (!data) return null const result = data.toObject() @@ -335,11 +370,6 @@ class BaseOrgRepository extends BaseRepository { result.reports_to = parentOrg.UUID } - delete result.__t - delete result.__v - delete result._id - delete result.inUse - delete result.in_use return deepRemoveEmpty(result) } @@ -534,22 +564,11 @@ class BaseOrgRepository extends BaseRepository { // Convert the actual model, back to a json model const legacyObjectRawJson = postUpdate.toObject() - // Remove private stuff - delete legacyObjectRawJson.__v - delete legacyObjectRawJson._id - delete legacyObjectRawJson.inUse - delete legacyObjectRawJson.in_use - return deepRemoveEmpty(legacyObjectRawJson) + return filterOrg(deepRemoveEmpty(legacyObjectRawJson), isSecretariat) } const rawRegistryOrgObject = registryObject.toObject() - delete rawRegistryOrgObject.__t - delete rawRegistryOrgObject.__v - delete rawRegistryOrgObject._id - delete rawRegistryOrgObject.inUse - delete rawRegistryOrgObject.in_use - - return deepRemoveEmpty(rawRegistryOrgObject) + return filterOrg(deepRemoveEmpty(rawRegistryOrgObject), isSecretariat) } /** @@ -662,6 +681,7 @@ class BaseOrgRepository extends BaseRepository { 'reports_to', 'contact_info', // Handles all nested contact_info fields automatically 'partner_role', + 'partner_number', 'partner_type', 'partner_country', 'vulnerability_advisory_locations', @@ -735,22 +755,10 @@ class BaseOrgRepository extends BaseRepository { await legacyOrg.save(options) await registryOrg.save(options) if (isLegacyObject) { - const plainJavascriptLegacyOrg = legacyOrg.toObject() - delete plainJavascriptLegacyOrg.__v - delete plainJavascriptLegacyOrg._id - delete plainJavascriptLegacyOrg.inUse - delete plainJavascriptLegacyOrg.in_use - return deepRemoveEmpty(plainJavascriptLegacyOrg) + return filterOrg(deepRemoveEmpty(legacyOrg.toObject()), isSecretariat) } - const plainJavascriptRegistryOrg = registryOrg.toObject() - // Remove private things - delete plainJavascriptRegistryOrg.__v - delete plainJavascriptRegistryOrg._id - delete plainJavascriptRegistryOrg.__t - delete plainJavascriptRegistryOrg.inUse - delete plainJavascriptRegistryOrg.in_use - return deepRemoveEmpty(plainJavascriptRegistryOrg) + return filterOrg(deepRemoveEmpty(registryOrg.toObject()), isSecretariat) } /** @@ -968,24 +976,14 @@ class BaseOrgRepository extends BaseRepository { if (isLegacyObject) { const plainJavascriptLegacyOrg = updatedLegacyOrg.toObject() - delete plainJavascriptLegacyOrg.__v - delete plainJavascriptLegacyOrg._id - delete plainJavascriptLegacyOrg.inUse - delete plainJavascriptLegacyOrg.in_use plainJavascriptLegacyOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) - return deepRemoveEmpty(plainJavascriptLegacyOrg) + return filterOrg(deepRemoveEmpty(plainJavascriptLegacyOrg), isSecretariat) } const plainJavascriptRegistryOrg = updatedRegistryOrg.toObject() plainJavascriptRegistryOrg.conversation = conversationArray - // Remove private things - delete plainJavascriptRegistryOrg.__v - delete plainJavascriptRegistryOrg._id - delete plainJavascriptRegistryOrg.__t - delete plainJavascriptRegistryOrg.inUse - delete plainJavascriptRegistryOrg.in_use plainJavascriptRegistryOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) - return deepRemoveEmpty(plainJavascriptRegistryOrg) + return filterOrg(deepRemoveEmpty(plainJavascriptRegistryOrg), isSecretariat) } /** diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index ef2356bce..6ec234157 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -377,6 +377,21 @@ describe('Testing Registry Org as org admin', () => { expect(res.body.error).to.be.equal('SECRETARIAT_ONLY') }) }) + it('Registry: Services api does not allow org admins to update their own orgs partner_data', async () => { + await chai.request(app) + .put('/api/registry/org/beat_10') + .set(adminHeaders) + .send({ + partner_data: { + status: 'active' + } + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(403) + expect(res.body.error).to.be.equal('SECRETARIAT_ONLY') + }) + }) it('Registry: Services api does not allow org admins to create other orgs', async () => { await chai.request(app) .post('/api/registry/org') diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index a36b6a419..695d8d616 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -14,6 +14,7 @@ const testRegistryOrg = { authority: ['CNA'], hard_quota: 1000, partner_role: 'Initial Partner Role', + partner_number: 'Initial Partner Number', partner_type: 'Initial Partner Type', partner_country: 'US' } @@ -53,6 +54,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('partner_role') expect(res.body.created.partner_role).to.equal(testRegistryOrg.partner_role) + expect(res.body.created).to.haveOwnProperty('partner_number') + expect(res.body.created.partner_number).to.equal(testRegistryOrg.partner_number) + expect(res.body.created).to.haveOwnProperty('partner_type') expect(res.body.created.partner_type).to.equal(testRegistryOrg.partner_type) @@ -147,6 +151,7 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body).to.have.property('short_name', createdOrg.short_name) expect(res.body.authority).to.be.an('array').that.includes('CNA') expect(res.body).to.have.property('partner_role', createdOrg.partner_role) + expect(res.body).to.have.property('partner_number', createdOrg.partner_number) expect(res.body).to.have.property('partner_type', createdOrg.partner_type) expect(res.body).to.have.property('partner_country', createdOrg.partner_country) }) @@ -226,6 +231,7 @@ describe('Testing /registryOrg endpoints', () => { ...createdOrg, long_name: 'Registry Org Test Updated', partner_role: 'Updated Partner Role', + partner_number: 'Updated Partner Number', partner_type: 'Updated Partner Type', partner_country: 'UK' }) @@ -256,6 +262,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated).to.haveOwnProperty('partner_role') expect(res.body.updated.partner_role).to.equal('Updated Partner Role') + expect(res.body.updated).to.haveOwnProperty('partner_number') + expect(res.body.updated.partner_number).to.equal('Updated Partner Number') + expect(res.body.updated).to.haveOwnProperty('partner_type') expect(res.body.updated.partner_type).to.equal('Updated Partner Type') @@ -263,6 +272,23 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated.partner_country).to.equal('UK') }) }) + it('Allows Secretariat to update partner_data', async () => { + await chai.request(app) + .put('/api/registry/org/registry_org_test') + .set(secretariatHeaders) + .send({ + ...createdOrg, + partner_data: { + status: 'active' + } + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.updated).to.haveOwnProperty('partner_data') + expect(res.body.updated.partner_data.status).to.equal('active') + }) + }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { // First create a temporary org const tempOrg = { From b3a57258430ec6216f73adb9302241fecccd9a19 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 16:00:53 -0400 Subject: [PATCH 19/40] Adding root org --- schemas/registry-org/BaseOrg.json | 3 +- schemas/registry-org/RootOrg.json | 60 +++++++++++ .../create-registry-org-request.json | 2 +- .../update-registry-org-request.json | 2 +- src/constants/index.js | 4 +- src/model/rootorg.js | 32 ++++++ src/repositories/baseOrgRepository.js | 21 +++- .../audit/registryOrgCreatesAuditTest.js | 2 +- .../org/roleValidationTest.js | 4 +- .../registry-org/rootOrgTest.js | 102 ++++++++++++++++++ 10 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 schemas/registry-org/RootOrg.json create mode 100644 src/model/rootorg.js create mode 100644 test/integration-tests/registry-org/rootOrgTest.js diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 63f81c9ac..df094ec13 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -39,7 +39,8 @@ "CNA", "SECRETARIAT", "BULK_DOWNLOAD", - "ADP" + "ADP", + "ROOT" ] } }, diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json new file mode 100644 index 000000000..fa7b27e51 --- /dev/null +++ b/schemas/registry-org/RootOrg.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "RootOrg", + "type": "object", + "title": "CVE Root Organization", + "description": "Schema for a CVE Root Organization", + "additionalProperties": false, + "properties": { + "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, + "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, + "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "new_short_name": { + "description": "Used to rename an organization's short name during an update.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, + "contact_info": { + "type": "object", + "properties": { + "poc": { "type": "string" }, + "poc_email": { "type": "string" }, + "poc_phone": { "type": "string" }, + "org_email": { "type": "string" }, + "website": { "type": "string" } + }, + "additionalProperties": false + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/authority" + } + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + }, + "partner_role": { + "type": "string" + }, + "partner_number": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" + }, + "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } + }, + "required": ["short_name"] +} diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 9ce81dc61..51a0c7ef1 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -24,7 +24,7 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT"] + "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT", "ROOT"] } }, "oversees": { diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 38d210e96..9c94fc290 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -32,7 +32,7 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"] + "enum": ["CNA", "ADP", "Secretariat", "ROOT"] } } }, diff --git a/src/constants/index.js b/src/constants/index.js index 40241631a..26c6afe10 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -31,14 +31,14 @@ function getConstants () { SECRETARIAT: 'SECRETARIAT', CNA: 'CNA', BULK_DOWNLOAD: 'BULK_DOWNLOAD', - ROOT_CNA: 'ROOT_CNA', + ROOT: 'ROOT', ADP: 'ADP' }, ORG_ROLES: [ 'CNA', 'SECRETARIAT', 'BULK_DOWNLOAD', - 'ROOT_CNA', + 'ROOT', 'ADP' ], USER_ROLES: [ diff --git a/src/model/rootorg.js b/src/model/rootorg.js new file mode 100644 index 000000000..8a798c8e3 --- /dev/null +++ b/src/model/rootorg.js @@ -0,0 +1,32 @@ +const mongoose = require('mongoose') +const BaseOrg = require('./baseorg') +const fs = require('fs') +const Ajv = require('ajv') +const addFormats = require('ajv-formats') +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const RootOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/RootOrg.json')) +const ajv = new Ajv({ allErrors: true }) +addFormats(ajv) +ajv.addSchema(BaseOrgSchema) + +const validate = ajv.compile(RootOrgSchema) + +const schema = { + authority: [String], + oversees: [String] +} + +const options = { discriminatorKey: 'kind' } +const ROOTSchema = new mongoose.Schema(schema, options) +ROOTSchema.statics.validateOrg = function (record) { + const validateObject = {} + validateObject.isValid = validate(record) + + if (!validateObject.isValid) { + validateObject.errors = validate.errors + } + return validateObject +} +const RootOrg = BaseOrg.discriminator('RootOrg', ROOTSchema, options) + +module.exports = RootOrg diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index d296c41c5..6a9c2c72f 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -4,6 +4,7 @@ const CNAOrgModel = require('../model/cnaorg') const ADPOrgModel = require('../model/adporg') const BulkDownloadModel = require('../model/bulkdownloadorg') const SecretariatOrgModel = require('../model/secretariatorg') +const RootOrgModel = require('../model/rootorg') const CveIdRepository = require('./cveIdRepository') const uuid = require('uuid') const _ = require('lodash') @@ -507,9 +508,16 @@ class BaseOrgRepository extends BaseRepository { } else { await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) } + } else if (registryObjectRaw.authority.includes('ROOT')) { + const rootObjectToSave = new RootOrgModel(registryObjectRaw) + if (isSecretariat) { + registryObject = await rootObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) + } } else { // Throw an Error instance so callers can catch and handle it properly - throw new Error("Unknown Org type requested. Please use either 'SECRETARIAT', 'CNA', 'ADP', or 'BULK_DOWNLOAD' as the authority role.") + throw new Error("Unknown Org type requested. Please use either 'SECRETARIAT', 'CNA', 'ADP', 'BULK_DOWNLOAD', or 'ROOT' as the authority role.") } // ADD AUDIT ENTRY AUTOMATICALLY for the registry object @@ -653,6 +661,8 @@ class BaseOrgRepository extends BaseRepository { TargetModel = ADPOrgModel } else if (finalRoles.includes('BULK_DOWNLOAD')) { TargetModel = BulkDownloadModel + } else if (finalRoles.includes('ROOT')) { + TargetModel = RootOrgModel } // Save changes - handle possible model type change @@ -950,6 +960,8 @@ class BaseOrgRepository extends BaseRepository { TargetModel = ADPOrgModel } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { TargetModel = BulkDownloadModel + } else if (updatedRegistryOrg.authority?.includes('ROOT')) { + TargetModel = RootOrgModel } // If the model type has changed, replace the document with a new one of the correct type @@ -1034,6 +1046,10 @@ class BaseOrgRepository extends BaseRepository { org.authority = ['BULK_DOWNLOAD'] validateObject = BulkDownloadModel.validateOrg(org) } + if (org.authority.includes('ROOT')) { + org.authority = ['ROOT'] + validateObject = RootOrgModel.validateOrg(org) + } } } else { if (org.authority === 'ADP') { @@ -1042,6 +1058,9 @@ class BaseOrgRepository extends BaseRepository { if (org.authority === 'SECRETARIAT') { validateObject = SecretariatOrgModel.validateOrg(org) } + if (org.authority === 'ROOT') { + validateObject = RootOrgModel.validateOrg(org) + } // We will default to CNA if a type is not given if (org.authority === 'CNA' || !org.authority) { validateObject = CNAOrgModel.validateOrg(org) diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js index 74cfc8fb2..92adb7849 100644 --- a/test/integration-tests/audit/registryOrgCreatesAuditTest.js +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -25,7 +25,7 @@ async function createTestOrg (customProps = {}) { .post('/api/registry/org') .set(secretariatHeaders) .send(orgData) - + if (res.status === 500) console.log('500 ERROR:', JSON.stringify(res.body, null, 2)) expect(res).to.have.status(200) return { diff --git a/test/integration-tests/org/roleValidationTest.js b/test/integration-tests/org/roleValidationTest.js index aca35c817..929387b42 100644 --- a/test/integration-tests/org/roleValidationTest.js +++ b/test/integration-tests/org/roleValidationTest.js @@ -32,8 +32,8 @@ describe('BaseOrgRepository Role Validation', () => { it('should add valid roles to an organization', async () => { // Setup: assume MITRE is CNA. Let's try to add ROOT_CNA if not present, or just ensure it accepts valid enums. - // CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA - const validRole = 'ROOT_CNA' + // CONSTANTS.AUTH_ROLE_ENUM.ROOT + const validRole = 'ROOT' const res = await chai.request(app) .put(`/api/org/${orgShortName}?active_roles.add=${validRole}`) diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js new file mode 100644 index 000000000..c78896ad8 --- /dev/null +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -0,0 +1,102 @@ +/* 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' } +// Create headers for a root admin (we'll create this user in the test) +let rootAdminHeaders + +const testRootOrg = { + short_name: 'root_org_test_4', + long_name: 'Root Org Test', + authority: ['ROOT'] +} +let createdOrg + +describe('Testing ROOT Organization Type', () => { + context('Creating a ROOT org', () => { + it('Secretariat creates a new ROOT org (without quotas)', async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(testRootOrg) + .then((res, err) => { + expect(err).to.be.undefined + 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']) + createdOrg = res.body.created + delete createdOrg.created + delete createdOrg.last_updated + }) + }) + + it('Fails to create ROOT org if quotas are provided', async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRootOrg, + short_name: 'root_org_fail', + hard_quota: 100 + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) + }) + + context('ROOT admin permissions', () => { + before(async () => { + // Create a Root Admin user + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/user`) + .set(secretariatHeaders) + .send({ + username: 'root_admin_test', + name: { + first: 'Root', + last: 'Admin' + }, + role: 'ADMIN' + }) + .then((res) => { + rootAdminHeaders = { + 'content-type': 'application/json', + 'CVE-API-ORG': testRootOrg.short_name, + 'CVE-API-USER': 'root_admin_test', + 'CVE-API-KEY': res.body.created.secret + } + }) + }) + + it('ROOT admin can update their own org', async () => { + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(rootAdminHeaders) + .send({ + ...createdOrg, + long_name: 'Updated Root Org Test' + }) + .then((res) => { + if (res.status === 400) console.log(JSON.stringify(res.body, null, 2)) + expect(res).to.have.status(200) + }) + }) + + it('ROOT admin cannot reserve CVE IDs', async () => { + await chai.request(app) + .post('/api/cve-id') + .set(rootAdminHeaders) + .query({ amount: 1, cve_year: 2026, short_name: testRootOrg.short_name }) + .then((res) => { + expect(res).to.have.status(403) + }) + }) + }) +}) From 5df3607c974396bfbacce213ae54f6d9e2e51c39 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 16:09:27 -0400 Subject: [PATCH 20/40] updating root_or_tlr to top level root --- api-docs/openapi.json | 2 +- schemas/registry-org/BaseOrg.json | 4 ++-- schemas/registry-org/create-registry-org-request.json | 4 ++-- schemas/registry-org/create-registry-org-response.json | 4 ++-- schemas/registry-org/get-registry-org-response.json | 4 ++-- schemas/registry-org/list-registry-orgs-response.json | 4 ++-- schemas/registry-org/update-registry-org-request.json | 4 ++-- schemas/registry-org/update-registry-org-response.json | 4 ++-- src/constants/index.js | 2 +- src/controller/org.controller/index.js | 2 +- src/controller/org.controller/org.middleware.js | 10 +++++----- .../registry-org.controller/registry-org.middleware.js | 2 +- src/model/baseorg.js | 2 +- src/repositories/baseOrgRepository.js | 4 ++-- src/scripts/migrate.js | 6 +++--- .../conversation/editConversationTest.js | 2 +- 16 files changed, 30 insertions(+), 30 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index eaf2feb02..39f3a9372 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2605,7 +2605,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)", - "description": "

Access Control

User must belong to an organization with the Secretariat role temporarily.

In the future, only the organization's admin will be able to request changes to its information.

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
  • root_or_tlr
  • charter_or_scope
  • product_list
  • disclosure_policy
  • contact_info.poc
  • contact_info.poc_email
  • contact_info.poc_phone
  • contact_info.org_email
  • partner_role
  • partner_type
  • partner_country
  • vulnerability_advisory_locations
  • advisory_location_require_credentials
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", + "description": "

Access Control

User must belong to an organization with the Secretariat role temporarily.

In the future, only the organization's admin will be able to request changes to its information.

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.poc
  • contact_info.poc_email
  • contact_info.poc_phone
  • contact_info.org_email
  • partner_role
  • partner_type
  • partner_country
  • vulnerability_advisory_locations
  • advisory_location_require_credentials
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", "operationId": "orgUpdateSingle", "parameters": [ { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index df094ec13..72770ecdc 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -71,8 +71,8 @@ "$ref": "#/definitions/authority" } }, - "root_or_tlr": { - "type": "boolean" + "top_level_root": { + "type": "string" }, "reports_to": { "$ref": "#/definitions/uuidType" diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 51a0c7ef1..10c182b50 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -34,8 +34,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 6f0bfb0ec..43dd86f1c 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -60,8 +60,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index b5390cbaa..ce4e5720e 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -37,8 +37,8 @@ }, "description": "The organization's function within the CVE program" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 93eacb702..12f6d6622 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -66,8 +66,8 @@ }, "description": "The organization's function within the CVE program" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 9c94fc290..40bdb7bef 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -45,8 +45,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index cbf41d925..6494839f2 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -55,8 +55,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/src/constants/index.js b/src/constants/index.js index 26c6afe10..c649441f4 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['partner_data'], diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 1c5e6f110..95e314b0f 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -548,7 +548,7 @@ router.put('/registry/org/:shortname',
  • authority
  • aliases
  • oversees
  • -
  • root_or_tlr
  • +
  • top_level_root
  • charter_or_scope
  • product_list
  • disclosure_policy
  • diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 78bdba0b5..08e1b8164 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -46,8 +46,8 @@ function validateCreateOrgParameters () { .isIn(orgOptions), body(['oversees']).default([]) .isArray(), - body(['root_or_tlr']).default(false) - .isBoolean(), + body(['top_level_root']).default('') + .isString(), body(['vulnerability_advisory_locations']) .default([]) .custom(isFlatStringArray), @@ -129,7 +129,7 @@ function validateCreateOrgParameters () { 'contact_info.admins', 'in_use', 'created', - 'root_or_tlr', + 'top_level_root', 'soft_quota', 'aliases', 'hard_quota', @@ -227,7 +227,7 @@ function validateUpdateOrgParameters () { if (useRegistry) { validations.push( query(['oversees']).optional().isArray(), - query(['root_or_tlr']).optional().isBoolean(), + query(['top_level_root']).optional().isString(), query([ 'charter_or_scope', 'disclosure_policy', @@ -321,7 +321,7 @@ const QUERY_PARAMETERS = { ], // Registry-only parameters registryOnly: [ - 'root_or_tlr', + 'top_level_root', 'charter_or_scope', 'disclosure_policy', 'product_list', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index e51511ffb..e981733fa 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -12,7 +12,7 @@ function parsePostParams (req, res, next) { 'long_name', 'short_name', 'aliases', 'cve_program_org_function', 'authority.active_roles', 'oversees', - 'root_or_tlr', 'users', + 'top_level_root', 'users', 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index f73218164..e405d39f9 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -11,7 +11,7 @@ const schema = { short_name: String, aliases: [String], authority: [String], - root_or_tlr: Boolean, + top_level_root: String, users: { type: [String], set: toUndefined }, admins: [String], contact_info: { diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 6a9c2c72f..517f7e810 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -592,7 +592,7 @@ class BaseOrgRepository extends BaseRepository { * @param {string[]} [incomingParameters.active_roles.add] - An array of role strings to add. * @param {string[]} [incomingParameters.active_roles.remove] - An array of role strings to remove. * @param {number} [incomingParameters.id_quota] - The ID quota for the organization. (Applied to legacy and CNA-type registry orgs) - * @param {string} [incomingParameters.root_or_tlr] - The root or Top-Level Root (TLR) status. (Registry only) + * @param {string} [incomingParameters.top_level_root] - The root or Top-Level Root (TLR) status. (Registry only) * @param {string} [incomingParameters.charter_or_scope] - The charter or scope description. (Registry only) * @param {string} [incomingParameters.disclosure_policy] - The disclosure policy. (Registry only) * @param {string[]} [incomingParameters.product_list] - A list of the organization's products. (Registry only) @@ -683,7 +683,7 @@ class BaseOrgRepository extends BaseRepository { _.set(legacyOrg, 'authority.active_roles', finalRoles) const directRegistryKeys = [ - 'root_or_tlr', + 'top_level_root', 'charter_or_scope', 'disclosure_policy', 'product_list', diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index a0097c1d6..b4dc9ee0b 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -101,7 +101,7 @@ async function addCVEBoard (db) { authority: null, reports_to: null, oversees: [mitreUUID], - root_or_tlr: true, + top_level_root: 'true', users: null, charter_or_scope: null, disclosure_policy: null, @@ -174,7 +174,7 @@ async function orgHelper (db) { // parent = mitreUUID // } - // Set root_or_tlr, charter_or_scope, disclosure_policy, org_email, website + // Set top_level_root, charter_or_scope, disclosure_policy, org_email, website let rootTlr = false let charterScope = null let disclosure = null @@ -206,7 +206,7 @@ async function orgHelper (db) { authority: doc.authority.active_roles, reports_to: parent, oversees: children, - root_or_tlr: rootTlr, + top_level_root: rootTlr ? 'true' : 'false', users: orgUsers, charter_or_scope: charterScope, disclosure_policy: disclosure, diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 08e101f6e..dbb59eda7 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -31,7 +31,7 @@ describe('Testing Conversation endpoints', () => { delete org.last_updated delete org.admins delete org.users - delete org.root_or_tlr + delete org.top_level_root }) await chai From 2a80e4d14c82d0390789127be171e3fdfad1e51a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 12:25:29 -0400 Subject: [PATCH 21/40] Partner type / cna active / inactive date --- api-docs/openapi.json | 2 +- schemas/registry-org/BaseOrg.json | 15 +++ schemas/registry-org/CNAOrg.json | 7 +- schemas/registry-org/RootOrg.json | 7 +- .../get-registry-org-response.json | 9 +- .../list-registry-orgs-response.json | 9 +- src/constants/index.js | 8 +- src/controller/org.controller/index.js | 3 +- .../org.controller/org.middleware.js | 18 +-- .../registry-org.middleware.js | 5 +- src/model/baseorg.js | 3 +- src/repositories/baseOrgRepository.js | 51 +++++++- .../registry-org/registryOrgCRUDTest.js | 109 +++++++++++------- .../registry-org/rootOrgTest.js | 29 +++++ test/integration-tests/user/updateUserTest.js | 1 + 15 files changed, 184 insertions(+), 92 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 39f3a9372..6dece2a8e 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2605,7 +2605,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)", - "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    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.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role
    • partner_type
    • partner_country
    • vulnerability_advisory_locations
    • advisory_location_require_credentials
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", + "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    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.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role_type
    • partner_country
    • vulnerability_advisory_locations
    • advisory_location_require_credentials
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", "operationId": "orgUpdateSingle", "parameters": [ { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 72770ecdc..085cdab1a 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -42,6 +42,21 @@ "ADP", "ROOT" ] + }, + "partnerRoleType": { + "description": "The type of role a partner holds", + "type": "string", + "enum": [ + "", + "Bug Bounty Provider", + "CERT", + "Consortium", + "Hosted Service", + "N/A", + "Open Source", + "Researcher", + "Vendor" + ] } }, "properties": { diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index fc8fc5cc9..a4d676b2d 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -62,15 +62,12 @@ "format": "uuid" } }, - "partner_role": { - "type": "string" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string" }, - "partner_type": { - "type": "string" - }, "partner_country": { "type": "string" }, diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index fa7b27e51..3c28e49eb 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -42,15 +42,12 @@ "format": "uuid" } }, - "partner_role": { - "type": "string" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string" }, - "partner_type": { - "type": "string" - }, "partner_country": { "type": "string" }, diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index ce4e5720e..2a2824f51 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -105,18 +105,13 @@ "org_email" ] }, - "partner_role": { - "type": "string", - "description": "Role of the partner" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string", "description": "Number of the partner" }, - "partner_type": { - "type": "string", - "description": "Type of the partner" - }, "partner_country": { "type": "string", "description": "Country of the partner" diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 12f6d6622..a074e701e 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -134,18 +134,13 @@ "org_email" ] }, - "partner_role": { - "type": "string", - "description": "Role of the partner" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string", "description": "Number of the partner" }, - "partner_type": { - "type": "string", - "description": "Type of the partner" - }, "partner_country": { "type": "string", "description": "Country of the partner" diff --git a/src/constants/index.js b/src/constants/index.js index c649441f4..c2264ecd9 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['partner_data'], @@ -53,9 +53,9 @@ function getConstants () { 'partner_data', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', - 'partner_data.status' + 'partner_data.status', + 'top_level_root', + 'oversees' ], USER_ROLE_ENUM: { ADMIN: 'ADMIN' diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 95e314b0f..0a0caa936 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -556,8 +556,7 @@ router.put('/registry/org/:shortname',
  • contact_info.poc_email
  • contact_info.poc_phone
  • contact_info.org_email
  • -
  • partner_role
  • -
  • partner_type
  • +
  • partner_role_type
  • partner_country
  • vulnerability_advisory_locations
  • advisory_location_require_credentials
  • diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 08e1b8164..b5e485eb6 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -80,9 +80,8 @@ function validateCreateOrgParameters () { 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.status', 'industry' @@ -146,14 +145,11 @@ function validateCreateOrgParameters () { 'contact_info.org_email', 'contact_info.additional_contact_users', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', @@ -237,14 +233,11 @@ function validateUpdateOrgParameters () { 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', @@ -332,14 +325,11 @@ const QUERY_PARAMETERS = { 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index e981733fa..4d51d54dc 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -17,14 +17,11 @@ function parsePostParams (req, res, next) { 'soft_quota', 'hard_quota', 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.admins', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index e405d39f9..0c72e8837 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -22,9 +22,8 @@ const schema = { org_email: String, website: String }, - partner_role: String, + partner_role_type: String, partner_number: String, - partner_type: String, partner_country: String, partner_data: { cve_website_update_date: Date, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 517f7e810..dad32b10b 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -460,6 +460,26 @@ class BaseOrgRepository extends BaseRepository { // Registry stuff // Add uuid to org object registryObjectRaw.UUID = sharedUUID + + // Automate partner_data dates based on status + if (!registryObjectRaw.partner_data) { + registryObjectRaw.partner_data = {} + } + + // Default to 'inactive' if not provided + if (!registryObjectRaw.partner_data.status) { + registryObjectRaw.partner_data.status = 'inactive' + } + + if (registryObjectRaw.partner_data.status === 'active') { + registryObjectRaw.partner_data.partner_active_date = new Date() + // ensure inactive is not set + delete registryObjectRaw.partner_data.partner_inactive_date + } else if (registryObjectRaw.partner_data.status === 'inactive') { + registryObjectRaw.partner_data.partner_inactive_date = new Date() + // ensure active is not set + delete registryObjectRaw.partner_data.partner_active_date + } // Figure out why this is not working.... // registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value)) @@ -690,9 +710,8 @@ class BaseOrgRepository extends BaseRepository { 'oversees', 'reports_to', 'contact_info', // Handles all nested contact_info fields automatically - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', @@ -858,6 +877,34 @@ class BaseOrgRepository extends BaseRepository { delete legacyObjectRaw.new_short_name delete incomingOrg.new_short_name // Keeping for existing logic } + + // Automate partner_data dates based on status + if (registryObjectRaw.partner_data && registryObjectRaw.partner_data.status) { + const incomingStatus = registryObjectRaw.partner_data.status + const currentStatus = registryOrg.partner_data?.status || 'inactive' + if (incomingStatus !== currentStatus) { + if (incomingStatus === 'active') { + registryObjectRaw.partner_data.partner_active_date = new Date() + if (registryObjectRaw.partner_data.partner_inactive_date !== undefined) { + delete registryObjectRaw.partner_data.partner_inactive_date + } + } else if (incomingStatus === 'inactive') { + registryObjectRaw.partner_data.partner_inactive_date = new Date() + if (registryObjectRaw.partner_data.partner_active_date !== undefined) { + delete registryObjectRaw.partner_data.partner_active_date + } + } + } else { + // Keep existing dates if status didn't change + if (registryOrg.partner_data?.partner_active_date) { + registryObjectRaw.partner_data.partner_active_date = registryOrg.partner_data.partner_active_date + } + if (registryOrg.partner_data?.partner_inactive_date) { + registryObjectRaw.partner_data.partner_inactive_date = registryOrg.partner_data.partner_inactive_date + } + } + } + // Checking for joint approval fields const jointApprovalFieldsRegistry = this.getJointApprovalFields(registryOrg, registryObjectRaw) const jointApprovalFieldsLegacy = this.getJointApprovalFields(legacyOrg, legacyObjectRaw, true) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 695d8d616..c85b0eb66 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -13,9 +13,8 @@ const testRegistryOrg = { long_name: 'Registry Org Test', authority: ['CNA'], hard_quota: 1000, - partner_role: 'Initial Partner Role', + partner_role_type: 'Vendor', partner_number: 'Initial Partner Number', - partner_type: 'Initial Partner Type', partner_country: 'US' } let createdOrg @@ -25,7 +24,7 @@ describe('Testing /registryOrg endpoints', () => { context('Positive Tests', () => { it('Creates a new registry org', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testRegistryOrg) .then((res, err) => { @@ -51,18 +50,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('hard_quota') expect(res.body.created.hard_quota).to.equal(testRegistryOrg.hard_quota) - expect(res.body.created).to.haveOwnProperty('partner_role') - expect(res.body.created.partner_role).to.equal(testRegistryOrg.partner_role) + expect(res.body.created).to.haveOwnProperty('partner_role_type') + expect(res.body.created.partner_role_type).to.equal(testRegistryOrg.partner_role_type) expect(res.body.created).to.haveOwnProperty('partner_number') expect(res.body.created.partner_number).to.equal(testRegistryOrg.partner_number) - expect(res.body.created).to.haveOwnProperty('partner_type') - expect(res.body.created.partner_type).to.equal(testRegistryOrg.partner_type) - expect(res.body.created).to.haveOwnProperty('partner_country') expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) + expect(res.body.created).to.haveOwnProperty('partner_data') + expect(res.body.created.partner_data.status).to.equal('inactive') + expect(res.body.created.partner_data).to.haveOwnProperty('partner_inactive_date') + expect(res.body.created.partner_data).to.not.haveOwnProperty('partner_active_date') + createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated @@ -72,7 +73,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to create a new registry organization with an existing short name', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testRegistryOrg) .then((res) => { @@ -82,7 +83,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to create a new registry organization with invalid data', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send({ ...testRegistryOrg, @@ -95,7 +96,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to create a new registry organization with reports_to manually provided', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send({ ...testRegistryOrg, @@ -105,12 +106,13 @@ describe('Testing /registryOrg endpoints', () => { .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0].msg).to.equal('reports_to must not be present') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + expect(res.body.errors[0].params.additionalProperty).to.equal('reports_to') }) }) it('Fails to create a new registry organization with an erroneous key not found in the schema', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send({ ...testRegistryOrg, @@ -122,13 +124,29 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.errors[0].message).to.equal('must NOT have additional properties') }) }) + it('Fails to create a new registry organization with an invalid partner_role_type enum value', async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + short_name: 'test_create_invalid_enum', + partner_role_type: 'Invalid Enum Value' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must be equal to one of the allowed values') + expect(res.body.errors[0].instancePath).to.equal('/partner_role_type') + }) + }) }) }) context('Testing GET /registryOrg endpoints', () => { context('Positive Tests', () => { it('Gets a list of all registry organizations', async () => { await chai.request(app) - .get('/api/registryOrg') + .get('/api/registry/org') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -143,16 +161,15 @@ describe('Testing /registryOrg endpoints', () => { }) it('Gets a registry organization by short name', async () => { await chai.request(app) - .get('/api/registryOrg/registry_org_test') + .get('/api/registry/org/registry_org_test') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) expect(res.body).to.have.property('long_name', createdOrg.long_name) expect(res.body).to.have.property('short_name', createdOrg.short_name) expect(res.body.authority).to.be.an('array').that.includes('CNA') - expect(res.body).to.have.property('partner_role', createdOrg.partner_role) + expect(res.body).to.have.property('partner_role_type', createdOrg.partner_role_type) expect(res.body).to.have.property('partner_number', createdOrg.partner_number) - expect(res.body).to.have.property('partner_type', createdOrg.partner_type) expect(res.body).to.have.property('partner_country', createdOrg.partner_country) }) }) @@ -160,7 +177,7 @@ describe('Testing /registryOrg endpoints', () => { // 1. Get win_5 org UUID let win5UUID await chai.request(app) - .get('/api/registryOrg/win_5') + .get('/api/registry/org/win_5') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -184,7 +201,7 @@ describe('Testing /registryOrg endpoints', () => { // 3. GET win_5 as Secretariat, verify author_id is present await chai.request(app) - .get('/api/registryOrg/win_5') + .get('/api/registry/org/win_5') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -212,7 +229,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to get a registry organization that does not exist', async () => { await chai.request(app) - .get('/api/registryOrg/registry_org_test2') + .get('/api/registry/org/registry_org_test2') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) @@ -225,14 +242,13 @@ describe('Testing /registryOrg endpoints', () => { context('Positive Tests', () => { it('Updates a registry organization providing a full organization object', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, long_name: 'Registry Org Test Updated', - partner_role: 'Updated Partner Role', + partner_role_type: 'Researcher', partner_number: 'Updated Partner Number', - partner_type: 'Updated Partner Type', partner_country: 'UK' }) .then((res, err) => { @@ -259,15 +275,12 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated).to.haveOwnProperty('hard_quota') expect(res.body.updated.hard_quota).to.equal(createdOrg.hard_quota) - expect(res.body.updated).to.haveOwnProperty('partner_role') - expect(res.body.updated.partner_role).to.equal('Updated Partner Role') + expect(res.body.updated).to.haveOwnProperty('partner_role_type') + expect(res.body.updated.partner_role_type).to.equal('Researcher') expect(res.body.updated).to.haveOwnProperty('partner_number') expect(res.body.updated.partner_number).to.equal('Updated Partner Number') - expect(res.body.updated).to.haveOwnProperty('partner_type') - expect(res.body.updated.partner_type).to.equal('Updated Partner Type') - expect(res.body.updated).to.haveOwnProperty('partner_country') expect(res.body.updated.partner_country).to.equal('UK') }) @@ -287,6 +300,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res).to.have.status(200) expect(res.body.updated).to.haveOwnProperty('partner_data') expect(res.body.updated.partner_data.status).to.equal('active') + expect(res.body.updated.partner_data).to.haveOwnProperty('partner_active_date') + expect(res.body.updated.partner_data).to.not.haveOwnProperty('partner_inactive_date') }) }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { @@ -334,7 +349,7 @@ describe('Testing /registryOrg endpoints', () => { } let createdSubOrgUUID await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(subOrg) .then(res => { @@ -344,7 +359,7 @@ describe('Testing /registryOrg endpoints', () => { // Update the main org to oversee it await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, @@ -357,7 +372,7 @@ describe('Testing /registryOrg endpoints', () => { // Assert that the sub org dynamically returns reports_to matching the main org's UUID await chai.request(app) - .get(`/api/registryOrg/${subOrg.short_name}`) + .get(`/api/registry/org/${subOrg.short_name}`) .set(secretariatHeaders) .then(res => { expect(res).to.have.status(200) @@ -373,7 +388,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test2') + .put('/api/registry/org/registry_org_test2') .set(secretariatHeaders) .send({ ...createdOrg, @@ -386,7 +401,7 @@ describe('Testing /registryOrg endpoints', () => { }) it("Fails to update a registry organization's short name to one that already exists", async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, @@ -399,7 +414,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to update a registry organization providing invalid data', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, @@ -410,12 +425,27 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + it('Fails to update a registry organization providing an invalid partner_role_type enum value', async () => { + await chai.request(app) + .put('/api/registry/org/registry_org_test') + .set(secretariatHeaders) + .send({ + ...createdOrg, + partner_role_type: 'Invalid Enum Value' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must be equal to one of the allowed values') + expect(res.body.errors[0].instancePath).to.equal('/partner_role_type') + }) + }) it('Ignores protected fields such as users and admins during an update', async () => { const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, @@ -429,7 +459,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to update a registry organization with reports_to manually provided', async () => { await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, @@ -438,12 +468,13 @@ describe('Testing /registryOrg endpoints', () => { .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0].msg).to.equal('reports_to must not be present') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + expect(res.body.errors[0].params.additionalProperty).to.equal('reports_to') }) }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, @@ -457,7 +488,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to update a registry organization with an invalidly high quota', async () => { await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index c78896ad8..d1b03836c 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -32,6 +32,7 @@ describe('Testing ROOT Organization Type', () => { createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated + delete createdOrg.partner_data }) }) @@ -89,6 +90,34 @@ describe('Testing ROOT Organization Type', () => { }) }) + it('ROOT admin cannot edit top_level_root', async () => { + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(rootAdminHeaders) + .send({ + ...createdOrg, + top_level_root: 'true' + }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + + it('ROOT admin cannot edit oversees', async () => { + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(rootAdminHeaders) + .send({ + ...createdOrg, + oversees: ['some_other_uuid'] + }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + it('ROOT admin cannot reserve CVE IDs', async () => { await chai.request(app) .post('/api/cve-id') diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 3871a4b2a..e03007bc5 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -326,6 +326,7 @@ describe('Testing Edit user endpoint', () => { test: 'additional key not in schema' }) .then((res) => { + if (res.status === 403) console.log(JSON.stringify(res.body, null, 2)) expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') expect(res.body.errors[0].message).to.equal('must NOT have additional properties') From bca191b846116eb864f3e55d7688c436cd268c7f Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 14:02:08 -0400 Subject: [PATCH 22/40] More tests, and work on advisory location changes --- api-docs/openapi.json | 2 +- schemas/registry-org/BaseOrg.json | 11 +++++ schemas/registry-org/CNAOrg.json | 4 ++ schemas/registry-org/RootOrg.json | 1 + .../create-registry-org-request.json | 5 ++ .../create-registry-org-response.json | 5 ++ .../get-registry-org-response.json | 15 ++++-- .../list-registry-orgs-response.json | 15 ++++-- .../update-registry-org-request.json | 5 ++ .../update-registry-org-response.json | 5 ++ src/constants/index.js | 4 +- src/controller/org.controller/index.js | 3 +- .../org.controller/org.middleware.js | 22 +++++---- .../registry-org.middleware.js | 5 +- src/model/baseorg.js | 7 +-- src/repositories/baseOrgRepository.js | 8 ++-- .../registry-org/registryOrgCRUDTest.js | 46 +++++++++++++++++-- 17 files changed, 129 insertions(+), 34 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 6dece2a8e..1044a3c4a 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2605,7 +2605,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)", - "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    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.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role_type
    • partner_country
    • vulnerability_advisory_locations
    • advisory_location_require_credentials
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", + "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    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.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role_type
    • partner_country
    • advisory_locations
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", "operationId": "orgUpdateSingle", "parameters": [ { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 085cdab1a..835bf3697 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -170,9 +170,20 @@ }, "status": { "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" } } }, "additionalProperties": false + }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" } } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index a4d676b2d..e5e6b8687 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -62,6 +62,9 @@ "format": "uuid" } }, + "charter_or_scope": { "type": "string" }, + "disclosure_policy": { "type": "string" }, + "product_list": { "type": "string" }, "partner_role_type": { "$ref": "/BaseOrg#/definitions/partnerRoleType" }, @@ -71,6 +74,7 @@ "partner_country": { "type": "string" }, + "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } }, "required": ["short_name", "hard_quota"] diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 3c28e49eb..41cd83827 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -51,6 +51,7 @@ "partner_country": { "type": "string" }, + "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } }, "required": ["short_name"] diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 10c182b50..6d9efc4cf 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -98,6 +98,11 @@ } }, "required": ["poc", "poc_email", "admins", "org_email"] + }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" } }, "required": [ diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 43dd86f1c..fb93b4e25 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -133,6 +133,11 @@ }, "required": ["poc", "poc_email", "admins", "org_email"] }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 2a2824f51..0d66b39a3 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -136,21 +136,26 @@ }, "status": { "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" }, + "description": "Advisory locations for web scraping" } }, "description": "Additional partner metadata (restricted)" }, - "vulnerability_advisory_locations": { + "advisory_locations": { "type": "array", "items": { "type": "string" }, "description": "Locations of vulnerability advisories" }, - "advisory_location_require_credentials": { - "type": "boolean", - "description": "Indicates if advisory locations require credentials" - }, "industry": { "type": "string", "description": "Industry sector of the organization" diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index a074e701e..3ce381ff1 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -165,21 +165,26 @@ }, "status": { "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" }, + "description": "Advisory locations for web scraping" } }, "description": "Additional partner metadata (restricted)" }, - "vulnerability_advisory_locations": { + "advisory_locations": { "type": "array", "items": { "type": "string" }, "description": "Locations of vulnerability advisories" }, - "advisory_location_require_credentials": { - "type": "boolean", - "description": "Indicates if advisory locations require credentials" - }, "industry": { "type": "string", "description": "Industry sector of the organization" diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 40bdb7bef..b4dca49ab 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -108,6 +108,11 @@ "description": "Organization's website URL" } } + }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" } } } diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 6494839f2..e2d2aee57 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -128,6 +128,11 @@ }, "required": ["poc", "poc_email", "admins", "org_email"] }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/src/constants/index.js b/src/constants/index.js index c2264ecd9..6b4494d28 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['partner_data'], @@ -54,6 +54,8 @@ function getConstants () { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', 'top_level_root', 'oversees' ], diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 0a0caa936..1dd51a767 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -558,8 +558,7 @@ router.put('/registry/org/:shortname',
  • contact_info.org_email
  • partner_role_type
  • partner_country
  • -
  • vulnerability_advisory_locations
  • -
  • advisory_location_require_credentials
  • +
  • advisory_locations
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
  • diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index b5e485eb6..3ff24f84b 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -48,12 +48,15 @@ function validateCreateOrgParameters () { .isArray(), body(['top_level_root']).default('') .isString(), - body(['vulnerability_advisory_locations']) + body(['advisory_locations']) .default([]) .custom(isFlatStringArray), - body(['advisory_location_require_credentials']) + body(['partner_data.advisory_location_require_credentials']) .default(false) .isBoolean(), + body(['partner_data.vulnerability_advisory_location_for_web_scraping']) + .default([]) + .custom(isFlatStringArray), body(['tl_root_start_date']) .default(null) .isDate(), @@ -151,8 +154,9 @@ function validateCreateOrgParameters () { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'advisory_locations', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list') @@ -239,8 +243,9 @@ function validateUpdateOrgParameters () { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' @@ -331,8 +336,9 @@ const QUERY_PARAMETERS = { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 4d51d54dc..2adf36675 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -23,8 +23,9 @@ function parsePostParams (req, res, next) { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'advisory_locations', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 0c72e8837..4b0d057d5 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -30,10 +30,11 @@ const schema = { cve_website_update_needed: Boolean, partner_active_date: Date, partner_inactive_date: Date, - status: String + status: String, + advisory_location_require_credentials: Boolean, + vulnerability_advisory_location_for_web_scraping: [String] }, - vulnerability_advisory_locations: [String], - advisory_location_require_credentials: Boolean, + advisory_locations: [String], industry: String, tl_root_start_date: Date, is_cna_discussion_list: Boolean, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index dad32b10b..cd58833eb 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -625,8 +625,9 @@ class BaseOrgRepository extends BaseRepository { * @param {string} [incomingParameters.contact_info.website] - The organization's website URL. (Registry only) * @param {string} [incomingParameters.cna_role_type] - (Registry only) * @param {string} [incomingParameters.cna_country] - (Registry only) - * @param {string[]} [incomingParameters.vulnerability_advisory_locations] - (Registry only) - * @param {boolean} [incomingParameters.advisory_location_require_credentials] - (Registry only) + * @param {string[]} [incomingParameters.advisory_locations] - (Registry only) + * @param {boolean} [incomingParameters.partner_data.advisory_location_require_credentials] - (Registry only) + * @param {string[]} [incomingParameters.partner_data.vulnerability_advisory_location_for_web_scraping] - (Registry only) * @param {string} [incomingParameters.industry] - (Registry only) * @param {string} [incomingParameters.tl_root_start_date] - (Registry only) * @param {boolean} [incomingParameters.is_cna_discussion_list] - (Registry only) @@ -713,8 +714,7 @@ class BaseOrgRepository extends BaseRepository { 'partner_role_type', 'partner_number', 'partner_country', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index c85b0eb66..8870616b7 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -15,7 +15,8 @@ const testRegistryOrg = { hard_quota: 1000, partner_role_type: 'Vendor', partner_number: 'Initial Partner Number', - partner_country: 'US' + partner_country: 'US', + advisory_locations: ['https://example.com/advisories'] } let createdOrg @@ -59,6 +60,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('partner_country') expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) + expect(res.body.created).to.haveOwnProperty('advisory_locations') + expect(res.body.created.advisory_locations).to.deep.equal(testRegistryOrg.advisory_locations) + expect(res.body.created).to.haveOwnProperty('partner_data') expect(res.body.created.partner_data.status).to.equal('inactive') expect(res.body.created.partner_data).to.haveOwnProperty('partner_inactive_date') @@ -171,6 +175,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body).to.have.property('partner_role_type', createdOrg.partner_role_type) expect(res.body).to.have.property('partner_number', createdOrg.partner_number) expect(res.body).to.have.property('partner_country', createdOrg.partner_country) + expect(res.body).to.have.property('partner_country', createdOrg.partner_country) + expect(res.body.advisory_locations).to.deep.equal(createdOrg.advisory_locations) }) }) it('Strips author_id from conversations for non-secretariats', async () => { @@ -249,7 +255,8 @@ describe('Testing /registryOrg endpoints', () => { long_name: 'Registry Org Test Updated', partner_role_type: 'Researcher', partner_number: 'Updated Partner Number', - partner_country: 'UK' + partner_country: 'UK', + advisory_locations: ['https://example.com/updated_advisories'] }) .then((res, err) => { expect(err).to.be.undefined @@ -283,6 +290,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated).to.haveOwnProperty('partner_country') expect(res.body.updated.partner_country).to.equal('UK') + + expect(res.body.updated).to.haveOwnProperty('advisory_locations') + expect(res.body.updated.advisory_locations).to.deep.equal(['https://example.com/updated_advisories']) }) }) it('Allows Secretariat to update partner_data', async () => { @@ -292,7 +302,9 @@ describe('Testing /registryOrg endpoints', () => { .send({ ...createdOrg, partner_data: { - status: 'active' + status: 'active', + advisory_location_require_credentials: true, + vulnerability_advisory_location_for_web_scraping: ['https://example.com/scraping'] } }) .then((res, err) => { @@ -302,6 +314,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated.partner_data.status).to.equal('active') expect(res.body.updated.partner_data).to.haveOwnProperty('partner_active_date') expect(res.body.updated.partner_data).to.not.haveOwnProperty('partner_inactive_date') + expect(res.body.updated.partner_data.advisory_location_require_credentials).to.be.true + expect(res.body.updated.partner_data.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping']) }) }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { @@ -472,6 +486,32 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.errors[0].params.additionalProperty).to.equal('reports_to') }) }) + it('Fails to allow an admin to set a secretariat-only field', async () => { + let win5Org + await chai.request(app) + .get('/api/registry/org/win_5') + .set(secretariatHeaders) + .then((res) => { + win5Org = res.body + }) + + await chai.request(app) + .put('/api/registry/org/win_5') + .set(constants.nonSecretariatUserHeaders2) + .send({ + short_name: win5Org.short_name, + long_name: win5Org.long_name, + authority: win5Org.authority, + hard_quota: win5Org.hard_quota, + partner_data: { + status: 'active' + } + }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: partner_data, partner_data.status.') + }) + }) 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') From a5d40578b6c87ca755917b895bac65242c08e7fb Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 14:29:36 -0400 Subject: [PATCH 23/40] moved poc, probably will need more work --- schemas/registry-org/BaseOrg.json | 37 ++-- schemas/registry-org/CNAOrg.json | 76 ++++++-- .../create-registry-org-request.json | 69 ++++--- .../create-registry-org-response.json | 78 +++++--- .../get-registry-org-response.json | 176 +++++++++--------- .../list-registry-orgs-response.json | 64 ++++--- .../update-registry-org-request.json | 66 ++++--- .../update-registry-org-response.json | 68 ++++--- .../org.controller/org.middleware.js | 30 +-- .../registry-org.middleware.js | 3 +- src/model/baseorg.js | 9 +- test/integration-tests/constants.js | 9 +- 12 files changed, 405 insertions(+), 280 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 835bf3697..91969b4e6 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -123,29 +123,36 @@ "properties": { "additional_contact_users": { "type": "array", - "uniqueItems": true, "items": { - "$ref": "#/definitions/uuidType" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, - "poc": { + "phone": { "type": "string" }, - "poc_email": { - "type": "string", - "format": "email" - }, - "poc_phone": { + "poc": { "type": "string" }, - "org_email": { + "poc_email": { "type": "string", "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, "additionalProperties": false @@ -176,14 +183,18 @@ }, "vulnerability_advisory_location_for_web_scraping": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } } }, "additionalProperties": false }, "advisory_locations": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index e5e6b8687..99a3d9e7d 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -6,24 +6,57 @@ "description": "Schema for a CVE CNA Organization", "additionalProperties": false, "properties": { - "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, - "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, - "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "UUID": { + "$ref": "/BaseOrg#/definitions/uuidType" + }, + "short_name": { + "$ref": "/BaseOrg#/definitions/shortName" + }, + "long_name": { + "$ref": "/BaseOrg#/definitions/longName" + }, "new_short_name": { "description": "Used to rename an organization's short name during an update.", "type": "string", "minLength": 2, "maxLength": 32 }, - "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, + "aliases": { + "$ref": "/BaseOrg#/properties/aliases" + }, "contact_info": { "type": "object", "properties": { - "poc": { "type": "string" }, - "poc_email": { "type": "string" }, - "poc_phone": { "type": "string" }, - "org_email": { "type": "string" }, - "website": { "type": "string" } + "additional_contact_users": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string" + }, + "website": { + "type": "string" + } }, "additionalProperties": false }, @@ -62,9 +95,15 @@ "format": "uuid" } }, - "charter_or_scope": { "type": "string" }, - "disclosure_policy": { "type": "string" }, - "product_list": { "type": "string" }, + "charter_or_scope": { + "type": "string" + }, + "disclosure_policy": { + "type": "string" + }, + "product_list": { + "type": "string" + }, "partner_role_type": { "$ref": "/BaseOrg#/definitions/partnerRoleType" }, @@ -74,8 +113,15 @@ "partner_country": { "type": "string" }, - "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } + "advisory_locations": { + "$ref": "/BaseOrg#/properties/advisory_locations" + }, + "partner_data": { + "$ref": "/BaseOrg#/properties/partner_data" + } }, - "required": ["short_name", "hard_quota"] + "required": [ + "short_name", + "hard_quota" + ] } \ No newline at end of file diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 6d9efc4cf..1a3fe8d76 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -22,10 +22,16 @@ }, "authority": { "type": "array", - "items": { - "type": "string", - "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT", "ROOT"] - } + "items": { + "type": "string", + "enum": [ + "CNA", + "ADP", + "BULK_DOWNLOAD", + "SECRETARIAT", + "ROOT" + ] + } }, "oversees": { "type": "array", @@ -63,45 +69,48 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, - "required": ["poc", "poc_email", "admins", "org_email"] + "required": [ + "poc", + "poc_email", + "admins" + ] }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" } }, @@ -110,4 +119,4 @@ "authority", "long_name" ] -} +} \ No newline at end of file diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index fb93b4e25..6fb87bcc7 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -5,7 +5,7 @@ "title": "CVE Create Registry Org Response", "description": "JSON Schema for CVE Create Registry Org response", "properties": { - "message": { + "message": { "type": "string", "description": "Success description" }, @@ -33,7 +33,11 @@ }, "cve_program_org_function": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"], + "enum": [ + "CNA", + "ADP", + "Secretariat" + ], "description": "The organization's function within the CVE program" }, "authority": { @@ -43,14 +47,23 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"] + "enum": [ + "CNA", + "ADP", + "Secretariat" + ] } } }, - "required": ["active_roles"] + "required": [ + "active_roles" + ] }, "reports_to": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "UUID of the parent organization, if any" }, "oversees": { @@ -97,45 +110,48 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, - "required": ["poc", "poc_email", "admins", "org_email"] + "required": [ + "poc", + "poc_email", + "admins" + ] }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" }, "in_use": { @@ -154,5 +170,5 @@ } } } - } + } } \ No newline at end of file diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 0d66b39a3..e2a9eb467 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -42,7 +42,10 @@ "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "UUID of the parent organization, if any" }, "oversees": { @@ -53,18 +56,18 @@ "description": "UUIDs of organizations overseen by this organization" }, "users": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of users associated with this organization" + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of users associated with this organization" }, "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of admin users" }, "contact_info": { "type": "object", @@ -72,102 +75,107 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, "required": [ "poc", - "poc_email", - "org_email" + "poc_email" ] }, "partner_role_type": { - "$ref": "/BaseOrg#/definitions/partnerRoleType" + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { - "type": "string", - "description": "Number of the partner" + "type": "string", + "description": "Number of the partner" }, "partner_country": { - "type": "string", - "description": "Country of the partner" + "type": "string", + "description": "Country of the partner" }, "partner_data": { - "type": "object", - "properties": { - "cve_website_update_date": { - "type": "string", - "format": "date-time" - }, - "cve_website_update_needed": { - "type": "boolean" - }, - "partner_active_date": { - "type": "string", - "format": "date-time" - }, - "partner_inactive_date": { - "type": "string", - "format": "date-time" - }, - "status": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" }, - "advisory_location_require_credentials": { - "type": "boolean", - "description": "Indicates if advisory locations require credentials" - }, - "vulnerability_advisory_location_for_web_scraping": { - "type": "array", - "items": { "type": "string" }, - "description": "Advisory locations for web scraping" - } - }, - "description": "Additional partner metadata (restricted)" + "description": "Advisory locations for web scraping" + } + }, + "description": "Additional partner metadata (restricted)" }, "advisory_locations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Locations of vulnerability advisories" + "type": "array", + "items": { + "type": "string" + }, + "description": "Locations of vulnerability advisories" }, "industry": { - "type": "string", - "description": "Industry sector of the organization" + "type": "string", + "description": "Industry sector of the organization" }, "tl_root_start_date": { - "type": "string", - "format": "date-time", - "description": "Start date for Top-Level Root role" + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" }, "is_cna_discussion_list": { - "type": "boolean", - "description": "Indicates if part of the CNA discussion list" + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" }, "in_use": { "type": "boolean", @@ -204,14 +212,16 @@ "description": "List of products associated with the organization" }, "conversation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "body": { "type": "string" } - } - }, - "description": "List of conversation messages associated with the organization" + "type": "array", + "items": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + } + }, + "description": "List of conversation messages associated with the organization" } } } \ No newline at end of file diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 3ce381ff1..61c02ce4a 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -71,7 +71,10 @@ "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "UUID of the parent organization, if any" }, "oversees": { @@ -101,37 +104,40 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, "required": [ "poc", - "poc_email", - "org_email" + "poc_email" ] }, "partner_role_type": { @@ -172,7 +178,9 @@ }, "vulnerability_advisory_location_for_web_scraping": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Advisory locations for web scraping" } }, @@ -233,14 +241,16 @@ "description": "List of products associated with the organization" }, "conversation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "body": { "type": "string" } + "type": "array", + "items": { + "type": "object", + "properties": { + "body": { + "type": "string" } - }, - "description": "List of conversation messages associated with the organization" + } + }, + "description": "List of conversation messages associated with the organization" } } } diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index b4dca49ab..48746b8c6 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -22,7 +22,11 @@ }, "cve_program_org_function": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"], + "enum": [ + "CNA", + "ADP", + "Secretariat" + ], "description": "The organization's function within the CVE program" }, "authority": { @@ -32,11 +36,18 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat", "ROOT"] + "enum": [ + "CNA", + "ADP", + "Secretariat", + "ROOT" + ] } } }, - "required": ["active_roles"] + "required": [ + "active_roles" + ] }, "oversees": { "type": "array", @@ -74,45 +85,44 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } } }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" } } -} +} \ No newline at end of file diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index e2d2aee57..d73cca451 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -5,7 +5,7 @@ "title": "CVE Update Registry Org Response", "description": "JSON Schema for CVE Update Registry Org response", "properties": { - "message": { + "message": { "type": "string", "description": "Success description" }, @@ -38,11 +38,18 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Root", "Secretariat"] + "enum": [ + "CNA", + "ADP", + "Root", + "Secretariat" + ] } } }, - "required": ["active_roles"] + "required": [ + "active_roles" + ] }, "reports_to": { "type": "string", @@ -92,45 +99,48 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, - "required": ["poc", "poc_email", "admins", "org_email"] + "required": [ + "poc", + "poc_email", + "admins" + ] }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" }, "in_use": { @@ -149,5 +159,5 @@ } } } - } + } } \ No newline at end of file diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 3ff24f84b..8f05be3bd 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -30,7 +30,7 @@ function validateCreateOrgParameters () { // Optional // soft_quota, // Not allowed - // users, contact_info.admins, in_use, created, last_updated + // users, , in_use, created, last_updated const orgOptions = ['CNA', 'Secretariat', 'Bulk Download', 'ADP'] validations = [ body(['short_name']).isString() @@ -80,9 +80,10 @@ function validateCreateOrgParameters () { 'product_list', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', + '', + '', 'partner_role_type', 'partner_number', 'partner_country', @@ -102,7 +103,7 @@ function validateCreateOrgParameters () { .isArray() .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) .withMessage(errorMsgs.ID_QUOTA), - ...isNotAllowed('reports_to', 'name', 'users', 'contact_info.admins', 'in_use', 'created', 'last_updated', 'policies.id_quota') + ...isNotAllowed('reports_to', 'name', 'users', '', 'in_use', 'created', 'last_updated', 'policies.id_quota') ] } else { validations = [ @@ -128,14 +129,14 @@ function validateCreateOrgParameters () { 'oversees', 'long_name', 'cve_program_org_function', - 'contact_info.admins', + '', 'in_use', 'created', 'top_level_root', 'soft_quota', 'aliases', 'hard_quota', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', 'contact_info', 'users', @@ -144,10 +145,11 @@ function validateCreateOrgParameters () { 'product_list', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', - 'contact_info.additional_contact_users', + 'contact_info.phone', 'contact_info.website', + '', + 'contact_info.additional_contact_users', + '', 'partner_role_type', 'partner_number', 'partner_country', @@ -234,9 +236,10 @@ function validateUpdateOrgParameters () { 'product_list', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', + '', + '', 'partner_role_type', 'partner_number', 'partner_country', @@ -327,9 +330,10 @@ const QUERY_PARAMETERS = { 'contact_info', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', + '', + '', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 2adf36675..48a278e83 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -15,8 +15,7 @@ function parsePostParams (req, res, next) { 'top_level_root', 'users', 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', - 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', - 'contact_info.admins', 'contact_info.org_email', 'contact_info.website', + 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 4b0d057d5..9e6021b3d 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -15,11 +15,14 @@ const schema = { users: { type: [String], set: toUndefined }, admins: [String], contact_info: { - additional_contact_users: [String], + additional_contact_users: [{ + phone: String, + poc: String, + poc_email: String + }], + phone: String, poc: String, poc_email: String, - poc_phone: String, - org_email: String, website: String }, partner_role_type: String, diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index 80700e31e..0fc8f9d68 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -383,8 +383,7 @@ const testRegistryOrg = { contact_info: { poc: 'Dave', poc_email: 'dave@test.org', - poc_phone: '555-1234', - org_email: 'contact@test.org', + phone: '555-1234', website: 'https://test.org' }, authority: ['CNA'], @@ -397,8 +396,7 @@ const testRegistryOrg2 = { contact_info: { poc: 'Dave', poc_email: 'dave@test.org', - poc_phone: '555-1234', - org_email: 'contact@test.org', + phone: '555-1234', website: 'https://test.org' }, authority: ['CNA'], @@ -425,8 +423,7 @@ const existingRegistryOrg = { contact_info: { poc: 'Dave', poc_email: 'dave@test.org', - poc_phone: '555-1234', - org_email: 'contact@test.org', + phone: '555-1234', website: 'https://test.org' }, authority: ['CNA'], From f26577424e24713c0b903cc49be7c8226d0bd8c1 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 14:45:04 -0400 Subject: [PATCH 24/40] remove some oddities --- src/controller/org.controller/org.middleware.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 8f05be3bd..7809afb84 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -129,7 +129,6 @@ function validateCreateOrgParameters () { 'oversees', 'long_name', 'cve_program_org_function', - '', 'in_use', 'created', 'top_level_root', @@ -147,9 +146,7 @@ function validateCreateOrgParameters () { 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', - '', 'contact_info.additional_contact_users', - '', 'partner_role_type', 'partner_number', 'partner_country', From 4cab75e6333baf123a037164c4c98bf70bcb846c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 27 Apr 2026 11:17:03 -0400 Subject: [PATCH 25/40] Update partner_active_date and partner_inactive_date to be date not datetime, and rename partner_data to program_data --- schemas/registry-org/BaseOrg.json | 6 +-- schemas/registry-org/CNAOrg.json | 4 +- schemas/registry-org/RootOrg.json | 2 +- .../get-registry-org-response.json | 6 +-- .../list-registry-orgs-response.json | 6 +-- src/constants/index.js | 16 +++--- .../org.controller/org.middleware.js | 44 +++++++-------- .../registry-org.middleware.js | 10 ++-- src/model/baseorg.js | 6 +-- src/repositories/baseOrgRepository.js | 54 +++++++++---------- .../org/registryOrgAsOrgAdmin.js | 4 +- .../registry-org/registryOrgCRUDTest.js | 28 +++++----- .../registry-org/rootOrgTest.js | 2 +- 13 files changed, 94 insertions(+), 94 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 91969b4e6..d04301fb3 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -157,7 +157,7 @@ }, "additionalProperties": false }, - "partner_data": { + "program_data": { "type": "object", "properties": { "cve_website_update_date": { @@ -169,11 +169,11 @@ }, "partner_active_date": { "type": "string", - "format": "date-time" + "format": "date" }, "partner_inactive_date": { "type": "string", - "format": "date-time" + "format": "date" }, "status": { "type": "string" diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 99a3d9e7d..982e62f38 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -116,8 +116,8 @@ "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "partner_data": { - "$ref": "/BaseOrg#/properties/partner_data" + "program_data": { + "$ref": "/BaseOrg#/properties/program_data" } }, "required": [ diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 41cd83827..664b432a6 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -52,7 +52,7 @@ "type": "string" }, "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } + "program_data": { "$ref": "/BaseOrg#/properties/program_data" } }, "required": ["short_name"] } diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index e2a9eb467..31f6bdd47 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -122,7 +122,7 @@ "type": "string", "description": "Country of the partner" }, - "partner_data": { + "program_data": { "type": "object", "properties": { "cve_website_update_date": { @@ -134,11 +134,11 @@ }, "partner_active_date": { "type": "string", - "format": "date-time" + "format": "date" }, "partner_inactive_date": { "type": "string", - "format": "date-time" + "format": "date" }, "status": { "type": "string" diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 61c02ce4a..75e372907 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -151,7 +151,7 @@ "type": "string", "description": "Country of the partner" }, - "partner_data": { + "program_data": { "type": "object", "properties": { "cve_website_update_date": { @@ -163,11 +163,11 @@ }, "partner_active_date": { "type": "string", - "format": "date-time" + "format": "date" }, "partner_inactive_date": { "type": "string", - "format": "date-time" + "format": "date" }, "status": { "type": "string" diff --git a/src/constants/index.js b/src/constants/index.js index 6b4494d28..efbe374aa 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,18 +44,18 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], - ORG_RESTRICTED_FIELDS: ['partner_data'], + ORG_RESTRICTED_FIELDS: ['program_data'], SECRETARIAT_ONLY_FIELDS: [ 'partner_number', - 'partner_data', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'top_level_root', 'oversees' ], diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 7809afb84..1d29764b4 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -51,10 +51,10 @@ function validateCreateOrgParameters () { body(['advisory_locations']) .default([]) .custom(isFlatStringArray), - body(['partner_data.advisory_location_require_credentials']) + body(['program_data.advisory_location_require_credentials']) .default(false) .isBoolean(), - body(['partner_data.vulnerability_advisory_location_for_web_scraping']) + body(['program_data.vulnerability_advisory_location_for_web_scraping']) .default([]) .custom(isFlatStringArray), body(['tl_root_start_date']) @@ -64,13 +64,13 @@ function validateCreateOrgParameters () { .default(false) .isBoolean(), body([ - 'partner_data.cve_website_update_date', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date' + 'program_data.cve_website_update_date', + 'program_data.partner_active_date', + 'program_data.partner_inactive_date' ]) .optional({ nullable: true }) .isDate(), - body(['partner_data.cve_website_update_needed']) + body(['program_data.cve_website_update_needed']) .optional() .isBoolean(), body( @@ -87,7 +87,7 @@ function validateCreateOrgParameters () { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.status', + 'program_data.status', 'industry' ]) .default('') @@ -150,12 +150,12 @@ function validateCreateOrgParameters () { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', 'advisory_locations', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list') @@ -240,11 +240,11 @@ function validateUpdateOrgParameters () { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'advisory_locations', 'industry', 'tl_root_start_date', @@ -334,11 +334,11 @@ const QUERY_PARAMETERS = { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'advisory_locations', 'industry', 'tl_root_start_date', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 48a278e83..b7c40e1fa 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -19,12 +19,12 @@ function parsePostParams (req, res, next) { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', 'advisory_locations', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 9e6021b3d..a92887902 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -28,11 +28,11 @@ const schema = { partner_role_type: String, partner_number: String, partner_country: String, - partner_data: { + program_data: { cve_website_update_date: Date, cve_website_update_needed: Boolean, - partner_active_date: Date, - partner_inactive_date: Date, + partner_active_date: String, + partner_inactive_date: String, status: String, advisory_location_require_credentials: Boolean, vulnerability_advisory_location_for_web_scraping: [String] diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index cd58833eb..bca9b0929 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -461,24 +461,24 @@ class BaseOrgRepository extends BaseRepository { // Add uuid to org object registryObjectRaw.UUID = sharedUUID - // Automate partner_data dates based on status - if (!registryObjectRaw.partner_data) { - registryObjectRaw.partner_data = {} + // Automate program_data dates based on status + if (!registryObjectRaw.program_data) { + registryObjectRaw.program_data = {} } // Default to 'inactive' if not provided - if (!registryObjectRaw.partner_data.status) { - registryObjectRaw.partner_data.status = 'inactive' + if (!registryObjectRaw.program_data.status) { + registryObjectRaw.program_data.status = 'inactive' } - if (registryObjectRaw.partner_data.status === 'active') { - registryObjectRaw.partner_data.partner_active_date = new Date() + if (registryObjectRaw.program_data.status === 'active') { + registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] // ensure inactive is not set - delete registryObjectRaw.partner_data.partner_inactive_date - } else if (registryObjectRaw.partner_data.status === 'inactive') { - registryObjectRaw.partner_data.partner_inactive_date = new Date() + delete registryObjectRaw.program_data.partner_inactive_date + } else if (registryObjectRaw.program_data.status === 'inactive') { + registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] // ensure active is not set - delete registryObjectRaw.partner_data.partner_active_date + delete registryObjectRaw.program_data.partner_active_date } // Figure out why this is not working.... // registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value)) @@ -626,8 +626,8 @@ class BaseOrgRepository extends BaseRepository { * @param {string} [incomingParameters.cna_role_type] - (Registry only) * @param {string} [incomingParameters.cna_country] - (Registry only) * @param {string[]} [incomingParameters.advisory_locations] - (Registry only) - * @param {boolean} [incomingParameters.partner_data.advisory_location_require_credentials] - (Registry only) - * @param {string[]} [incomingParameters.partner_data.vulnerability_advisory_location_for_web_scraping] - (Registry only) + * @param {boolean} [incomingParameters.program_data.advisory_location_require_credentials] - (Registry only) + * @param {string[]} [incomingParameters.program_data.vulnerability_advisory_location_for_web_scraping] - (Registry only) * @param {string} [incomingParameters.industry] - (Registry only) * @param {string} [incomingParameters.tl_root_start_date] - (Registry only) * @param {boolean} [incomingParameters.is_cna_discussion_list] - (Registry only) @@ -878,29 +878,29 @@ class BaseOrgRepository extends BaseRepository { delete incomingOrg.new_short_name // Keeping for existing logic } - // Automate partner_data dates based on status - if (registryObjectRaw.partner_data && registryObjectRaw.partner_data.status) { - const incomingStatus = registryObjectRaw.partner_data.status - const currentStatus = registryOrg.partner_data?.status || 'inactive' + // Automate program_data dates based on status + if (registryObjectRaw.program_data && registryObjectRaw.program_data.status) { + const incomingStatus = registryObjectRaw.program_data.status + const currentStatus = registryOrg.program_data?.status || 'inactive' if (incomingStatus !== currentStatus) { if (incomingStatus === 'active') { - registryObjectRaw.partner_data.partner_active_date = new Date() - if (registryObjectRaw.partner_data.partner_inactive_date !== undefined) { - delete registryObjectRaw.partner_data.partner_inactive_date + registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_inactive_date !== undefined) { + delete registryObjectRaw.program_data.partner_inactive_date } } else if (incomingStatus === 'inactive') { - registryObjectRaw.partner_data.partner_inactive_date = new Date() - if (registryObjectRaw.partner_data.partner_active_date !== undefined) { - delete registryObjectRaw.partner_data.partner_active_date + registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_active_date !== undefined) { + delete registryObjectRaw.program_data.partner_active_date } } } else { // Keep existing dates if status didn't change - if (registryOrg.partner_data?.partner_active_date) { - registryObjectRaw.partner_data.partner_active_date = registryOrg.partner_data.partner_active_date + if (registryOrg.program_data?.partner_active_date) { + registryObjectRaw.program_data.partner_active_date = registryOrg.program_data.partner_active_date } - if (registryOrg.partner_data?.partner_inactive_date) { - registryObjectRaw.partner_data.partner_inactive_date = registryOrg.partner_data.partner_inactive_date + if (registryOrg.program_data?.partner_inactive_date) { + registryObjectRaw.program_data.partner_inactive_date = registryOrg.program_data.partner_inactive_date } } } diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index 6ec234157..e6a4fb9cd 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -377,12 +377,12 @@ describe('Testing Registry Org as org admin', () => { expect(res.body.error).to.be.equal('SECRETARIAT_ONLY') }) }) - it('Registry: Services api does not allow org admins to update their own orgs partner_data', async () => { + it('Registry: Services api does not allow org admins to update their own orgs program_data', async () => { await chai.request(app) .put('/api/registry/org/beat_10') .set(adminHeaders) .send({ - partner_data: { + program_data: { status: 'active' } }) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 8870616b7..5acf4ee2a 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -63,10 +63,10 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('advisory_locations') expect(res.body.created.advisory_locations).to.deep.equal(testRegistryOrg.advisory_locations) - expect(res.body.created).to.haveOwnProperty('partner_data') - expect(res.body.created.partner_data.status).to.equal('inactive') - expect(res.body.created.partner_data).to.haveOwnProperty('partner_inactive_date') - expect(res.body.created.partner_data).to.not.haveOwnProperty('partner_active_date') + 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') + expect(res.body.created.program_data).to.not.haveOwnProperty('partner_active_date') createdOrg = res.body.created delete createdOrg.created @@ -295,13 +295,13 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated.advisory_locations).to.deep.equal(['https://example.com/updated_advisories']) }) }) - it('Allows Secretariat to update partner_data', async () => { + it('Allows Secretariat to update program_data', async () => { await chai.request(app) .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, - partner_data: { + program_data: { status: 'active', advisory_location_require_credentials: true, vulnerability_advisory_location_for_web_scraping: ['https://example.com/scraping'] @@ -310,12 +310,12 @@ describe('Testing /registryOrg endpoints', () => { .then((res, err) => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body.updated).to.haveOwnProperty('partner_data') - expect(res.body.updated.partner_data.status).to.equal('active') - expect(res.body.updated.partner_data).to.haveOwnProperty('partner_active_date') - expect(res.body.updated.partner_data).to.not.haveOwnProperty('partner_inactive_date') - expect(res.body.updated.partner_data.advisory_location_require_credentials).to.be.true - expect(res.body.updated.partner_data.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping']) + expect(res.body.updated).to.haveOwnProperty('program_data') + expect(res.body.updated.program_data.status).to.equal('active') + expect(res.body.updated.program_data).to.haveOwnProperty('partner_active_date') + expect(res.body.updated.program_data).to.not.haveOwnProperty('partner_inactive_date') + expect(res.body.updated.program_data.advisory_location_require_credentials).to.be.true + expect(res.body.updated.program_data.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping']) }) }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { @@ -503,13 +503,13 @@ describe('Testing /registryOrg endpoints', () => { long_name: win5Org.long_name, authority: win5Org.authority, hard_quota: win5Org.hard_quota, - partner_data: { + program_data: { status: 'active' } }) .then((res) => { expect(res).to.have.status(403) - expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: partner_data, partner_data.status.') + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: program_data, program_data.status.') }) }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index d1b03836c..0ea61393a 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -32,7 +32,7 @@ describe('Testing ROOT Organization Type', () => { createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated - delete createdOrg.partner_data + delete createdOrg.program_data }) }) From 237d7582a0f9dc99c26221f262f2ad309031fd2d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 27 Apr 2026 14:44:41 -0400 Subject: [PATCH 26/40] Fixing various bugs --- src/constants/index.js | 2 +- src/repositories/baseOrgRepository.js | 9 +++- src/repositories/reviewObjectRepository.js | 17 +++++++ test/integration-tests/audit/auditTest.js | 1 + .../org/registryOrgAsOrgAdmin.js | 44 +++++++++++++++++++ .../review-object/reviewObjectTest.js | 25 +++++++++++ 6 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/constants/index.js b/src/constants/index.js index efbe374aa..4c0a38b7a 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'partner_role_type', 'partner_number', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['program_data'], diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index bca9b0929..da27ff3d6 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -920,9 +920,14 @@ class BaseOrgRepository extends BaseRepository { const requestingUsername = requestingUser ? requestingUser.username : null const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] + let registryProtectedFields = [...protectedFields] + if (!isSecretariat) { + registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] + } + if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), _.omit(legacyObjectRaw, protectedFields), skipNulls)) - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), protectedFields), _.omit(registryObjectRaw, protectedFields), skipNulls)) + updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), registryProtectedFields), _.omit(registryObjectRaw, registryProtectedFields), skipNulls)) } else { // Check if there are actual changes to joint approval fields compared to current org object (not current review) // Only compare fields that are actually in the incoming data @@ -945,7 +950,7 @@ class BaseOrgRepository extends BaseRepository { await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUsername, options) } } - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...protectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) + updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...registryProtectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...registryProtectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, [...protectedFields, ...jointApprovalFieldsLegacy]), skipNulls)) } // handle conversation diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 0062da212..61400d343 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -34,6 +34,9 @@ class ReviewObjectRepository extends BaseRepository { reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.target_object_uuid, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined + if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) { + delete reviewObject.new_review_data.program_data + } } return reviewObject || null @@ -103,6 +106,9 @@ class ReviewObjectRepository extends BaseRepository { reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined + if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) { + delete reviewObject.new_review_data.program_data + } } return reviewObject || null @@ -132,6 +138,9 @@ class ReviewObjectRepository extends BaseRepository { reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined + if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) { + delete reviewObject.new_review_data.program_data + } } return reviewObject || null @@ -212,6 +221,14 @@ class ReviewObjectRepository extends BaseRepository { data.nextPage = pg.nextPage } + if (!isSecretariat && data.reviewObjects) { + for (const review of data.reviewObjects) { + if (review.new_review_data && review.new_review_data.program_data) { + delete review.new_review_data.program_data + } + } + } + // Optionally attach conversations if (includeConversations && pg.itemsList && pg.itemsList.length) { const ConversationRepository = require('./conversationRepository') diff --git a/test/integration-tests/audit/auditTest.js b/test/integration-tests/audit/auditTest.js index 7b8d7daf7..5b23e2a9a 100644 --- a/test/integration-tests/audit/auditTest.js +++ b/test/integration-tests/audit/auditTest.js @@ -247,6 +247,7 @@ describe('Testing Audit Org endpoints', () => { .get(`/api/audit/org/document/${fakeUUID}`) .set(constants.headers) .then((res, err) => { + if (res.status === 403) console.log(JSON.stringify(res.body, null, 2)) expect(err).to.be.undefined expect(res).to.have.status(404) diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index e6a4fb9cd..dabe763fd 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -234,6 +234,50 @@ describe('Testing Registry Org as org admin', () => { expect(res.body.hard_quota).to.be.greaterThan(0) }) }) + it('Registry: allows admin users to update their own org without losing program_data', async () => { + // Secretariat sets program_data + const secretariatHeaders = { ...constants.headers } + await chai.request(app) + .put(`/api/registry/org/${shortName}`) + .set(secretariatHeaders) + .send({ + short_name: shortName, + long_name: 'Test Org', + authority: ['CNA'], + hard_quota: 1000, + program_data: { + status: 'active' + } + }) + .then((res, err) => { + if (res.status === 400) console.log(JSON.stringify(res.body, null, 2)) + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + + // Admin updates org + await chai.request(app) + .put(`/api/registry/org/${shortName}`) + .set(adminHeaders) + .send({ + short_name: shortName, + long_name: 'Test Org2', + authority: ['CNA'], + hard_quota: 1000 + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + + // Secretariat retrieves org and verifies program_data is preserved + const res = await chai.request(app) + .get(`/api/registry/org/${shortName}`) + .set(secretariatHeaders) + + expect(res.body).to.have.property('program_data') + expect(res.body.program_data).to.have.property('status', 'active') + }) }) context('Negative Tests', () => { it('Registry: reset secret for fails user in other org', async () => { diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js index 9f8be169c..6fa5fa212 100644 --- a/test/integration-tests/review-object/reviewObjectTest.js +++ b/test/integration-tests/review-object/reviewObjectTest.js @@ -281,6 +281,31 @@ describe('Review Object Controller Integration Tests', () => { expect(res).to.have.status(200) expect(res.body).to.have.property('reviewObjects') expect(res.body.reviewObjects).to.be.an('array') + if (res.body.reviewObjects.length > 0) { + expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('program_data') + } + }) + + it('Secretariat can see program_data in review history', async () => { + const res = await chai + .request(app) + .get(`/api/review/org/${constants.existingOrg.short_name}/reviews`) + .set({ ...constants.headers }) + expect(res).to.have.status(200) + expect(res.body).to.have.property('reviewObjects') + expect(res.body.reviewObjects).to.be.an('array') + if (res.body.reviewObjects.length > 0) { + expect(res.body.reviewObjects[0].new_review_data).to.have.property('program_data') + } + }) + + it('Admin cannot see program_data when getting review object by UUID', async () => { + const res = await chai + .request(app) + .get(`/api/review/byUUID/${approveTestReviewUUID}`) + .set({ ...constants.nonSecretariatUserHeaders2 }) + expect(res).to.have.status(200) + expect(res.body.new_review_data).to.not.have.property('program_data') }) // ------------------------------------------------------------------------------------------------ From 41a3f9bb6b4a28f5ae25485252c55fdf5238f5dc Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 28 Apr 2026 12:48:49 -0400 Subject: [PATCH 27/40] Fixing some CNAOrg missing fields --- schemas/registry-org/BaseOrg.json | 3 +++ schemas/registry-org/CNAOrg.json | 6 ++++++ schemas/registry-org/RootOrg.json | 4 +++- schemas/registry-org/create-registry-org-request.json | 4 ++++ schemas/registry-org/create-registry-org-response.json | 4 ++++ schemas/registry-org/update-registry-org-request.json | 4 ++++ schemas/registry-org/update-registry-org-response.json | 4 ++++ 7 files changed, 28 insertions(+), 1 deletion(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index d04301fb3..22238a28d 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -195,6 +195,9 @@ "items": { "type": "string" } + }, + "industry": { + "type": "string" } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 982e62f38..dbb8875d9 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -118,6 +118,12 @@ }, "program_data": { "$ref": "/BaseOrg#/properties/program_data" + }, + "industry": { + "$ref": "/BaseOrg#/properties/industry" + }, + "top_level_root": { + "$ref": "/BaseOrg#/properties/top_level_root" } }, "required": [ diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 664b432a6..d519b2181 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -52,7 +52,9 @@ "type": "string" }, "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "program_data": { "$ref": "/BaseOrg#/properties/program_data" } + "program_data": { "$ref": "/BaseOrg#/properties/program_data" }, + "industry": { "$ref": "/BaseOrg#/properties/industry" }, + "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" } }, "required": ["short_name"] } diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 1a3fe8d76..476aa5175 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -112,6 +112,10 @@ "type": "string" }, "description": "Locations of vulnerability advisories" + }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" } }, "required": [ diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 6fb87bcc7..b57bea9a4 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -154,6 +154,10 @@ }, "description": "Locations of vulnerability advisories" }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 48746b8c6..e214f7cb4 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -123,6 +123,10 @@ "type": "string" }, "description": "Locations of vulnerability advisories" + }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" } } } \ No newline at end of file diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index d73cca451..3dd15eb41 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -143,6 +143,10 @@ }, "description": "Locations of vulnerability advisories" }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" From 81d6909af4cd5c3fcda5de4a0e5a556d313954f3 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 28 Apr 2026 13:04:46 -0400 Subject: [PATCH 28/40] more schema updateS --- schemas/registry-org/BaseOrg.json | 16 +++++ schemas/registry-org/CNAOrg.json | 6 ++ schemas/registry-org/RootOrg.json | 4 +- .../create-registry-org-request.json | 67 +++++++++++++++++++ .../create-registry-org-response.json | 9 +++ .../update-registry-org-request.json | 67 +++++++++++++++++++ .../update-registry-org-response.json | 9 +++ 7 files changed, 177 insertions(+), 1 deletion(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 22238a28d..29cf5e1ef 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -198,6 +198,22 @@ }, "industry": { "type": "string" + }, + "tl_root_start_date": { + "type": "string", + "format": "date-time" + }, + "is_cna_discussion_list": { + "type": "boolean" + }, + "partner_role_type": { + "$ref": "#/definitions/partnerRoleType" + }, + "partner_number": { + "type": "string" + }, + "partner_country": { + "type": "string" } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index dbb8875d9..ff8881f4c 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -124,6 +124,12 @@ }, "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" + }, + "tl_root_start_date": { + "$ref": "/BaseOrg#/properties/tl_root_start_date" + }, + "is_cna_discussion_list": { + "$ref": "/BaseOrg#/properties/is_cna_discussion_list" } }, "required": [ diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index d519b2181..9015ebdf5 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -54,7 +54,9 @@ "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, "program_data": { "$ref": "/BaseOrg#/properties/program_data" }, "industry": { "$ref": "/BaseOrg#/properties/industry" }, - "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" } + "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" }, + "tl_root_start_date": { "$ref": "/BaseOrg#/properties/tl_root_start_date" }, + "is_cna_discussion_list": { "$ref": "/BaseOrg#/properties/is_cna_discussion_list" } }, "required": ["short_name"] } diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 476aa5175..ee65de54c 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -116,6 +116,73 @@ "industry": { "type": "string", "description": "Industry sector of the organization" + }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, + "partner_role_type": { + "type": "string", + "enum": [ + "", + "Bug Bounty Provider", + "CERT", + "Consortium", + "Hosted Service", + "N/A", + "Open Source", + "Researcher", + "Vendor" + ], + "description": "The type of role a partner holds" + }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, + "partner_country": { + "type": "string", + "description": "Country of the partner" + }, + "program_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date" + }, + "partner_inactive_date": { + "type": "string", + "format": "date" + }, + "status": { + "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Advisory locations for web scraping" + } + }, + "description": "Additional partner metadata (restricted)" } }, "required": [ diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index b57bea9a4..4bb60d9b0 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -158,6 +158,15 @@ "type": "string", "description": "Industry sector of the organization" }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index e214f7cb4..14fe4c3a8 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -127,6 +127,73 @@ "industry": { "type": "string", "description": "Industry sector of the organization" + }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, + "partner_role_type": { + "type": "string", + "enum": [ + "", + "Bug Bounty Provider", + "CERT", + "Consortium", + "Hosted Service", + "N/A", + "Open Source", + "Researcher", + "Vendor" + ], + "description": "The type of role a partner holds" + }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, + "partner_country": { + "type": "string", + "description": "Country of the partner" + }, + "program_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date" + }, + "partner_inactive_date": { + "type": "string", + "format": "date" + }, + "status": { + "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Advisory locations for web scraping" + } + }, + "description": "Additional partner metadata (restricted)" } } } \ No newline at end of file diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 3dd15eb41..6a2b86adc 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -147,6 +147,15 @@ "type": "string", "description": "Industry sector of the organization" }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" From 573f7b94e28a4252fe0bccaa6d1f27cbb2286072 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 28 Apr 2026 13:38:26 -0400 Subject: [PATCH 29/40] renamed to additional contacts --- schemas/registry-org/BaseOrg.json | 2 +- schemas/registry-org/CNAOrg.json | 2 +- schemas/registry-org/create-registry-org-request.json | 2 +- schemas/registry-org/create-registry-org-response.json | 2 +- schemas/registry-org/get-registry-org-response.json | 2 +- schemas/registry-org/list-registry-orgs-response.json | 2 +- schemas/registry-org/update-registry-org-request.json | 2 +- schemas/registry-org/update-registry-org-response.json | 2 +- src/controller/org.controller/org.middleware.js | 2 +- .../registry-org.controller/registry-org.middleware.js | 2 +- src/model/baseorg.js | 2 +- src/scripts/migrate.js | 4 ++-- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 29cf5e1ef..6c91ce741 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -121,7 +121,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index ff8881f4c..7c80f0b95 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -27,7 +27,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index ee65de54c..6b556101d 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -66,7 +66,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 4bb60d9b0..899cdf682 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -107,7 +107,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 31f6bdd47..5c647ecc0 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -72,7 +72,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 75e372907..0863811d0 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -101,7 +101,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 14fe4c3a8..fb4070acd 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -82,7 +82,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 6a2b86adc..805bd961e 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -96,7 +96,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 1d29764b4..5080e6a89 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -146,7 +146,7 @@ function validateCreateOrgParameters () { 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', - 'contact_info.additional_contact_users', + 'contact_info.additional_contacts', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index b7c40e1fa..c9acfc0f3 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -15,7 +15,7 @@ function parsePostParams (req, res, next) { 'top_level_root', 'users', 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', - 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', + 'contact_info.additional_contacts', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index a92887902..82462dd63 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -15,7 +15,7 @@ const schema = { users: { type: [String], set: toUndefined }, admins: [String], contact_info: { - additional_contact_users: [{ + additional_contacts: [{ phone: String, poc: String, poc_email: String diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index b4dc9ee0b..d494cf53f 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -109,7 +109,7 @@ async function addCVEBoard (db) { soft_quota: null, hard_quota: null, contact_info: { - additional_contact_users: [], + additional_contacts: [], poc: null, poc_email: null, poc_phone: null, @@ -215,7 +215,7 @@ async function orgHelper (db) { hard_quota: doc.policies?.id_quota, admins: admins, contact_info: { - additional_contact_users: [], // don't have now + additional_contacts: [], // don't have now poc: null, // don't have now poc_email: null, // don't have now poc_phone: null, // don't have now From f9064f5d948e8c8a29ffa3b294a86a511cdcb285 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 14:46:31 -0400 Subject: [PATCH 30/40] missing conflict --- test/integration-tests/conversation/editConversationTest.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index db5ef413c..24b1ae440 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -30,11 +30,7 @@ describe('Testing Conversation edit by index endpoint', () => { delete org.last_updated delete org.admins delete org.users -<<<<<<< HEAD delete org.top_level_root -======= - delete org.root_or_tlr ->>>>>>> v2.8.0_feature }) await chai From 962eda91799a173b7c8abe9e941ed2f23015456c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 30 Apr 2026 10:00:06 -0400 Subject: [PATCH 31/40] Fixing unit tests --- test/unit-tests/org/orgUpdateTest.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/unit-tests/org/orgUpdateTest.js b/test/unit-tests/org/orgUpdateTest.js index 4292acb21..e53f8cbd3 100644 --- a/test/unit-tests/org/orgUpdateTest.js +++ b/test/unit-tests/org/orgUpdateTest.js @@ -49,7 +49,7 @@ class OrgUpdatedAddingRole { async updateOrg () { const temp = orgFixtures.owningOrg - temp.authority.active_roles = [...new Set([...temp.authority.active_roles, 'ROOT_CNA'])] + temp.authority.active_roles = [...new Set([...temp.authority.active_roles, 'ROOT'])] return temp } @@ -200,7 +200,7 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { }, orgParams.parsePostParams, orgController.ORG_UPDATE_SINGLE) chai.request(app) - .put(`/org-updated-adding-role-1/${orgFixtures.owningOrg.short_name}?active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}&active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}`) + .put(`/org-updated-adding-role-1/${orgFixtures.owningOrg.short_name}?active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}&active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}`) .set(orgFixtures.secretariatHeader) .end((err, res) => { if (err) { @@ -212,7 +212,7 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { expect(res.body).to.have.property('updated').and.to.be.a('object') expect(res.body.updated.authority.active_roles).to.have.lengthOf(2) expect(res.body.updated.authority.active_roles[0]).to.equal(CONSTANTS.AUTH_ROLE_ENUM.CNA) - expect(res.body.updated.authority.active_roles[1]).to.equal(CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA) + expect(res.body.updated.authority.active_roles[1]).to.equal(CONSTANTS.AUTH_ROLE_ENUM.ROOT) expect(res.body.updated.short_name).to.equal(orgFixtures.owningOrg.short_name) expect(res.body.updated.name).to.equal(orgFixtures.owningOrg.name) expect(res.body.updated.UUID).to.equal(orgFixtures.owningOrg.UUID) @@ -235,7 +235,7 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { }, orgParams.parsePostParams, orgController.ORG_UPDATE_SINGLE) chai.request(app) - .put(`/org-updated-adding-role-2/${orgFixtures.owningOrg.short_name}?active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}&active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}`) + .put(`/org-updated-adding-role-2/${orgFixtures.owningOrg.short_name}?active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}&active_roles.add=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}`) .set(orgFixtures.secretariatHeader) .end((err, res) => { if (err) { @@ -246,7 +246,7 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { expect(res.body).to.have.property('updated').and.to.be.a('object') expect(res.body.updated.authority.active_roles).to.have.lengthOf(2) expect(res.body.updated.authority.active_roles[0]).to.equal(CONSTANTS.AUTH_ROLE_ENUM.CNA) - expect(res.body.updated.authority.active_roles[1]).to.equal(CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA) + expect(res.body.updated.authority.active_roles[1]).to.equal(CONSTANTS.AUTH_ROLE_ENUM.ROOT) expect(res.body.updated.short_name).to.equal(orgFixtures.owningOrg.short_name) expect(res.body.updated.name).to.equal(orgFixtures.owningOrg.name) expect(res.body.updated.UUID).to.equal(orgFixtures.owningOrg.UUID) @@ -269,7 +269,7 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { }, orgParams.parsePostParams, orgController.ORG_UPDATE_SINGLE) chai.request(app) - .put(`/org-updated-removing-role-1/${orgFixtures.owningOrg.short_name}?active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}&active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}`) + .put(`/org-updated-removing-role-1/${orgFixtures.owningOrg.short_name}?active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}&active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}`) .set(orgFixtures.secretariatHeader) .end((err, res) => { if (err) { @@ -302,7 +302,7 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { }, orgParams.parsePostParams, orgController.ORG_UPDATE_SINGLE) chai.request(app) - .put(`/org-updated-removing-role-2/${orgFixtures.owningOrg.short_name}?active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}&active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA}`) + .put(`/org-updated-removing-role-2/${orgFixtures.owningOrg.short_name}?active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}&active_roles.remove=${CONSTANTS.AUTH_ROLE_ENUM.ROOT}`) .set(orgFixtures.secretariatHeader) .end((err, res) => { if (err) { From 13d01f7f297be8be1f59ff1fdabd4bc940bb8344 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 30 Apr 2026 13:42:40 -0400 Subject: [PATCH 32/40] renamed and moved private_contacts --- schemas/registry-org/BaseOrg.json | 38 +++++++++---------- schemas/registry-org/CNAOrg.json | 36 +++++++++--------- .../create-registry-org-request.json | 38 +++++++++---------- .../create-registry-org-response.json | 38 +++++++++---------- .../get-registry-org-response.json | 38 +++++++++---------- .../list-registry-orgs-response.json | 38 +++++++++---------- .../update-registry-org-request.json | 38 +++++++++---------- .../update-registry-org-response.json | 38 +++++++++---------- .../org.controller/org.middleware.js | 2 +- .../registry-org.middleware.js | 2 +- src/model/baseorg.js | 10 ++--- src/scripts/migrate.js | 4 +- test/integration-tests/constants.js | 5 +++ test/integration-tests/org/postOrgTest.js | 3 ++ 14 files changed, 168 insertions(+), 160 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 6c91ce741..d47c19729 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -118,28 +118,28 @@ "minimum": 0, "maximum": 100000 }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 7c80f0b95..0b86c1a9a 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -24,27 +24,27 @@ "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 6b556101d..718778634 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -63,28 +63,28 @@ "type": "string", "description": "List of products associated with the organization" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 899cdf682..9c8cb073e 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -104,28 +104,28 @@ "type": "integer", "description": "Hard quota for CVE IDs" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 5c647ecc0..121ec7899 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -69,28 +69,28 @@ }, "description": "UUIDs of admin users" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 0863811d0..fa6d832de 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -98,28 +98,28 @@ }, "description": "UUIDs of admin users" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index fb4070acd..456be9213 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -79,28 +79,28 @@ "type": "string", "description": "List of products associated with the organization" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 805bd961e..2a5e2cdcb 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -93,28 +93,28 @@ "type": "integer", "description": "Hard quota for CVE IDs" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { - "additional_contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "phone": { - "type": "string" - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - } - }, - "additionalProperties": false - } - }, "phone": { "type": "string" }, diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 5080e6a89..1e986bb6e 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -146,7 +146,7 @@ function validateCreateOrgParameters () { 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', - 'contact_info.additional_contacts', + 'private_contacts', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index c9acfc0f3..939897911 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -15,7 +15,7 @@ function parsePostParams (req, res, next) { 'top_level_root', 'users', 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', - 'contact_info.additional_contacts', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', + 'private_contacts', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 82462dd63..1f59c2cc7 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -15,16 +15,16 @@ const schema = { users: { type: [String], set: toUndefined }, admins: [String], contact_info: { - additional_contacts: [{ - phone: String, - poc: String, - poc_email: String - }], phone: String, poc: String, poc_email: String, website: String }, + private_contacts: [{ + phone: String, + poc: String, + poc_email: String + }], partner_role_type: String, partner_number: String, partner_country: String, diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index d494cf53f..ad72a2875 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -108,8 +108,8 @@ async function addCVEBoard (db) { product_list: null, soft_quota: null, hard_quota: null, + private_contacts: [], contact_info: { - additional_contacts: [], poc: null, poc_email: null, poc_phone: null, @@ -214,8 +214,8 @@ async function orgHelper (db) { soft_quota: null, // don't have now hard_quota: doc.policies?.id_quota, admins: admins, + private_contacts: [], // don't have now contact_info: { - additional_contacts: [], // don't have now poc: null, // don't have now poc_email: null, // don't have now poc_phone: null, // don't have now diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index 0fc8f9d68..df85f8051 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -386,6 +386,11 @@ const testRegistryOrg = { phone: '555-1234', website: 'https://test.org' }, + private_contacts: [{ + poc: 'Dave Private', + poc_email: 'daveprivate@test.org', + phone: '555-4321' + }], authority: ['CNA'], hard_quota: 100000 } diff --git a/test/integration-tests/org/postOrgTest.js b/test/integration-tests/org/postOrgTest.js index a94245fd6..5f11e07d5 100644 --- a/test/integration-tests/org/postOrgTest.js +++ b/test/integration-tests/org/postOrgTest.js @@ -62,6 +62,9 @@ describe('Testing Org post endpoint', () => { expect(res.body.created).to.haveOwnProperty('contact_info') expect(res.body.created.contact_info).to.include(constants.testRegistryOrg.contact_info) + expect(res.body.created).to.haveOwnProperty('private_contacts') + expect(res.body.created.private_contacts).to.deep.equal(constants.testRegistryOrg.private_contacts) + expect(res.body.created).to.haveOwnProperty('authority') expect(res.body.created.authority).to.deep.equal(['CNA']) From f1fa8eacec6d32490eb94d0a1cdbca9d3d706d40 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 30 Apr 2026 16:35:32 -0400 Subject: [PATCH 33/40] Should now respect empty arrays --- api-docs/openapi.json | 116 ++++++++++++++++++ package.json | 2 +- schemas/registry-org/RootOrg.json | 25 +++- src/model/baseorg.js | 1 + src/repositories/baseOrgRepository.js | 2 +- src/utils/utils.js | 3 +- .../conversation/editConversationTest.js | 2 + .../registry-org/registryOrgCRUDTest.js | 2 + .../registry-org/rootOrgTest.js | 5 +- 9 files changed, 148 insertions(+), 10 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 1044a3c4a..5fd0bee55 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -4788,6 +4788,122 @@ } } }, + "/conversation/{uuid}": { + "put": { + "tags": [ + "Conversation" + ], + "summary": "Updates a conversation by UUID (accessible to Secretariat only)", + "description": "

    Access Control

    User must belong to an organization with the Secretariat role

    Expected Behavior

    Secretariat: Updates the conversation with the specified UUID

    ", + "operationId": "updateConversationByUUID", + "parameters": [ + { + "name": "uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The UUID of the conversation to update" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns the updated conversation", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/conversation/conversation.json" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "401": { + "description": "Not Authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "The updated content of the conversation message" + }, + "visibility": { + "type": "string", + "enum": [ + "private", + "public" + ], + "description": "The updated visibility of the conversation message" + } + } + } + } + } + } + } + }, "/review/byUUID/{uuid}": { "get": { "tags": [ diff --git a/package.json b/package.json index 4cb9b3f9e..1e7be477c 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "start:prd": "node src/swagger.js && NODE_ENV=production node src/scripts/updateOpenapiHost.js && NODE_ENV=production node src/index.js", "swagger-autogen": "node src/swagger.js", "test": "NODE_ENV=test mocha --recursive --exit || true", - "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://docdb:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", + "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://localhost:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", "test:unit-tests": "NODE_ENV=test mocha test/unit-tests --recursive --exit || true", "test:coverage": "NODE_ENV=test nyc --reporter=text mocha src/* --recursive --exit || true", "test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true", diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 9015ebdf5..d4c090444 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -16,6 +16,24 @@ "maxLength": 32 }, "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, + "private_contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string" + } + }, + "additionalProperties": false + } + }, "contact_info": { "type": "object", "properties": { @@ -38,8 +56,7 @@ "type": "array", "uniqueItems": true, "items": { - "type": "string", - "format": "uuid" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "partner_role_type": { @@ -54,9 +71,7 @@ "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, "program_data": { "$ref": "/BaseOrg#/properties/program_data" }, "industry": { "$ref": "/BaseOrg#/properties/industry" }, - "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" }, - "tl_root_start_date": { "$ref": "/BaseOrg#/properties/tl_root_start_date" }, - "is_cna_discussion_list": { "$ref": "/BaseOrg#/properties/is_cna_discussion_list" } + "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" } }, "required": ["short_name"] } diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 1f59c2cc7..13777a5af 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -21,6 +21,7 @@ const schema = { website: String }, private_contacts: [{ + _id: false, phone: String, poc: String, poc_email: String diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index da27ff3d6..ca6684f06 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -938,7 +938,7 @@ class BaseOrgRepository extends BaseRepository { if (hasJointApprovalChanges) { // write the joint approval to the database - jointApprovalRegistry = _.merge({}, registryOrg.toObject(), registryObjectRaw) + jointApprovalRegistry = _.mergeWith({}, registryOrg.toObject(), registryObjectRaw, skipNulls) if (reviewObject) { await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options) } else { diff --git a/src/utils/utils.js b/src/utils/utils.js index 82e022ec7..d30c4252d 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -309,8 +309,7 @@ function deepRemoveEmpty (obj) { // This will catch both initially empty fields and nested objects that became empty. if ( value === null || - (_.isObject(value) && !_.isDate(value) && _.isEmpty(value)) || - (_.isArray(value) && _.isEmpty(value)) + (_.isObject(value) && !_.isArray(value) && !_.isDate(value) && _.isEmpty(value)) ) { delete currentObj[key] } diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 24b1ae440..9697a2173 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -31,6 +31,8 @@ describe('Testing Conversation edit by index endpoint', () => { delete org.admins delete org.users delete org.top_level_root + delete org.oversees + delete org.program_data }) await chai diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index ee5dd557d..2c49f3a63 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -71,6 +71,8 @@ describe('Testing /registryOrg endpoints', () => { createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated + delete createdOrg.users + delete createdOrg.admins }) }) }) diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index 0ea61393a..cbb251087 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -33,6 +33,9 @@ describe('Testing ROOT Organization Type', () => { delete createdOrg.created delete createdOrg.last_updated delete createdOrg.program_data + delete createdOrg.users + delete createdOrg.admins + delete createdOrg.oversees }) }) @@ -85,7 +88,7 @@ describe('Testing ROOT Organization Type', () => { long_name: 'Updated Root Org Test' }) .then((res) => { - if (res.status === 400) console.log(JSON.stringify(res.body, null, 2)) + if (res.status !== 200) console.log('403 Response:', JSON.stringify(res.body, null, 2)) expect(res).to.have.status(200) }) }) From a11961becac6f4742da505185fd174d66c24053e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 30 Apr 2026 16:45:30 -0400 Subject: [PATCH 34/40] Deal with string vs date issue --- src/repositories/baseOrgRepository.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index ca6684f06..32ab42b20 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -807,10 +807,15 @@ class BaseOrgRepository extends BaseRepository { jointApprovalFields = getConstants().JOINT_APPROVAL_FIELDS } + // Convert Mongoose docs to plain objects and serialize to match stringified formats + const originalPlain = orgObjectOriginal.toObject ? orgObjectOriginal.toObject() : orgObjectOriginal + const originalSerialized = JSON.parse(JSON.stringify(originalPlain)) + const updatedSerialized = JSON.parse(JSON.stringify(orgObjectUpdated)) + // Filter the list to find only fields that have changed const changedFields = _.filter(jointApprovalFields, field => { // Check if the value in the original object is different from the updated object - return _.get(orgObjectOriginal, field) !== _.get(orgObjectUpdated, field) + return !_.isEqual(_.get(originalSerialized, field), _.get(updatedSerialized, field)) }) // Return the array of fields that had changes (will be empty if none changed) @@ -932,8 +937,8 @@ class BaseOrgRepository extends BaseRepository { // Check if there are actual changes to joint approval fields compared to current org object (not current review) // Only compare fields that are actually in the incoming data const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry)) - const currentJointApprovalData = _.pick(registryOrg.toObject(), incomingJointApprovalKeys) - const incomingJointApprovalData = _.pick(registryObjectRaw, incomingJointApprovalKeys) + const currentJointApprovalData = JSON.parse(JSON.stringify(_.pick(registryOrg.toObject(), incomingJointApprovalKeys))) + const incomingJointApprovalData = JSON.parse(JSON.stringify(_.pick(registryObjectRaw, incomingJointApprovalKeys))) const hasJointApprovalChanges = !_.isEqual(currentJointApprovalData, incomingJointApprovalData) if (hasJointApprovalChanges) { From 62d5b275555d6f359e9293baf4ab32cf133a96ce Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 30 Apr 2026 16:50:18 -0400 Subject: [PATCH 35/40] removed accidental commit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e7be477c..4cb9b3f9e 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "start:prd": "node src/swagger.js && NODE_ENV=production node src/scripts/updateOpenapiHost.js && NODE_ENV=production node src/index.js", "swagger-autogen": "node src/swagger.js", "test": "NODE_ENV=test mocha --recursive --exit || true", - "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://localhost:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", + "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://docdb:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", "test:unit-tests": "NODE_ENV=test mocha test/unit-tests --recursive --exit || true", "test:coverage": "NODE_ENV=test nyc --reporter=text mocha src/* --recursive --exit || true", "test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true", From 24f4606fd179cdddc6a701b23b11345678e0750e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 30 Apr 2026 16:50:57 -0400 Subject: [PATCH 36/40] fixed accidental commit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e7be477c..4cb9b3f9e 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "start:prd": "node src/swagger.js && NODE_ENV=production node src/scripts/updateOpenapiHost.js && NODE_ENV=production node src/index.js", "swagger-autogen": "node src/swagger.js", "test": "NODE_ENV=test mocha --recursive --exit || true", - "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://localhost:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", + "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://docdb:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", "test:unit-tests": "NODE_ENV=test mocha test/unit-tests --recursive --exit || true", "test:coverage": "NODE_ENV=test nyc --reporter=text mocha src/* --recursive --exit || true", "test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true", From 71d9d9e156ef9b97e5f44acdf197bcfb725a3c6c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 1 May 2026 10:49:35 -0400 Subject: [PATCH 37/40] A bit of refactoring --- src/repositories/baseOrgRepositoryHelpers.js | 206 +++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 src/repositories/baseOrgRepositoryHelpers.js diff --git a/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js new file mode 100644 index 000000000..d91c90a8c --- /dev/null +++ b/src/repositories/baseOrgRepositoryHelpers.js @@ -0,0 +1,206 @@ +const _ = require('lodash') +const getConstants = require('../constants').getConstants +const BaseOrgModel = require('../model/baseorg') +const CNAOrgModel = require('../model/cnaorg') +const ADPOrgModel = require('../model/adporg') +const BulkDownloadModel = require('../model/bulkdownloadorg') +const SecretariatOrgModel = require('../model/secretariatorg') +const RootOrgModel = require('../model/rootorg') + +const skipNulls = (objValue, srcValue) => { + if (_.isArray(objValue)) { + return srcValue + } + return undefined +} + +function handleShortNameUpdate (incomingOrg, registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw) { + if (incomingOrg?.new_short_name) { + const newName = incomingOrg.new_short_name + registryOrg.short_name = newName + legacyOrg.short_name = newName + registryObjectRaw.short_name = newName + legacyObjectRaw.short_name = newName + delete registryObjectRaw.new_short_name + delete legacyObjectRaw.new_short_name + delete incomingOrg.new_short_name + } +} + +function automateProgramDataDates (registryObjectRaw, registryOrg) { + if (registryObjectRaw.program_data && registryObjectRaw.program_data.status) { + const incomingStatus = registryObjectRaw.program_data.status + const currentStatus = registryOrg.program_data?.status || 'inactive' + if (incomingStatus !== currentStatus) { + if (incomingStatus === 'active') { + registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_inactive_date !== undefined) { + delete registryObjectRaw.program_data.partner_inactive_date + } + } else if (incomingStatus === 'inactive') { + registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_active_date !== undefined) { + delete registryObjectRaw.program_data.partner_active_date + } + } + } else { + if (registryOrg.program_data?.partner_active_date) { + registryObjectRaw.program_data.partner_active_date = registryOrg.program_data.partner_active_date + } + if (registryOrg.program_data?.partner_inactive_date) { + registryObjectRaw.program_data.partner_inactive_date = registryOrg.program_data.partner_inactive_date + } + } + } +} + +function mergeAllowedFields (targetDoc, rawData, fieldsToProtect) { + return targetDoc.overwrite( + _.mergeWith( + _.pick(targetDoc.toObject(), fieldsToProtect), + _.omit(rawData, fieldsToProtect), + skipNulls + ) + ) +} + +async function manageReviewObject (registryOrg, registryObjectRaw, jointApprovalFieldsRegistry, reviewObject, requestingUsername, options) { + const ReviewObjectRepository = require('./reviewObjectRepository') + const reviewObjectRepo = new ReviewObjectRepository() + + const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry)) + + const currentJointApprovalData = _.pick(registryOrg.toObject(), incomingJointApprovalKeys) + const incomingJointApprovalData = _.pick(registryObjectRaw, incomingJointApprovalKeys) + + // Normalize dates before comparison + Object.keys(currentJointApprovalData).forEach(key => { + if (currentJointApprovalData[key] instanceof Date) { + currentJointApprovalData[key] = currentJointApprovalData[key].toISOString() + } + }) + Object.keys(incomingJointApprovalData).forEach(key => { + if (incomingJointApprovalData[key] instanceof Date) { + incomingJointApprovalData[key] = incomingJointApprovalData[key].toISOString() + } + }) + + const hasJointApprovalChanges = !_.isEqual(currentJointApprovalData, incomingJointApprovalData) + + if (hasJointApprovalChanges) { + const jointApprovalRegistry = _.mergeWith({}, registryOrg.toObject(), registryObjectRaw, skipNulls) + if (reviewObject) { + await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options) + } else { + await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, requestingUsername, options) + } + } else { + if (reviewObject) { + await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUsername, options) + } + } +} + +async function processJointApprovalAndMerge (registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw, reviewObject, isSecretariat, options, requestingUsername, jointApprovalFieldsRegistry, jointApprovalFieldsLegacy) { + const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] + let registryProtectedFields = [...protectedFields] + if (!isSecretariat) { + registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] + } + + let updatedRegistryOrg = null + let updatedLegacyOrg = null + + if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { + updatedLegacyOrg = mergeAllowedFields(legacyOrg, legacyObjectRaw, protectedFields) + updatedRegistryOrg = mergeAllowedFields(registryOrg, registryObjectRaw, registryProtectedFields) + } else { + await manageReviewObject(registryOrg, registryObjectRaw, jointApprovalFieldsRegistry, reviewObject, requestingUsername, options) + + const restrictedRegistryFields = [...registryProtectedFields, ...jointApprovalFieldsRegistry] + const restrictedLegacyFields = [...protectedFields, ...jointApprovalFieldsLegacy] + + updatedRegistryOrg = mergeAllowedFields(registryOrg, registryObjectRaw, restrictedRegistryFields) + updatedLegacyOrg = mergeAllowedFields(legacyOrg, legacyObjectRaw, restrictedLegacyFields) + } + + return { + updatedRegistryOrg, + updatedLegacyOrg + } +} + +async function createAuditLogEntry (registryOrg, originalRegistryOrgObject, requestingUserUUID, options) { + if (!requestingUserUUID) return + try { + const AuditRepository = require('./auditRepository') + const auditRepo = new AuditRepository() + await auditRepo.seedAuditHistoryForOrg( + registryOrg.UUID, + originalRegistryOrgObject, + requestingUserUUID, + { ...options, upsert: true } + ) + const beforeUpdateObject = originalRegistryOrgObject + const afterUpdateObject = registryOrg.toObject() + + const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + + if (!_.isEqual(cleanBefore, cleanAfter)) { + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + registryOrg.toObject(), + requestingUserUUID, + { ...options, upsert: true } + ) + } + console.log('Audit entry created for registry object') + } catch (auditError) { + console.error('Audit entry creation failed:', auditError) + } +} + +async function handleAuthorityModelChange (updatedRegistryOrg, originalRoles, options) { + let roleChange = false + if (!_.isEqual([...originalRoles].sort(), [...updatedRegistryOrg?.authority].sort())) { + roleChange = true + } + + let TargetModel = null + if (updatedRegistryOrg.authority?.includes('SECRETARIAT')) { + TargetModel = SecretariatOrgModel + } else if (updatedRegistryOrg.authority?.includes('CNA')) { + TargetModel = CNAOrgModel + } else if (updatedRegistryOrg.authority?.includes('ADP')) { + TargetModel = ADPOrgModel + } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { + TargetModel = BulkDownloadModel + } else if (updatedRegistryOrg.authority?.includes('ROOT')) { + TargetModel = RootOrgModel + } + + if (TargetModel && roleChange) { + const oldId = updatedRegistryOrg._id + await BaseOrgModel.deleteOne({ _id: oldId }, options) + const newDocData = updatedRegistryOrg.toObject() + delete newDocData.__t + newDocData._id = oldId + const newDoc = new TargetModel(newDocData) + await newDoc.save(options) + updatedRegistryOrg = newDoc + } + + return updatedRegistryOrg +} + +module.exports = { + skipNulls, + handleShortNameUpdate, + automateProgramDataDates, + mergeAllowedFields, + manageReviewObject, + processJointApprovalAndMerge, + createAuditLogEntry, + handleAuthorityModelChange +} From 4c6e1066aaa9563a7df827f9ab1d1eef685ea8e2 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 1 May 2026 10:52:23 -0400 Subject: [PATCH 38/40] Some comments --- src/repositories/baseOrgRepository.js | 188 +++---------------- src/repositories/baseOrgRepositoryHelpers.js | 75 ++++++++ 2 files changed, 96 insertions(+), 167 deletions(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index ca6684f06..ec113d252 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -11,13 +11,13 @@ const _ = require('lodash') const BaseOrg = require('../model/baseorg') const ConversationRepository = require('./conversationRepository') const getConstants = require('../constants').getConstants - -const skipNulls = (objValue, srcValue) => { - if (_.isArray(objValue)) { - return srcValue - } - return undefined -} +const { + handleShortNameUpdate, + automateProgramDataDates, + processJointApprovalAndMerge, + createAuditLogEntry, + handleAuthorityModelChange +} = require('./baseOrgRepositoryHelpers') /** * @function setAggregateOrgObj @@ -817,7 +817,6 @@ class BaseOrgRepository extends BaseRepository { return changedFields } - /** /** * @async * @function updateOrgFull @@ -834,22 +833,25 @@ class BaseOrgRepository extends BaseRepository { * @returns {Promise} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values. */ async updateOrgFull (shortName, incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) { - // TODO: Fix these imports, remove the circular imports const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') const ReviewObjectRepository = require('./reviewObjectRepository') const BaseUserRepository = require('./baseUserRepository') + const ConversationRepository = require('./conversationRepository') const legacyOrgRepo = new OrgRepository() const reviewObjectRepo = new ReviewObjectRepository() const userRepo = new BaseUserRepository() const conversationRepo = new ConversationRepository() + const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) const registryOrg = await this.findOneByShortName(shortName, options) const originalRegistryOrgObject = registryOrg.toObject() - // check to see if there is a PENDING review object: + const originalRoles = registryOrg.authority + const reviewObject = await reviewObjectRepo.getOrgReviewObjectByOrgShortname(shortName, isSecretariat, options) const { conversation, ...incomingOrgBody } = incomingOrg + let legacyObjectRaw let registryObjectRaw @@ -861,175 +863,27 @@ class BaseOrgRepository extends BaseRepository { legacyObjectRaw = this.convertRegistryToLegacy(incomingOrgBody) } - if (incomingOrg?.new_short_name) { - const newName = incomingOrg.new_short_name - - // 1. Update the Mongoose instances - registryOrg.short_name = newName - legacyOrg.short_name = newName - - // 2. Update the raw tracking objects so lodash.merge doesn't restore the old short_name - registryObjectRaw.short_name = newName - legacyObjectRaw.short_name = newName - - // 3. Remove new_short_name from the raw objects so it doesn't merge into the DB - delete registryObjectRaw.new_short_name - delete legacyObjectRaw.new_short_name - delete incomingOrg.new_short_name // Keeping for existing logic - } + handleShortNameUpdate(incomingOrg, registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw) + automateProgramDataDates(registryObjectRaw, registryOrg) - // Automate program_data dates based on status - if (registryObjectRaw.program_data && registryObjectRaw.program_data.status) { - const incomingStatus = registryObjectRaw.program_data.status - const currentStatus = registryOrg.program_data?.status || 'inactive' - if (incomingStatus !== currentStatus) { - if (incomingStatus === 'active') { - registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] - if (registryObjectRaw.program_data.partner_inactive_date !== undefined) { - delete registryObjectRaw.program_data.partner_inactive_date - } - } else if (incomingStatus === 'inactive') { - registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] - if (registryObjectRaw.program_data.partner_active_date !== undefined) { - delete registryObjectRaw.program_data.partner_active_date - } - } - } else { - // Keep existing dates if status didn't change - if (registryOrg.program_data?.partner_active_date) { - registryObjectRaw.program_data.partner_active_date = registryOrg.program_data.partner_active_date - } - if (registryOrg.program_data?.partner_inactive_date) { - registryObjectRaw.program_data.partner_inactive_date = registryOrg.program_data.partner_inactive_date - } - } - } + const requestingUser = requestingUserUUID ? await userRepo.findUserByUUID(requestingUserUUID, options) : null + const requestingUsername = requestingUser ? requestingUser.username : null - // Checking for joint approval fields const jointApprovalFieldsRegistry = this.getJointApprovalFields(registryOrg, registryObjectRaw) const jointApprovalFieldsLegacy = this.getJointApprovalFields(legacyOrg, legacyObjectRaw, true) - let updatedRegistryOrg = null - let updatedLegacyOrg = null - let jointApprovalRegistry = null - // If there are no joint approval fields, merge the original and updated objects. Otherwise, update the registry object and legacy object separately considering joint approval. - // Dealing with roles requires a bit of extra control. - const originalRoles = registryOrg.authority - - const requestingUser = requestingUserUUID ? await userRepo.findUserByUUID(requestingUserUUID, options) : null - const requestingUsername = requestingUser ? requestingUser.username : null - - const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] - let registryProtectedFields = [...protectedFields] - if (!isSecretariat) { - registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] - } + let { updatedRegistryOrg, updatedLegacyOrg } = await processJointApprovalAndMerge( + registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw, reviewObject, isSecretariat, options, requestingUsername, jointApprovalFieldsRegistry, jointApprovalFieldsLegacy + ) - if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { - updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), _.omit(legacyObjectRaw, protectedFields), skipNulls)) - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), registryProtectedFields), _.omit(registryObjectRaw, registryProtectedFields), skipNulls)) - } else { - // Check if there are actual changes to joint approval fields compared to current org object (not current review) - // Only compare fields that are actually in the incoming data - const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry)) - const currentJointApprovalData = _.pick(registryOrg.toObject(), incomingJointApprovalKeys) - const incomingJointApprovalData = _.pick(registryObjectRaw, incomingJointApprovalKeys) - const hasJointApprovalChanges = !_.isEqual(currentJointApprovalData, incomingJointApprovalData) - - if (hasJointApprovalChanges) { - // write the joint approval to the database - jointApprovalRegistry = _.mergeWith({}, registryOrg.toObject(), registryObjectRaw, skipNulls) - if (reviewObject) { - await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options) - } else { - await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, requestingUsername, options) - } - } else { - // If no changes between org and new object but a review object exists, remove it since joint approval is no longer needed - if (reviewObject) { - await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUsername, options) - } - } - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...registryProtectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...registryProtectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) - updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, [...protectedFields, ...jointApprovalFieldsLegacy]), skipNulls)) - } - // handle conversation const conversationArray = [] if (conversation) { conversationArray.push(await conversationRepo.createConversation(registryOrg.UUID, conversation, requestingUser, isSecretariat, options)) } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object before it gets saved. - if (requestingUserUUID) { - try { - const AuditRepository = require('./auditRepository') - const auditRepo = new AuditRepository() - // Seed the audit history with the existing org data if an audit document doesn't already exist. - // This is necessary because older entities might not have an audit log yet, and we want - // the first entry to be their baseline state before this update. - await auditRepo.seedAuditHistoryForOrg( - registryOrg.UUID, - originalRegistryOrgObject, - requestingUserUUID, - { ...options, upsert: true } - ) - // Get the org state before save for comparison - const beforeUpdateObject = originalRegistryOrgObject - const afterUpdateObject = registryOrg.toObject() - - // Clean objects for comparison (remove Mongoose metadata) - const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) - const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) - - // Only add audit entry if there are changes - if (!_.isEqual(cleanBefore, cleanAfter)) { - await auditRepo.appendToAuditHistoryForOrg( - registryOrg.UUID, - registryOrg.toObject(), - requestingUserUUID, - { ...options, upsert: true } - ) - } - console.log('Audit entry created for registry object') - } catch (auditError) { - console.error('Audit entry creation failed:', auditError) - } - } - - // Handle possible authority (discriminator) changes that require a different Mongoose model - let roleChange = false - if (!_.isEqual([...originalRoles].sort(), [...updatedRegistryOrg?.authority].sort())) { - roleChange = true - } - - // Determine the correct model based on the updated authority - let TargetModel = null - if (updatedRegistryOrg.authority?.includes('SECRETARIAT')) { - TargetModel = SecretariatOrgModel - } else if (updatedRegistryOrg.authority?.includes('CNA')) { - TargetModel = CNAOrgModel - } else if (updatedRegistryOrg.authority?.includes('ADP')) { - TargetModel = ADPOrgModel - } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { - TargetModel = BulkDownloadModel - } else if (updatedRegistryOrg.authority?.includes('ROOT')) { - TargetModel = RootOrgModel - } + await createAuditLogEntry(registryOrg, originalRegistryOrgObject, requestingUserUUID, options) - // If the model type has changed, replace the document with a new one of the correct type - if (TargetModel && roleChange) { - const oldId = updatedRegistryOrg._id - // Remove the old document - await BaseOrgModel.deleteOne({ _id: oldId }, options) - // Prepare data for the new document, preserving the UUID and _id - const newDocData = updatedRegistryOrg.toObject() - delete newDocData.__t - newDocData._id = oldId - const newDoc = new TargetModel(newDocData) - await newDoc.save(options) - // Update reference so subsequent code works with the newly saved document - updatedRegistryOrg = newDoc - } + updatedRegistryOrg = await handleAuthorityModelChange(updatedRegistryOrg, originalRoles, options) try { await updatedLegacyOrg.save(options) diff --git a/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js index d91c90a8c..e23494b9e 100644 --- a/src/repositories/baseOrgRepositoryHelpers.js +++ b/src/repositories/baseOrgRepositoryHelpers.js @@ -7,6 +7,13 @@ const BulkDownloadModel = require('../model/bulkdownloadorg') const SecretariatOrgModel = require('../model/secretariatorg') const RootOrgModel = require('../model/rootorg') +/** + * @function skipNulls + * @description A custom customizer for lodash's mergeWith. If the target value is an array, it replaces it completely with the source value instead of merging elements. + * @param {*} objValue - The value from the target object. + * @param {*} srcValue - The value from the source object. + * @returns {*} The source array if the target was an array, otherwise undefined to let mergeWith handle it natively. + */ const skipNulls = (objValue, srcValue) => { if (_.isArray(objValue)) { return srcValue @@ -14,6 +21,15 @@ const skipNulls = (objValue, srcValue) => { return undefined } +/** + * @function handleShortNameUpdate + * @description Manages the complex process of updating an organization's short name, ensuring all instances and references (legacy and registry) are synchronized with the new short name. + * @param {object} incomingOrg - The payload containing the requested updates. + * @param {object} registryOrg - The current registry organization Mongoose document. + * @param {object} legacyOrg - The current legacy organization Mongoose document. + * @param {object} registryObjectRaw - The raw data payload mapped for the registry schema. + * @param {object} legacyObjectRaw - The raw data payload mapped for the legacy schema. + */ function handleShortNameUpdate (incomingOrg, registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw) { if (incomingOrg?.new_short_name) { const newName = incomingOrg.new_short_name @@ -27,6 +43,12 @@ function handleShortNameUpdate (incomingOrg, registryOrg, legacyOrg, registryObj } } +/** + * @function automateProgramDataDates + * @description Automatically sets or removes `partner_active_date` and `partner_inactive_date` based on state transitions in the organization's program_data status. + * @param {object} registryObjectRaw - The raw data payload mapped for the registry schema. + * @param {object} registryOrg - The current registry organization Mongoose document. + */ function automateProgramDataDates (registryObjectRaw, registryOrg) { if (registryObjectRaw.program_data && registryObjectRaw.program_data.status) { const incomingStatus = registryObjectRaw.program_data.status @@ -54,6 +76,14 @@ function automateProgramDataDates (registryObjectRaw, registryOrg) { } } +/** + * @function mergeAllowedFields + * @description Deep merges new fields into a Mongoose document while strictly protecting explicitly defined system fields from being overwritten. + * @param {object} targetDoc - The Mongoose document being updated. + * @param {object} rawData - The raw incoming data payload. + * @param {string[]} fieldsToProtect - Array of field keys that cannot be overwritten by the rawData. + * @returns {object} The mutated Mongoose document with the merged data. + */ function mergeAllowedFields (targetDoc, rawData, fieldsToProtect) { return targetDoc.overwrite( _.mergeWith( @@ -64,6 +94,17 @@ function mergeAllowedFields (targetDoc, rawData, fieldsToProtect) { ) } +/** + * @async + * @function manageReviewObject + * @description Manages the lifecycle of an organization Review Object for changes that require Secretariat approval. Evaluates if restricted fields changed and will create, update, or reject the pending review object accordingly. + * @param {object} registryOrg - The current registry organization Mongoose document. + * @param {object} registryObjectRaw - The raw incoming data payload. + * @param {string[]} jointApprovalFieldsRegistry - List of field paths that require joint approval. + * @param {object} reviewObject - An existing review object, or null if one does not exist. + * @param {string} requestingUsername - Username of the user making the request. + * @param {object} options - Mongoose options for database queries. + */ async function manageReviewObject (registryOrg, registryObjectRaw, jointApprovalFieldsRegistry, reviewObject, requestingUsername, options) { const ReviewObjectRepository = require('./reviewObjectRepository') const reviewObjectRepo = new ReviewObjectRepository() @@ -101,6 +142,22 @@ async function manageReviewObject (registryOrg, registryObjectRaw, jointApproval } } +/** + * @async + * @function processJointApprovalAndMerge + * @description Orchestrates the merging of new data into both legacy and registry documents. If the user is a standard user modifying restricted fields, it defers those changes by triggering a review object. + * @param {object} registryOrg - The current registry organization Mongoose document. + * @param {object} legacyOrg - The current legacy organization Mongoose document. + * @param {object} registryObjectRaw - The raw incoming data payload for the registry schema. + * @param {object} legacyObjectRaw - The raw incoming data payload for the legacy schema. + * @param {object} reviewObject - An existing review object for the organization. + * @param {boolean} isSecretariat - Whether the requester has Secretariat privileges. + * @param {object} options - Mongoose options for database queries. + * @param {string} requestingUsername - Username of the user making the request. + * @param {string[]} jointApprovalFieldsRegistry - Restricted fields that were changed in the registry object. + * @param {string[]} jointApprovalFieldsLegacy - Restricted fields that were changed in the legacy object. + * @returns {Promise} An object containing the securely updated `updatedRegistryOrg` and `updatedLegacyOrg` documents. + */ async function processJointApprovalAndMerge (registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw, reviewObject, isSecretariat, options, requestingUsername, jointApprovalFieldsRegistry, jointApprovalFieldsLegacy) { const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] let registryProtectedFields = [...protectedFields] @@ -130,6 +187,15 @@ async function processJointApprovalAndMerge (registryOrg, legacyOrg, registryObj } } +/** + * @async + * @function createAuditLogEntry + * @description Creates an audit log tracing changes to an organization. It seeds the history if none exists, and appends the new change if differences are detected. + * @param {object} registryOrg - The updated registry organization Mongoose document. + * @param {object} originalRegistryOrgObject - A plain javascript object representing the organization before updates were applied. + * @param {string} requestingUserUUID - The UUID of the user who made the request. + * @param {object} options - Mongoose options for database queries. + */ async function createAuditLogEntry (registryOrg, originalRegistryOrgObject, requestingUserUUID, options) { if (!requestingUserUUID) return try { @@ -161,6 +227,15 @@ async function createAuditLogEntry (registryOrg, originalRegistryOrgObject, requ } } +/** + * @async + * @function handleAuthorityModelChange + * @description Detects if the organization's roles (authority) have changed and gracefully re-casts the underlying Mongoose document to the appropriate discriminator type (e.g., CNAOrg, ADPOrg, SecretariatOrg) to match the new roles. + * @param {object} updatedRegistryOrg - The updated organization Mongoose document. + * @param {string[]} originalRoles - The array of roles the organization had prior to the update. + * @param {object} options - Mongoose options for database queries. + * @returns {Promise} The organization document, cast to the correct Mongoose discriminator model if a change occurred. + */ async function handleAuthorityModelChange (updatedRegistryOrg, originalRoles, options) { let roleChange = false if (!_.isEqual([...originalRoles].sort(), [...updatedRegistryOrg?.authority].sort())) { From 6fe7c13a3dbb4eb4a6a3f5f6636c14577283c4b3 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 1 May 2026 11:17:56 -0400 Subject: [PATCH 39/40] Securing inUse and in_use --- src/repositories/baseOrgRepositoryHelpers.js | 3 +- .../registry-org/registryOrgCRUDTest.js | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js index e23494b9e..0bac9a580 100644 --- a/src/repositories/baseOrgRepositoryHelpers.js +++ b/src/repositories/baseOrgRepositoryHelpers.js @@ -110,7 +110,6 @@ async function manageReviewObject (registryOrg, registryObjectRaw, jointApproval const reviewObjectRepo = new ReviewObjectRepository() const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry)) - const currentJointApprovalData = _.pick(registryOrg.toObject(), incomingJointApprovalKeys) const incomingJointApprovalData = _.pick(registryObjectRaw, incomingJointApprovalKeys) @@ -159,7 +158,7 @@ async function manageReviewObject (registryOrg, registryObjectRaw, jointApproval * @returns {Promise} An object containing the securely updated `updatedRegistryOrg` and `updatedLegacyOrg` documents. */ async function processJointApprovalAndMerge (registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw, reviewObject, isSecretariat, options, requestingUsername, jointApprovalFieldsRegistry, jointApprovalFieldsLegacy) { - const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] + 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] diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 2c49f3a63..60886b86f 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -400,6 +400,48 @@ describe('Testing /registryOrg endpoints', () => { .delete(`/api/registryOrg/${subOrg.short_name}`) .set(secretariatHeaders) }) + it('Preserves inUse and in_use properties across updates', async () => { + // Create an organization + const tempOrg = { + short_name: 'temp_org_for_in_use_test', + long_name: 'Temp Org In Use Test', + authority: ['CNA'], + hard_quota: 10 + } + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(tempOrg) + + // Set the inUse and in_use flags via mongo directly + const Org = require('../../../src/model/org') + const BaseOrg = require('../../../src/model/baseorg') + await Org.findOneAndUpdate({ short_name: tempOrg.short_name }, { $set: { inUse: true } }) + await BaseOrg.findOneAndUpdate({ short_name: tempOrg.short_name }, { $set: { in_use: true } }) + + // Update the org using the API + await chai.request(app) + .put(`/api/registry/org/${tempOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...tempOrg, + long_name: 'Temp Org In Use Test Updated' + }) + .then((res) => { + expect(res).to.have.status(200) + }) + + // Verify the inUse flags are preserved + const legacyOrgCheck = await Org.findOne({ short_name: tempOrg.short_name }) + const registryOrgCheck = await BaseOrg.findOne({ short_name: tempOrg.short_name }) + expect(legacyOrgCheck.inUse).to.be.true + expect(registryOrgCheck.in_use).to.be.true + + // Cleanup + await chai.request(app) + .delete(`/api/registryOrg/${tempOrg.short_name}`) + .set(secretariatHeaders) + }) }) context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { From 78d906f5d6fdb684af3a74922b8370e3df3d105e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 1 May 2026 12:27:15 -0400 Subject: [PATCH 40/40] fixing linting issues: --- src/repositories/baseOrgRepository.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index ec113d252..10c2b4118 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -9,7 +9,6 @@ const CveIdRepository = require('./cveIdRepository') const uuid = require('uuid') const _ = require('lodash') const BaseOrg = require('../model/baseorg') -const ConversationRepository = require('./conversationRepository') const getConstants = require('../constants').getConstants const { handleShortNameUpdate,