diff --git a/api-docs/openapi.json b/api-docs/openapi.json
index eaf2feb02..5fd0bee55 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_type partner_country advisory_locations industry tl_root_start_date is_cna_discussion_list ",
"operationId": "orgUpdateSingle",
"parameters": [
{
@@ -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-lock.json b/package-lock.json
index ca4a802a6..ecb64fd58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,20 +1,12 @@
{
"name": "cve-services",
-<<<<<<< HEAD
"version": "2.8.0",
-=======
- "version": "2.7.5",
->>>>>>> 8904859f (Bump lodash from 4.17.23 to 4.18.1)
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cve-services",
-<<<<<<< HEAD
"version": "2.8.0",
-=======
- "version": "2.7.5",
->>>>>>> 8904859f (Bump lodash from 4.17.23 to 4.18.1)
"license": "(CC0)",
"dependencies": {
"ajv": "^8.6.2",
diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json
index d2a42bbf5..d47c19729 100644
--- a/schemas/registry-org/BaseOrg.json
+++ b/schemas/registry-org/BaseOrg.json
@@ -39,7 +39,23 @@
"CNA",
"SECRETARIAT",
"BULK_DOWNLOAD",
- "ADP"
+ "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"
]
}
},
@@ -70,8 +86,8 @@
"$ref": "#/definitions/authority"
}
},
- "root_or_tlr": {
- "type": "boolean"
+ "top_level_root": {
+ "type": "string"
},
"reports_to": {
"$ref": "#/definitions/uuidType"
@@ -102,15 +118,30 @@
"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_contact_users": {
- "type": "array",
- "uniqueItems": true,
- "items": {
- "$ref": "#/definitions/uuidType"
- }
+ "phone": {
+ "type": "string"
},
"poc": {
"type": "string"
@@ -119,20 +150,70 @@
"type": "string",
"format": "email"
},
- "poc_phone": {
- "type": "string"
+ "website": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "additionalProperties": false
+ },
+ "program_data": {
+ "type": "object",
+ "properties": {
+ "cve_website_update_date": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "cve_website_update_needed": {
+ "type": "boolean"
},
- "org_email": {
+ "partner_active_date": {
"type": "string",
- "format": "email"
+ "format": "date"
},
- "website": {
+ "partner_inactive_date": {
"type": "string",
- "format": "uri",
- "description": "Organization's website URL"
+ "format": "date"
+ },
+ "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"
+ }
+ },
+ "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 367302530..0b86c1a9a 100644
--- a/schemas/registry-org/CNAOrg.json
+++ b/schemas/registry-org/CNAOrg.json
@@ -6,23 +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"
+ },
+ "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": {
- "poc": { "type": "string" },
- "poc_email": { "type": "string" },
- "poc_phone": { "type": "string" },
- "org_email": { "type": "string" },
- "website": { "type": "string" }
+ "phone": {
+ "type": "string"
+ },
+ "poc": {
+ "type": "string"
+ },
+ "poc_email": {
+ "type": "string"
+ },
+ "website": {
+ "type": "string"
+ }
},
"additionalProperties": false
},
@@ -61,15 +95,45 @@
"format": "uuid"
}
},
- "partner_role": {
+ "charter_or_scope": {
+ "type": "string"
+ },
+ "disclosure_policy": {
"type": "string"
},
- "partner_type": {
+ "product_list": {
+ "type": "string"
+ },
+ "partner_role_type": {
+ "$ref": "/BaseOrg#/definitions/partnerRoleType"
+ },
+ "partner_number": {
"type": "string"
},
"partner_country": {
"type": "string"
+ },
+ "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"
}
},
- "required": ["short_name", "hard_quota"]
+ "required": [
+ "short_name",
+ "hard_quota"
+ ]
}
\ No newline at end of file
diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json
new file mode 100644
index 000000000..d4c090444
--- /dev/null
+++ b/schemas/registry-org/RootOrg.json
@@ -0,0 +1,77 @@
+{
+ "$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" },
+ "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": {
+ "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": {
+ "$ref": "/BaseOrg#/definitions/uuidType"
+ }
+ },
+ "partner_role_type": {
+ "$ref": "/BaseOrg#/definitions/partnerRoleType"
+ },
+ "partner_number": {
+ "type": "string"
+ },
+ "partner_country": {
+ "type": "string"
+ },
+ "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" }
+ },
+ "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..718778634 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"]
- }
+ "items": {
+ "type": "string",
+ "enum": [
+ "CNA",
+ "ADP",
+ "BULK_DOWNLOAD",
+ "SECRETARIAT",
+ "ROOT"
+ ]
+ }
},
"oversees": {
"type": "array",
@@ -34,8 +40,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": {
@@ -57,47 +63,126 @@
"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_contact_users": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "phone": {
+ "type": "string"
},
"poc": {
- "type": "string",
- "description": "Point of contact name"
+ "type": "string"
},
"poc_email": {
"type": "string",
- "format": "email",
- "description": "Point of contact email"
+ "format": "email"
},
- "poc_phone": {
+ "website": {
"type": "string",
- "description": "Point of contact phone number"
+ "format": "uri"
+ }
+ },
+ "required": [
+ "poc",
+ "poc_email",
+ "admins"
+ ]
+ },
+ "advisory_locations": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Locations of vulnerability advisories"
+ },
+ "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"
},
- "admins": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "UUIDs of admin users"
+ "cve_website_update_needed": {
+ "type": "boolean"
},
- "org_email": {
+ "partner_active_date": {
"type": "string",
- "format": "email",
- "description": "Organization's email address"
+ "format": "date"
},
- "website": {
+ "partner_inactive_date": {
"type": "string",
- "format": "uri",
- "description": "Organization's website URL"
+ "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"
}
},
- "required": ["poc", "poc_email", "admins", "org_email"]
+ "description": "Additional partner metadata (restricted)"
}
},
"required": [
@@ -105,4 +190,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 6f0bfb0ec..9c8cb073e 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": {
@@ -60,8 +73,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": {
@@ -91,47 +104,68 @@
"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_contact_users": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "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"
+ },
+ "description": "Locations of vulnerability advisories"
+ },
+ "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"
},
"in_use": {
"type": "boolean",
@@ -149,5 +183,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 79ac6a30d..121ec7899 100644
--- a/schemas/registry-org/get-registry-org-response.json
+++ b/schemas/registry-org/get-registry-org-response.json
@@ -37,12 +37,15 @@
},
"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": {
- "type": ["string", "null"],
+ "type": [
+ "string",
+ "null"
+ ],
"description": "UUID of the parent organization, if any"
},
"oversees": {
@@ -53,93 +56,126 @@
"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"
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "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"
+ }
},
- "description": "UUIDs of admin users"
+ "additionalProperties": false
+ }
},
"contact_info": {
"type": "object",
"properties": {
- "additional_contact_users": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "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": "string",
- "description": "Role of the partner"
+ "partner_role_type": {
+ "$ref": "/BaseOrg#/definitions/partnerRoleType"
},
- "partner_type": {
- "type": "string",
- "description": "Type of the partner"
+ "partner_number": {
+ "type": "string",
+ "description": "Number of the partner"
},
"partner_country": {
- "type": "string",
- "description": "Country of the partner"
+ "type": "string",
+ "description": "Country of the partner"
},
- "vulnerability_advisory_locations": {
- "type": "array",
- "items": {
- "type": "string"
+ "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"
},
- "description": "Locations of vulnerability advisories"
+ "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)"
},
- "advisory_location_require_credentials": {
- "type": "boolean",
- "description": "Indicates if advisory locations require credentials"
+ "advisory_locations": {
+ "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",
@@ -176,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 c578c6f7a..fa6d832de 100644
--- a/schemas/registry-org/list-registry-orgs-response.json
+++ b/schemas/registry-org/list-registry-orgs-response.json
@@ -66,12 +66,15 @@
},
"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": {
- "type": ["string", "null"],
+ "type": [
+ "string",
+ "null"
+ ],
"description": "UUID of the parent organization, if any"
},
"oversees": {
@@ -95,68 +98,101 @@
},
"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_contact_users": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "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": "string",
- "description": "Role of the partner"
+ "partner_role_type": {
+ "$ref": "/BaseOrg#/definitions/partnerRoleType"
},
- "partner_type": {
+ "partner_number": {
"type": "string",
- "description": "Type of the partner"
+ "description": "Number of the partner"
},
"partner_country": {
"type": "string",
"description": "Country of the partner"
},
- "vulnerability_advisory_locations": {
+ "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)"
+ },
+ "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"
@@ -205,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 38d210e96..456be9213 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"]
+ "enum": [
+ "CNA",
+ "ADP",
+ "Secretariat",
+ "ROOT"
+ ]
}
}
},
- "required": ["active_roles"]
+ "required": [
+ "active_roles"
+ ]
},
"oversees": {
"type": "array",
@@ -45,8 +56,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": {
@@ -68,46 +79,121 @@
"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_contact_users": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "phone": {
+ "type": "string"
},
"poc": {
- "type": "string",
- "description": "Point of contact name"
+ "type": "string"
},
"poc_email": {
"type": "string",
- "format": "email",
- "description": "Point of contact email"
+ "format": "email"
+ },
+ "website": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ },
+ "advisory_locations": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Locations of vulnerability advisories"
+ },
+ "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"
},
- "poc_phone": {
+ "cve_website_update_needed": {
+ "type": "boolean"
+ },
+ "partner_active_date": {
"type": "string",
- "description": "Point of contact phone number"
+ "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"
},
- "admins": {
+ "vulnerability_advisory_location_for_web_scraping": {
"type": "array",
"items": {
"type": "string"
},
- "description": "UUIDs of admin users"
- },
- "org_email": {
- "type": "string",
- "format": "email",
- "description": "Organization's email address"
- },
- "website": {
- "type": "string",
- "format": "uri",
- "description": "Organization's website URL"
+ "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 cbf41d925..2a5e2cdcb 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",
@@ -55,8 +62,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": {
@@ -86,47 +93,68 @@
"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_contact_users": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "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"
+ },
+ "description": "Locations of vulnerability advisories"
+ },
+ "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"
},
"in_use": {
"type": "boolean",
@@ -144,5 +172,5 @@
}
}
}
- }
+ }
}
\ No newline at end of file
diff --git a/src/constants/index.js b/src/constants/index.js
index 69f295e9f..4c0a38b7a 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -31,21 +31,34 @@ 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: [
'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', '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'],
+ SECRETARIAT_ONLY_FIELDS: [
+ 'partner_number',
+ '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'
+ ],
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/index.js b/src/controller/org.controller/index.js
index 1c5e6f110..1dd51a767 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
@@ -556,11 +556,9 @@ 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
+ advisory_locations
industry
tl_root_start_date
is_cna_discussion_list
diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js
index b562bae9c..4af27cbd3 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.
@@ -366,6 +367,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..1e986bb6e 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()
@@ -46,20 +46,33 @@ function validateCreateOrgParameters () {
.isIn(orgOptions),
body(['oversees']).default([])
.isArray(),
- body(['root_or_tlr']).default(false)
- .isBoolean(),
- body(['vulnerability_advisory_locations'])
+ body(['top_level_root']).default('')
+ .isString(),
+ body(['advisory_locations'])
.default([])
.custom(isFlatStringArray),
- body(['advisory_location_require_credentials'])
+ body(['program_data.advisory_location_require_credentials'])
.default(false)
.isBoolean(),
+ body(['program_data.vulnerability_advisory_location_for_web_scraping'])
+ .default([])
+ .custom(isFlatStringArray),
body(['tl_root_start_date'])
.default(null)
.isDate(),
body(['is_cna_discussion_list'])
.default(false)
.isBoolean(),
+ body([
+ 'program_data.cve_website_update_date',
+ 'program_data.partner_active_date',
+ 'program_data.partner_inactive_date'
+ ])
+ .optional({ nullable: true })
+ .isDate(),
+ body(['program_data.cve_website_update_needed'])
+ .optional()
+ .isBoolean(),
body(
[
'charter_or_scope',
@@ -67,12 +80,14 @@ 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',
- 'partner_type',
+ '',
+ '',
+ 'partner_role_type',
+ 'partner_number',
'partner_country',
+ 'program_data.status',
'industry'
])
.default('')
@@ -88,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 = [
@@ -114,14 +129,13 @@ function validateCreateOrgParameters () {
'oversees',
'long_name',
'cve_program_org_function',
- 'contact_info.admins',
'in_use',
'created',
- 'root_or_tlr',
+ 'top_level_root',
'soft_quota',
'aliases',
'hard_quota',
- 'contact_info.org_email',
+ 'contact_info.phone',
'contact_info.website',
'contact_info',
'users',
@@ -130,15 +144,18 @@ 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',
- 'partner_role',
- 'partner_type',
+ 'private_contacts',
+ 'partner_role_type',
+ 'partner_number',
'partner_country',
- 'vulnerability_advisory_locations',
- 'advisory_location_require_credentials',
+ 'program_data.cve_website_update_date',
+ 'program_data.cve_website_update_needed',
+ 'program_data.status',
+ 'advisory_locations',
+ 'program_data.advisory_location_require_credentials',
+ 'program_data.vulnerability_advisory_location_for_web_scraping',
'industry',
'tl_root_start_date',
'is_cna_discussion_list')
@@ -209,21 +226,26 @@ 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',
'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',
- 'partner_type',
+ '',
+ '',
+ 'partner_role_type',
+ 'partner_number',
'partner_country',
- 'vulnerability_advisory_locations',
- 'advisory_location_require_credentials',
+ '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',
'is_cna_discussion_list'
@@ -297,7 +319,7 @@ const QUERY_PARAMETERS = {
],
// Registry-only parameters
registryOnly: [
- 'root_or_tlr',
+ 'top_level_root',
'charter_or_scope',
'disclosure_policy',
'product_list',
@@ -305,14 +327,19 @@ 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',
- 'partner_type',
+ '',
+ '',
+ 'partner_role_type',
+ 'partner_number',
'partner_country',
- 'vulnerability_advisory_locations',
- 'advisory_location_require_credentials',
+ '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',
'is_cna_discussion_list',
diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js
index dd5ffe352..aa54d2fc1 100644
--- a/src/controller/registry-org.controller/error.js
+++ b/src/controller/registry-org.controller/error.js
@@ -91,6 +91,41 @@ class RegistryOrgControllerError 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
}
+
+ conversationDne (shortname, index) {
+ const err = {}
+ err.error = 'CONVERSATION_DNE'
+ err.message = `The conversation at index ${index} does not exist for the ${shortname} organization.`
+ return err
+ }
+
+ notAllowedToEditConversation () {
+ const err = {}
+ err.error = 'NOT_ALLOWED_TO_EDIT_CONVERSATION'
+ err.message = 'You must be the original author or Secretariat to edit this conversation.'
+ return err
+ }
+
+ notAllowedToChangeConversationVisibility () {
+ const err = {}
+ err.error = 'NOT_ALLOWED_TO_CHANGE_CONVERSATION_VISIBILITY'
+ err.message = 'Only the Secretariat is allowed to change the visibility of a conversation.'
+ return err
+ }
+
+ invalidConversationObject () {
+ const err = {}
+ err.error = 'BAD_INPUT'
+ 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 8d4292079..0b247fbe6 100644
--- a/src/controller/registry-org.controller/registry-org.controller.js
+++ b/src/controller/registry-org.controller/registry-org.controller.js
@@ -39,7 +39,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)
@@ -91,8 +91,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
@@ -147,6 +146,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 })
@@ -256,6 +264,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..939897911 100644
--- a/src/controller/registry-org.controller/registry-org.middleware.js
+++ b/src/controller/registry-org.controller/registry-org.middleware.js
@@ -12,16 +12,19 @@ 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',
- 'contact_info.admins', 'contact_info.org_email', 'contact_info.website',
- 'partner_role',
- 'partner_type',
+ 'private_contacts', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website',
+ 'partner_role_type',
+ 'partner_number',
'partner_country',
- 'vulnerability_advisory_locations',
- 'advisory_location_require_credentials',
+ 'program_data.cve_website_update_date',
+ 'program_data.cve_website_update_needed',
+ 'program_data.status',
+ 'advisory_locations',
+ '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/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json
index 21797fe93..966056f75 100644
--- a/src/middleware/schemas/CNAOrg.json
+++ b/src/middleware/schemas/CNAOrg.json
@@ -7,10 +7,7 @@
"allOf": [
{ "$ref": "/BaseOrg" },
{
-<<<<<<< HEAD
-=======
"type": "object",
->>>>>>> b62d7ad4 (Conflicts)
"properties": {
"authority": {
"const": ["CNA"]
diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json
index 63c452a0b..f4e4e1637 100644
--- a/src/middleware/schemas/SecretariatOrg.json
+++ b/src/middleware/schemas/SecretariatOrg.json
@@ -7,10 +7,7 @@
"allOf": [
{ "$ref": "/BaseOrg" },
{
-<<<<<<< HEAD
-=======
"type": "object",
->>>>>>> b62d7ad4 (Conflicts)
"properties": {
"authority": {
"const": ["SECRETARIAT"]
diff --git a/src/model/baseorg.js b/src/model/baseorg.js
index daff859fc..13777a5af 100644
--- a/src/model/baseorg.js
+++ b/src/model/baseorg.js
@@ -11,22 +11,34 @@ 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: {
- additional_contact_users: [String],
+ phone: String,
poc: String,
poc_email: String,
- poc_phone: String,
- org_email: String,
website: String
},
- partner_role: String,
- partner_type: String,
+ private_contacts: [{
+ _id: false,
+ phone: String,
+ poc: String,
+ poc_email: String
+ }],
+ partner_role_type: String,
+ partner_number: String,
partner_country: String,
- vulnerability_advisory_locations: [String],
- advisory_location_require_credentials: Boolean,
+ program_data: {
+ cve_website_update_date: Date,
+ cve_website_update_needed: Boolean,
+ partner_active_date: String,
+ partner_inactive_date: String,
+ status: String,
+ advisory_location_require_credentials: Boolean,
+ vulnerability_advisory_location_for_web_scraping: [String]
+ },
+ advisory_locations: [String],
industry: String,
tl_root_start_date: Date,
is_cna_discussion_list: Boolean,
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 a95065e7a..a4edec5af 100644
--- a/src/repositories/baseOrgRepository.js
+++ b/src/repositories/baseOrgRepository.js
@@ -4,19 +4,19 @@ 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')
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
@@ -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)
}
@@ -429,6 +459,26 @@ class BaseOrgRepository extends BaseRepository {
// Registry stuff
// Add uuid to org object
registryObjectRaw.UUID = sharedUUID
+
+ // Automate program_data dates based on status
+ if (!registryObjectRaw.program_data) {
+ registryObjectRaw.program_data = {}
+ }
+
+ // Default to 'inactive' if not provided
+ if (!registryObjectRaw.program_data.status) {
+ registryObjectRaw.program_data.status = 'inactive'
+ }
+
+ 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.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.program_data.partner_active_date
+ }
// Figure out why this is not working....
// registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value))
@@ -477,9 +527,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
@@ -534,22 +591,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)
}
/**
@@ -565,7 +611,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)
@@ -578,8 +624,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.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)
@@ -634,6 +681,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
@@ -654,18 +703,17 @@ 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',
'oversees',
'reports_to',
'contact_info', // Handles all nested contact_info fields automatically
- 'partner_role',
- 'partner_type',
+ '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'
@@ -735,22 +783,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)
}
/**
@@ -770,17 +806,21 @@ 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)
return changedFields
}
- /**
/**
* @async
* @function updateOrgFull
@@ -797,22 +837,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
@@ -824,140 +867,27 @@ class BaseOrgRepository extends BaseRepository {
legacyObjectRaw = this.convertRegistryToLegacy(incomingOrgBody)
}
- if (incomingOrg?.new_short_name) {
- const newName = incomingOrg.new_short_name
+ handleShortNameUpdate(incomingOrg, registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw)
+ automateProgramDataDates(registryObjectRaw, registryOrg)
- // 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
+ const requestingUser = requestingUserUUID ? await userRepo.findUserByUUID(requestingUserUUID, options) : null
+ const requestingUsername = requestingUser ? requestingUser.username : null
- // 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
- }
- // 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
+ let { updatedRegistryOrg, updatedLegacyOrg } = await processJointApprovalAndMerge(
+ registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw, reviewObject, isSecretariat, options, requestingUsername, jointApprovalFieldsRegistry, jointApprovalFieldsLegacy
+ )
- const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins']
- 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))
- } 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 = _.merge({}, registryOrg.toObject(), registryObjectRaw)
- 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(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...protectedFields, ...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
- }
+ 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)
@@ -968,24 +898,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)
}
/**
@@ -1036,6 +956,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') {
@@ -1044,6 +968,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/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js
new file mode 100644
index 000000000..0bac9a580
--- /dev/null
+++ b/src/repositories/baseOrgRepositoryHelpers.js
@@ -0,0 +1,280 @@
+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')
+
+/**
+ * @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
+ }
+ 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
+ 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
+ * @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
+ 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
+ * @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(
+ _.pick(targetDoc.toObject(), fieldsToProtect),
+ _.omit(rawData, fieldsToProtect),
+ skipNulls
+ )
+ )
+}
+
+/**
+ * @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()
+
+ 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
+ * @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', 'inUse', 'in_use']
+ 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
+ * @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 {
+ 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
+ * @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())) {
+ 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
+}
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/src/scripts/migrate.js b/src/scripts/migrate.js
index a0097c1d6..ad72a2875 100644
--- a/src/scripts/migrate.js
+++ b/src/scripts/migrate.js
@@ -101,15 +101,15 @@ 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,
product_list: null,
soft_quota: null,
hard_quota: null,
+ private_contacts: [],
contact_info: {
- additional_contact_users: [],
poc: null,
poc_email: null,
poc_phone: 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,
@@ -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_contact_users: [], // 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/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/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/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/constants.js b/test/integration-tests/constants.js
index 80700e31e..df85f8051 100644
--- a/test/integration-tests/constants.js
+++ b/test/integration-tests/constants.js
@@ -383,10 +383,14 @@ 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'
},
+ private_contacts: [{
+ poc: 'Dave Private',
+ poc_email: 'daveprivate@test.org',
+ phone: '555-4321'
+ }],
authority: ['CNA'],
hard_quota: 100000
}
@@ -397,8 +401,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 +428,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'],
diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js
index 7a219c385..9697a2173 100644
--- a/test/integration-tests/conversation/editConversationTest.js
+++ b/test/integration-tests/conversation/editConversationTest.js
@@ -30,7 +30,9 @@ describe('Testing Conversation edit by index endpoint', () => {
delete org.last_updated
delete org.admins
delete org.users
- delete org.root_or_tlr
+ delete org.top_level_root
+ delete org.oversees
+ delete org.program_data
})
await chai
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'])
diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js
index ef2356bce..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 () => {
@@ -377,6 +421,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 program_data', async () => {
+ await chai.request(app)
+ .put('/api/registry/org/beat_10')
+ .set(adminHeaders)
+ .send({
+ program_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/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/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js
index 9a07abd8c..60886b86f 100644
--- a/test/integration-tests/registry-org/registryOrgCRUDTest.js
+++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js
@@ -13,9 +13,10 @@ const testRegistryOrg = {
long_name: 'Registry Org Test',
authority: ['CNA'],
hard_quota: 1000,
- partner_role: 'Initial Partner Role',
- partner_type: 'Initial Partner Type',
- partner_country: 'US'
+ partner_role_type: 'Vendor',
+ partner_number: 'Initial Partner Number',
+ partner_country: 'US',
+ advisory_locations: ['https://example.com/advisories']
}
let createdOrg
@@ -24,7 +25,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) => {
@@ -50,25 +51,35 @@ 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_type')
- expect(res.body.created.partner_type).to.equal(testRegistryOrg.partner_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_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('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
delete createdOrg.last_updated
+ delete createdOrg.users
+ delete createdOrg.admins
})
})
})
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) => {
@@ -78,7 +89,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,
@@ -91,7 +102,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,
@@ -101,12 +112,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,
@@ -118,13 +130,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)
@@ -139,23 +167,25 @@ 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_type', createdOrg.partner_type)
+ 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 () => {
// 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)
@@ -179,7 +209,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)
@@ -207,7 +237,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)
@@ -220,14 +250,15 @@ 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_type: 'Updated Partner Type',
- partner_country: 'UK'
+ partner_role_type: 'Researcher',
+ partner_number: 'Updated Partner Number',
+ partner_country: 'UK',
+ advisory_locations: ['https://example.com/updated_advisories']
})
.then((res, err) => {
expect(err).to.be.undefined
@@ -253,14 +284,40 @@ 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_type')
- expect(res.body.updated.partner_type).to.equal('Updated Partner Type')
+ 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_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 program_data', async () => {
+ await chai.request(app)
+ .put('/api/registry/org/registry_org_test')
+ .set(secretariatHeaders)
+ .send({
+ ...createdOrg,
+ program_data: {
+ status: 'active',
+ advisory_location_require_credentials: true,
+ vulnerability_advisory_location_for_web_scraping: ['https://example.com/scraping']
+ }
+ })
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(200)
+ 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 () => {
@@ -308,7 +365,7 @@ describe('Testing /registryOrg endpoints', () => {
}
let createdSubOrgUUID
await chai.request(app)
- .post('/api/registryOrg')
+ .post('/api/registry/org')
.set(secretariatHeaders)
.send(subOrg)
.then(res => {
@@ -318,7 +375,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,
@@ -331,7 +388,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)
@@ -343,11 +400,53 @@ 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 () => {
await chai.request(app)
- .put('/api/registryOrg/registry_org_test2')
+ .put('/api/registry/org/registry_org_test2')
.set(secretariatHeaders)
.send({
...createdOrg,
@@ -360,7 +459,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,
@@ -373,7 +472,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,
@@ -384,12 +483,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,
@@ -403,7 +517,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,
@@ -412,12 +526,39 @@ 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 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,
+ 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: program_data, program_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/registryOrg/registry_org_test')
+ .put('/api/registry/org/registry_org_test')
.set(secretariatHeaders)
.send({
...createdOrg,
diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js
new file mode 100644
index 000000000..cbb251087
--- /dev/null
+++ b/test/integration-tests/registry-org/rootOrgTest.js
@@ -0,0 +1,134 @@
+/* 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
+ delete createdOrg.program_data
+ delete createdOrg.users
+ delete createdOrg.admins
+ delete createdOrg.oversees
+ })
+ })
+
+ 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 !== 200) console.log('403 Response:', JSON.stringify(res.body, null, 2))
+ expect(res).to.have.status(200)
+ })
+ })
+
+ 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')
+ .set(rootAdminHeaders)
+ .query({ amount: 1, cve_year: 2026, short_name: testRootOrg.short_name })
+ .then((res) => {
+ expect(res).to.have.status(403)
+ })
+ })
+ })
+})
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')
})
// ------------------------------------------------------------------------------------------------
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) {