diff --git a/api-docs/openapi.json b/api-docs/openapi.json
index 01ab0fad9..83e6177e0 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.websites contact_info.emails contact_info.phone partner_role_type partner_country advisory_locations industry tl_root_start_date is_cna_discussion_list ",
"operationId": "orgUpdateSingle",
"parameters": [
{
@@ -4788,14 +4788,14 @@
}
}
},
- "/review/byUUID/{uuid}": {
- "get": {
+ "/conversation/{uuid}": {
+ "put": {
"tags": [
- "Review Object"
+ "Conversation"
],
- "summary": "Retrieves a review object by its UUID (accessible to Secretariat or Admin)",
- "description": " Access Control User must belong to an organization with the Secretariat role or have the Admin role
",
- "operationId": "getReviewObjectByUUID",
+ "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",
@@ -4804,7 +4804,7 @@
"schema": {
"type": "string"
},
- "description": "The UUID of the review object"
+ "description": "The UUID of the conversation to update"
},
{
"$ref": "#/components/parameters/apiEntityHeader"
@@ -4818,11 +4818,11 @@
],
"responses": {
"200": {
- "description": "Returns the review object",
+ "description": "Returns the updated conversation",
"content": {
"application/json": {
"schema": {
- "$ref": "../schemas/review/review.json"
+ "$ref": "../schemas/conversation/conversation.json"
}
}
}
@@ -4877,26 +4877,50 @@
}
}
}
+ },
+ "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/org/{identifier}": {
+ "/review/byUUID/{uuid}": {
"get": {
"tags": [
"Review Object"
],
- "summary": "Retrieves the PENDING review object for an organization (accessible to Secretariat only)",
- "description": " Access Control User must belong to an organization with the Secretariat role
",
- "operationId": "getReviewObjectByOrgIdentifier",
+ "summary": "Retrieves a review object by its UUID (accessible to Secretariat or Admin)",
+ "description": " Access Control User must belong to an organization with the Secretariat role or have the Admin role
",
+ "operationId": "getReviewObjectByUUID",
"parameters": [
{
- "name": "identifier",
+ "name": "uuid",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
- "description": "The short name or UUID of the organization"
+ "description": "The UUID of the review object"
},
{
"$ref": "#/components/parameters/apiEntityHeader"
@@ -4910,7 +4934,7 @@
],
"responses": {
"200": {
- "description": "Returns the pending review object",
+ "description": "Returns the review object",
"content": {
"application/json": {
"schema": {
@@ -4972,30 +4996,23 @@
}
}
},
- "/review/orgs": {
+ "/review/org/{identifier}": {
"get": {
"tags": [
"Review Object"
],
- "summary": "Retrieves all review objects (accessible to Secretariat only)",
+ "summary": "Retrieves the PENDING review object for an organization (accessible to Secretariat only)",
"description": " Access Control User must belong to an organization with the Secretariat role
",
- "operationId": "getAllReviewObjects",
+ "operationId": "getReviewObjectByOrgIdentifier",
"parameters": [
{
- "name": "page",
- "in": "query",
- "description": "The page of results to retrieve",
- "schema": {
- "type": "integer"
- }
- },
- {
- "name": "status",
- "in": "query",
- "description": "Filter by review object status",
+ "name": "identifier",
+ "in": "path",
+ "required": true,
"schema": {
"type": "string"
- }
+ },
+ "description": "The short name or UUID of the organization"
},
{
"$ref": "#/components/parameters/apiEntityHeader"
@@ -5009,11 +5026,11 @@
],
"responses": {
"200": {
- "description": "Returns a list of review objects",
+ "description": "Returns the pending review object",
"content": {
"application/json": {
"schema": {
- "$ref": "../schemas/review/list-reviews-response.json"
+ "$ref": "../schemas/review/review.json"
}
}
}
@@ -5071,24 +5088,15 @@
}
}
},
- "/review/org/{identifier}/reviews": {
+ "/review/orgs": {
"get": {
"tags": [
"Review Object"
],
- "summary": "Retrieves the review history for an organization (accessible to Secretariat or Admin)",
- "description": " Access Control User must belong to an organization with the Secretariat role or have the Admin role
",
- "operationId": "getReviewHistoryByOrgShortNamePaginated",
+ "summary": "Retrieves all review objects (accessible to Secretariat only)",
+ "description": " Access Control User must belong to an organization with the Secretariat role
",
+ "operationId": "getAllReviewObjects",
"parameters": [
- {
- "name": "identifier",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- },
- "description": "The short name of the organization"
- },
{
"name": "page",
"in": "query",
@@ -5098,11 +5106,11 @@
}
},
{
- "name": "include_conversations",
+ "name": "status",
"in": "query",
- "description": "Whether to include conversation history",
+ "description": "Filter by review object status",
"schema": {
- "type": "boolean"
+ "type": "string"
}
},
{
@@ -5117,7 +5125,7 @@
],
"responses": {
"200": {
- "description": "Returns the review history",
+ "description": "Returns a list of review objects",
"content": {
"application/json": {
"schema": {
@@ -5179,23 +5187,39 @@
}
}
},
- "/review/{uuid}": {
- "put": {
+ "/review/org/{identifier}/reviews": {
+ "get": {
"tags": [
"Review Object"
],
- "summary": "Updates a review object (accessible to Secretariat only)",
- "description": " Access Control User must belong to an organization with the Secretariat role
",
- "operationId": "updateReviewObjectByReviewUUID",
+ "summary": "Retrieves the review history for an organization (accessible to Secretariat or Admin)",
+ "description": " Access Control User must belong to an organization with the Secretariat role or have the Admin role
",
+ "operationId": "getReviewHistoryByOrgShortNamePaginated",
"parameters": [
{
- "name": "uuid",
+ "name": "identifier",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
- "description": "The UUID of the review object"
+ "description": "The short name of the organization"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "description": "The page of results to retrieve",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "include_conversations",
+ "in": "query",
+ "description": "Whether to include conversation history",
+ "schema": {
+ "type": "boolean"
+ }
},
{
"$ref": "#/components/parameters/apiEntityHeader"
@@ -5209,11 +5233,11 @@
],
"responses": {
"200": {
- "description": "Returns the updated review object",
+ "description": "Returns the review history",
"content": {
"application/json": {
"schema": {
- "$ref": "../schemas/review/review.json"
+ "$ref": "../schemas/review/list-reviews-response.json"
}
}
}
@@ -5268,28 +5292,17 @@
}
}
}
- },
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "description": "The updated review data"
- }
- }
- }
}
}
},
- "/review/{uuid}/approve": {
+ "/review/{uuid}": {
"put": {
"tags": [
"Review Object"
],
- "summary": "Approves a review object and applies changes to the organization (accessible to Secretariat only)",
+ "summary": "Updates a review object (accessible to Secretariat only)",
"description": " Access Control User must belong to an organization with the Secretariat role
",
- "operationId": "approveReviewObject",
+ "operationId": "updateReviewObjectByReviewUUID",
"parameters": [
{
"name": "uuid",
@@ -5312,12 +5325,11 @@
],
"responses": {
"200": {
- "description": "Returns the updated organization",
+ "description": "Returns the updated review object",
"content": {
"application/json": {
"schema": {
- "type": "object",
- "description": "The updated organization object"
+ "$ref": "../schemas/review/review.json"
}
}
}
@@ -5374,12 +5386,12 @@
}
},
"requestBody": {
- "required": false,
+ "required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
- "description": "Optional override data to apply instead of the review object data"
+ "description": "The updated review data"
}
}
}
@@ -5477,90 +5489,6 @@
}
}
}
- },
- "/review/org/": {
- "post": {
- "tags": [
- "Review Object"
- ],
- "summary": "Creates a new review object (accessible to Secretariat only)",
- "description": " Access Control User must belong to an organization with the Secretariat role
",
- "operationId": "createReviewObject",
- "parameters": [
- {
- "$ref": "#/components/parameters/apiEntityHeader"
- },
- {
- "$ref": "#/components/parameters/apiUserHeader"
- },
- {
- "$ref": "#/components/parameters/apiSecretHeader"
- }
- ],
- "responses": {
- "200": {
- "description": "Returns the created review object",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/review/review.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"
- }
- }
- }
- },
- "500": {
- "description": "Internal Server Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- }
- },
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "description": "The review object data"
- }
- }
- }
- }
- }
}
},
"components": {
diff --git a/datadump/pre-population/glossary.json b/datadump/pre-population/glossary.json
new file mode 100644
index 000000000..10ee659ce
--- /dev/null
+++ b/datadump/pre-population/glossary.json
@@ -0,0 +1,83 @@
+[
+ {
+ "services_short_name": "long_name",
+ "label": "Long Name",
+ "def": "Partner long name displayed on the Partners list on website"
+ },
+ {
+ "services_short_name": "partner_number",
+ "label": "Partner Number",
+ "def": "Unique Identifier that provides insight into when the partner joined the program (used for reporting and website updates)"
+ },
+ {
+ "services_short_name": "short_name",
+ "label": "Short Name",
+ "def": "Partner short name, computer readable name, used for API requests and posted with the CVE record."
+ },
+ {
+ "services_short_name": "status",
+ "label": "Status",
+ "def": "Partner status"
+ },
+ {
+ "services_short_name": "authority",
+ "label": "Authority",
+ "def": "Provides the type of partner."
+ },
+ {
+ "services_short_name": "website_update_date",
+ "label": "CVE Website Update Date",
+ "def": "Date Partner information was last updated on the website."
+ },
+ {
+ "services_short_name": "cve_website_update_needed",
+ "label": "CVE Website Update Needed",
+ "def": "Tracks if partner information updates are completed or pending."
+ },
+ {
+ "services_short_name": "top_level_root",
+ "label": "Top Level Root",
+ "def": "Provides the CNA Top Level Root."
+ },
+ {
+ "services_short_name": "reports_to",
+ "label": "Reports To",
+ "def": "Provides who the partner reports to."
+ },
+ {
+ "services_short_name": "partner_country",
+ "label": "Partner Country",
+ "def": "Partner country that is self identified."
+ },
+ {
+ "services_short_name": "partner_active_date",
+ "label": "Partner Active Date",
+ "def": "Date the partner joined the program."
+ },
+ {
+ "services_short_name": "partner_inactive_date",
+ "label": "Partner Inactive Date",
+ "def": "Date the partner was removed from the program."
+ },
+ {
+ "services_short_name": "charter_or_scope",
+ "label": "Charter or Scope",
+ "def": "Partner charter or scope."
+ },
+ {
+ "services_short_name": "disclosure_policy",
+ "label": "Disclosure Policy",
+ "def": "Partner disclosure policy."
+ },
+ {
+ "services_short_name": "advisory_location",
+ "label": "Advisory Location",
+ "def": "Partner vulnerability advisory locations."
+ },
+ {
+ "services_short_name": "vulnerability_advisory_location_for_web_scraping",
+ "label": "Vulnerability Advisory Locations for Web Scraping.",
+ "def": "Partner vulnerability advisory location for web scraping."
+ }
+ ]
+
diff --git a/package-lock.json b/package-lock.json
index 88c96fde7..ecb64fd58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cve-services",
- "version": "2.7.0",
+ "version": "2.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cve-services",
- "version": "2.7.0",
+ "version": "2.8.0",
"license": "(CC0)",
"dependencies": {
"ajv": "^8.6.2",
@@ -25,7 +25,7 @@
"jsonschema": "^1.4.0",
"JSONStream": "^1.3.5",
"kleur": "^4.1.4",
- "lodash": "^4.17.23",
+ "lodash": "^4.18.1",
"luxon": "^3.4.4",
"mongo-cursor-pagination": "^8.1.3",
"mongoose": "^8.9.5",
@@ -38,7 +38,7 @@
"replace-json-property": "^1.8.0",
"swagger-autogen": "^2.19.0",
"swagger-ui-express": "^4.3.0",
- "uuid": "^8.3.2",
+ "uuid": "^14.0.0",
"validator": ">=13.7.0",
"winston": "^3.2.1",
"yamljs": "^0.3.0"
@@ -431,9 +431,9 @@
"license": "Python-2.0"
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -512,9 +512,9 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1510,9 +1510,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2883,9 +2883,9 @@
}
},
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2978,9 +2978,9 @@
}
},
"node_modules/eslint-plugin-node/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3126,9 +3126,9 @@
"license": "Python-2.0"
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4973,6 +4973,16 @@
"node": ">=8"
}
},
+ "node_modules/istanbul-lib-processinfo/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
@@ -5305,9 +5315,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.flattendeep": {
@@ -5846,9 +5856,9 @@
}
},
"node_modules/multimatch/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6020,6 +6030,15 @@
"which": "^2.0.2"
}
},
+ "node_modules/node-notifier/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/node-preload": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
@@ -6116,9 +6135,9 @@
}
},
"node_modules/nyc/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6685,9 +6704,9 @@
"license": "MIT"
},
"node_modules/path-to-regexp": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
- "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/path-type": {
@@ -7482,9 +7501,9 @@
}
},
"node_modules/replace-in-file/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -7685,9 +7704,9 @@
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8359,9 +8378,9 @@
}
},
"node_modules/standard/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9059,9 +9078,9 @@
}
},
"node_modules/swagger-autogen/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -9158,9 +9177,9 @@
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9541,12 +9560,16 @@
}
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
+ "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"license": "MIT",
"bin": {
- "uuid": "dist/bin/uuid"
+ "uuid": "dist-node/bin/uuid"
}
},
"node_modules/v8-compile-cache": {
@@ -9867,9 +9890,9 @@
}
},
"node_modules/yamljs/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
diff --git a/package.json b/package.json
index 8e0ec46bb..4cb9b3f9e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "cve-services",
"author": "Automation Working Group",
- "version": "2.7.5",
+ "version": "2.8.0",
"license": "(CC0)",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
@@ -42,7 +42,7 @@
"jsonschema": "^1.4.0",
"JSONStream": "^1.3.5",
"kleur": "^4.1.4",
- "lodash": "^4.17.23",
+ "lodash": "^4.18.1",
"luxon": "^3.4.4",
"mongo-cursor-pagination": "^8.1.3",
"mongoose": "^8.9.5",
@@ -55,7 +55,7 @@
"replace-json-property": "^1.8.0",
"swagger-autogen": "^2.19.0",
"swagger-ui-express": "^4.3.0",
- "uuid": "^8.3.2",
+ "uuid": "^14.0.0",
"validator": ">=13.7.0",
"winston": "^3.2.1",
"yamljs": "^0.3.0"
diff --git a/src/middleware/schemas/5.2.0_published_cna_container.json b/schemas/5.2.0_published_cna_container.json
similarity index 100%
rename from src/middleware/schemas/5.2.0_published_cna_container.json
rename to schemas/5.2.0_published_cna_container.json
diff --git a/src/middleware/schemas/5.2.0_rejected_cna_container.json b/schemas/5.2.0_rejected_cna_container.json
similarity index 100%
rename from src/middleware/schemas/5.2.0_rejected_cna_container.json
rename to schemas/5.2.0_rejected_cna_container.json
diff --git a/src/middleware/schemas/Audit.json b/schemas/Audit.json
similarity index 100%
rename from src/middleware/schemas/Audit.json
rename to schemas/Audit.json
diff --git a/src/middleware/schemas/CVE_JSON_5.2.0_bundled.json b/schemas/CVE_JSON_5.2.0_bundled.json
similarity index 100%
rename from src/middleware/schemas/CVE_JSON_5.2.0_bundled.json
rename to schemas/CVE_JSON_5.2.0_bundled.json
diff --git a/schemas/glossary/create-glossary-item-response.json b/schemas/glossary/create-glossary-item-response.json
new file mode 100644
index 000000000..c4d5a2394
--- /dev/null
+++ b/schemas/glossary/create-glossary-item-response.json
@@ -0,0 +1,17 @@
+{
+ "$id": "https://cve.mitre.org/api-docs/schema/glossary/create-glossary-item-response.json",
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "created": {
+ "$ref": "glossary.json"
+ }
+ },
+ "required": [
+ "message",
+ "created"
+ ],
+ "additionalProperties": false
+}
diff --git a/schemas/glossary/glossary.json b/schemas/glossary/glossary.json
new file mode 100644
index 000000000..d5e59f8b0
--- /dev/null
+++ b/schemas/glossary/glossary.json
@@ -0,0 +1,24 @@
+{
+ "$id": "https://cve.mitre.org/api-docs/schema/glossary/glossary.json",
+ "type": "object",
+ "properties": {
+ "services_short_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1
+ },
+ "def": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "services_short_name",
+ "label",
+ "def"
+ ],
+ "additionalProperties": false
+}
diff --git a/schemas/glossary/list-glossary-items-response.json b/schemas/glossary/list-glossary-items-response.json
new file mode 100644
index 000000000..fee135e11
--- /dev/null
+++ b/schemas/glossary/list-glossary-items-response.json
@@ -0,0 +1,16 @@
+{
+ "$id": "https://cve.mitre.org/api-docs/schema/glossary/list-glossary-items-response.json",
+ "type": "object",
+ "properties": {
+ "glossary": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ },
+ "required": [
+ "glossary"
+ ],
+ "additionalProperties": false
+}
diff --git a/schemas/glossary/update-glossary-item-response.json b/schemas/glossary/update-glossary-item-response.json
new file mode 100644
index 000000000..64bc27db6
--- /dev/null
+++ b/schemas/glossary/update-glossary-item-response.json
@@ -0,0 +1,17 @@
+{
+ "$id": "https://cve.mitre.org/api-docs/schema/glossary/update-glossary-item-response.json",
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "updated": {
+ "$ref": "glossary.json"
+ }
+ },
+ "required": [
+ "message",
+ "updated"
+ ],
+ "additionalProperties": false
+}
diff --git a/schemas/registry-org/ADPOrg.json b/schemas/registry-org/ADPOrg.json
index be9829003..7979d1f55 100644
--- a/schemas/registry-org/ADPOrg.json
+++ b/schemas/registry-org/ADPOrg.json
@@ -5,7 +5,7 @@
"title": "CVE ADP Organization",
"description": "Schema for a CVE ADP Organization",
"allOf": [
- { "$ref": "./BaseOrg.json" },
+ { "$ref": "/BaseOrg" },
{
"properties": {
"authority": {
diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json
index 87f1b1e57..f1e8ef02f 100644
--- a/schemas/registry-org/BaseOrg.json
+++ b/schemas/registry-org/BaseOrg.json
@@ -1,9 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "./BaseOrg.json",
+ "$id": "/BaseOrg",
"type": "object",
"title": "CVE Base Organization",
"description": "Base schema for a CVE Organization",
+ "additionalProperties": false,
"definitions": {
"uuidType": {
"description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).",
@@ -34,7 +35,28 @@
"authority": {
"description": "The authority (role) of this organization within the CVE program",
"type": "string",
- "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"]
+ "enum": [
+ "CNA",
+ "SECRETARIAT",
+ "BULK_DOWNLOAD",
+ "ADP",
+ "ROOT"
+ ]
+ },
+ "partnerRoleType": {
+ "description": "The type of role a partner holds",
+ "type": "string",
+ "enum": [
+ "",
+ "Bug Bounty Provider",
+ "CERT",
+ "Consortium",
+ "Hosted Service",
+ "N/A",
+ "Open Source",
+ "Researcher",
+ "Vendor"
+ ]
}
},
"properties": {
@@ -47,6 +69,9 @@
"long_name": {
"$ref": "#/definitions/longName"
},
+ "new_short_name": {
+ "$ref": "#/definitions/shortName"
+ },
"aliases": {
"type": "array",
"uniqueItems": true,
@@ -61,8 +86,8 @@
"$ref": "#/definitions/authority"
}
},
- "root_or_tlr": {
- "type": "boolean"
+ "top_level_root": {
+ "type": "string"
},
"reports_to": {
"$ref": "#/definitions/uuidType"
@@ -81,37 +106,119 @@
"$ref": "#/definitions/uuidType"
}
},
+ "hard_quota": {
+ "description": "The maximum number of CVE IDs this organization can reserve.",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100000
+ },
+ "soft_quota": {
+ "description": "The threshold for notifying the organization about their remaining CVE ID count.",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100000
+ },
+ "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": {
+ "websites": {
"type": "array",
- "uniqueItems": true,
"items": {
- "$ref": "#/definitions/uuidType"
- }
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "poc": {
- "type": "string"
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
},
- "poc_email": {
+ "phone": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "program_data": {
+ "type": "object",
+ "properties": {
+ "cve_website_update_date": {
"type": "string",
- "format": "email"
+ "format": "date-time"
},
- "poc_phone": {
- "type": "string"
+ "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/BulkDownloadOrg.json b/schemas/registry-org/BulkDownloadOrg.json
index cabc0777a..526626f17 100644
--- a/schemas/registry-org/BulkDownloadOrg.json
+++ b/schemas/registry-org/BulkDownloadOrg.json
@@ -1,11 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "BaseOrg",
+ "$id": "BulkDownloadOrg",
"type": "object",
"title": "CVE Bulk Download Organization",
"description": "Schema for a CVE Bulk Download Organization",
"allOf": [
- { "$ref": "./BaseOrg.json" },
+ { "$ref": "/BaseOrg" },
{
"properties": {
"authority": {
diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json
index 0402e8338..bdcfedc85 100644
--- a/schemas/registry-org/CNAOrg.json
+++ b/schemas/registry-org/CNAOrg.json
@@ -1,42 +1,146 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
"$id": "CNAOrg",
+ "type": "object",
"title": "CVE CNA Organization",
"description": "Schema for a CVE CNA Organization",
- "allOf": [
- { "$ref": "./BaseOrg.json" },
- {
- "properties": {
- "authority": {
- "const": ["CNA"]
+ "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"
+ }
},
- "oversees": {
+ "additionalProperties": false
+ }
+ },
+ "contact_info": {
+ "type": "object",
+ "properties": {
+ "websites": {
"type": "array",
- "uniqueItems": true,
"items": {
- "$ref": "./BaseOrg.json#/definitions/uuidType"
- }
- },
- "hard_quota": {
- "type": "integer",
- "minimum": 0
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "soft_quota": {
- "type": "integer",
- "minimum": 0
- },
- "charter_or_scope": {
- "$ref": "/BaseOrg#/definitions/uriType"
- },
- "disclosure_policy": {
- "$ref": "/BaseOrg#/definitions/uriType"
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
},
- "product_list": {
- "$ref": "/BaseOrg#/definitions/uriType"
+ "phone": {
+ "type": "string"
}
},
- "required": ["hard_quota"]
+ "additionalProperties": false
+ },
+ "authority": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "$ref": "/BaseOrg#/definitions/authority"
+ }
+ },
+ "policies": {
+ "type": "object",
+ "properties": {
+ "id_quota": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100000
+ }
+ }
+ },
+ "hard_quota": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100000
+ },
+ "soft_quota": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100000
+ },
+ "oversees": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "charter_or_scope": {
+ "type": "string"
+ },
+ "disclosure_policy": {
+ "type": "string"
+ },
+ "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"
]
}
diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json
new file mode 100644
index 000000000..aef1f5494
--- /dev/null
+++ b/schemas/registry-org/RootOrg.json
@@ -0,0 +1,109 @@
+{
+ "$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": {
+ "websites": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
+ },
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
+ },
+ "phone": {
+ "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/SecretariatOrg.json b/schemas/registry-org/SecretariatOrg.json
index 469bd7df5..4e658b571 100644
--- a/schemas/registry-org/SecretariatOrg.json
+++ b/schemas/registry-org/SecretariatOrg.json
@@ -5,7 +5,7 @@
"title": "CVE Secretariat Organization",
"description": "Schema for a CVE Secretariat Organization",
"allOf": [
- { "$ref": "./BaseOrg.json" },
+ { "$ref": "/BaseOrg" },
{
"properties": {
"authority": {
@@ -15,7 +15,7 @@
"type": "array",
"uniqueItems": true,
"items": {
- "$ref": "./BaseOrg.json#/definitions/uuidType"
+ "$ref": "/BaseOrg#/definitions/uuidType"
}
},
"hard_quota": {
diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json
index 9ce81dc61..cfa8b9f09 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,127 @@
"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": {
+ "websites": {
"type": "array",
"items": {
- "type": "string"
- }
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "poc": {
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
+ },
+ "phone": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "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",
- "description": "Point of contact name"
+ "format": "date-time"
+ },
+ "cve_website_update_needed": {
+ "type": "boolean"
},
- "poc_email": {
+ "partner_active_date": {
"type": "string",
- "format": "email",
- "description": "Point of contact email"
+ "format": "date"
},
- "poc_phone": {
+ "partner_inactive_date": {
"type": "string",
- "description": "Point of contact phone number"
+ "format": "date"
},
- "admins": {
+ "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": "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"
}
},
- "required": ["poc", "poc_email", "admins", "org_email"]
+ "description": "Additional partner metadata (restricted)"
}
},
"required": [
diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json
index 6f0bfb0ec..a8ef091f1 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,69 @@
"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": {
+ "websites": {
"type": "array",
"items": {
- "type": "string"
- }
- },
- "poc": {
- "type": "string",
- "description": "Point of contact name"
- },
- "poc_email": {
- "type": "string",
- "format": "email",
- "description": "Point of contact email"
- },
- "poc_phone": {
- "type": "string",
- "description": "Point of contact phone number"
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "admins": {
+ "emails": {
"type": "array",
"items": {
- "type": "string"
+ "type": "string",
+ "format": "email"
},
- "description": "UUIDs of admin users"
- },
- "org_email": {
- "type": "string",
- "format": "email",
- "description": "Organization's email address"
+ "uniqueItems": true
},
- "website": {
- "type": "string",
- "format": "uri",
- "description": "Organization's website URL"
+ "phone": {
+ "type": "string"
}
},
- "required": ["poc", "poc_email", "admins", "org_email"]
+ "additionalProperties": false
+ },
+ "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 +184,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..a847dc290 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,128 @@
"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": {
+ "websites": {
"type": "array",
"items": {
- "type": "string"
- }
- },
- "poc": {
- "type": "string",
- "description": "Point of contact name"
- },
- "poc_email": {
- "type": "string",
- "format": "email",
- "description": "Point of contact email"
- },
- "poc_phone": {
- "type": "string",
- "description": "Point of contact phone number"
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "org_email": {
- "type": "string",
- "format": "email",
- "description": "Organization's email address"
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
},
- "website": {
- "type": "string",
- "format": "uri",
- "description": "Organization's website URL"
+ "phone": {
+ "type": "string"
}
},
- "required": [
- "poc",
- "poc_email",
- "org_email"
- ]
+ "additionalProperties": false
},
- "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"
+ },
+ "status": {
+ "type": "string"
},
- "description": "Locations of vulnerability advisories"
+ "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 +214,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..78b9f215c 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,103 @@
},
"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": {
+ "websites": {
"type": "array",
"items": {
- "type": "string"
- }
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "poc": {
- "type": "string",
- "description": "Point of contact name"
- },
- "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"
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
},
- "website": {
- "type": "string",
- "format": "uri",
- "description": "Organization's website URL"
+ "phone": {
+ "type": "string"
}
},
- "required": [
- "poc",
- "poc_email",
- "org_email"
- ]
+ "additionalProperties": false
},
- "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,17 +243,19 @@
"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"
}
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json
index 38d210e96..f171addee 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,127 @@
"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": {
+ "websites": {
"type": "array",
"items": {
- "type": "string"
- }
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "poc": {
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
+ },
+ "phone": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "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",
- "description": "Point of contact name"
+ "format": "date-time"
},
- "poc_email": {
+ "cve_website_update_needed": {
+ "type": "boolean"
+ },
+ "partner_active_date": {
"type": "string",
- "format": "email",
- "description": "Point of contact email"
+ "format": "date"
},
- "poc_phone": {
+ "partner_inactive_date": {
"type": "string",
- "description": "Point of contact phone number"
+ "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)"
}
}
}
diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json
index cbf41d925..33a498cf7 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,69 @@
"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": {
+ "websites": {
"type": "array",
"items": {
- "type": "string"
- }
- },
- "poc": {
- "type": "string",
- "description": "Point of contact name"
- },
- "poc_email": {
- "type": "string",
- "format": "email",
- "description": "Point of contact email"
- },
- "poc_phone": {
- "type": "string",
- "description": "Point of contact phone number"
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "admins": {
+ "emails": {
"type": "array",
"items": {
- "type": "string"
+ "type": "string",
+ "format": "email"
},
- "description": "UUIDs of admin users"
- },
- "org_email": {
- "type": "string",
- "format": "email",
- "description": "Organization's email address"
+ "uniqueItems": true
},
- "website": {
- "type": "string",
- "format": "uri",
- "description": "Organization's website URL"
+ "phone": {
+ "type": "string"
}
},
- "required": ["poc", "poc_email", "admins", "org_email"]
+ "additionalProperties": false
+ },
+ "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 +173,5 @@
}
}
}
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/schemas/registry-user/BaseUser.json b/schemas/registry-user/BaseUser.json
new file mode 100644
index 000000000..5fa85687e
--- /dev/null
+++ b/schemas/registry-user/BaseUser.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/BaseUser",
+ "type": "object",
+ "title": "CVE Base User Schema",
+ "additionalProperties": false,
+ "description": "The schema for CVE Services Users",
+ "definitions": {
+ "uuidType": {
+ "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).",
+ "type": "string",
+ "format": "uuid",
+ "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
+ },
+ "name": {
+ "description": "User's name components",
+ "type": "object",
+ "required": [
+ "first",
+ "last"
+ ],
+ "properties": {
+ "first": {
+ "type": "string",
+ "maxLength": 100
+ },
+ "middle": {
+ "type": "string",
+ "maxLength": 100
+ },
+ "last": {
+ "type": "string",
+ "maxLength": 100
+ },
+ "suffix": {
+ "type": "string",
+ "maxLength": 100
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/name"
+ },
+ "username": {
+ "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.",
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 128,
+ "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$"
+ },
+ "active": {
+ "description": "Whether the user account is active. Supports boolean or string based on legacy test constants.",
+ "type": [
+ "boolean",
+ "string"
+ ]
+ },
+ "authority": {
+ "description": "The user's authority and roles, often used in joint review contexts.",
+ "type": "object",
+ "properties": {
+ "active_roles": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "secret": {
+ "description": "Hashed secret for user authentication",
+ "type": "string"
+ },
+ "UUID": {
+ "$ref": "#/definitions/uuidType"
+ },
+ "status": {
+ "description": "User status: 'active' or 'inactive'",
+ "type": "string",
+ "enum": [
+ "active",
+ "inactive"
+ ]
+ },
+ "role": {
+ "description": "The user's role in the organization",
+ "type": "string"
+ },
+ "org_short_name": {
+ "description": "Used to update the organization association of a user",
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 32
+ }
+ },
+ "required": [
+ "username"
+ ]
+}
\ No newline at end of file
diff --git a/src/constants/index.js b/src/constants/index.js
index a4c73c910..4c0a38b7a 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -1,5 +1,5 @@
const fs = require('fs')
-const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json'))
+const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json'))
/**
* Return default values.
@@ -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/audit.controller/audit.controller.js b/src/controller/audit.controller/audit.controller.js
index dc1c8c8a5..dd3103a2d 100644
--- a/src/controller/audit.controller/audit.controller.js
+++ b/src/controller/audit.controller/audit.controller.js
@@ -99,8 +99,7 @@ async function createAuditDocumentForOrg (req, res, next) {
audit_uuid: returnValue.uuid
})
} catch (err) {
- await session.abortTransaction()
- throw err
+ console.error('REAL ERROR WAS:', err.stack); try { await session.abortTransaction() } catch (e) {}; throw err
} finally {
await session.endSession()
}
diff --git a/src/controller/audit.controller/index.js b/src/controller/audit.controller/index.js
index 10da8f70f..07930c35c 100644
--- a/src/controller/audit.controller/index.js
+++ b/src/controller/audit.controller/index.js
@@ -29,14 +29,14 @@ router.get('/audit/org/document/:document_uuid',
// Get audit by org identifier (Secretariat or Admin)
router.get('/audit/org/:org_identifier',
mw.validateUser,
- mw.onlySecretariatOrAdmin,
+ mw.onlySecretariat,
auditMw.parseGetParams,
controller.AUDIT_GET_BY_ORG_IDENTIFIER
)
// Get last X changes (Secretariat or Org Admin)
router.get('/audit/org/:org_identifier/:number_of_changes',
- mw.onlySecretariatOrAdmin,
+ mw.onlySecretariat,
mw.validateUser,
auditMw.parseGetParams,
controller.AUDIT_GET_LAST
diff --git a/src/controller/conversation.controller/conversation.controller.js b/src/controller/conversation.controller/conversation.controller.js
index 73d5335bf..0813c0955 100644
--- a/src/controller/conversation.controller/conversation.controller.js
+++ b/src/controller/conversation.controller/conversation.controller.js
@@ -2,6 +2,8 @@ const mongoose = require('mongoose')
const logger = require('../../middleware/logger')
const getConstants = require('../../../src/constants').getConstants
const CONSTANTS = getConstants()
+const errors = require('./error')
+const error = new errors.ConversationControllerError()
async function getAllConversations (req, res, next) {
const repo = req.ctx.repositories.getConversationRepository()
@@ -35,6 +37,7 @@ async function createConversationForTargetUUID (req, res, next) {
const repo = req.ctx.repositories.getConversationRepository()
const userRepo = req.ctx.repositories.getBaseUserRepository()
+ const orgRepo = req.ctx.repositories.getBaseOrgRepository()
const requesterOrg = req.ctx.org
const requesterUsername = req.ctx.user
const targetUUID = req.params.uuid
@@ -42,11 +45,12 @@ async function createConversationForTargetUUID (req, res, next) {
const user = await userRepo.findOneByUsernameAndOrgShortname(requesterUsername, requesterOrg, { session })
- if (!body.body) {
- return res.status(400).json({ message: 'Missing required field body' })
+ if (typeof body !== 'object' || !body.body || !repo.validateConversation(body)) {
+ return res.status(400).json(error.invalidConversationObject())
}
- const result = await repo.createConversation(targetUUID, body, user, true, { session })
+ const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org)
+ const result = await repo.createConversation(targetUUID, body, user, isSecretariat, { session })
await session.commitTransaction()
if (!result) {
return res.status(500).json({ message: 'Failed to create conversation' })
@@ -73,8 +77,56 @@ async function createConversationForTargetUUID (req, res, next) {
}
}
+async function updateConversationByUUID (req, res, next) {
+ const session = await mongoose.startSession()
+
+ try {
+ session.startTransaction()
+
+ const repo = req.ctx.repositories.getConversationRepository()
+ const conversationUUID = req.params.uuid
+ const body = req.body
+
+ // Check if conversation exists
+ const conversation = await repo.findOneByUUID(conversationUUID, { session })
+ if (!conversation) {
+ logger.info({ uuid: req.ctx.uuid, message: `No conversation found with UUID ${conversationUUID}` })
+ return res.status(404).json(error.conversationDne(conversationUUID))
+ }
+
+ // Validate body
+ if (typeof body !== 'object' || !(body.body || body.visibility) || !repo.validateConversation(body)) {
+ logger.info({ uuid: req.ctx.uuid, message: 'The conversation could not be edited because the request body was invalid.' })
+ return res.status(400).json(error.invalidConversationEditObject())
+ }
+
+ const result = await repo.editConversation(conversationUUID, body, { session })
+ await session.commitTransaction()
+ return res.status(200).json(result)
+ } catch (err) {
+ if (session && session.inTransaction()) {
+ await session.abortTransaction()
+ }
+ next(err)
+ } finally {
+ if (session && session.id) {
+ // Check if session is still valid before trying to end
+ try {
+ await session.endSession()
+ } catch (sessionEndError) {
+ logger.error({
+ uuid: req.ctx.uuid,
+ message: 'Error ending session in finally block',
+ error: sessionEndError
+ })
+ }
+ }
+ }
+}
+
module.exports = {
getAllConversations,
getConversationsForTargetUUID,
- createConversationForTargetUUID
+ createConversationForTargetUUID,
+ updateConversationByUUID
}
diff --git a/src/controller/conversation.controller/error.js b/src/controller/conversation.controller/error.js
new file mode 100644
index 000000000..8d116c691
--- /dev/null
+++ b/src/controller/conversation.controller/error.js
@@ -0,0 +1,49 @@
+const idrErr = require('../../utils/error')
+
+class ConversationControllerError extends idrErr.IDRError {
+ conversationDne (uuid) {
+ const err = {}
+ err.error = 'CONVERSATION_DNE'
+ err.message = `The conversation with UUID ${uuid} does not exist.`
+ return err
+ }
+
+ conversationIndexDne (shortname, index) {
+ const err = {}
+ err.error = 'CONVERSATION_INDEX_DNE'
+ err.message = `No conversation exists at index ${index} 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 object must include property 'body' (string) and optionally 'visibility' ('public' or 'private')."
+ return err
+ }
+
+ invalidConversationEditObject () {
+ const err = {}
+ err.error = 'BAD_INPUT'
+ err.message = "Parameters were invalid: conversation object must include at least one of the following properties: 'body' (string) or 'visibility' ('public' or 'private')."
+ return err
+ }
+}
+
+module.exports = {
+ ConversationControllerError
+}
diff --git a/src/controller/conversation.controller/index.js b/src/controller/conversation.controller/index.js
index 421cc8e5e..ea2bf62ed 100644
--- a/src/controller/conversation.controller/index.js
+++ b/src/controller/conversation.controller/index.js
@@ -252,9 +252,104 @@ router.post('/conversation/target/:uuid',
}
*/
mw.validateUser,
- mw.onlySecretariat,
+ mw.onlySecretariatOrAdmin,
param(['uuid']).isUUID(4),
controller.createConversationForTargetUUID
)
+// Update conversation - SEC only
+router.put('/conversation/:uuid',
+ /*
+ #swagger.tags = ['Conversation']
+ #swagger.operationId = 'updateConversationByUUID'
+ #swagger.summary = "Updates a conversation by UUID (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
+ Expected Behavior
+ Secretariat: Updates the conversation with the specified UUID
"
+ #swagger.parameters['uuid'] = { description: 'The UUID of the conversation to update' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.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'
+ }
+ }
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Returns the updated conversation',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/conversation/conversation.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.validateUser,
+ mw.onlySecretariat,
+ param(['uuid']).isUUID(4),
+ controller.updateConversationByUUID
+)
+
module.exports = router
diff --git a/src/controller/cve.controller/cve.middleware.js b/src/controller/cve.controller/cve.middleware.js
index 90b5a64e7..576bdad3f 100644
--- a/src/controller/cve.controller/cve.middleware.js
+++ b/src/controller/cve.controller/cve.middleware.js
@@ -4,8 +4,8 @@ const errors = require('./error')
const error = new errors.CveControllerError()
const utils = require('../../utils/utils')
const fs = require('fs')
-const RejectedSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_rejected_cna_container.json'))
-const cnaContainerSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_published_cna_container.json'))
+const RejectedSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_rejected_cna_container.json'))
+const cnaContainerSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_published_cna_container.json'))
const logger = require('../../middleware/logger')
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js
new file mode 100644
index 000000000..b55d65850
--- /dev/null
+++ b/src/controller/glossary.controller/glossary.controller.js
@@ -0,0 +1,140 @@
+const errors = require('../../utils/error')
+const error = new errors.IDRError()
+
+/**
+ * Retrieves all glossary items.
+ *
+ * @param {Object} req - The Express request object.
+ * @param {Object} res - The Express response object.
+ * @param {Function} next - The Express next middleware function.
+ * @returns {Promise} Returns a JSON response containing an array of all glossary items.
+ */
+async function getAllGlossaryItems (req, res, next) {
+ try {
+ const glossaryRepo = req.ctx.repositories.getGlossaryRepository()
+ const result = await glossaryRepo.getAll()
+ return res.status(200).json({ glossary: result })
+ } catch (err) {
+ next(err)
+ }
+}
+
+/**
+ * Retrieves a single glossary item by its short name.
+ *
+ * @param {Object} req - The Express request object.
+ * @param {Object} res - The Express response object.
+ * @param {Function} next - The Express next middleware function.
+ * @returns {Promise} Returns a JSON response containing the requested glossary item, or a 404 if not found.
+ */
+async function getGlossaryItem (req, res, next) {
+ try {
+ const glossaryRepo = req.ctx.repositories.getGlossaryRepository()
+ const servicesShortName = req.params.services_short_name
+
+ const result = await glossaryRepo.findOneByServicesShortName(servicesShortName)
+ if (!result) {
+ return res.status(404).json(error.notFound())
+ }
+ return res.status(200).json(result)
+ } catch (err) {
+ next(err)
+ }
+}
+
+/**
+ * Creates a new glossary item.
+ *
+ * @param {Object} req - The Express request object.
+ * @param {Object} res - The Express response object.
+ * @param {Function} next - The Express next middleware function.
+ * @returns {Promise} Returns a JSON response containing the newly created glossary item, or a 400 if it already exists.
+ */
+async function createGlossaryItem (req, res, next) {
+ try {
+ const glossaryRepo = req.ctx.repositories.getGlossaryRepository()
+ const glossaryData = req.body
+
+ const existing = await glossaryRepo.findOneByServicesShortName(glossaryData.services_short_name)
+ if (existing) {
+ return res.status(400).json(error.badInput(['Glossary item with this services_short_name already exists']))
+ }
+
+ const createdDoc = await glossaryRepo.collection.create(glossaryData)
+ const result = createdDoc.toObject()
+ delete result._id
+ delete result.__v
+ delete result.createdAt
+ delete result.updatedAt
+
+ return res.status(200).json({
+ message: 'glossary item successfully added',
+ created: result
+ })
+ } catch (err) {
+ next(err)
+ }
+}
+
+/**
+ * Updates an existing glossary item by its short name.
+ *
+ * @param {Object} req - The Express request object.
+ * @param {Object} res - The Express response object.
+ * @param {Function} next - The Express next middleware function.
+ * @returns {Promise} Returns a JSON response containing the updated glossary item, or a 404 if not found.
+ */
+async function updateGlossaryItem (req, res, next) {
+ try {
+ const glossaryRepo = req.ctx.repositories.getGlossaryRepository()
+ const servicesShortName = req.params.services_short_name
+ const glossaryData = req.body
+
+ if (glossaryData.services_short_name && glossaryData.services_short_name !== servicesShortName) {
+ return res.status(400).json(error.badInput(['Cannot change services_short_name through this endpoint.']))
+ }
+
+ const result = await glossaryRepo.updateByServicesShortName(servicesShortName, glossaryData)
+ if (!result) {
+ return res.status(404).json(error.notFound())
+ }
+
+ return res.status(200).json({
+ message: 'glossary item successfully updated',
+ updated: result
+ })
+ } catch (err) {
+ next(err)
+ }
+}
+
+/**
+ * Deletes an existing glossary item by its short name.
+ *
+ * @param {Object} req - The Express request object.
+ * @param {Object} res - The Express response object.
+ * @param {Function} next - The Express next middleware function.
+ * @returns {Promise} Returns a JSON message confirming deletion, or a 404 if not found.
+ */
+async function deleteGlossaryItem (req, res, next) {
+ try {
+ const glossaryRepo = req.ctx.repositories.getGlossaryRepository()
+ const servicesShortName = req.params.services_short_name
+
+ const result = await glossaryRepo.deleteByServicesShortName(servicesShortName)
+ if (!result) {
+ return res.status(404).json(error.notFound())
+ }
+ return res.status(200).json({ message: 'Glossary item deleted' })
+ } catch (err) {
+ next(err)
+ }
+}
+
+module.exports = {
+ getAllGlossaryItems,
+ getGlossaryItem,
+ createGlossaryItem,
+ updateGlossaryItem,
+ deleteGlossaryItem
+}
diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js
new file mode 100644
index 000000000..6d097b5e3
--- /dev/null
+++ b/src/controller/glossary.controller/index.js
@@ -0,0 +1,331 @@
+const router = require('express').Router()
+const controller = require('./glossary.controller')
+const mw = require('../../middleware/middleware')
+
+// Get all glossary items
+router.get('/glossary',
+ /*
+ #swagger.tags = ['Glossary']
+ #swagger.operationId = 'glossaryAll'
+ #swagger.summary = "Retrieves all glossary items"
+ #swagger.description = "Retrieves all glossary items
"
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns a list of all glossary items',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/glossary/list-glossary-items-response.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.validateUser,
+ controller.getAllGlossaryItems
+)
+
+// Get glossary item by services_short_name
+router.get('/glossary/:services_short_name',
+ /*
+ #swagger.tags = ['Glossary']
+ #swagger.operationId = 'glossarySingle'
+ #swagger.summary = "Retrieves a single glossary item by its short name"
+ #swagger.description = "Retrieves the specified glossary item
"
+ #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Returns the specified glossary item',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/glossary/glossary.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.validateUser,
+ controller.getGlossaryItem
+)
+
+// Create a glossary item - SEC only
+router.post('/glossary',
+ /*
+ #swagger.tags = ['Glossary']
+ #swagger.operationId = 'glossaryCreate'
+ #swagger.summary = "Creates a new glossary item (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
+ Expected Behavior
+ Secretariat: Creates a new glossary item
"
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '../schemas/glossary/glossary.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Returns the created glossary item wrapped in a success message',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/glossary/create-glossary-item-response.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.validateUser,
+ mw.onlySecretariat,
+ controller.createGlossaryItem
+)
+
+// Update a glossary item - SEC only
+router.put('/glossary/:services_short_name',
+ /*
+ #swagger.tags = ['Glossary']
+ #swagger.operationId = 'glossaryUpdate'
+ #swagger.summary = "Updates an existing glossary item (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
+ Expected Behavior
+ Secretariat: Updates the specified glossary item
"
+ #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.requestBody = {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '../schemas/glossary/glossary.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Returns the updated glossary item wrapped in a success message',
+ content: {
+ "application/json": {
+ schema: {
+ $ref: '../schemas/glossary/update-glossary-item-response.json'
+ }
+ }
+ }
+ }
+ #swagger.responses[400] = {
+ description: 'Bad Request',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/bad-request.json' }
+ }
+ }
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.validateUser,
+ mw.onlySecretariat,
+ controller.updateGlossaryItem
+)
+
+// Delete a glossary item - SEC only
+router.delete('/glossary/:services_short_name',
+ /*
+ #swagger.tags = ['Glossary']
+ #swagger.operationId = 'glossaryDelete'
+ #swagger.summary = "Deletes an existing glossary item (accessible to Secretariat only)"
+ #swagger.description = "
+ Access Control
+ User must belong to an organization with the Secretariat role
+ Expected Behavior
+ Secretariat: Deletes the specified glossary item
"
+ #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' }
+ #swagger.parameters['$ref'] = [
+ '#/components/parameters/apiEntityHeader',
+ '#/components/parameters/apiUserHeader',
+ '#/components/parameters/apiSecretHeader'
+ ]
+ #swagger.responses[200] = {
+ description: 'Confirms deletion of the glossary item'
+ }
+ #swagger.responses[401] = {
+ description: 'Not Authenticated',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Not Found',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error',
+ content: {
+ "application/json": {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.validateUser,
+ mw.onlySecretariat,
+ controller.deleteGlossaryItem
+)
+
+module.exports = router
diff --git a/src/controller/org.controller/error.js b/src/controller/org.controller/error.js
index 4c4df5fd3..67c305c31 100644
--- a/src/controller/org.controller/error.js
+++ b/src/controller/org.controller/error.js
@@ -36,6 +36,13 @@ class OrgControllerError extends idrErr.IDRError {
return err
}
+ aliasCollision (conflictingString) {
+ const err = {}
+ err.error = 'ALIAS_COLLISION'
+ err.message = `The organization could not be created or updated because the string '${conflictingString}' is already in use as a short_name, name, or alias by another organization.`
+ return err
+ }
+
userExists (username) { // org
const err = {}
err.error = 'USER_EXISTS'
@@ -91,6 +98,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 f7805c496..d69ae7c4a 100644
--- a/src/controller/org.controller/index.js
+++ b/src/controller/org.controller/index.js
@@ -548,19 +548,16 @@ router.put('/registry/org/:shortname',
authority
aliases
oversees
- root_or_tlr
+ top_level_root
charter_or_scope
product_list
disclosure_policy
- contact_info.poc
- contact_info.poc_email
- contact_info.poc_phone
- contact_info.org_email
- partner_role
- partner_type
+ contact_info.websites
+ contact_info.emails
+ contact_info.phone
+ partner_role_type
partner_country
- vulnerability_advisory_locations
- advisory_location_require_credentials
+ advisory_locations
industry
tl_root_start_date
is_cna_discussion_list
@@ -640,7 +637,7 @@ router.put('/registry/org/:shortname',
*/
mw.useRegistry(),
mw.validateUser,
- mw.onlySecretariat,
+ // mw.onlySecretariat,
parseError,
parsePutParams,
registryOrgController.UPDATE_ORG
diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js
index 297887707..a1bc0cf58 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.
@@ -61,22 +62,35 @@ async function getOrg (req, res, next) {
try {
const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, {}, returnLegacyFormat)
+
+ // Ensure requester org exists
+ if (!requesterOrg) {
+ return res.status(404).json(error.orgDne(requesterOrgShortName, 'requesterOrgShortName', 'header'))
+ }
+
const requesterOrgIdentifier = identifierIsUUID ? requesterOrg.UUID : requesterOrgShortName
const isSecretariat = await repo.isSecretariat(requesterOrg, {}, returnLegacyFormat)
+ // Ensure that if the requester is not Secretariat, they can't view orgs other than their own
if (requesterOrgIdentifier !== identifier && !isSecretariat) {
- logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' })
+ logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by same-org users or Secretariat.' })
return res.status(403).json(error.notSameOrgOrSecretariat())
}
returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, returnLegacyFormat)
- } catch (error) {
- // Handle the specific error thrown by BaseOrgRepository.createOrg
- if (error.message && error.message.includes('Unknown Org type requested')) {
- return res.status(400).json({ message: error.message })
+ } catch (err) {
+ // Handle the specific error thrown by BaseOrgRepository.getOrg
+ if (err.message && err.message.includes('Unknown Org type requested')) {
+ return res.status(400).json({ message: err.message })
}
+
+ // Handle database / network errors
+ logger.error({ uuid: req.ctx.uuid, message: 'Internal Server Error', error: err.stack })
+ return res.status(500).json(error.internal())
}
- if (!returnValue) { // an empty result can only happen if the requestor is the Secretariat
+
+ // Handle the error where the org can't be found
+ if (!returnValue) {
logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization does not exist.' })
return res.status(404).json(error.orgDne(identifier, 'identifier', 'path'))
}
@@ -267,6 +281,16 @@ async function createOrg (req, res, next) {
return res.status(400).json(error.orgExists(body?.short_name))
}
+ // Check for alias collisions
+ const collisionString = await repo.checkAliasCollisions(body?.short_name, body?.name, body?.aliases, null, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${body?.short_name} organization was not created because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
const userRepo = req.ctx.repositories.getBaseUserRepository()
const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session })
const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
@@ -349,10 +373,32 @@ async function updateOrg (req, res, next) {
return res.status(403).json(error.duplicateShortname(queryParametersJson.new_short_name))
}
+ // Check for alias collisions
+ const shortNameToExclude = shortNameUrlParameter
+ const collisionString = await orgRepository.checkAliasCollisions(queryParametersJson.new_short_name || shortNameUrlParameter, queryParametersJson.name, queryParametersJson.aliases, shortNameToExclude, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${shortNameUrlParameter} organization could not be updated because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
+
const userRepo = req.ctx.repositories.getBaseUserRepository()
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
@@ -448,7 +494,8 @@ async function createUser (req, res, next) {
return res.status(400).json(error.userLimitReached())
}
- returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, !!req.useRegistry)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, !!req.useRegistry, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -623,7 +670,8 @@ async function updateUser (req, res, next) {
}
}
- const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, !!req.useRegistry)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, !!req.useRegistry, requestingUserUUID)
await session.commitTransaction()
return res.status(200).json({ message: `${usernameParams} was successfully updated.`, updated: payload })
} catch (err) {
diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js
index 204f6066b..931c01a5e 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,33 +46,47 @@ 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',
'disclosure_policy',
'product_list',
- 'contact_info.poc',
- 'contact_info.poc_email',
- 'contact_info.poc_phone',
- 'contact_info.org_email',
- 'contact_info.website',
- 'partner_role',
- 'partner_type',
+ 'contact_info.websites',
+ 'contact_info.emails',
+ 'contact_info.phone',
+ '',
+ '',
+ 'partner_role_type',
+ 'partner_number',
'partner_country',
+ 'program_data.status',
'industry'
])
.default('')
@@ -88,7 +102,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,31 +128,33 @@ 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.website',
+ 'contact_info.phone',
+ 'contact_info.websites',
+ 'contact_info.emails',
'contact_info',
'users',
'charter_or_scope',
'disclosure_policy',
'product_list',
- 'contact_info.poc',
- 'contact_info.poc_email',
- 'contact_info.poc_phone',
- 'contact_info.org_email',
- 'contact_info.additional_contact_users',
- 'contact_info.website',
- 'partner_role',
- 'partner_type',
+ 'contact_info.websites',
+ 'contact_info.emails',
+ 'contact_info.phone',
+ '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 +225,25 @@ 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.website',
- 'partner_role',
- 'partner_type',
+ 'contact_info.websites',
+ 'contact_info.emails',
+ 'contact_info.phone',
+ '',
+ '',
+ '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,22 +317,26 @@ const QUERY_PARAMETERS = {
],
// Registry-only parameters
registryOnly: [
- 'root_or_tlr',
+ 'top_level_root',
'charter_or_scope',
'disclosure_policy',
'product_list',
'oversees',
'contact_info',
- 'contact_info.poc',
- 'contact_info.poc_email',
- 'contact_info.poc_phone',
- 'contact_info.org_email',
- 'contact_info.website',
- 'partner_role',
- 'partner_type',
+ 'contact_info.websites',
+ 'contact_info.emails',
+ 'contact_info.phone',
+ '',
+ '',
+ '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 d4af5f1e6..b105e373e 100644
--- a/src/controller/registry-org.controller/error.js
+++ b/src/controller/registry-org.controller/error.js
@@ -36,6 +36,13 @@ class RegistryOrgControllerError extends idrErr.IDRError {
return err
}
+ aliasCollision (conflictingString) {
+ const err = {}
+ err.error = 'ALIAS_COLLISION'
+ err.message = `The organization could not be created or updated because the string '${conflictingString}' is already in use as a short_name, name, or alias by another organization.`
+ return err
+ }
+
userExists (username) { // org
const err = {}
err.error = 'USER_EXISTS'
@@ -119,6 +126,13 @@ class RegistryOrgControllerError extends idrErr.IDRError {
err.message = 'Parameters were invalid: conversation must be an object with a body.'
return err
}
+
+ secretariatOnlyEditing (fields) {
+ const err = {}
+ err.error = 'SECRETARIAT_ONLY'
+ err.message = `The following fields can only be modified by the Secretariat: ${fields.join(', ')}.`
+ return err
+ }
}
module.exports = {
diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js
index 64da4d038..d32c8577a 100644
--- a/src/controller/registry-org.controller/registry-org.controller.js
+++ b/src/controller/registry-org.controller/registry-org.controller.js
@@ -4,6 +4,8 @@ const { getConstants } = require('../../constants')
const _ = require('lodash')
const errors = require('./error')
const error = new errors.RegistryOrgControllerError()
+const conversationErrors = require('../conversation.controller/error')
+const convoError = new conversationErrors.ConversationControllerError()
const validateUUID = require('uuid').validate
/**
@@ -37,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)
@@ -89,14 +91,13 @@ 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
const conversation = await conversationRepo.getAllByTargetUUID(returnValue.UUID, isSecretariat)
if (isSecretariat) {
- returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid'])) : undefined
+ returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid'])) : undefined
} else {
returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid', 'visibility'])) : undefined
}
@@ -145,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 })
@@ -167,6 +177,17 @@ async function createOrg (req, res, next) {
return res.status(400).json(error.orgExists(body?.short_name))
}
+ // Check for alias collisions
+ const collisionString = await repo.checkAliasCollisions(body?.short_name, body?.name, body?.aliases, null, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${body?.short_name} organization was not created because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
+
const userRepo = req.ctx.repositories.getBaseUserRepository()
const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
// Create the org – repo.createOrg will handle field mapping
@@ -241,10 +262,6 @@ async function updateOrg (req, res, next) {
let updatedOrg
let jointApprovalRequired
- if (conversation && (typeof conversation !== 'object' || !conversation.body)) {
- return res.status(400).json(error.invalidConversationObject())
- }
-
try {
session.startTransaction()
const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session })
@@ -258,6 +275,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) {
@@ -280,6 +307,7 @@ async function updateOrg (req, res, next) {
}
}
+ // Validate org
const result = repo.validateOrg(body, { session })
if (!result.isValid) {
logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' }))
@@ -287,6 +315,19 @@ async function updateOrg (req, res, next) {
return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors })
}
+ // Validate conversation (if it exists)
+ if (conversation) {
+ if (
+ typeof conversation !== 'object' ||
+ !conversation.body ||
+ !conversationRepo.validateConversation(conversation)
+ ) {
+ logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'Invalid conversation object.' }))
+ await session.abortTransaction()
+ return res.status(400).json(convoError.invalidConversationObject())
+ }
+ }
+
// Check for duplicate short_name
if (body?.short_name !== shortName && await repo.orgExists(body?.short_name, { session })) {
logger.info({
@@ -297,6 +338,18 @@ async function updateOrg (req, res, next) {
return res.status(400).json(error.duplicateShortname(body?.short_name))
}
+ // Check for alias collisions
+ const shortNameToExclude = shortName
+ const collisionString = await repo.checkAliasCollisions(body?.short_name || shortName, body?.name, body?.aliases, shortNameToExclude, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${shortName} organization could not be updated because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
+
// Handle secretariat "stomping" of pending review objects
if (isSecretariat) {
const reviewRepo = req.ctx.repositories.getReviewObjectRepository()
@@ -561,7 +614,8 @@ async function createUserByOrg (req, res, next) {
return res.status(400).json(error.userLimitReached())
}
- returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -634,7 +688,17 @@ async function editConversationForOrg (req, res, next) {
const conversation = await conversationRepo.findByTargetUUIDAndIndex(orgUUID, index, { session })
if (!conversation) {
logger.info({ uuid: req.ctx.uuid, message: `The conversation at index ${index} does not exist for the ${orgShortName} organization.` })
- return res.status(404).json(error.conversationDne(orgShortName, index))
+ return res.status(404).json(convoError.conversationIndexDne(orgShortName, index))
+ }
+
+ // Validate body
+ if (
+ typeof incomingParameters !== 'object' ||
+ !(incomingParameters.body || incomingParameters.visibility) ||
+ !conversationRepo.validateConversation(incomingParameters)
+ ) {
+ logger.info({ uuid: req.ctx.uuid, message: 'The conversation could not be edited because the request body was invalid.' })
+ return res.status(400).json(convoError.invalidConversationObject())
}
// Check if user has permissions to edit conversation
@@ -642,19 +706,20 @@ async function editConversationForOrg (req, res, next) {
const userUUID = await userRepo.getUserUUID(requesterUsername, req.ctx.org, { session })
if (conversation.author_id !== userUUID && !isSecretariat) {
logger.info({ uuid: req.ctx.uuid, message: 'The user does not have permission to edit this conversation.' })
- return res.status(403).json(error.notAllowedToEditConversation())
+ return res.status(403).json(convoError.notAllowedToEditConversation())
}
// Check if user has permission to change visibility of conversation
if (incomingParameters.visibility && !isSecretariat) {
logger.info({ uuid: req.ctx.uuid, message: 'Only the Secretariat is allowed to change the visibility of a conversation.' })
- return res.status(403).json(error.notAllowedToChangeConversationVisibility())
+ return res.status(403).json(convoError.notAllowedToChangeConversationVisibility())
}
// Make the edit
returnValue = await conversationRepo.editConversation(conversation.UUID, incomingParameters, { session })
- if (!isSecretariat && returnValue) {
+ if (!isSecretariat && returnValue && returnValue.author_role === 'Secretariat') {
delete returnValue.author_id
+ delete returnValue.author_name
}
await session.commitTransaction()
} catch (error) {
diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js
index 3b1c51a81..88319d131 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.websites', 'contact_info.emails', 'contact_info.phone',
+ '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/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js
index 872c6fe11..4bb60022f 100644
--- a/src/controller/registry-user.controller/index.js
+++ b/src/controller/registry-user.controller/index.js
@@ -3,7 +3,7 @@ const router = express.Router()
const mw = require('../../middleware/middleware')
const { param, query } = require('express-validator')
const controller = require('./registry-user.controller')
-const { parseGetParams, parsePostParams, parseDeleteParams } = require('./registry-user.middleware')
+const { parseGetParams, parsePostParams, parseDeleteParams, parseError } = require('./registry-user.middleware')
const getConstants = require('../../constants').getConstants
const CONSTANTS = getConstants()
@@ -69,7 +69,7 @@ router.get('/registryUser',
mw.onlySecretariat,
query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
- // parseError,
+ parseError,
parseGetParams,
controller.ALL_USERS
)
@@ -140,7 +140,7 @@ router.get('/registryUser/:identifier',
mw.validateUser,
mw.onlySecretariat,
param(['identifier']).isString().trim(),
- // parseError,
+ parseError,
parseGetParams,
controller.SINGLE_USER
)
@@ -212,6 +212,8 @@ router.post('/registryUser/:shortname',
*/
mw.validateUser,
mw.onlySecretariat,
+ param(['shortname']).isString().trim(),
+ parseError,
parsePostParams,
controller.CREATE_USER
)
@@ -299,7 +301,7 @@ router.put('/registryUser/:identifier',
mw.onlySecretariat,
param(['identifier']).isString().trim(),
// TODO: do more validation here
- // parseError,
+ parseError,
parsePostParams,
controller.UPDATE_USER
)
@@ -387,7 +389,7 @@ router.delete(
mw.validateUser,
mw.onlySecretariat,
param(['identifier']).isString().trim(),
- // parseError,
+ parseError,
parseDeleteParams,
controller.DELETE_USER
)
diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js
index 3133c197e..4fabccc37 100644
--- a/src/controller/registry-user.controller/registry-user.controller.js
+++ b/src/controller/registry-user.controller/registry-user.controller.js
@@ -186,7 +186,8 @@ async function createUser (req, res, next) {
return res.status(400).json(error.userLimitReached())
}
- returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true })
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -351,8 +352,9 @@ async function updateUser (req, res, next) {
}
// UUID of the user will not change, lets get it before we write to avoid read after write issues.
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
updatedUserUUID = await userRepo.getUserUUID(req.ctx.user, org.UUID)
- updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session })
+ updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session }, true, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -395,7 +397,8 @@ async function deleteUser (req, res, next) {
return res.status(404).json(error.userDne(userUUID))
}
- await userRepo.deleteUserByUUID(userUUID)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org)
+ await userRepo.deleteUserByUUID(userUUID, {}, requestingUserUUID)
const payload = {
action: 'delete_registry_user',
@@ -462,7 +465,8 @@ async function grantRole (req, res, next) {
try {
session.startTransaction()
- await orgRepo.addAdmin(orgShortName, targetUser.UUID, { session })
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ await orgRepo.addAdmin(orgShortName, targetUser.UUID, { session }, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -530,7 +534,8 @@ async function revokeRole (req, res, next) {
try {
session.startTransaction()
- await orgRepo.removeAdmin(orgShortName, targetUser.UUID, { session })
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ await orgRepo.removeAdmin(orgShortName, targetUser.UUID, { session }, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
diff --git a/src/controller/registry-user.controller/registry-user.middleware.js b/src/controller/registry-user.controller/registry-user.middleware.js
index 6b30b69e0..e39b721c0 100644
--- a/src/controller/registry-user.controller/registry-user.middleware.js
+++ b/src/controller/registry-user.controller/registry-user.middleware.js
@@ -1,8 +1,11 @@
const utils = require('../../utils/utils')
+const { validationResult } = require('express-validator')
+const errors = require('../registry-org.controller/error')
+const error = new errors.RegistryOrgControllerError()
function parsePostParams (req, res, next) {
utils.reqCtxMapping(req, 'body', [])
- utils.reqCtxMapping(req, 'params', ['identifier'])
+ utils.reqCtxMapping(req, 'params', ['identifier', 'shortname'])
utils.reqCtxMapping(req, 'query', [
'new_username',
'name.first', 'name.last', 'name.middle', 'name.suffix',
@@ -23,8 +26,19 @@ function parseDeleteParams (req, res, next) {
next()
}
+function parseError (req, res, next) {
+ const err = validationResult(req).formatWith(({ location, msg, param, value, nestedErrors }) => {
+ return { msg: msg, param: param, location: location }
+ })
+ if (!err.isEmpty()) {
+ return res.status(400).json(error.badInput(err.array()))
+ }
+ next()
+}
+
module.exports = {
parsePostParams,
parseGetParams,
- parseDeleteParams
+ parseDeleteParams,
+ parseError
}
diff --git a/src/controller/review-object.controller/index.js b/src/controller/review-object.controller/index.js
index 702da7529..8dfbecc01 100644
--- a/src/controller/review-object.controller/index.js
+++ b/src/controller/review-object.controller/index.js
@@ -146,7 +146,7 @@ router.get('/review/org/:identifier',
*/
mw.useRegistry(),
mw.validateUser,
- mw.onlySecretariat,
+ mw.onlySecretariatOrAdmin,
controller.getReviewObjectByOrgIdentifier
)
@@ -411,6 +411,7 @@ router.put('/review/:uuid/approve',
/*
#swagger.tags = ['Review Object']
#swagger.operationId = 'approveReviewObject'
+ #swagger.ignore = true
#swagger.summary = "Approves a review object and applies changes to the organization (accessible to Secretariat only)"
#swagger.description = "
Access Control
@@ -567,6 +568,7 @@ router.post('/review/org/',
/*
#swagger.tags = ['Review Object']
#swagger.operationId = 'createReviewObject'
+ #swagger.ignore = true
#swagger.summary = "Creates a new review object (accessible to Secretariat only)"
#swagger.description = "
Access Control
diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js
index 7b4556d29..b30a17e94 100644
--- a/src/controller/review-object.controller/review-object.controller.js
+++ b/src/controller/review-object.controller/review-object.controller.js
@@ -20,6 +20,16 @@ async function getReviewObjectByOrgIdentifier (req, res, next) {
return res.status(400).json({ message: 'Missing identifier parameter' })
}
let value
+
+ if (!isSecretariat) {
+ const orgUUID = await orgRepo.getOrgUUID(req.ctx.org)
+ if (identifierIsUUID && identifier !== orgUUID) {
+ return res.status(403).json({ error: 'NOT_SAME_ORG_OR_SECRETARIAT', message: 'This information can only be viewed by the users of the same organization or the Secretariat.' })
+ } else if (!identifierIsUUID && identifier !== req.ctx.org) {
+ return res.status(403).json({ error: 'NOT_SAME_ORG_OR_SECRETARIAT', message: 'This information can only be viewed by the users of the same organization or the Secretariat.' })
+ }
+ }
+
// We may want this to be something different, but for now we are just testing
if (identifierIsUUID) {
value = await repo.getOrgReviewObjectByOrgUUID(identifier, isSecretariat, {})
@@ -38,6 +48,18 @@ async function getReviewObjectByUUID (req, res, next) {
const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org)
const UUID = req.params.uuid
const value = await repo.findOneByUUIDWithConversation(UUID, isSecretariat)
+
+ if (!value) {
+ return res.status(404).json({ message: 'Review object not found' })
+ }
+
+ if (!isSecretariat) {
+ const orgUUID = await orgRepo.getOrgUUID(req.ctx.org)
+ if (value.target_object_uuid !== orgUUID) {
+ return res.status(403).json({ error: 'NOT_SAME_ORG_OR_SECRETARIAT', message: 'This information can only be viewed by the users of the same organization or the Secretariat.' })
+ }
+ }
+
return res.status(200).json(value)
}
@@ -197,6 +219,10 @@ async function getReviewHistoryByOrgShortNamePaginated (req, res, next) {
return res.status(404).json(error.orgDnePathParam(orgShortName))
}
+ if (!isSecretariat && req.ctx.org !== orgShortName) {
+ return res.status(403).json({ error: 'NOT_SAME_ORG_OR_SECRETARIAT', message: 'This information can only be viewed by the users of the same organization or the Secretariat.' })
+ }
+
if (req.TEST_PAGINATOR_LIMIT) {
CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
}
diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js
index 67e47e7b8..6cd272531 100644
--- a/src/middleware/middleware.js
+++ b/src/middleware/middleware.js
@@ -1,6 +1,6 @@
const getConstants = require('../constants').getConstants
const fs = require('fs')
-const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json'))
+const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json'))
const argon2 = require('argon2')
const logger = require('./logger')
const Ajv = require('ajv')
diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json
index 7979d1f55..b5bea44cb 100644
--- a/src/middleware/schemas/ADPOrg.json
+++ b/src/middleware/schemas/ADPOrg.json
@@ -7,6 +7,7 @@
"allOf": [
{ "$ref": "/BaseOrg" },
{
+ "type": "object",
"properties": {
"authority": {
"const": ["ADP"]
diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json
index f7039bcca..b1982cfcb 100644
--- a/src/middleware/schemas/BaseOrg.json
+++ b/src/middleware/schemas/BaseOrg.json
@@ -100,23 +100,24 @@
"$ref": "#/definitions/uuidType"
}
},
- "poc": {
- "type": "string"
+ "websites": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ },
+ "uniqueItems": true
},
- "poc_email": {
- "type": "string",
- "format": "email"
+ "emails": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "email"
+ },
+ "uniqueItems": true
},
- "poc_phone": {
+ "phone": {
"type": "string"
- },
- "org_email": {
- "type": "string",
- "format": "email"
- },
- "website": {
- "$ref": "#/definitions/uriType",
- "pattern": "^(ftp|http)s?://\\S+$"
}
},
"additionalProperties": false
diff --git a/src/middleware/schemas/BaseUser.json b/src/middleware/schemas/BaseUser.json
deleted file mode 100644
index 2c1bf93de..000000000
--- a/src/middleware/schemas/BaseUser.json
+++ /dev/null
@@ -1,72 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "/BaseUser",
- "type": "object",
- "title": "CVE Base User Schema",
- "description": "The schema for CVE Services Users",
- "definitions": {
- "uuidType": {
- "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).",
- "type": "string",
- "format": "uuid",
- "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
- },
- "name": {
- "description": "User's name components",
- "type": "object",
- "required": [
- "first",
- "last"
- ],
- "properties": {
- "first": {
- "type": "string",
- "maxLength": 100
- },
- "middle": {
- "type": "string",
- "maxLength": 100
- },
- "last": {
- "type": "string",
- "maxLength": 100
- },
- "suffix": {
- "type": "string",
- "maxLength": 100
- }
- },
- "additionalProperties": false
- }
- },
- "properties": {
- "name": {
- "$ref": "#/definitions/name"
- },
- "username": {
- "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.",
- "type": "string",
- "minLength": 3,
- "maxLength": 128,
- "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$"
- },
- "secret": {
- "description": "Hashed secret for user authentication",
- "type": "string"
- },
- "UUID": {
- "$ref": "#/definitions/uuidType"
- },
- "status": {
- "description": "User status: 'active' or 'inactive'",
- "type": "string",
- "enum": [
- "active",
- "inactive"
- ]
- }
- },
- "required": [
- "username"
- ]
-}
\ No newline at end of file
diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json
index ada140853..768ae1123 100644
--- a/src/middleware/schemas/BulkDownloadOrg.json
+++ b/src/middleware/schemas/BulkDownloadOrg.json
@@ -7,6 +7,7 @@
"allOf": [
{ "$ref": "/BaseOrg" },
{
+ "type": "object",
"properties": {
"authority": {
"const": ["BULK_DOWNLOAD"]
diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json
index 5dcb3f3db..55e0211cf 100644
--- a/src/middleware/schemas/CNAOrg.json
+++ b/src/middleware/schemas/CNAOrg.json
@@ -7,6 +7,7 @@
"allOf": [
{ "$ref": "/BaseOrg" },
{
+ "type": "object",
"properties": {
"authority": {
"const": ["CNA"]
@@ -29,7 +30,7 @@
"maximum": 100000
},
"charter_or_scope": {
- "$ref": "/BaseOrg#/definitions/uriType"
+ "type": "string"
},
"disclosure_policy": {
"$ref": "/BaseOrg#/definitions/uriType"
diff --git a/src/middleware/schemas/RegistryUser.json b/src/middleware/schemas/RegistryUser.json
deleted file mode 100644
index de95595ab..000000000
--- a/src/middleware/schemas/RegistryUser.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "$id": "RegistryUser",
- "title": "CVE Registry User Schema",
- "description": "Schema for a CVE Registry User",
- "allOf": [
- { "$ref": "/BaseUser" }
- ]
-}
diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json
index 125ba92b1..f4e4e1637 100644
--- a/src/middleware/schemas/SecretariatOrg.json
+++ b/src/middleware/schemas/SecretariatOrg.json
@@ -7,6 +7,7 @@
"allOf": [
{ "$ref": "/BaseOrg" },
{
+ "type": "object",
"properties": {
"authority": {
"const": ["SECRETARIAT"]
diff --git a/src/model/adporg.js b/src/model/adporg.js
index f5efa867c..0c5a80799 100644
--- a/src/model/adporg.js
+++ b/src/model/adporg.js
@@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg')
const fs = require('fs')
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
-const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json'))
-const AdpOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/ADPOrg.json'))
+const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json'))
+const AdpOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/ADPOrg.json'))
const ajv = new Ajv({ allErrors: true })
addFormats(ajv)
ajv.addSchema(BaseOrgSchema)
diff --git a/src/model/audit.js b/src/model/audit.js
index 9d346566c..d749b691d 100644
--- a/src/model/audit.js
+++ b/src/model/audit.js
@@ -4,7 +4,7 @@ const aggregatePaginate = require('mongoose-aggregate-paginate-v2')
const MongoPaging = require('mongo-cursor-pagination')
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
-const AuditSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/Audit.json'))
+const AuditSchemaJSON = JSON.parse(fs.readFileSync('schemas/Audit.json'))
// Initialize AJV
const ajv = new Ajv({ allErrors: true })
diff --git a/src/model/baseorg.js b/src/model/baseorg.js
index daff859fc..48a5205aa 100644
--- a/src/model/baseorg.js
+++ b/src/model/baseorg.js
@@ -11,22 +11,33 @@ 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],
- poc: String,
- poc_email: String,
- poc_phone: String,
- org_email: String,
- website: String
+ websites: [String],
+ emails: [String],
+ phone: 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/baseuser.js b/src/model/baseuser.js
index 260179970..fcd4b1777 100644
--- a/src/model/baseuser.js
+++ b/src/model/baseuser.js
@@ -6,7 +6,7 @@ const Ajv = require('ajv')
const addFormats = require('ajv-formats')
// Load BaseUser JSON schema
-const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseUser.json'))
+const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('schemas/registry-user/BaseUser.json'))
// Initialize AJV
const ajv = new Ajv({ allErrors: true })
diff --git a/src/model/bulkdownloadorg.js b/src/model/bulkdownloadorg.js
index e196b5ff3..cdf06cf9d 100644
--- a/src/model/bulkdownloadorg.js
+++ b/src/model/bulkdownloadorg.js
@@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg')
const fs = require('fs')
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
-const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json'))
-const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BulkDownloadOrg.json'))
+const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json'))
+const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BulkDownloadOrg.json'))
const ajv = new Ajv({ allErrors: true })
addFormats(ajv)
ajv.addSchema(BaseOrgSchema)
diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js
index ab17599c9..45f2e0233 100644
--- a/src/model/cnaorg.js
+++ b/src/model/cnaorg.js
@@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg')
const fs = require('fs')
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
-const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json'))
-const CnaOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/CNAOrg.json'))
+const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json'))
+const CnaOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/CNAOrg.json'))
const ajv = new Ajv({ allErrors: true })
addFormats(ajv)
ajv.addSchema(BaseOrgSchema)
diff --git a/src/model/cve.js b/src/model/cve.js
index 59e23aeee..81f1eee87 100644
--- a/src/model/cve.js
+++ b/src/model/cve.js
@@ -2,7 +2,7 @@ const mongoose = require('mongoose')
const aggregatePaginate = require('mongoose-aggregate-paginate-v2')
const MongoPaging = require('mongo-cursor-pagination')
const fs = require('fs')
-const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json'))
+const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json'))
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
diff --git a/src/model/glossary.js b/src/model/glossary.js
new file mode 100644
index 000000000..d06f09a61
--- /dev/null
+++ b/src/model/glossary.js
@@ -0,0 +1,14 @@
+const mongoose = require('mongoose')
+
+const schema = {
+ services_short_name: { type: String, required: true },
+ label: { type: String, required: true },
+ def: { type: String, required: true }
+}
+
+const GlossarySchema = new mongoose.Schema(schema, { collection: 'Glossary', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
+
+GlossarySchema.index({ services_short_name: 1 }, { unique: true })
+
+const Glossary = mongoose.model('Glossary', GlossarySchema)
+module.exports = Glossary
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/model/secretariatorg.js b/src/model/secretariatorg.js
index 127d236a6..073f1c9d7 100644
--- a/src/model/secretariatorg.js
+++ b/src/model/secretariatorg.js
@@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg')
const fs = require('fs')
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
-const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json'))
-const SecretariatOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/SecretariatOrg.json'))
+const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json'))
+const SecretariatOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/SecretariatOrg.json'))
const ajv = new Ajv({ allErrors: true })
addFormats(ajv)
ajv.addSchema(BaseOrgSchema)
diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js
index a95065e7a..1bd9500aa 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)
+ if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options, projection)
+ 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)
}
/**
@@ -164,12 +198,68 @@ class BaseOrgRepository extends BaseRepository {
* @returns {Promise} True if the organization exists, false otherwise.
*/
async orgExists (shortName, options = {}, returnLegacyFormat = false) {
- if (await this.findOneByShortName(shortName, options, returnLegacyFormat)) {
+ if (!shortName) return false
+ const query = { $expr: { $eq: [{ $toLower: '$short_name' }, shortName.toLowerCase()] } }
+ const exists = await BaseOrgModel.findOne(query, null, options)
+ if (exists) {
return true
}
return false
}
+ /**
+ * @async
+ * @function checkAliasCollisions
+ * @description Checks if the provided short_name, name, or aliases collide with any other organization's short_name, long_name, or aliases.
+ * @param {string} shortName - The proposed short name of the organization.
+ * @param {string} name - The proposed name of the organization.
+ * @param {string[]} aliases - The proposed aliases of the organization.
+ * @param {string} [shortNameToExclude=null] - The short name of the organization to exclude from the check (e.g., during an update).
+ * @param {object} [options={}] - Optional settings for the repository query.
+ * @returns {Promise} The conflicting string if a collision is found, otherwise null.
+ */
+ async checkAliasCollisions (shortName, name, aliases, shortNameToExclude = null, options = {}) {
+ const searchStrings = [shortName, name, ...(aliases || [])].filter(str => str != null && str !== '')
+ if (searchStrings.length === 0) {
+ return null
+ }
+
+ const searchStringsLower = searchStrings.map(s => s.toLowerCase())
+
+ const query = {
+ $or: [
+ {
+ $expr: {
+ $or: searchStringsLower.map(s => ({
+ $eq: [{ $toLower: '$short_name' }, s]
+ }))
+ }
+ },
+ { long_name: { $in: searchStrings } },
+ { aliases: { $in: searchStrings } }
+ ]
+ }
+
+ if (shortNameToExclude) {
+ query.$and = [
+ { $expr: { $ne: [{ $toLower: '$short_name' }, shortNameToExclude.toLowerCase()] } }
+ ]
+ }
+
+ const collisionOrg = await BaseOrgModel.findOne(query, 'short_name long_name aliases', options)
+ if (collisionOrg) {
+ // Determine which string collided for better error reporting
+ for (const str of searchStrings) {
+ if (collisionOrg.short_name?.toLowerCase() === str.toLowerCase() || collisionOrg.long_name === str || (collisionOrg.aliases && collisionOrg.aliases.includes(str))) {
+ return str
+ }
+ }
+ return searchStrings[0] // Fallback
+ }
+
+ return null
+ }
+
/**
* @async
* @function addUserToOrg
@@ -181,7 +271,7 @@ class BaseOrgRepository extends BaseRepository {
* @param {boolean} [isLegacyObject=false] - Unused parameter.
* @returns {Promise}
*/
- async addUserToOrg (orgShortName, userUUID, isAdmin = false, options = {}, isLegacyObject = false) {
+ async addUserToOrg (orgShortName, userUUID, isAdmin = false, options = {}, isLegacyObject = false, requestingUserUUID = null) {
const update = {
$addToSet: { users: userUUID }
}
@@ -190,7 +280,11 @@ class BaseOrgRepository extends BaseRepository {
update.$addToSet.admins = userUUID
}
- await BaseOrgModel.updateOne({ short_name: orgShortName }, update, options)
+ const originalOrg = await BaseOrgModel.findOneAndUpdate({ short_name: orgShortName }, update, options)
+ if (requestingUserUUID && originalOrg) {
+ const updatedOrg = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options)
+ await createAuditLogEntry(updatedOrg, originalOrg.toObject(), requestingUserUUID, options)
+ }
}
/**
@@ -202,18 +296,24 @@ class BaseOrgRepository extends BaseRepository {
* @param {object} [options={}] - Optional settings for the repository query.
* @returns {Promise}
*/
- async addAdmin (orgShortName, userUUID, options = {}) {
+ async addAdmin (orgShortName, userUUID, options = {}, requestingUserUUID = null) {
const UserRepository = require('./userRepository')
const legacyUserRepo = new UserRepository()
- const executeOptions = { ...options, new: true }
+ const executeOptions = { ...options, new: false }
- const updatedOrg = await BaseOrgModel.findOneAndUpdate(
+ const originalOrg = await BaseOrgModel.findOneAndUpdate(
{ short_name: orgShortName },
{ $addToSet: { admins: userUUID } },
executeOptions
)
+ const updatedOrg = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options)
+
+ if (requestingUserUUID && originalOrg) {
+ await createAuditLogEntry(updatedOrg, originalOrg.toObject(), requestingUserUUID, options)
+ }
+
await legacyUserRepo.collection.findOneAndUpdate(
{ UUID: userUUID },
{ $addToSet: { 'authority.active_roles': 'ADMIN' } },
@@ -232,18 +332,24 @@ class BaseOrgRepository extends BaseRepository {
* @param {object} [options={}] - Optional settings for the repository query.
* @returns {Promise}
*/
- async removeAdmin (orgShortName, userUUID, options = {}) {
+ async removeAdmin (orgShortName, userUUID, options = {}, requestingUserUUID = null) {
const UserRepository = require('./userRepository')
const legacyUserRepo = new UserRepository()
- const executeOptions = { ...options, new: true }
+ const executeOptions = { ...options, new: false }
- const updatedOrg = await BaseOrgModel.findOneAndUpdate(
+ const originalOrg = await BaseOrgModel.findOneAndUpdate(
{ short_name: orgShortName },
{ $pull: { admins: userUUID } },
executeOptions
)
+ const updatedOrg = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options)
+
+ if (requestingUserUUID && originalOrg) {
+ await createAuditLogEntry(updatedOrg, originalOrg.toObject(), requestingUserUUID, options)
+ }
+
await legacyUserRepo.collection.findOneAndUpdate(
{ UUID: userUUID },
{ $pull: { 'authority.active_roles': 'ADMIN' } },
@@ -261,7 +367,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 +375,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 +428,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 +442,25 @@ 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
+ const relatedOrgUUIDs = new Set()
+ if (Array.isArray(result.oversees)) {
+ result.oversees.forEach(uuid => relatedOrgUUIDs.add(uuid))
+ }
+ if (result.reports_to) {
+ relatedOrgUUIDs.add(result.reports_to)
+ }
+
+ if (relatedOrgUUIDs.size > 0) {
+ const relatedOrgs = await BaseOrgModel.find({ UUID: { $in: Array.from(relatedOrgUUIDs) } }).select('short_name long_name UUID').lean()
+ if (relatedOrgs.length > 0) {
+ result._relatedOrganizations = relatedOrgs.map(org => ({
+ short_name: org.short_name,
+ long_name: org.long_name,
+ UUID: org.UUID
+ }))
+ }
+ }
+
return deepRemoveEmpty(result)
}
@@ -429,6 +550,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 +618,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 +682,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,21 +702,20 @@ 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)
* @param {string[]} [incomingParameters.oversees] - A list of short names of organizations this org oversees. (Registry only)
* @param {string} [incomingParameters.reports_to] - The short name of the organization this org reports to. (Registry only)
- * @param {string} [incomingParameters.contact_info.poc] - The primary point of contact's name. (Registry only)
- * @param {string} [incomingParameters.contact_info.poc_email] - The primary point of contact's email. (Registry only)
- * @param {string} [incomingParameters.contact_info.poc_phone] - The primary point of contact's phone number. (Registry only)
- * @param {string} [incomingParameters.contact_info.org_email] - The general organization email address. (Registry only)
- * @param {string} [incomingParameters.contact_info.website] - The organization's website URL. (Registry only)
+ * @param {string[]} [incomingParameters.contact_info.websites] - The organization's website URLs. (Registry only)
+ * @param {string[]} [incomingParameters.contact_info.emails] - The organization's email addresses. (Registry only)
+ * @param {string} [incomingParameters.contact_info.phone] - The organization's phone number. (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 +770,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 +792,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 +872,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 +895,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 +926,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 +956,27 @@ class BaseOrgRepository extends BaseRepository {
legacyObjectRaw = this.convertRegistryToLegacy(incomingOrgBody)
}
- if (incomingOrg?.new_short_name) {
- const newName = incomingOrg.new_short_name
-
- // 1. Update the Mongoose instances
- registryOrg.short_name = newName
- legacyOrg.short_name = newName
+ handleShortNameUpdate(incomingOrg, registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw)
+ automateProgramDataDates(registryObjectRaw, registryOrg)
- // 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()
+ await createAuditLogEntry(registryOrg, originalRegistryOrgObject, requestingUserUUID, options)
- // 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
- }
-
- // 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 +987,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 +1045,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 +1057,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..869d619c7
--- /dev/null
+++ b/src/repositories/baseOrgRepositoryHelpers.js
@@ -0,0 +1,282 @@
+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()
+
+ 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'])
+
+ await auditRepo.seedAuditHistoryForOrg(
+ registryOrg.UUID,
+ cleanBefore,
+ requestingUserUUID,
+ { ...options, upsert: true }
+ )
+
+ if (!_.isEqual(cleanBefore, cleanAfter)) {
+ await auditRepo.appendToAuditHistoryForOrg(
+ registryOrg.UUID,
+ cleanAfter,
+ 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/baseUserRepository.js b/src/repositories/baseUserRepository.js
index be6164758..720fae515 100644
--- a/src/repositories/baseUserRepository.js
+++ b/src/repositories/baseUserRepository.js
@@ -187,7 +187,7 @@ class BaseUserRepository extends BaseRepository {
* @param {object} options - Mongoose options for the delete operations.
* @returns {Promise} Number of deleted documents (should be 1 if successful).
*/
- async deleteUserByUUID (uuid, options = {}) {
+ async deleteUserByUUID (uuid, options = {}, requestingUserUUID = null) {
// Delete from BaseUser collection
const deleteResult = await BaseUser.deleteOne({ UUID: uuid }, options)
@@ -195,13 +195,20 @@ class BaseUserRepository extends BaseRepository {
await RegistryUser.deleteOne({ UUID: uuid }, options)
// Remove user from any organization’s users and admins arrays
+ const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
const orgs = await BaseOrgModel.find({ $or: [{ users: uuid }, { admins: uuid }] })
for (const org of orgs) {
- org.users = org.users.filter(u => u !== uuid)
+ const originalOrg = org.toObject()
+ if (Array.isArray(org.users)) {
+ org.users.pull(uuid)
+ }
if (Array.isArray(org.admins)) {
- org.admins = org.admins.filter(a => a !== uuid)
+ org.admins.pull(uuid)
}
await org.save(options)
+ if (requestingUserUUID) {
+ await createAuditLogEntry(org, originalOrg, requestingUserUUID, options)
+ }
}
return deleteResult.deletedCount
@@ -332,7 +339,7 @@ class BaseUserRepository extends BaseRepository {
* @param {boolean} [isRegistryObject=true] - If false, accepts legacy user object.
* @returns {Promise} The created user object (registry or legacy format).
*/
- async createUser (orgShortName, incomingUser, options = {}, isRegistryObject = true) {
+ async createUser (orgShortName, incomingUser, options = {}, isRegistryObject = true, requestingUserUUID = null) {
const { deepRemoveEmpty } = require('../utils/utils')
// TO-DO: org_UUID is not necessarily the shortname. Is this info lost during conversion?
let legacyObjectRaw = null
@@ -373,7 +380,7 @@ class BaseUserRepository extends BaseRepository {
const registryUserToSave = new RegistryUser(registryObjectRaw)
registryObject = await registryUserToSave.save(options)
- baseOrgRepository.addUserToOrg(orgShortName, incomingUser.UUID, (incomingUser.role === 'ADMIN' || incomingUser.authority?.active_roles?.includes('ADMIN')))
+ await baseOrgRepository.addUserToOrg(orgShortName, incomingUser.UUID, (incomingUser.role === 'ADMIN' || incomingUser.authority?.active_roles?.includes('ADMIN')), options, false, requestingUserUUID)
// We now have to make sure the user is added to the ORG's user array
await legacyUserRepo.updateByUserNameAndOrgUUID(incomingUser.username, existingOrg.UUID, legacyObjectRaw, { ...options, upsert: true })
@@ -405,11 +412,13 @@ class BaseUserRepository extends BaseRepository {
* @param {boolean} [isRegistryObject=true] - If false, returns a legacy user object.
* @returns {Promise} The updated user object.
*/
- async updateUser (username, orgShortname, incomingParameters, options = {}, isRegistryObject = true) {
+ async updateUser (username, orgShortname, incomingParameters, options = {}, isRegistryObject = true, requestingUserUUID = null) {
const { deepRemoveEmpty } = require('../utils/utils')
const baseOrgRepository = new BaseOrgRepository()
const legacyUserRepo = new UserRepository()
+ const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
const registryOrg = await baseOrgRepository.getOrgObject(orgShortname, false, options)
+ const originalRegistryOrg = registryOrg.toObject()
const legacyUser = await legacyUserRepo.findOneByUserNameAndOrgUUID(username, registryOrg.UUID, null, options)
const registryUser = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, true) // WE always want the registry user
@@ -430,13 +439,17 @@ class BaseUserRepository extends BaseRepository {
const rolesToAdd = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.add')))
const rolesToRemove = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.remove')))
if (rolesToRemove.includes('ADMIN')) {
- const filteredUuids = registryOrg.admins.filter(uuid => uuid !== registryUser.UUID)
- registryOrg.admins = filteredUuids
+ if (Array.isArray(registryOrg.admins)) {
+ registryOrg.admins.pull(registryUser.UUID)
+ }
}
if (rolesToAdd.includes('ADMIN') && !incomingParameters?.org_short_name) {
// Use the already fetched registryOrg instead of querying again
- registryOrg.admins = [...new Set([...(registryOrg.admins || []), registryUser.UUID])]
+ if (!Array.isArray(registryOrg.admins)) {
+ registryOrg.admins = []
+ }
+ registryOrg.admins.addToSet(registryUser.UUID)
}
const initialRoles = legacyUser.authority?.active_roles ?? []
@@ -446,23 +459,40 @@ class BaseUserRepository extends BaseRepository {
if (incomingParameters?.org_short_name) {
// Remove us from the old users Array
- const filteredUuids = registryOrg.users.filter(uuid => uuid !== registryUser.UUID)
- registryOrg.users = filteredUuids
+ if (Array.isArray(registryOrg.users)) {
+ registryOrg.users.pull(registryUser.UUID)
+ }
+ if (registryOrg.admins && registryOrg.admins.includes(registryUser.UUID)) {
+ registryOrg.admins.pull(registryUser.UUID)
+ }
// Add us to the new org (this is a genuine cross-org migration, so we must fetch the new org)
const newOrg = await baseOrgRepository.getOrgObject(incomingParameters.org_short_name)
- newOrg.users = [...new Set([...newOrg.users, registryUser.UUID])]
+ const originalNewOrg = newOrg.toObject()
+ if (!Array.isArray(newOrg.users)) {
+ newOrg.users = []
+ }
+ newOrg.users.addToSet(registryUser.UUID)
if (registryUser.role.includes('ADMIN')) {
- newOrg.admins = [...new Set([...(newOrg.admins || []), registryUser.UUID])]
+ if (!Array.isArray(newOrg.admins)) {
+ newOrg.admins = []
+ }
+ newOrg.admins.addToSet(registryUser.UUID)
}
legacyUser.org_UUID = newOrg.UUID
await newOrg.save(options)
+ if (requestingUserUUID) {
+ await createAuditLogEntry(newOrg, originalNewOrg, requestingUserUUID, options)
+ }
}
delete registryUser.role
// Single unified save for the primary org at the end
await registryOrg.save(options)
+ if (requestingUserUUID) {
+ await createAuditLogEntry(registryOrg, originalRegistryOrg, requestingUserUUID, options)
+ }
await legacyUser.save(options)
await registryUser.save(options)
@@ -496,7 +526,7 @@ class BaseUserRepository extends BaseRepository {
* @param {boolean} [isRegistryObject=true] - If false, accepts/returns legacy format.
* @returns {Promise} The updated user object.
*/
- async updateUserFull (identifier, incomingUser, options = {}, isRegistryObject = true) {
+ async updateUserFull (identifier, incomingUser, options = {}, isRegistryObject = true, requestingUserUUID = null) {
const legacyUserRepo = new UserRepository()
// Find registry user by UUID
@@ -539,30 +569,41 @@ class BaseUserRepository extends BaseRepository {
try {
if (incomingUser.org_short_name) {
const baseOrgRepository = new BaseOrgRepository()
+ const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
const currentOrgUUID = legacyUser.org_UUID
const currentOrg = await baseOrgRepository.findOneByUUID(currentOrgUUID)
+ const originalCurrentOrg = currentOrg.toObject()
const newOrg = await baseOrgRepository.findOneByShortName(incomingUser.org_short_name)
+ const originalNewOrg = newOrg.toObject()
if (!newOrg) {
throw new Error(`Organization ${incomingUser.org_short_name} not found`)
}
// 1. Remove user from old org's users list
- currentOrg.users = currentOrg.users.filter(u => u !== identifier)
+ if (Array.isArray(currentOrg.users)) {
+ currentOrg.users.pull(identifier)
+ }
// 2. Remove user from old org's admins list (if present)
if (currentOrg.admins && currentOrg.admins.includes(identifier)) {
- currentOrg.admins = currentOrg.admins.filter(a => a !== identifier)
+ currentOrg.admins.pull(identifier)
}
// 3. Add user to new org's users list
- newOrg.users = [...new Set([...newOrg.users, identifier])]
+ if (!Array.isArray(newOrg.users)) {
+ newOrg.users = []
+ }
+ newOrg.users.addToSet(identifier)
// 4. Add user to new org's admins list (if they are an admin)
const isAdmin = updatedRegistryUser.role === 'ADMIN' || (updatedLegacyUser.authority && updatedLegacyUser.authority.active_roles && updatedLegacyUser.authority.active_roles.includes('ADMIN'))
if (isAdmin) {
- newOrg.admins = [...new Set([...(newOrg.admins || []), identifier])]
+ if (!Array.isArray(newOrg.admins)) {
+ newOrg.admins = []
+ }
+ newOrg.admins.addToSet(identifier)
}
// 5. Update user's org_UUID
@@ -571,6 +612,11 @@ class BaseUserRepository extends BaseRepository {
// Save org changes
await currentOrg.save(options)
await newOrg.save(options)
+
+ if (requestingUserUUID) {
+ await createAuditLogEntry(currentOrg, originalCurrentOrg, requestingUserUUID, options)
+ await createAuditLogEntry(newOrg, originalNewOrg, requestingUserUUID, options)
+ }
}
await updatedLegacyUser.save(options)
diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js
index 02a61d807..047de1c41 100644
--- a/src/repositories/conversationRepository.js
+++ b/src/repositories/conversationRepository.js
@@ -44,8 +44,9 @@ class ConversationRepository extends BaseRepository {
}
})
return conversations.map(convo => convo.toObject()).filter(conv => isSecretariat || conv.visibility === 'public').map(conv => {
- if (!isSecretariat) {
+ if (!isSecretariat && conv.author_role === 'Secretariat') {
delete conv.author_id
+ delete conv.author_name
}
return conv
})
@@ -62,6 +63,13 @@ class ConversationRepository extends BaseRepository {
return conversation[0]
}
+ validateConversation (conversation) {
+ if ((conversation.body && typeof conversation.body !== 'string') || (conversation.visibility && !['public', 'private'].includes(conversation.visibility))) {
+ return false
+ }
+ return true
+ }
+
async createConversation (targetUUID, body, user, isSecretariat, options = {}) {
const { getUserFullName } = require('../utils/utils')
const newUUID = uuid.v4()
@@ -85,7 +93,15 @@ class ConversationRepository extends BaseRepository {
}
const newConversation = new ConversationModel(conversationObj)
const result = await newConversation.save(options)
- return result.toObject()
+
+ const rawObject = result.toObject()
+
+ delete rawObject._id
+ delete rawObject.__v
+ delete rawObject.previous_conversation_uuid
+ delete rawObject.next_conversation_uuid
+
+ return rawObject
}
async editConversation (UUID, incomingParameters, options = {}) {
diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js
new file mode 100644
index 000000000..3ff74abe4
--- /dev/null
+++ b/src/repositories/glossaryRepository.js
@@ -0,0 +1,26 @@
+const BaseRepository = require('./baseRepository')
+const Glossary = require('../model/glossary')
+
+class GlossaryRepository extends BaseRepository {
+ constructor () {
+ super(Glossary)
+ }
+
+ async getAll () {
+ return this.collection.find({}, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec()
+ }
+
+ async findOneByServicesShortName (servicesShortName) {
+ return this.collection.findOne({ services_short_name: servicesShortName }, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec()
+ }
+
+ async updateByServicesShortName (servicesShortName, newGlossaryData) {
+ return this.collection.findOneAndUpdate({ services_short_name: servicesShortName }, newGlossaryData, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }, new: true }).exec()
+ }
+
+ async deleteByServicesShortName (servicesShortName) {
+ return this.collection.findOneAndDelete({ services_short_name: servicesShortName }, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 } }).exec()
+ }
+}
+
+module.exports = GlossaryRepository
diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js
index 099cc2fa8..d514434dd 100644
--- a/src/repositories/orgRepository.js
+++ b/src/repositories/orgRepository.js
@@ -7,9 +7,9 @@ class OrgRepository extends BaseRepository {
super(Org)
}
- async findOneByShortName (shortName, options = {}) {
+ async findOneByShortName (shortName, options = {}, projection = {}) {
const query = { short_name: shortName }
- return this.collection.findOne(query, null, options)
+ return this.collection.findOne(query, projection, options)
}
async findOneByUUID (UUID) {
diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js
index 4750fffea..7f97e1177 100644
--- a/src/repositories/repositoryFactory.js
+++ b/src/repositories/repositoryFactory.js
@@ -7,6 +7,7 @@ const BaseOrgRepository = require('./baseOrgRepository')
const BaseUserRepository = require('./baseUserRepository')
const ConversationRepository = require('./conversationRepository')
const ReviewObjectRepository = require('./reviewObjectRepository')
+const GlossaryRepository = require('./glossaryRepository')
class RepositoryFactory {
getOrgRepository () {
@@ -54,6 +55,11 @@ class RepositoryFactory {
return repo
}
+ getGlossaryRepository () {
+ const repo = new GlossaryRepository()
+ return repo
+ }
+
getAuditRepository () {
const AuditRepository = require('./auditRepository')
const repo = new AuditRepository()
diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js
index 0062da212..401aa8c42 100644
--- a/src/repositories/reviewObjectRepository.js
+++ b/src/repositories/reviewObjectRepository.js
@@ -2,6 +2,18 @@ const ReviewObjectModel = require('../model/reviewobject')
const BaseRepository = require('./baseRepository')
const BaseOrgRepository = require('./baseOrgRepository')
const uuid = require('uuid')
+const _ = require('lodash')
+const getConstants = require('../constants').getConstants
+
+function filterReviewOrgData (orgData, isSecretariat = false) {
+ if (!orgData) return orgData
+ const CONSTANTS = getConstants()
+ let fieldsToOmit = [...CONSTANTS.ORG_EXCLUDED_FIELDS]
+ if (!isSecretariat) {
+ fieldsToOmit = [...fieldsToOmit, ...CONSTANTS.ORG_RESTRICTED_FIELDS]
+ }
+ return _.omit(orgData, fieldsToOmit)
+}
class ReviewObjectRepository extends BaseRepository {
constructor () {
@@ -14,13 +26,13 @@ class ReviewObjectRepository extends BaseRepository {
if (!org) {
return null
}
- const reviewObject = await ReviewObjectModel.find({ target_object_uuid: org.UUID }, null, options)
+ const reviewObject = await ReviewObjectModel.find({ target_object_uuid: org.UUID }, { _id: 0, __v: 0 }, options)
return reviewObject || null
}
async findOneByUUID (UUID, options = {}) {
- const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, { _id: 0, __v: 0 }, options)
return reviewObject || null
}
@@ -29,18 +41,19 @@ class ReviewObjectRepository extends BaseRepository {
const conversationRepository = new ConversationRepository()
let reviewObject
const query = pending ? { uuid: UUID, status: 'pending' } : { uuid: UUID }
- const reviewObjectRaw = await ReviewObjectModel.findOne(query, null, options)
+ const reviewObjectRaw = await ReviewObjectModel.findOne(query, { _id: 0, __v: 0 }, options)
if (reviewObjectRaw) {
reviewObject = reviewObjectRaw.toObject()
const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.target_object_uuid, isSecretariat, options)
reviewObject.conversation = conversations?.length ? conversations : undefined
+ reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat)
}
return reviewObject || null
}
async getAllReviewObjects (options = {}) {
- const reviewObjects = await ReviewObjectModel.find({}, null, {
+ const reviewObjects = await ReviewObjectModel.find({}, { _id: 0, __v: 0 }, {
...options,
sort: { created: -1 }
})
@@ -57,7 +70,8 @@ class ReviewObjectRepository extends BaseRepository {
const agt = [
{ $match: query },
- { $sort: { created: -1 } }
+ { $sort: { created: -1 } },
+ { $project: { _id: 0, __v: 0 } }
]
const pg = await this.aggregatePaginate(agt, options)
@@ -93,7 +107,7 @@ class ReviewObjectRepository extends BaseRepository {
target_object_uuid: org.UUID,
status: 'pending'
},
- null,
+ { _id: 0, __v: 0 },
{
...options,
sort: { created: -1 }
@@ -103,6 +117,7 @@ class ReviewObjectRepository extends BaseRepository {
reviewObject = reviewObjectRaw.toObject()
const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options)
reviewObject.conversation = conversations?.length ? conversations : undefined
+ reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat)
}
return reviewObject || null
@@ -122,7 +137,7 @@ class ReviewObjectRepository extends BaseRepository {
target_object_uuid: org.UUID,
status: 'pending'
},
- null,
+ { _id: 0, __v: 0 },
{
...options,
sort: { created: -1 }
@@ -132,6 +147,7 @@ class ReviewObjectRepository extends BaseRepository {
reviewObject = reviewObjectRaw.toObject()
const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options)
reviewObject.conversation = conversations?.length ? conversations : undefined
+ reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat)
}
return reviewObject || null
@@ -151,12 +167,12 @@ class ReviewObjectRepository extends BaseRepository {
const reviewObject = new ReviewObjectModel(reviewObjectRaw)
await reviewObject.save(options)
- return reviewObject.toObject()
+ return _.omit(reviewObject.toObject(), ['_id', '__v'])
}
async updateReviewOrgObject (body, UUID, options = {}) {
console.log('Updating review object with UUID:', UUID)
- const reviewObject = await this.findOneByUUID(UUID, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
if (!reviewObject) {
return null
}
@@ -164,12 +180,12 @@ class ReviewObjectRepository extends BaseRepository {
reviewObject.new_review_data = body
const result = await reviewObject.save(options)
- return result.toObject()
+ return _.omit(result.toObject(), ['_id', '__v'])
}
async approveReviewOrgObject (UUID, approverUsername, options = {}) {
console.log('Approving review object with UUID:', UUID)
- const reviewObject = await this.findOneByUUID(UUID, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
if (!reviewObject) {
return null
}
@@ -182,7 +198,7 @@ class ReviewObjectRepository extends BaseRepository {
}
await reviewObject.save(options)
- return reviewObject.toObject()
+ return _.omit(reviewObject.toObject(), ['_id', '__v'])
}
/**
@@ -198,7 +214,8 @@ class ReviewObjectRepository extends BaseRepository {
const agt = [
{ $match: { target_object_uuid: org.UUID } },
- { $sort: { created: -1 } }
+ { $sort: { created: -1 } },
+ { $project: { _id: 0, __v: 0 } }
]
const pg = await this.aggregatePaginate(agt, options)
@@ -212,6 +229,12 @@ class ReviewObjectRepository extends BaseRepository {
data.nextPage = pg.nextPage
}
+ if (data.reviewObjects) {
+ for (const review of data.reviewObjects) {
+ review.new_review_data = filterReviewOrgData(review.new_review_data, isSecretariat)
+ }
+ }
+
// Optionally attach conversations
if (includeConversations && pg.itemsList && pg.itemsList.length) {
const ConversationRepository = require('./conversationRepository')
@@ -225,7 +248,10 @@ class ReviewObjectRepository extends BaseRepository {
// If non-secretariat, remove author_id
if (!isSecretariat) {
conversations = conversations.map(c => {
- delete c.author_id
+ if (c.author_role === 'Secretariat') {
+ delete c.author_id
+ delete c.author_name
+ }
return c
})
}
@@ -243,7 +269,7 @@ class ReviewObjectRepository extends BaseRepository {
async rejectReviewOrgObject (UUID, rejectorUsername, options = {}) {
console.log('Rejecting review object with UUID:', UUID)
- const reviewObject = await this.findOneByUUID(UUID, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
if (!reviewObject) {
return null
}
@@ -256,7 +282,7 @@ class ReviewObjectRepository extends BaseRepository {
}
await reviewObject.save(options)
- return reviewObject.toObject()
+ return _.omit(reviewObject.toObject(), ['_id', '__v'])
}
}
module.exports = ReviewObjectRepository
diff --git a/src/routes.config.js b/src/routes.config.js
index 1cf2fe159..9cf95cdc3 100644
--- a/src/routes.config.js
+++ b/src/routes.config.js
@@ -12,6 +12,7 @@ const RegistryOrgController = require('./controller/registry-org.controller')
const AuditController = require('./controller/audit.controller')
const ConversationController = require('./controller/conversation.controller')
const ReviewObjectController = require('./controller/review-object.controller')
+const GlossaryController = require('./controller/glossary.controller')
var options = {
swaggerOptions: {
@@ -40,6 +41,7 @@ module.exports = async function configureRoutes (app) {
app.use('/api/', RegistryOrgController)
app.use('/api/', ConversationController)
app.use('/api/', ReviewObjectController)
+ app.use('/api/', GlossaryController)
app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification))
app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions))
app.use('/schemas/', SchemasController)
diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js
index 7f4bd5879..e4a8ffdea 100644
--- a/src/scripts/migrate.js
+++ b/src/scripts/migrate.js
@@ -65,6 +65,7 @@ async function run () {
// Each helper handlers querying changes from srcDB and updating trgDB
await orgHelper(db)
await userHelper(db)
+ await glossaryHelper(db)
} catch (err) {
// Ensures that the client will close when you finish/error
await dbClient.close()
@@ -100,21 +101,18 @@ 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,
- admins: [],
- org_email: null,
- website: null
+ phone: null,
+ emails: [],
+ websites: []
},
inUse: null,
created: null,
@@ -173,7 +171,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
@@ -205,7 +203,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,
@@ -213,13 +211,11 @@ 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
- org_email: email,
- website: site
+ emails: email ? [email] : [],
+ websites: site ? [site] : [],
+ phone: null
},
inUse: doc.inUse,
created: doc.time.created,
@@ -265,3 +261,13 @@ async function userHelper (db) {
await trgUserCol.updateOne(trgQuery, updateDoc, options)
}
}
+
+async function glossaryHelper (db) {
+ console.log('Ensuring Glossary collection exists...')
+ // Create collection if it doesn't exist
+ await db.createCollection('Glossary').catch((err) => {
+ if (err.codeName !== 'NamespaceExists') {
+ console.warn('Could not create Glossary collection', err)
+ }
+ })
+}
diff --git a/src/scripts/populate.js b/src/scripts/populate.js
index 28fe3d057..b92659901 100644
--- a/src/scripts/populate.js
+++ b/src/scripts/populate.js
@@ -21,6 +21,7 @@ const BaseUser = require('../model/baseuser')
const ReviewObject = require('../model/reviewobject')
const Conversation = require('../model/conversation')
const Audit = require('../model/audit')
+const Glossary = require('../model/glossary')
const error = new errors.IDRError()
@@ -34,14 +35,16 @@ const populateTheseCollections = {
BaseUser: BaseUser,
ReviewObject: ReviewObject,
Conversation: Conversation,
- Audit: Audit
+ Audit: Audit,
+ Glossary: Glossary
}
const indexesToCreate = {
Cve: [{ 'cve.cveMetadata.cveId': 1 }, { 'cve.cveMetadata.dateUpdated': 1 }],
'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }],
User: [{ UUID: 1 }],
- Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }]
+ Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }],
+ Glossary: [{ services_short_name: 1 }]
}
// Body Parser Middleware
@@ -122,6 +125,12 @@ db.once('open', async () => {
CveId, dataUtils.newCveIdTransform
))
+ // Glossary
+ populatePromises.push(dataUtils.populateCollection(
+ './datadump/pre-population/glossary.json',
+ Glossary
+ ))
+
// don't close database connection until all remaining populate
// promises are resolved
Promise.all(populatePromises).then(async function () {
@@ -143,6 +152,7 @@ db.once('open', async () => {
await Audit.createCollection()
await ReviewObject.createCollection()
await Conversation.createCollection()
+ await Glossary.createCollection()
} catch (err) {
logger.error('Error creating indexes:', err)
} finally {
diff --git a/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..d9777c1d8 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 {
@@ -249,4 +249,76 @@ describe('Create and Update Audit Collection with Org Endpoints', () => {
// Should have 2 entries: initial creation of current org object + new update
expect(auditResCreation.body.history).to.have.lengthOf(2)
})
+
+ it('Should add audit entry when users or admins are modified via user endpoints', async () => {
+ const testOrg = await createTestOrg({
+ hard_quota: 1500,
+ authority: ['CNA']
+ })
+
+ // 1. Create User
+ const username = `user_${uuidv4().slice(0, 8)}`
+ const createUserRes = await chai.request(app)
+ .post(`/api/registry/org/${testOrg.shortName}/user`)
+ .set(secretariatHeaders)
+ .send({
+ username: username,
+ name: {
+ first: 'Test',
+ last: 'User'
+ }
+ })
+
+ expect(createUserRes).to.have.status(200)
+
+ // Check audit history
+ const auditRes1 = await chai.request(app)
+ .get(`/api/audit/org/${testOrg.uuid}`)
+ .set(constants.headers)
+
+ expect(auditRes1.body.history).to.have.lengthOf(2) // 1. Org creation, 2. User creation
+
+ const historyAfterCreate = auditRes1.body.history[1].audit_object
+ expect(historyAfterCreate.users).to.be.an('array').that.includes(createUserRes.body.created.UUID)
+ expect(historyAfterCreate.admins).to.be.an('array').that.does.not.include(createUserRes.body.created.UUID)
+
+ // 2. Grant Admin Role
+ const grantRoleRes = await chai.request(app)
+ .post(`/api/registry/org/${testOrg.shortName}/user/${username}/grant-role`)
+ .set(secretariatHeaders)
+ .send({
+ role: 'ADMIN'
+ })
+
+ expect(grantRoleRes).to.have.status(200)
+
+ // Check audit history again
+ const auditRes2 = await chai.request(app)
+ .get(`/api/audit/org/${testOrg.uuid}`)
+ .set(constants.headers)
+
+ expect(auditRes2.body.history).to.have.lengthOf(3) // 3. Admin role granted
+
+ const historyAfterGrant = auditRes2.body.history[2].audit_object
+ expect(historyAfterGrant.admins).to.be.an('array').that.includes(createUserRes.body.created.UUID)
+
+ // 3. Revoke Admin Role
+ const revokeRoleRes = await chai.request(app)
+ .post(`/api/registry/org/${testOrg.shortName}/user/${username}/revoke-role`)
+ .set(secretariatHeaders)
+ .send({
+ role: 'ADMIN'
+ })
+ expect(revokeRoleRes).to.have.status(200)
+
+ // Check audit history again
+ const auditRes3 = await chai.request(app)
+ .get(`/api/audit/org/${testOrg.uuid}`)
+ .set(constants.headers)
+
+ expect(auditRes3.body.history).to.have.lengthOf(4) // 4. Admin role revoked
+
+ const historyAfterRevoke = auditRes3.body.history[3].audit_object
+ expect(historyAfterRevoke.admins).to.be.an('array').that.does.not.include(createUserRes.body.created.UUID)
+ })
})
diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js
index de4946825..1d3c343d0 100644
--- a/test/integration-tests/constants.js
+++ b/test/integration-tests/constants.js
@@ -363,16 +363,33 @@ const testOrg = {
}
}
+const testOrg2 = {
+
+ short_name: 'test_org2',
+ name: 'Test Organization 2',
+ authority: {
+ active_roles: [
+ 'CNA'
+ ]
+ },
+ policies: {
+ id_quota: 100000
+ }
+}
+
const testRegistryOrg = {
short_name: 'test_registry_org',
long_name: 'Test Registry Organization',
contact_info: {
- poc: 'Dave',
- poc_email: 'dave@test.org',
- poc_phone: '555-1234',
- org_email: 'contact@test.org',
- website: 'https://test.org'
+ websites: ['https://test.org'],
+ emails: ['dave@test.org'],
+ phone: '555-1234'
},
+ private_contacts: [{
+ poc: 'Dave Private',
+ poc_email: 'daveprivate@test.org',
+ phone: '555-4321'
+ }],
authority: ['CNA'],
hard_quota: 100000
}
@@ -381,11 +398,9 @@ const testRegistryOrg2 = {
short_name: 'test_registry_org2',
long_name: 'Test Registry Organization2',
contact_info: {
- poc: 'Dave',
- poc_email: 'dave@test.org',
- poc_phone: '555-1234',
- org_email: 'contact@test.org',
- website: 'https://test.org'
+ websites: ['https://test.org'],
+ emails: ['dave@test.org'],
+ phone: '555-1234'
},
authority: ['CNA'],
hard_quota: 100000
@@ -409,11 +424,9 @@ const existingRegistryOrg = {
short_name: 'win_5',
long_name: 'Test Registry Organization',
contact_info: {
- poc: 'Dave',
- poc_email: 'dave@test.org',
- poc_phone: '555-1234',
- org_email: 'contact@test.org',
- website: 'https://test.org'
+ websites: ['https://test.org'],
+ emails: ['dave@test.org'],
+ phone: '555-1234'
},
authority: ['CNA'],
hard_quota: 100000
@@ -432,6 +445,7 @@ module.exports = {
testAdp,
testAdp2,
testOrg,
+ testOrg2,
testRegistryOrg,
testRegistryOrg2,
existingOrg,
diff --git a/test/integration-tests/conversation/conversationTest.js b/test/integration-tests/conversation/conversationTest.js
index 802199ff2..8e14ea32e 100644
--- a/test/integration-tests/conversation/conversationTest.js
+++ b/test/integration-tests/conversation/conversationTest.js
@@ -54,11 +54,6 @@ describe('Testing Conversation endpoints', () => {
expect(res.body).to.haveOwnProperty('target_uuid')
expect(res.body.target_uuid).to.equal(orgUUID)
- expect(res.body).to.haveOwnProperty('previous_conversation_uuid')
- expect(res.body.previous_conversation_uuid).to.be.null
- expect(res.body).to.haveOwnProperty('next_conversation_uuid')
- expect(res.body.next_conversation_uuid).to.be.null
-
expect(res.body).to.haveOwnProperty('author_id')
expect(res.body.author_id).to.equal(secUserUUID)
@@ -110,11 +105,6 @@ describe('Testing Conversation endpoints', () => {
expect(rootMessage).to.exist
expect(rootMessage.previous_conversation_uuid).to.be.null
expect(rootMessage.next_conversation_uuid).to.be.equal(secondUUID)
-
- expect(res.body).to.haveOwnProperty('previous_conversation_uuid')
- expect(res.body.previous_conversation_uuid).to.be.equal(rootConvoUUID)
- expect(res.body).to.haveOwnProperty('next_conversation_uuid')
- expect(res.body.next_conversation_uuid).to.be.null
})
it('Should get all conversations', async () => {
await chai.request(app)
@@ -145,6 +135,28 @@ describe('Testing Conversation endpoints', () => {
})
})
})
+ it('Should update a conversation by UUID as Secretariat', async () => {
+ await chai.request(app)
+ .put(`/api/conversation/${rootConvoUUID}`)
+ .set(constants.headers)
+ .send({
+ body: 'test updated',
+ visibility: 'private'
+ })
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(200)
+
+ expect(res.body).to.haveOwnProperty('UUID')
+ expect(res.body.UUID).to.equal(rootConvoUUID)
+
+ expect(res.body).to.haveOwnProperty('body')
+ expect(res.body.body).to.equal('test updated')
+
+ expect(res.body).to.haveOwnProperty('visibility')
+ expect(res.body.visibility).to.equal('private')
+ })
+ })
})
context('Negative Tests', () => {
@@ -158,7 +170,53 @@ describe('Testing Conversation endpoints', () => {
expect(res).to.have.status(400)
expect(res.body).to.haveOwnProperty('message')
- expect(res.body.message).to.equal('Missing required field body')
+ expect(res.body.message).to.equal("Parameters were invalid: conversation object must include property 'body' (string) and optionally 'visibility' ('public' or 'private').")
+ })
+ })
+ it('Should fail to post a conversation with invalid body', async () => {
+ await chai.request(app)
+ .post(`/api/conversation/target/${orgUUID}`)
+ .set(constants.headers)
+ .send({
+ body: 123
+ })
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(400)
+
+ expect(res.body).to.haveOwnProperty('message')
+ expect(res.body.message).to.equal("Parameters were invalid: conversation object must include property 'body' (string) and optionally 'visibility' ('public' or 'private').")
+ })
+ })
+ it('Should fail to update a conversation that does not exist', async () => {
+ await chai.request(app)
+ .put('/api/conversation/non-existent-uuid')
+ .set(constants.headers)
+ .send({
+ body: 'test updated',
+ visibility: 'private'
+ })
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(404)
+
+ expect(res.body).to.haveOwnProperty('message')
+ expect(res.body.message).to.equal('The conversation with UUID non-existent-uuid does not exist.')
+ })
+ })
+ it('Should fail to update a conversation with invalid body', async () => {
+ await chai.request(app)
+ .put(`/api/conversation/${rootConvoUUID}`)
+ .set(constants.headers)
+ .send({
+ body: 123
+ })
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(400)
+
+ expect(res.body).to.haveOwnProperty('message')
+ expect(res.body.message).to.equal("Parameters were invalid: conversation object must include at least one of the following properties: 'body' (string) or 'visibility' ('public' or 'private').")
})
})
})
diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js
index 4dc371ddb..9697a2173 100644
--- a/test/integration-tests/conversation/editConversationTest.js
+++ b/test/integration-tests/conversation/editConversationTest.js
@@ -14,9 +14,8 @@ const orgAdminHeaders = {
'CVE-API-USER': 'activity_6_admin@activity_6.com'
}
-describe('Testing Conversation endpoints', () => {
+describe('Testing Conversation edit by index endpoint', () => {
let org
- // let rootConvoUUID
before(async () => {
await chai
@@ -27,6 +26,13 @@ describe('Testing Conversation endpoints', () => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
org = res.body
+ delete org.created
+ delete org.last_updated
+ delete org.admins
+ delete org.users
+ delete org.top_level_root
+ delete org.oversees
+ delete org.program_data
})
await chai
@@ -176,7 +182,7 @@ describe('Testing Conversation endpoints', () => {
expect(res).to.have.status(404)
expect(res.body).to.haveOwnProperty('message')
- expect(res.body.message).to.equal('The conversation at index 5 does not exist for the activity_6 organization.')
+ expect(res.body.message).to.equal('No conversation exists at index 5 for the activity_6 organization.')
})
})
it('Should fail if admin tries to update a conversation they do not own', async () => {
diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js
new file mode 100644
index 000000000..844f4b4c2
--- /dev/null
+++ b/test/integration-tests/glossary/glossaryCRUDTest.js
@@ -0,0 +1,189 @@
+/* eslint-disable no-unused-expressions */
+const chai = require('chai')
+const expect = chai.expect
+chai.use(require('chai-http'))
+
+const constants = require('../constants.js')
+const app = require('../../../src/index.js')
+
+const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' }
+
+const testGlossaryItem = {
+ services_short_name: 'test_glossary_item',
+ label: 'Test Glossary Item',
+ def: 'The definition of Test Glossary Item'
+}
+
+describe('Testing /glossary endpoints', () => {
+ context('Testing POST /glossary endpoint', () => {
+ context('Positive Tests', () => {
+ it('Creates a new glossary item', async () => {
+ await chai.request(app)
+ .post('/api/glossary')
+ .set(secretariatHeaders)
+ .send(testGlossaryItem)
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(200)
+
+ expect(res.body).to.haveOwnProperty('message')
+ expect(res.body.message).to.equal('glossary item successfully added')
+
+ expect(res.body).to.haveOwnProperty('created')
+ const item = res.body.created
+
+ expect(item).to.haveOwnProperty('services_short_name')
+ expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name)
+
+ expect(item).to.haveOwnProperty('label')
+ expect(item.label).to.equal(testGlossaryItem.label)
+
+ expect(item).to.haveOwnProperty('def')
+ expect(item.def).to.equal(testGlossaryItem.def)
+
+ expect(item).to.not.have.property('_id')
+ expect(item).to.not.have.property('__v')
+ expect(item).to.not.have.property('createdAt')
+ expect(item).to.not.have.property('updatedAt')
+ })
+ })
+ })
+ context('Negative Tests', () => {
+ it('Fails to create a new glossary item with an existing short name', async () => {
+ await chai.request(app)
+ .post('/api/glossary')
+ .set(secretariatHeaders)
+ .send(testGlossaryItem)
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ expect(res.body.details[0]).to.equal('Glossary item with this services_short_name already exists')
+ })
+ })
+ })
+ })
+ context('Testing GET /glossary endpoints', () => {
+ context('Positive Tests', () => {
+ it('Gets a list of all glossary items', async () => {
+ await chai.request(app)
+ .get('/api/glossary')
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.glossary).to.be.an('array').that.is.not.empty
+ res.body.glossary.forEach(item => {
+ expect(item).to.not.have.property('_id')
+ expect(item).to.not.have.property('__v')
+ expect(item).to.not.have.property('createdAt')
+ expect(item).to.not.have.property('updatedAt')
+ })
+ })
+ })
+ it('Gets a glossary item by short name', async () => {
+ await chai.request(app)
+ .get(`/api/glossary/${testGlossaryItem.services_short_name}`)
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('label', testGlossaryItem.label)
+ expect(res.body).to.have.property('services_short_name', testGlossaryItem.services_short_name)
+ expect(res.body).to.have.property('def', testGlossaryItem.def)
+
+ expect(res.body).to.not.have.property('_id')
+ expect(res.body).to.not.have.property('__v')
+ expect(res.body).to.not.have.property('createdAt')
+ expect(res.body).to.not.have.property('updatedAt')
+ })
+ })
+ })
+ context('Negative Tests', () => {
+ it('Fails to get a glossary item that does not exist', async () => {
+ await chai.request(app)
+ .get('/api/glossary/nonexistent_item')
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(404)
+ expect(res.body.message).to.equal('404: resource not found')
+ })
+ })
+ })
+ })
+ context('Testing PUT /glossary endpoint', () => {
+ context('Positive Tests', () => {
+ it('Updates a glossary item', async () => {
+ await chai.request(app)
+ .put(`/api/glossary/${testGlossaryItem.services_short_name}`)
+ .set(secretariatHeaders)
+ .send({
+ ...testGlossaryItem,
+ label: 'Updated Glossary Item',
+ def: 'Updated definition'
+ })
+ .then((res, err) => {
+ expect(err).to.be.undefined
+ expect(res).to.have.status(200)
+
+ expect(res.body).to.haveOwnProperty('message')
+ expect(res.body.message).to.equal('glossary item successfully updated')
+
+ expect(res.body).to.haveOwnProperty('updated')
+ const item = res.body.updated
+
+ expect(item).to.haveOwnProperty('services_short_name')
+ expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name)
+
+ expect(item).to.haveOwnProperty('label')
+ expect(item.label).to.equal('Updated Glossary Item')
+
+ expect(item).to.haveOwnProperty('def')
+ expect(item.def).to.equal('Updated definition')
+
+ expect(item).to.not.have.property('_id')
+ expect(item).to.not.have.property('__v')
+ expect(item).to.not.have.property('createdAt')
+ expect(item).to.not.have.property('updatedAt')
+ })
+ })
+ })
+ context('Negative Tests', () => {
+ it('Fails to update a glossary item changing its services_short_name', async () => {
+ await chai.request(app)
+ .put(`/api/glossary/${testGlossaryItem.services_short_name}`)
+ .set(secretariatHeaders)
+ .send({
+ ...testGlossaryItem,
+ services_short_name: 'new_services_short_name'
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ expect(res.body.details[0]).to.equal('Cannot change services_short_name through this endpoint.')
+ })
+ })
+ })
+ })
+ context('Testing DELETE /glossary endpoint', () => {
+ context('Positive Tests', () => {
+ it('Deletes a glossary item', async () => {
+ await chai.request(app)
+ .delete(`/api/glossary/${testGlossaryItem.services_short_name}`)
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.message).to.equal('Glossary item deleted')
+ })
+ })
+ })
+ context('Negative Tests', () => {
+ it('Fails to delete a glossary item that does not exist', async () => {
+ await chai.request(app)
+ .delete('/api/glossary/nonexistent_item')
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(404)
+ expect(res.body.message).to.equal('404: resource not found')
+ })
+ })
+ })
+ })
+})
diff --git a/test/integration-tests/helpers.js b/test/integration-tests/helpers.js
index 2b8685d4e..de16ccfd8 100644
--- a/test/integration-tests/helpers.js
+++ b/test/integration-tests/helpers.js
@@ -122,6 +122,9 @@ async function userDeactivateAsSecHelper (userName, orgShortName) {
.set(constants.headers)
.then(res => res.body)
+ delete user.created
+ delete user.last_updated
+ delete user.created_by
await chai.request(app)
.put(`/api/registry/org/${orgShortName}/user/${userName}`)
.set(constants.headers)
@@ -139,6 +142,9 @@ async function userReactivateAsSecHelper (userName, orgShortName) {
.then(res => res.body)
user.status = 'active'
+ delete user.created
+ delete user.last_updated
+ delete user.created_by
await chai.request(app)
.put(`/api/registry/org/${orgShortName}/user/${userName}`)
diff --git a/test/integration-tests/org/postOrgTest.js b/test/integration-tests/org/postOrgTest.js
index fbc8b8dde..209ec984a 100644
--- a/test/integration-tests/org/postOrgTest.js
+++ b/test/integration-tests/org/postOrgTest.js
@@ -60,7 +60,10 @@ describe('Testing Org post endpoint', () => {
expect(res.body.created.long_name).to.equal(constants.testRegistryOrg.long_name)
expect(res.body.created).to.haveOwnProperty('contact_info')
- expect(res.body.created.contact_info).to.include(constants.testRegistryOrg.contact_info)
+ expect(res.body.created.contact_info).to.deep.equal(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'])
@@ -97,5 +100,19 @@ describe('Testing Org post endpoint', () => {
expect(res.body.error).to.equal('ORG_EXISTS')
})
})
+ it('Should fail to create an org with an erroneous key not found in the schema with registry enabled', async () => {
+ await chai.request(app)
+ .post('/api/registry/org')
+ .set({ ...constants.headers })
+ .send({
+ ...constants.testRegistryOrg,
+ test: 'additional key not in schema'
+ })
+ .then((res, err) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ expect(res.body.errors[0].message).to.equal('must NOT have additional properties')
+ })
+ })
})
})
diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js
index 098763b1b..d952c7237 100644
--- a/test/integration-tests/org/postOrgUsersTest.js
+++ b/test/integration-tests/org/postOrgUsersTest.js
@@ -332,5 +332,27 @@ describe('Testing user post endpoint', () => {
)
})
})
+ it('Fails creation of user with registry enabled and an erroneous key not found in the schema', async () => {
+ await chai
+ .request(app)
+ .post('/api/registry/org/mitre/user')
+ .set({ ...constants.headers, ...shortName })
+ .send({
+ username: 'fakeregistryuser1002',
+ name: {
+ first: 'FirstName',
+ last: 'LastName',
+ middle: 'MiddleName',
+ suffix: 'Suffix'
+ },
+ role: 'ADMIN',
+ test: 'additional key not in schema'
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ expect(res.body.errors[0].message).to.equal('must NOT have additional properties')
+ })
+ })
})
})
diff --git a/test/integration-tests/org/registryOrg.js b/test/integration-tests/org/registryOrg.js
index 9328f32dc..0dec2824e 100644
--- a/test/integration-tests/org/registryOrg.js
+++ b/test/integration-tests/org/registryOrg.js
@@ -157,21 +157,6 @@ describe('Testing Secretariat functionality for Orgs', () => {
})
})
- it('A new user is created even if extra data is in the body', async () => {
- const username = uuidv4()
- await chai.request(app)
- .post('/api/registry/org/mitre/user')
- .set(secretariatHeaders)
- .send({
- username,
- ubiquitous: 'mendacious'
- })
- .then((res) => {
- expect(res).to.have.status(200)
- expect(res.body.message).to.equal(`${username} was successfully created.`)
- })
- })
-
it('A users username can be updated', async function () {
const { orgShortName, username } = await createNewUserWithNewOrg()
const newUsername = uuidv4()
@@ -179,6 +164,8 @@ describe('Testing Secretariat functionality for Orgs', () => {
await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body })
+ delete user.created
+ delete user.last_updated
await chai.request(app)
.put(`/api/registry/org/${orgShortName}/user/${username}`)
.set(secretariatHeaders)
@@ -209,6 +196,8 @@ describe('Testing Secretariat functionality for Orgs', () => {
let user
await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body })
+ delete user.created
+ delete user.last_updated
await chai.request(app)
.put(`/api/registry/org/${orgShortName}/user/${username}`)
.set(secretariatHeaders)
@@ -244,6 +233,8 @@ describe('Testing Secretariat functionality for Orgs', () => {
await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body })
+ delete user.created
+ delete user.last_updated
await chai.request(app)
.put(`/api/registry/org/${orgShortName}/user/${username}`)
.set(secretariatHeaders)
@@ -319,6 +310,21 @@ describe('Testing Secretariat functionality for Orgs', () => {
})
context('Negative Tests', () => {
+ it('A new user is not created if extra data is in the body', async () => {
+ const username = uuidv4()
+ await chai.request(app)
+ .post('/api/registry/org/mitre/user')
+ .set(secretariatHeaders)
+ .send({
+ username,
+ ubiquitous: 'mendacious'
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ })
+ })
+
it('Should not retrieve an org for a non-existent UUID', async () => {
const nonExistentUUID = 'nonexistent123'
await chai.request(app)
diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js
index d248215dd..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 () => {
@@ -282,6 +326,10 @@ describe('Testing Registry Org as org admin', () => {
it('Registry: Services api prevents org admins from updating a users username if that user already exists', async () => {
let user
await chai.request(app).get('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com').set(adminHeaders).then((res) => { user = res.body })
+
+ delete user.created
+ delete user.last_updated
+ delete user.created_by
await chai.request(app)
.put('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com')
.set(adminHeaders)
@@ -373,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/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js
index 348df2336..b67aee73f 100644
--- a/test/integration-tests/org/regularUsersTestRegistry.js
+++ b/test/integration-tests/org/regularUsersTestRegistry.js
@@ -24,6 +24,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with
.set(constants.nonSecretariatUserHeaders)
.then((res) => { previousBody = res.body })
+ delete previousBody.created_by
await chai.request(app)
.put(`/api/registry/org/${org}/user/${user}`)
.set(constants.nonSecretariatUserHeaders)
diff --git a/test/integration-tests/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/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js
index 9397eb8db..3cba5a152 100644
--- a/test/integration-tests/registry-org/createUserByOrgTest.js
+++ b/test/integration-tests/registry-org/createUserByOrgTest.js
@@ -9,98 +9,111 @@ const constants = require('../constants.js')
const app = require('../../../src/index.js')
describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => {
- // Positive test
- it('Should create a new user in an organization', (done) => {
- const orgShortName = 'mitre'
- const newUser = {
- username: 'testuser@example.com',
- name: {
- first: 'Test',
- last: 'User'
- },
- role: 'ADMIN'
- }
-
- chai.request(app)
- .post(`/api/registryOrg/${orgShortName}/user`)
- .set(constants.headers)
- .send(newUser)
- .end((err, res) => {
- expect(err).to.be.null
- expect(res).to.have.status(200)
- expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`)
- expect(res.body).to.have.property('created')
- expect(res.body.created).to.have.property('username', newUser.username)
- expect(res.body.created).to.have.property('secret')
- done()
- })
- })
-
- // Negative test: Organization does not exist
- it('Should not create a user in a non-existent organization', (done) => {
- const orgShortName = 'nonexistentorg'
- const newUser = {
- username: 'testuser2@example.com',
- name: {
- first: 'Test',
- last: 'User'
+ context('Positive Tests', () => {
+ it('Should create a new user in an organization', (done) => {
+ const orgShortName = 'mitre'
+ const newUser = {
+ username: 'testuser@example.com',
+ name: {
+ first: 'Test',
+ last: 'User'
+ },
+ role: 'ADMIN'
}
- }
-
- chai.request(app)
- .post(`/api/registryOrg/${orgShortName}/user`)
- .set(constants.headers)
- .send(newUser)
- .end((err, res) => {
- expect(err).to.be.null
- expect(res).to.have.status(404)
- expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`)
- done()
- })
+ chai.request(app)
+ .post(`/api/registryOrg/${orgShortName}/user`)
+ .set(constants.headers)
+ .send(newUser)
+ .end((err, res) => {
+ expect(err).to.be.null
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`)
+ expect(res.body).to.have.property('created')
+ expect(res.body.created).to.have.property('username', newUser.username)
+ expect(res.body.created).to.have.property('secret')
+ done()
+ })
+ })
})
-
- // Negative test: User already exists
- it('Should not create a user that already exists', (done) => {
- const orgShortName = 'mitre'
- const existingUser = {
- username: 'testuser@example.com',
- name: {
- first: 'Test',
- last: 'User'
+ context('Negative Tests', () => {
+ it('Should not create a user in a non-existent organization', (done) => {
+ const orgShortName = 'nonexistentorg'
+ const newUser = {
+ username: 'testuser2@example.com',
+ name: {
+ first: 'Test',
+ last: 'User'
+ }
}
- }
-
- chai.request(app)
- .post(`/api/registryOrg/${orgShortName}/user`)
- .set(constants.headers)
- .send(existingUser)
- .end((err, res) => {
- expect(err).to.be.null
- expect(res).to.have.status(400)
- expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`)
- done()
- })
- })
-
- // Negative test: Validation error (missing username)
- it('Should not create a user with a missing username', (done) => {
- const orgShortName = 'mitre'
- const invalidUser = {
- name: {
- first: 'Test',
- last: 'User'
+ chai.request(app)
+ .post(`/api/registryOrg/${orgShortName}/user`)
+ .set(constants.headers)
+ .send(newUser)
+ .end((err, res) => {
+ expect(err).to.be.null
+ expect(res).to.have.status(404)
+ expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`)
+ done()
+ })
+ })
+ it('Should not create a user that already exists', (done) => {
+ const orgShortName = 'mitre'
+ const existingUser = {
+ username: 'testuser@example.com',
+ name: {
+ first: 'Test',
+ last: 'User'
+ }
}
- }
-
- chai.request(app)
- .post(`/api/registryOrg/${orgShortName}/user`)
- .set(constants.headers)
- .send(invalidUser)
- .end((err, res) => {
- expect(err).to.be.null
- expect(res).to.have.status(400)
- expect(res.body).to.have.property('message').equal('Parameters were invalid')
- done()
- })
+ chai.request(app)
+ .post(`/api/registryOrg/${orgShortName}/user`)
+ .set(constants.headers)
+ .send(existingUser)
+ .end((err, res) => {
+ expect(err).to.be.null
+ expect(res).to.have.status(400)
+ expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`)
+ done()
+ })
+ })
+ it('Should not create a user with a missing username', (done) => {
+ const orgShortName = 'mitre'
+ const invalidUser = {
+ name: {
+ first: 'Test',
+ last: 'User'
+ }
+ }
+ chai.request(app)
+ .post(`/api/registryOrg/${orgShortName}/user`)
+ .set(constants.headers)
+ .send(invalidUser)
+ .end((err, res) => {
+ expect(err).to.be.null
+ expect(res).to.have.status(400)
+ expect(res.body).to.have.property('message').equal('Parameters were invalid')
+ done()
+ })
+ })
+ it('Should not create a user with an erroneous key not found in the schema', async () => {
+ const orgShortName = 'mitre'
+ const existingUser = {
+ username: 'testuser@example.com',
+ name: {
+ first: 'Test',
+ last: 'User'
+ },
+ test: 'additional key not in schema'
+ }
+ chai.request(app)
+ .post(`/api/registryOrg/${orgShortName}/user`)
+ .set(constants.headers)
+ .send(existingUser)
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ expect(res.body.errors[0].message).to.equal('must NOT have additional properties')
+ })
+ })
})
})
diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js
index db15adecc..52ca111e5 100644
--- a/test/integration-tests/registry-org/registryOrgCRUDTest.js
+++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js
@@ -13,9 +13,11 @@ 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'],
+ charter_or_scope: 'This is a normal string, not a URI'
}
let createdOrg
@@ -24,7 +26,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,23 +52,38 @@ 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('charter_or_scope')
+ expect(res.body.created.charter_or_scope).to.equal(testRegistryOrg.charter_or_scope)
+
+ 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) => {
@@ -74,9 +91,24 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.message).to.equal(`The '${testRegistryOrg.short_name}' organization already exists.`)
})
})
+ it('Fails to create a new registry organization with an existing short name (case-insensitive)', async () => {
+ const bodyWithDuplicateCase = { ...createdOrg }
+ delete bodyWithDuplicateCase.UUID
+ delete bodyWithDuplicateCase.uuid
+ bodyWithDuplicateCase.short_name = bodyWithDuplicateCase.short_name.toUpperCase()
+
+ const res = await chai.request(app)
+ .post('/api/registry/org')
+ .set(secretariatHeaders)
+ .send(bodyWithDuplicateCase)
+
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal(`The '${bodyWithDuplicateCase.short_name}' organization already exists.`)
+ })
+
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,
@@ -89,7 +121,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,
@@ -99,7 +131,53 @@ 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/registry/org')
+ .set(secretariatHeaders)
+ .send({
+ ...testRegistryOrg,
+ test: 'additional key not in schema'
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ expect(res.body.errors[0].message).to.equal('must NOT have additional properties')
+ })
+ })
+ 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')
+ })
+ })
+ it('Fails to create a new registry organization with an alias that collides with an existing short_name', async () => {
+ await chai.request(app)
+ .post('/api/registry/org')
+ .set(secretariatHeaders)
+ .send({
+ ...testRegistryOrg,
+ short_name: 'test_create_alias_collision',
+ aliases: ['mitre']
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.equal('ALIAS_COLLISION')
+ expect(res.body.message).to.equal("The organization could not be created or updated because the string 'mitre' is already in use as a short_name, name, or alias by another organization.")
})
})
})
@@ -108,7 +186,7 @@ describe('Testing /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)
@@ -123,23 +201,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)
@@ -163,7 +243,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)
@@ -191,7 +271,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)
@@ -204,14 +284,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
@@ -237,14 +318,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 () => {
@@ -292,7 +399,7 @@ describe('Testing /registryOrg endpoints', () => {
}
let createdSubOrgUUID
await chai.request(app)
- .post('/api/registryOrg')
+ .post('/api/registry/org')
.set(secretariatHeaders)
.send(subOrg)
.then(res => {
@@ -302,7 +409,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,
@@ -315,11 +422,27 @@ 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)
expect(res.body).to.have.property('reports_to', createdOrg.UUID)
+ expect(res.body).to.have.property('_relatedOrganizations')
+ expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1)
+ expect(res.body._relatedOrganizations[0].UUID).to.equal(createdOrg.UUID)
+ expect(res.body._relatedOrganizations[0].short_name).to.equal(createdOrg.short_name)
+ })
+
+ // Assert that the main org also has _relatedOrganizations for the sub org it oversees
+ await chai.request(app)
+ .get(`/api/registry/org/${createdOrg.short_name}`)
+ .set(secretariatHeaders)
+ .then(res => {
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('_relatedOrganizations')
+ expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1)
+ expect(res.body._relatedOrganizations[0].UUID).to.equal(createdSubOrgUUID)
+ expect(res.body._relatedOrganizations[0].short_name).to.equal(subOrg.short_name)
})
// Cleanup sub org
@@ -327,32 +450,53 @@ describe('Testing /registryOrg endpoints', () => {
.delete(`/api/registryOrg/${subOrg.short_name}`)
.set(secretariatHeaders)
})
- it('Ignores protected fields such as users and admins during an update', async () => {
- const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e']
- const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e']
+ 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/registryOrg/${createdOrg.short_name}`)
+ .put(`/api/registry/org/${tempOrg.short_name}`)
.set(secretariatHeaders)
.send({
- ...createdOrg,
- users: maliciousUsers,
- admins: maliciousAdmins
+ ...tempOrg,
+ long_name: 'Temp Org In Use Test Updated'
})
- .then((res, err) => {
- expect(err).to.be.undefined
+ .then((res) => {
expect(res).to.have.status(200)
-
- // Ensure the response body.updated does not contain the malicious data
- expect(res.body.updated.users || []).to.not.include(maliciousUsers[0])
- expect(res.body.updated.admins || []).to.not.include(maliciousAdmins[0])
})
+
+ // 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,
@@ -365,7 +509,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,
@@ -376,9 +520,23 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.message).to.equal("The organization cannot be renamed as 'mitre' because this shortname is used by another organization.")
})
})
+ it('Fails to update a registry organization with an alias that collides with an existing short_name', async () => {
+ await chai.request(app)
+ .put('/api/registry/org/registry_org_test')
+ .set(secretariatHeaders)
+ .send({
+ ...createdOrg,
+ aliases: ['mitre']
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.equal('ALIAS_COLLISION')
+ expect(res.body.message).to.equal("The organization could not be created or updated because the string 'mitre' is already in use as a short_name, name, or alias by another organization.")
+ })
+ })
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,
@@ -389,9 +547,41 @@ 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/registry/org/${createdOrg.short_name}`)
+ .set(secretariatHeaders)
+ .send({
+ ...createdOrg,
+ users: maliciousUsers,
+ admins: maliciousAdmins
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ })
+ })
it('Fails to update a registry organization with reports_to manually provided', async () => {
await chai.request(app)
- .put(`/api/registryOrg/${createdOrg.short_name}`)
+ .put(`/api/registry/org/${createdOrg.short_name}`)
.set(secretariatHeaders)
.send({
...createdOrg,
@@ -400,20 +590,48 @@ 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 with an invalidly high quota', async () => {
+ it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => {
await chai.request(app)
- .put(`/api/registryOrg/${createdOrg.short_name}`)
+ .put('/api/registry/org/registry_org_test')
.set(secretariatHeaders)
.send({
...createdOrg,
- hard_quota: 1000000
+ test: 'additional key not in schema'
})
.then((res) => {
expect(res).to.have.status(400)
expect(res.body.message).to.equal('Parameters were invalid')
+ expect(res.body.errors[0].message).to.equal('must NOT have additional properties')
})
})
})
diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
index db236a830..bbcd6f41d 100644
--- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
+++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
@@ -116,7 +116,7 @@ describe('Testing Joint approval', () => {
await chai.request(app)
.put('/api/registry/org/non_secretariat_org')
.set(nonAdminHeaders)
- .send({ ...testRegistryOrgForReview, short_name: 'new_non_secretariat_org', contact_info: { website: 'https://www.example.com' } })
+ .send({ ...testRegistryOrgForReview, short_name: 'new_non_secretariat_org', contact_info: { websites: ['https://www.example.com'] } })
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.')
@@ -144,11 +144,11 @@ describe('Testing Joint approval', () => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.short_name).to.equal('non_secretariat_org')
- expect(res.body.contact_info.website).to.equal('https://www.example.com')
+ expect(res.body.contact_info.websites[0]).to.equal('https://www.example.com')
})
})
it('Secretariat can approve the ORG review with body parameter', async function () {
- const newBody = { short_name: 'final_non_secretariat_org', contact_info: { website: 'https://final.example.com' }, hard_quota: 1000, authority: ['CNA'], long_name: 'Final Non Secretariat Organization' }
+ const newBody = { short_name: 'final_non_secretariat_org', contact_info: { websites: ['https://final.example.com'] }, hard_quota: 1000, authority: ['CNA'], long_name: 'Final Non Secretariat Organization' }
await chai.request(app)
.put(`/api/review/${reviewUUID}/approve`)
.set(secretariatHeaders)
@@ -164,7 +164,7 @@ describe('Testing Joint approval', () => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.short_name).to.equal('final_non_secretariat_org')
- expect(res.body.contact_info.website).to.equal('https://final.example.com')
+ expect(res.body.contact_info.websites[0]).to.equal('https://final.example.com')
})
})
})
@@ -219,7 +219,7 @@ describe('Testing Joint approval', () => {
await chai.request(app)
.put('/api/registry/org/non_with_comments')
.set(nonAdminHeaders2)
- .send({ ...testRegistryOrgForReviewWithComments, short_name: 'new_non_with_comments', contact_info: { website: 'https://www.example.com' } })
+ .send({ ...testRegistryOrgForReviewWithComments, short_name: 'new_non_with_comments', contact_info: { websites: ['https://www.example.com'] } })
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.')
@@ -247,7 +247,7 @@ describe('Testing Joint approval', () => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.short_name).to.equal('non_with_comments')
- expect(res.body.contact_info.website).to.equal('https://www.example.com')
+ expect(res.body.contact_info.websites[0]).to.equal('https://www.example.com')
})
})
it('Secretariat leaves a public comment on the org review', async () => {
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/registry-org/verifyDeepRemoveEmpty.js b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js
index d2c056768..c88959459 100644
--- a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js
+++ b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js
@@ -14,8 +14,7 @@ const testNullRemovalOrg = {
authority: ['CNA'],
hard_quota: 1000,
contact_info: {
- website: null, // Should be removed
- org_email: undefined // Should be removed (or not present)
+ phone: null // Should be removed
}
}
@@ -40,14 +39,13 @@ describe('Testing Deep Remove Empty in Create Org', () => {
expect(createdOrg).to.haveOwnProperty('short_name')
expect(createdOrg.short_name).to.equal(testNullRemovalOrg.short_name)
- // Verify contact_info exists but does NOT contain website or org_email
+ // Verify contact_info exists but does NOT contain websites or emails
// Ideally if contact_info becomes empty, deepRemoveEmpty might remove the whole object if it recurses well.
// Let's check what happened.
if (createdOrg.contact_info) {
- expect(createdOrg.contact_info).to.not.have.property('website')
- expect(createdOrg.contact_info).to.not.have.property('org_email')
- // If deepRemoveEmpty works on nested empty objects, contact_info might be gone or empty.
- expect(Object.keys(createdOrg.contact_info)).to.be.empty
+ expect(createdOrg.contact_info).to.not.have.property('phone')
+ expect(createdOrg.contact_info).to.have.property('websites').that.is.an('array')
+ expect(createdOrg.contact_info).to.have.property('emails').that.is.an('array')
} else {
// This is also acceptable if deepRemoveEmpty removes empty objects
expect(createdOrg).to.not.have.property('contact_info')
diff --git a/test/integration-tests/registry-user/registryUserCRUDTest.js b/test/integration-tests/registry-user/registryUserCRUDTest.js
new file mode 100644
index 000000000..84a76d36d
--- /dev/null
+++ b/test/integration-tests/registry-user/registryUserCRUDTest.js
@@ -0,0 +1,64 @@
+const chai = require('chai')
+const expect = chai.expect
+chai.use(require('chai-http'))
+
+const constants = require('../constants.js')
+const app = require('../../../src/index.js')
+
+const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' }
+
+describe('Testing /registryUser endpoints', () => {
+ context('Positive Tests', () => {
+ // TODO
+ })
+ context('Negative Tests', () => {
+ it('Fails when page query parameter is not an integer', async () => {
+ await chai.request(app)
+ .get('/api/registryUser')
+ .set(secretariatHeaders) // Must be secretariat to reach validation
+ .query({ page: 'not-a-number' }) // Invalid data
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal('Parameters were invalid')
+ })
+ })
+
+ it('Fails when page query parameter is below the minimum', async () => {
+ await chai.request(app)
+ .get('/api/registryUser')
+ .set(secretariatHeaders)
+ .query({ page: 0 }) // Assuming min is 1
+ .then((res) => {
+ expect(res).to.have.status(400)
+ })
+ })
+
+ it('Fails when identifier contains invalid characters', async () => {
+ await chai.request(app)
+ .get('/api/registryUser/uuid