diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 01ab0fad9..eaf2feb02 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.7.5", + "version": "2.8.0", "title": "CVE Services API", "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:

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

Contact the CVE Services team", "contact": { diff --git a/datadump/pre-population/glossary.json b/datadump/pre-population/glossary.json new file mode 100644 index 000000000..ac9ccc4e7 --- /dev/null +++ b/datadump/pre-population/glossary.json @@ -0,0 +1,7 @@ +[ + { + "services_short_name": "long_name", + "label": "Long Name", + "def": "The full, official name of an organization participating in the CVE program." + } +] diff --git a/package-lock.json b/package-lock.json index 88c96fde7..ca4a802a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,20 @@ { "name": "cve-services", - "version": "2.7.0", +<<<<<<< HEAD + "version": "2.8.0", +======= + "version": "2.7.5", +>>>>>>> 8904859f (Bump lodash from 4.17.23 to 4.18.1) "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.0", +<<<<<<< HEAD + "version": "2.8.0", +======= + "version": "2.7.5", +>>>>>>> 8904859f (Bump lodash from 4.17.23 to 4.18.1) "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", @@ -25,7 +33,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 +46,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 +439,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 +520,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 +1518,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 +2891,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 +2986,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 +3134,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 +4981,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 +5323,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 +5864,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 +6038,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 +6143,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 +6712,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 +7509,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 +7712,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 +8386,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 +9086,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 +9185,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 +9568,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 +9898,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..d2a42bbf5 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,12 @@ "authority": { "description": "The authority (role) of this organization within the CVE program", "type": "string", - "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + "enum": [ + "CNA", + "SECRETARIAT", + "BULK_DOWNLOAD", + "ADP" + ] } }, "properties": { @@ -47,6 +53,9 @@ "long_name": { "$ref": "#/definitions/longName" }, + "new_short_name": { + "$ref": "#/definitions/shortName" + }, "aliases": { "type": "array", "uniqueItems": true, @@ -81,6 +90,18 @@ "$ref": "#/definitions/uuidType" } }, + "hard_quota": { + "description": "The maximum number of CVE IDs this organization can reserve.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "description": "The threshold for notifying the organization about their remaining CVE ID count.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, "contact_info": { "type": "object", "properties": { diff --git a/schemas/registry-org/BulkDownloadOrg.json b/schemas/registry-org/BulkDownloadOrg.json index cabc0777a..526626f17 100644 --- a/schemas/registry-org/BulkDownloadOrg.json +++ b/schemas/registry-org/BulkDownloadOrg.json @@ -1,11 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "BaseOrg", + "$id": "BulkDownloadOrg", "type": "object", "title": "CVE Bulk Download Organization", "description": "Schema for a CVE Bulk Download Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 0402e8338..367302530 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -1,42 +1,75 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", "$id": "CNAOrg", + "type": "object", "title": "CVE CNA Organization", "description": "Schema for a CVE CNA Organization", - "allOf": [ - { "$ref": "./BaseOrg.json" }, - { + "additionalProperties": false, + "properties": { + "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, + "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, + "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "new_short_name": { + "description": "Used to rename an organization's short name during an update.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "contact_info": { + "type": "object", "properties": { - "authority": { - "const": ["CNA"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { + "poc": { "type": "string" }, + "poc_email": { "type": "string" }, + "poc_phone": { "type": "string" }, + "org_email": { "type": "string" }, + "website": { "type": "string" } + }, + "additionalProperties": false + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/authority" + } + }, + "policies": { + "type": "object", + "properties": { + "id_quota": { "type": "integer", - "minimum": 0 - }, - "charter_or_scope": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "disclosure_policy": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "product_list": { - "$ref": "/BaseOrg#/definitions/uriType" + "minimum": 0, + "maximum": 100000 } - }, - "required": ["hard_quota"] + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + }, + "partner_role": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" } - ] -} + }, + "required": ["short_name", "hard_quota"] +} \ No newline at end of file diff --git a/schemas/registry-org/SecretariatOrg.json b/schemas/registry-org/SecretariatOrg.json index 469bd7df5..4e658b571 100644 --- a/schemas/registry-org/SecretariatOrg.json +++ b/schemas/registry-org/SecretariatOrg.json @@ -5,7 +5,7 @@ "title": "CVE Secretariat Organization", "description": "Schema for a CVE Secretariat Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { @@ -15,7 +15,7 @@ "type": "array", "uniqueItems": true, "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "hard_quota": { diff --git a/schemas/registry-user/BaseUser.json b/schemas/registry-user/BaseUser.json new file mode 100644 index 000000000..5fa85687e --- /dev/null +++ b/schemas/registry-user/BaseUser.json @@ -0,0 +1,104 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/BaseUser", + "type": "object", + "title": "CVE Base User Schema", + "additionalProperties": false, + "description": "The schema for CVE Services Users", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "name": { + "description": "User's name components", + "type": "object", + "required": [ + "first", + "last" + ], + "properties": { + "first": { + "type": "string", + "maxLength": 100 + }, + "middle": { + "type": "string", + "maxLength": 100 + }, + "last": { + "type": "string", + "maxLength": 100 + }, + "suffix": { + "type": "string", + "maxLength": 100 + } + }, + "additionalProperties": false + } + }, + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "username": { + "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.", + "type": "string", + "minLength": 3, + "maxLength": 128, + "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$" + }, + "active": { + "description": "Whether the user account is active. Supports boolean or string based on legacy test constants.", + "type": [ + "boolean", + "string" + ] + }, + "authority": { + "description": "The user's authority and roles, often used in joint review contexts.", + "type": "object", + "properties": { + "active_roles": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "secret": { + "description": "Hashed secret for user authentication", + "type": "string" + }, + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "status": { + "description": "User status: 'active' or 'inactive'", + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "role": { + "description": "The user's role in the organization", + "type": "string" + }, + "org_short_name": { + "description": "Used to update the organization association of a user", + "type": "string", + "minLength": 2, + "maxLength": 32 + } + }, + "required": [ + "username" + ] +} \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js index a4c73c910..69f295e9f 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,5 +1,5 @@ const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) /** * Return default values. diff --git a/src/controller/conversation.controller/conversation.controller.js b/src/controller/conversation.controller/conversation.controller.js index 73d5335bf..1b4dbb3ad 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() @@ -42,8 +44,8 @@ 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 }) @@ -73,8 +75,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..45eeeee53 100644 --- a/src/controller/conversation.controller/index.js +++ b/src/controller/conversation.controller/index.js @@ -257,4 +257,99 @@ router.post('/conversation/target/:uuid', 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..8c374131b --- /dev/null +++ b/src/controller/glossary.controller/index.js @@ -0,0 +1,341 @@ +const router = require('express').Router() +const controller = require('./glossary.controller') +const mw = require('../../middleware/middleware') + +// Get all glossary items - SEC only +router.get('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryAll' + #swagger.summary = "Retrieves all glossary items (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves all glossary items

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

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves the specified glossary item

" + #swagger.parameters['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, + mw.onlySecretariat, + controller.getGlossaryItem +) + +// Create a glossary item - SEC only +router.post('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryCreate' + #swagger.summary = "Creates a new glossary item (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Creates a new glossary item

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the created glossary item 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/index.js b/src/controller/org.controller/index.js index f7805c496..1c5e6f110 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, + // mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 297887707..b562bae9c 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -61,22 +61,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')) } diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js index d4af5f1e6..dd5ffe352 100644 --- a/src/controller/registry-org.controller/error.js +++ b/src/controller/registry-org.controller/error.js @@ -91,34 +91,6 @@ class RegistryOrgControllerError extends idrErr.IDRError { err.message = 'The requested user can not be created and added to the organization because the organization has hit its limit of 100 users. Contact the Secretariat.' return err } - - conversationDne (shortname, index) { - const err = {} - err.error = 'CONVERSATION_DNE' - err.message = `The conversation at index ${index} does not exist for the ${shortname} organization.` - return err - } - - notAllowedToEditConversation () { - const err = {} - err.error = 'NOT_ALLOWED_TO_EDIT_CONVERSATION' - err.message = 'You must be the original author or Secretariat to edit this conversation.' - return err - } - - notAllowedToChangeConversationVisibility () { - const err = {} - err.error = 'NOT_ALLOWED_TO_CHANGE_CONVERSATION_VISIBILITY' - err.message = 'Only the Secretariat is allowed to change the visibility of a conversation.' - return err - } - - invalidConversationObject () { - const err = {} - err.error = 'BAD_INPUT' - err.message = 'Parameters were invalid: conversation must be an object with a body.' - return err - } } 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..8d4292079 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 /** @@ -241,10 +243,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 }) @@ -280,6 +278,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 +286,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({ @@ -634,7 +646,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,13 +664,13 @@ 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 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.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/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..a87e55fe4 100644 --- a/src/middleware/schemas/BaseOrg.json +++ b/src/middleware/schemas/BaseOrg.json @@ -116,6 +116,7 @@ }, "website": { "$ref": "#/definitions/uriType", + "type": "string", "pattern": "^(ftp|http)s?://\\S+$" } }, 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..21797fe93 100644 --- a/src/middleware/schemas/CNAOrg.json +++ b/src/middleware/schemas/CNAOrg.json @@ -7,6 +7,10 @@ "allOf": [ { "$ref": "/BaseOrg" }, { +<<<<<<< HEAD +======= + "type": "object", +>>>>>>> b62d7ad4 (Conflicts) "properties": { "authority": { "const": ["CNA"] 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..63c452a0b 100644 --- a/src/middleware/schemas/SecretariatOrg.json +++ b/src/middleware/schemas/SecretariatOrg.json @@ -7,6 +7,10 @@ "allOf": [ { "$ref": "/BaseOrg" }, { +<<<<<<< HEAD +======= + "type": "object", +>>>>>>> b62d7ad4 (Conflicts) "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/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/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/conversationRepository.js b/src/repositories/conversationRepository.js index 02a61d807..c8a8d48cc 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -62,6 +62,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() 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/repositoryFactory.js b/src/repositories/repositoryFactory.js index 4750fffea..7f97e1177 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -7,6 +7,7 @@ const BaseOrgRepository = require('./baseOrgRepository') const BaseUserRepository = require('./baseUserRepository') const ConversationRepository = require('./conversationRepository') const ReviewObjectRepository = require('./reviewObjectRepository') +const GlossaryRepository = require('./glossaryRepository') class RepositoryFactory { getOrgRepository () { @@ -54,6 +55,11 @@ class RepositoryFactory { return repo } + getGlossaryRepository () { + const repo = new GlossaryRepository() + return repo + } + getAuditRepository () { const AuditRepository = require('./auditRepository') const repo = new AuditRepository() diff --git a/src/routes.config.js b/src/routes.config.js index 1cf2fe159..9cf95cdc3 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -12,6 +12,7 @@ const RegistryOrgController = require('./controller/registry-org.controller') const AuditController = require('./controller/audit.controller') const ConversationController = require('./controller/conversation.controller') const ReviewObjectController = require('./controller/review-object.controller') +const GlossaryController = require('./controller/glossary.controller') var options = { swaggerOptions: { @@ -40,6 +41,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', RegistryOrgController) app.use('/api/', ConversationController) app.use('/api/', ReviewObjectController) + app.use('/api/', GlossaryController) app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)) app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions)) app.use('/schemas/', SchemasController) diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 7f4bd5879..a0097c1d6 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -65,6 +65,7 @@ async function run () { // Each helper handlers querying changes from srcDB and updating trgDB await orgHelper(db) await userHelper(db) + await glossaryHelper(db) } catch (err) { // Ensures that the client will close when you finish/error await dbClient.close() @@ -265,3 +266,13 @@ async function userHelper (db) { await trgUserCol.updateOne(trgQuery, updateDoc, options) } } + +async function glossaryHelper (db) { + console.log('Ensuring Glossary collection exists...') + // Create collection if it doesn't exist + await db.createCollection('Glossary').catch((err) => { + if (err.codeName !== 'NamespaceExists') { + console.warn('Could not create Glossary collection', err) + } + }) +} diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 28fe3d057..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/test/integration-tests/constants.js b/test/integration-tests/constants.js index de4946825..80700e31e 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -363,6 +363,20 @@ const testOrg = { } } +const testOrg2 = { + + short_name: 'test_org2', + name: 'Test Organization 2', + authority: { + active_roles: [ + 'CNA' + ] + }, + policies: { + id_quota: 100000 + } +} + const testRegistryOrg = { short_name: 'test_registry_org', long_name: 'Test Registry Organization', @@ -432,6 +446,7 @@ module.exports = { testAdp, testAdp2, testOrg, + testOrg2, testRegistryOrg, testRegistryOrg2, existingOrg, diff --git a/test/integration-tests/conversation/conversationTest.js b/test/integration-tests/conversation/conversationTest.js index 802199ff2..d2c5db976 100644 --- a/test/integration-tests/conversation/conversationTest.js +++ b/test/integration-tests/conversation/conversationTest.js @@ -145,6 +145,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 +180,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..7a219c385 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,11 @@ describe('Testing Conversation endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) org = res.body + delete org.created + delete org.last_updated + delete org.admins + delete org.users + delete org.root_or_tlr }) await chai @@ -176,7 +180,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..a94245fd6 100644 --- a/test/integration-tests/org/postOrgTest.js +++ b/test/integration-tests/org/postOrgTest.js @@ -97,5 +97,19 @@ describe('Testing Org post endpoint', () => { expect(res.body.error).to.equal('ORG_EXISTS') }) }) + it('Should fail to create an org with an erroneous key not found in the schema with registry enabled', async () => { + await chai.request(app) + .post('/api/registry/org') + .set({ ...constants.headers }) + .send({ + ...constants.testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res, err) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js index 098763b1b..d952c7237 100644 --- a/test/integration-tests/org/postOrgUsersTest.js +++ b/test/integration-tests/org/postOrgUsersTest.js @@ -332,5 +332,27 @@ describe('Testing user post endpoint', () => { ) }) }) + it('Fails creation of user with registry enabled and an erroneous key not found in the schema', async () => { + await chai + .request(app) + .post('/api/registry/org/mitre/user') + .set({ ...constants.headers, ...shortName }) + .send({ + username: 'fakeregistryuser1002', + name: { + first: 'FirstName', + last: 'LastName', + middle: 'MiddleName', + suffix: 'Suffix' + }, + role: 'ADMIN', + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/registryOrg.js b/test/integration-tests/org/registryOrg.js index 9328f32dc..0dec2824e 100644 --- a/test/integration-tests/org/registryOrg.js +++ b/test/integration-tests/org/registryOrg.js @@ -157,21 +157,6 @@ describe('Testing Secretariat functionality for Orgs', () => { }) }) - it('A new user is created even if extra data is in the body', async () => { - const username = uuidv4() - await chai.request(app) - .post('/api/registry/org/mitre/user') - .set(secretariatHeaders) - .send({ - username, - ubiquitous: 'mendacious' - }) - .then((res) => { - expect(res).to.have.status(200) - expect(res.body.message).to.equal(`${username} was successfully created.`) - }) - }) - it('A users username can be updated', async function () { const { orgShortName, username } = await createNewUserWithNewOrg() const newUsername = uuidv4() @@ -179,6 +164,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -209,6 +196,8 @@ describe('Testing Secretariat functionality for Orgs', () => { let user await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -244,6 +233,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -319,6 +310,21 @@ describe('Testing Secretariat functionality for Orgs', () => { }) context('Negative Tests', () => { + it('A new user is not created if extra data is in the body', async () => { + const username = uuidv4() + await chai.request(app) + .post('/api/registry/org/mitre/user') + .set(secretariatHeaders) + .send({ + username, + ubiquitous: 'mendacious' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) + it('Should not retrieve an org for a non-existent UUID', async () => { const nonExistentUUID = 'nonexistent123' await chai.request(app) diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index d248215dd..ef2356bce 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -282,6 +282,10 @@ describe('Testing Registry Org as org admin', () => { it('Registry: Services api prevents org admins from updating a users username if that user already exists', async () => { let user await chai.request(app).get('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com').set(adminHeaders).then((res) => { user = res.body }) + + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com') .set(adminHeaders) diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js index 348df2336..b67aee73f 100644 --- a/test/integration-tests/org/regularUsersTestRegistry.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -24,6 +24,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with .set(constants.nonSecretariatUserHeaders) .then((res) => { previousBody = res.body }) + delete previousBody.created_by await chai.request(app) .put(`/api/registry/org/${org}/user/${user}`) .set(constants.nonSecretariatUserHeaders) diff --git a/test/integration-tests/registry-org/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js index 9397eb8db..3cba5a152 100644 --- a/test/integration-tests/registry-org/createUserByOrgTest.js +++ b/test/integration-tests/registry-org/createUserByOrgTest.js @@ -9,98 +9,111 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { - // Positive test - it('Should create a new user in an organization', (done) => { - const orgShortName = 'mitre' - const newUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' - }, - role: 'ADMIN' - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(200) - expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) - expect(res.body).to.have.property('created') - expect(res.body.created).to.have.property('username', newUser.username) - expect(res.body.created).to.have.property('secret') - done() - }) - }) - - // Negative test: Organization does not exist - it('Should not create a user in a non-existent organization', (done) => { - const orgShortName = 'nonexistentorg' - const newUser = { - username: 'testuser2@example.com', - name: { - first: 'Test', - last: 'User' + context('Positive Tests', () => { + it('Should create a new user in an organization', (done) => { + const orgShortName = 'mitre' + const newUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + role: 'ADMIN' } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(404) - expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) + expect(res.body).to.have.property('created') + expect(res.body.created).to.have.property('username', newUser.username) + expect(res.body.created).to.have.property('secret') + done() + }) + }) }) - - // Negative test: User already exists - it('Should not create a user that already exists', (done) => { - const orgShortName = 'mitre' - const existingUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' + context('Negative Tests', () => { + it('Should not create a user in a non-existent organization', (done) => { + const orgShortName = 'nonexistentorg' + const newUser = { + username: 'testuser2@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(existingUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) - done() - }) - }) - - // Negative test: Validation error (missing username) - it('Should not create a user with a missing username', (done) => { - const orgShortName = 'mitre' - const invalidUser = { - name: { - first: 'Test', - last: 'User' + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(404) + expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) + done() + }) + }) + it('Should not create a user that already exists', (done) => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(invalidUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal('Parameters were invalid') - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) + done() + }) + }) + it('Should not create a user with a missing username', (done) => { + const orgShortName = 'mitre' + const invalidUser = { + name: { + first: 'Test', + last: 'User' + } + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(invalidUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal('Parameters were invalid') + done() + }) + }) + it('Should not create a user with an erroneous key not found in the schema', async () => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + test: 'additional key not in schema' + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index db15adecc..9a07abd8c 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -60,6 +60,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) createdOrg = res.body.created + delete createdOrg.created + delete createdOrg.last_updated }) }) }) @@ -102,6 +104,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) + it('Fails to create a new registry organization with an erroneous key not found in the schema', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) context('Testing GET /registryOrg endpoints', () => { @@ -327,27 +343,6 @@ describe('Testing /registryOrg endpoints', () => { .delete(`/api/registryOrg/${subOrg.short_name}`) .set(secretariatHeaders) }) - it('Ignores protected fields such as users and admins during an update', async () => { - const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - - await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) - .set(secretariatHeaders) - .send({ - ...createdOrg, - users: maliciousUsers, - admins: maliciousAdmins - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(200) - - // Ensure the response body.updated does not contain the malicious data - expect(res.body.updated.users || []).to.not.include(maliciousUsers[0]) - expect(res.body.updated.admins || []).to.not.include(maliciousAdmins[0]) - }) - }) }) context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { @@ -389,6 +384,23 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + it('Ignores protected fields such as users and admins during an update', async () => { + const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + + await chai.request(app) + .put(`/api/registryOrg/${createdOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...createdOrg, + users: maliciousUsers, + admins: maliciousAdmins + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) it('Fails to update a registry organization with reports_to manually provided', async () => { await chai.request(app) .put(`/api/registryOrg/${createdOrg.short_name}`) @@ -403,17 +415,18 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) - it('Fails to update a registry organization 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/registryOrg/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-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