From 159059784c5bffc0b876cf7c2aa7160be07015fc Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 13 Jul 2026 10:38:40 -0400 Subject: [PATCH 01/23] New stuff from staging --- .../controller/sessionOptionsTest.js | 45 ++++++++++++++++ .../registry-org/registryOrgCRUDTest.js | 30 +++++++++++ test/unit-tests/org/baseOrgRepositoryTest.js | 53 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 test/integration-tests/controller/sessionOptionsTest.js create mode 100644 test/unit-tests/org/baseOrgRepositoryTest.js diff --git a/test/integration-tests/controller/sessionOptionsTest.js b/test/integration-tests/controller/sessionOptionsTest.js new file mode 100644 index 000000000..4a8e3cb63 --- /dev/null +++ b/test/integration-tests/controller/sessionOptionsTest.js @@ -0,0 +1,45 @@ +/* global describe, it */ + +const fs = require('fs') +const path = require('path') +const { expect } = require('chai') + +function getControllerFiles (dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + return getControllerFiles(fullPath) + } + + if (entry.isFile() && entry.name.endsWith('.js')) { + return [fullPath] + } + + return [] + }) +} + +function lineNumberForIndex (content, index) { + return content.slice(0, index).split('\n').length +} + +describe('Controller mongoose session options', () => { + it('starts all mongoose sessions with causal consistency disabled', () => { + const controllerRoot = path.join(__dirname, '../../../src/controller') + const missingOptions = [] + + for (const file of getControllerFiles(controllerRoot)) { + const content = fs.readFileSync(file, 'utf8') + const startSessionCalls = content.matchAll(/mongoose\.startSession\(([^)]*)\)/gs) + + for (const match of startSessionCalls) { + if (!/causalConsistency\s*:\s*false/.test(match[1])) { + missingOptions.push(`${path.relative(process.cwd(), file)}:${lineNumberForIndex(content, match.index)}`) + } + } + } + + expect(missingOptions).to.deep.equal([]) + }) +}) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 6908d400c..1ebd05f80 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -154,6 +154,36 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.program_data.cve_website_update_date).to.equal(cveWebsiteUpdateDate) }) }) + it('Creates distinct registry orgs when a short name contains regex metacharacters', async () => { + const plainShortName = 'regexxdot_org_test' + const regexShortName = 'regex.dot_org_test' + + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + short_name: plainShortName, + long_name: 'Regex X Dot Org Test' + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.created.short_name).to.equal(plainShortName) + }) + + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + short_name: regexShortName, + long_name: 'Regex Dot Org Test' + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.created.short_name).to.equal(regexShortName) + }) + }) }) context('Negative Tests', () => { it('Fails to create a new registry organization with an existing short name', async () => { diff --git a/test/unit-tests/org/baseOrgRepositoryTest.js b/test/unit-tests/org/baseOrgRepositoryTest.js new file mode 100644 index 000000000..e5e578e71 --- /dev/null +++ b/test/unit-tests/org/baseOrgRepositoryTest.js @@ -0,0 +1,53 @@ +const { expect } = require('chai') +const sinon = require('sinon') + +const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository') +const BaseOrgModel = require('../../../src/model/baseorg') + +describe('Testing BaseOrgRepository lookup queries', () => { + afterEach(() => { + sinon.restore() + }) + + it('Checks org existence without using $expr', async () => { + const findOne = sinon.stub(BaseOrgModel, 'findOne').resolves(null) + const repo = new BaseOrgRepository() + const options = { session: 'session' } + + const exists = await repo.orgExists('Acme.Test', options) + + expect(exists).to.equal(false) + expect(findOne.calledOnce).to.equal(true) + const query = findOne.firstCall.args[0] + expect(query).to.not.have.property('$expr') + expect(query.short_name).to.be.instanceOf(RegExp) + expect(query.short_name.source).to.equal('^Acme\\.Test$') + expect(query.short_name.flags).to.include('i') + expect(findOne.firstCall.args[2]).to.equal(options) + }) + + it('Checks alias collisions without using $expr', async () => { + const findOne = sinon.stub(BaseOrgModel, 'findOne').resolves(null) + const repo = new BaseOrgRepository() + + const collision = await repo.checkAliasCollisions( + 'Acme.Test', + 'Acme Long Name', + ['Alias?'], + 'Existing.Org', + {} + ) + + expect(collision).to.equal(null) + expect(findOne.calledOnce).to.equal(true) + const query = findOne.firstCall.args[0] + expect(query).to.not.have.property('$expr') + expect(query.$or.some(clause => Object.hasOwn(clause, '$expr'))).to.equal(false) + expect(query.$or[0].short_name).to.be.instanceOf(RegExp) + expect(query.$or[0].short_name.source).to.equal('^Acme\\.Test$') + expect(query.$or[0].short_name.flags).to.include('i') + expect(query.$or[2].short_name.source).to.equal('^Alias\\?$') + expect(query.$and[0].short_name.$not).to.be.instanceOf(RegExp) + expect(query.$and[0].short_name.$not.source).to.equal('^Existing\\.Org$') + }) +}) From 4c690406377b648c8f79b09e01cf88434af23f0e Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Mon, 13 Jul 2026 11:40:19 -0400 Subject: [PATCH 02/23] #1843 Use field projections for orgs and users in getFilteredCveId Add optional projection parameters to OrgRepository.getAllOrgs and UserRepository.getAllUsers so callers can limit returned fields. getFilteredCveId now fetches only UUID and short_name for orgs, and UUID, username, and org_UUID for users, instead of entire documents. --- src/controller/cve-id.controller/cve-id.controller.js | 5 +++-- src/repositories/orgRepository.js | 4 ++-- src/repositories/userRepository.js | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/controller/cve-id.controller/cve-id.controller.js b/src/controller/cve-id.controller/cve-id.controller.js index 8172793ac..a82938b69 100644 --- a/src/controller/cve-id.controller/cve-id.controller.js +++ b/src/controller/cve-id.controller/cve-id.controller.js @@ -39,8 +39,9 @@ async function getFilteredCveId (req, res, next) { const requesterOrgUUID = await authContext.getRequesterOrgUUID(req, orgRepo) // Create map of orgUUID to shortnames and users to simplify aggregation later - const orgs = await orgRepo.getAllOrgs() - const users = await userRepo.getAllUsers() + // Only project the fields needed for the maps to avoid fetching full documents + const orgs = await orgRepo.getAllOrgs({}, { UUID: 1, short_name: 1, _id: 0 }) + const users = await userRepo.getAllUsers({}, { UUID: 1, username: 1, org_UUID: 1, _id: 0 }) const orgMap = {} const userMap = {} diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js index 3eb5d7684..48f47ee93 100644 --- a/src/repositories/orgRepository.js +++ b/src/repositories/orgRepository.js @@ -47,8 +47,8 @@ class OrgRepository extends BaseRepository { return utils.isBulkDownload(shortName) } - async getAllOrgs () { - return this.collection.find() + async getAllOrgs (options = {}, projection = {}) { + return this.collection.find({}, projection, options) } async deleteOneByShortName (shortName, options = {}) { diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js index 9cd152b28..1d2d43f5e 100644 --- a/src/repositories/userRepository.js +++ b/src/repositories/userRepository.js @@ -61,8 +61,8 @@ class UserRepository extends BaseRepository { return this.collection.findOneAndUpdate(filter, updatePayload, options) } - async getAllUsers () { - return this.collection.find() + async getAllUsers (options = {}, projection = {}) { + return this.collection.find({}, projection, options) } } From 00df2d52c396fe5d8dfeef0c520d56c7ce4b64bc Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Mon, 13 Jul 2026 12:33:25 -0400 Subject: [PATCH 03/23] Add local Mongo replica cluster for integration tests Add a Mongo-only compose file that starts one primary and two replica members with localhost-advertised replica set hosts. Support explicit Mongo connection string overrides, add replica-aware docker env examples, and add a test:integration:replicas command that runs against secondaryPreferred without using the Mocha CLI path that fails under Node 24. --- docker/.docker-env.example | 1 + docker/.docker-env.int-example | 1 + docker/README.md | 30 ++++++ docker/docker-compose.mongo-cluster.yml | 132 ++++++++++++++++++++++++ package.json | 1 + src/scripts/runMocha.js | 67 ++++++++++++ src/utils/db.js | 6 ++ 7 files changed, 238 insertions(+) create mode 100644 docker/docker-compose.mongo-cluster.yml create mode 100644 src/scripts/runMocha.js diff --git a/docker/.docker-env.example b/docker/.docker-env.example index 277b5d01b..988311f94 100644 --- a/docker/.docker-env.example +++ b/docker/.docker-env.example @@ -1,4 +1,5 @@ LOCAL_KEY=TCF25YM-39C4H6D-KA32EGF-V5XSHN3 +MONGO_CONN_STRING=mongodb://docdb:27017,docdb-read-1:27017,docdb-read-2:27017/cve_dev?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false MONGO_HOST=docdb MONGO_PORT=27017 NODE_ENV=development diff --git a/docker/.docker-env.int-example b/docker/.docker-env.int-example index 924f472ab..c3a82240d 100644 --- a/docker/.docker-env.int-example +++ b/docker/.docker-env.int-example @@ -1,3 +1,4 @@ +MONGO_CONN_STRING=mongodb://docdb:27017,docdb-read-1:27017,docdb-read-2:27017/cve_int?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false MONGO_HOST=docdb MONGO_PORT=27017 NODE_ENV=integration diff --git a/docker/README.md b/docker/README.md index 37c4b66ab..37f875d88 100644 --- a/docker/README.md +++ b/docker/README.md @@ -177,6 +177,36 @@ See the [API documentation](https://github.com/CVEProject/cve-services#api-docum The `docker-compose.yml` file exposes the default Mongo port to the host: `localhost:27017`. You can connect using any Mongo viewer such as [Mongo Express](https://github.com/mongo-express/mongo-express) or [Compass](https://www.mongodb.com/try/download/compass) on the host. +### Run the Mongo Replica Cluster Only + +The Mongo-only compose file starts one primary and two replica members without starting the CVE Services app. By default it uses `mongo:5.0`. + +```bash +cd docker/ +docker compose -f docker-compose.mongo-cluster.yml up -d docdb docdb-read-1 docdb-read-2 mongo-init +``` + +Use this connection string from the host, including MongoDB Compass: + +```text +mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false +``` + +To run the same local replica topology with Mongo 8, override the image: + +```bash +cd docker/ +MONGO_IMAGE=mongo:8.0 docker compose -f docker-compose.mongo-cluster.yml up -d --force-recreate docdb docdb-read-1 docdb-read-2 mongo-init +``` + +If you already created the local volumes with Mongo 5, Mongo 8 may fail to start against those files. For a fresh Mongo 8 local cluster, remove the Mongo-only volumes first. This deletes local Mongo data for this compose file: + +```bash +cd docker/ +docker compose -f docker-compose.mongo-cluster.yml down -v +MONGO_IMAGE=mongo:8.0 docker compose -f docker-compose.mongo-cluster.yml up -d docdb docdb-read-1 docdb-read-2 mongo-init +``` + ## Running unit tests You can run unit tests using the docker image by running the following command: diff --git a/docker/docker-compose.mongo-cluster.yml b/docker/docker-compose.mongo-cluster.yml new file mode 100644 index 000000000..60e925b7e --- /dev/null +++ b/docker/docker-compose.mongo-cluster.yml @@ -0,0 +1,132 @@ +services: + docdb: + image: ${MONGO_IMAGE:-mongo:5.0} + container_name: mongo + ports: + - "27017:27017" + - "27018:27018" + - "27019:27019" + volumes: + - docdb-host-data:/data/db + command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27017"] + healthcheck: + test: ["CMD-SHELL", "mongosh --quiet --port 27017 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + interval: 10s + timeout: 10s + retries: 12 + start_period: 30s + + docdb-read-1: + image: ${MONGO_IMAGE:-mongo:5.0} + container_name: mongo-read-1 + network_mode: "service:docdb" + depends_on: + docdb: + condition: service_healthy + volumes: + - docdb-host-read-1-data:/data/db + command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27018"] + healthcheck: + test: ["CMD-SHELL", "mongosh --quiet --port 27018 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + interval: 10s + timeout: 10s + retries: 12 + start_period: 30s + + docdb-read-2: + image: ${MONGO_IMAGE:-mongo:5.0} + container_name: mongo-read-2 + network_mode: "service:docdb" + depends_on: + docdb: + condition: service_healthy + volumes: + - docdb-host-read-2-data:/data/db + command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27019"] + healthcheck: + test: ["CMD-SHELL", "mongosh --quiet --port 27019 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + interval: 10s + timeout: 10s + retries: 12 + start_period: 30s + + mongo-init: + image: ${MONGO_IMAGE:-mongo:5.0} + network_mode: "service:docdb" + depends_on: + docdb: + condition: service_healthy + docdb-read-1: + condition: service_healthy + docdb-read-2: + condition: service_healthy + command: > + sh -c " + mongosh --host localhost --port 27017 --eval ' + const desiredConfig = { + _id: \"rs0\", + members: [ + { _id: 0, host: \"localhost:27017\", priority: 2 }, + { _id: 1, host: \"localhost:27018\", priority: 1 }, + { _id: 2, host: \"localhost:27019\", priority: 1 } + ] + }; + + function memberKey(member) { + return member._id + \":\" + member.host + \":\" + (member.priority ?? 1); + } + + function configMatches(currentConfig) { + const currentMembers = currentConfig.members.map(memberKey).sort().join(\"|\"); + const desiredMembers = desiredConfig.members.map(memberKey).sort().join(\"|\"); + return currentConfig._id === desiredConfig._id && currentMembers === desiredMembers; + } + + try { + rs.status(); + const currentConfig = rs.conf(); + if (configMatches(currentConfig)) { + print(\"Replica set already initialized with the expected localhost members.\"); + } else { + print(\"Updating replica set members...\"); + rs.reconfig({ ...desiredConfig, version: currentConfig.version + 1 }); + } + } catch (e) { + if (e.codeName == \"NotYetInitialized\") { + print(\"Initiating replica set...\"); + rs.initiate(desiredConfig); + } else { + throw e; + } + } + + for (let i = 0; i < 120; i++) { + let status; + try { + status = rs.status(); + } catch (e) { + print(\"Waiting for replica set status: \" + e.message); + sleep(2000); + continue; + } + + const primaryCount = status.members.filter(member => member.stateStr === \"PRIMARY\").length; + const secondaryCount = status.members.filter(member => member.stateStr === \"SECONDARY\").length; + + if (primaryCount === 1 && secondaryCount === 2) { + print(\"Replica set is ready with one primary and two secondaries.\"); + quit(0); + } + + print(\"Waiting for replica set readiness: primary=\" + primaryCount + \", secondary=\" + secondaryCount); + sleep(2000); + } + + throw new Error(\"Replica set did not become ready in time.\"); + ' + " + +volumes: + docdb-host-data: + docdb-host-read-1-data: + docdb-host-read-2-data: diff --git a/package.json b/package.json index 22f262218..c232ebb58 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "swagger-autogen": "node src/swagger.js", "test": "NODE_ENV=test mocha --recursive --exit || true", "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://docdb:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", + "test:integration:replicas": "NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node src/scripts/runMocha.js test/integration-tests --recursive --exit", "test:unit-tests": "NODE_ENV=test mocha test/unit-tests --recursive --exit || true", "test:coverage": "NODE_ENV=test nyc --reporter=text mocha src/* --recursive --exit || true", "test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true", diff --git a/src/scripts/runMocha.js b/src/scripts/runMocha.js new file mode 100644 index 000000000..178ab22d6 --- /dev/null +++ b/src/scripts/runMocha.js @@ -0,0 +1,67 @@ +const fs = require('fs') +const path = require('path') +const Mocha = require('mocha') + +function collectTestFiles (targetPath, recursive) { + const resolvedPath = path.resolve(targetPath) + const stat = fs.statSync(resolvedPath) + + if (stat.isFile()) { + return [resolvedPath] + } + + if (!stat.isDirectory()) { + return [] + } + + return fs.readdirSync(resolvedPath).flatMap(entry => { + const entryPath = path.join(resolvedPath, entry) + const entryStat = fs.statSync(entryPath) + + if (entryStat.isDirectory()) { + return recursive ? collectTestFiles(entryPath, recursive) : [] + } + + return entryPath.endsWith('.js') ? [entryPath] : [] + }) +} + +function parseArgs (argv) { + return argv.reduce((options, arg) => { + if (arg === '--recursive') { + options.recursive = true + } else if (arg === '--exit') { + options.exit = true + } else { + options.paths.push(arg) + } + + return options + }, { exit: false, paths: [], recursive: false }) +} + +async function run () { + const options = parseArgs(process.argv.slice(2)) + const mocha = new Mocha() + const testPaths = options.paths.length > 0 ? options.paths : ['test'] + + testPaths + .flatMap(testPath => collectTestFiles(testPath, options.recursive)) + .sort() + .forEach(testFile => mocha.addFile(testFile)) + + await mocha.loadFilesAsync() + + mocha.run(failures => { + process.exitCode = failures ? 1 : 0 + + if (options.exit) { + setImmediate(() => process.exit(process.exitCode)) + } + }) +} + +run().catch(err => { + console.error(err) + process.exitCode = 1 +}) diff --git a/src/utils/db.js b/src/utils/db.js index 6fde268f5..aef595f98 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -7,6 +7,12 @@ const logger = require('../middleware/logger') */ function getMongoConnectionString () { const appEnv = process.env.NODE_ENV + if (process.env.MONGO_CONN_STRING) { + logger.info(`Using NODE_ENV '${process.env.NODE_ENV}' and app environment '${appEnv}'`) + logger.info('Using MONGO_CONN_STRING override') + return process.env.MONGO_CONN_STRING + } + let dbUser, dbPassword if (process.env.MONGO_USER && process.env.MONGO_PASSWORD) { dbUser = process.env.MONGO_USER From 7c8b5778d91e314f4bbaa395000131a353df1c75 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Mon, 13 Jul 2026 14:50:10 -0400 Subject: [PATCH 04/23] Add environment-configurable Postman registry tests - Add sequential registry API collection for org/user, role, key reset, permission, and CVE-ID reservation flows - Add configurable Postman environment and README instructions for pointing the collection at local or remote API environments --- src/scripts/test_data/postman/README.md | 51 + ...-services-registry.postman_collection.json | 1522 +++++++++++++++++ .../cve-services.postman_environment.json | 30 + 3 files changed, 1603 insertions(+) create mode 100644 src/scripts/test_data/postman/README.md create mode 100644 src/scripts/test_data/postman/cve-services-registry.postman_collection.json create mode 100644 src/scripts/test_data/postman/cve-services.postman_environment.json diff --git a/src/scripts/test_data/postman/README.md b/src/scripts/test_data/postman/README.md new file mode 100644 index 000000000..b9613310e --- /dev/null +++ b/src/scripts/test_data/postman/README.md @@ -0,0 +1,51 @@ +# CVE Services Registry Postman Tests + +This folder contains a Postman collection and environment for exercising the registry API flow: + +- `cve-services-registry.postman_collection.json` +- `cve-services.postman_environment.json` + +## Default Setup + +The default environment assumes a local development database and API created with: + +```sh +npm run populate:dev; npm run migrate:dev; npm run dev +``` + +If you are targeting a different database or API host, update the imported environment values before running the collection. + +## Import Into Postman + +1. Open Postman. +2. Select **Import**. +3. Import `cve-services-registry.postman_collection.json`. +4. Import `cve-services.postman_environment.json`. +5. Select the imported **CVE Services Registry** environment. + +## Environment Values + +Confirm these environment values match the API you want to test: + +- `baseUrl` +- `apiUser` +- `apiOrg` +- `apiKey` + +The default values are intended for the local development setup above. + +## Run Sequentially + +Use the Postman Collection Runner rather than sending each request manually. + +1. Open the `CVE Services Registry Tests` collection. +2. Select **Run collection**. +3. Start with `Create Registry Org`. +4. Keep the collection order unchanged. +5. Run the collection with the `CVE Services Registry` environment selected. + +The collection uses `postman.setNextRequest` to move through the requests in order. If a setup request does not return the expected response, the runner stops instead of continuing with dependent requests. + +## CVE-ID Year + +The collection variable `registryCveYear` defaults to `2023`, which matches the pre-populated development data. Update that collection variable if the target database is configured for a different CVE-ID range year. diff --git a/src/scripts/test_data/postman/cve-services-registry.postman_collection.json b/src/scripts/test_data/postman/cve-services-registry.postman_collection.json new file mode 100644 index 000000000..b99b7dd41 --- /dev/null +++ b/src/scripts/test_data/postman/cve-services-registry.postman_collection.json @@ -0,0 +1,1522 @@ +{ + "info": { + "name": "CVE Services Registry Tests", + "description": "Registry API tests for the flow previously covered by src/scripts/test_data/generate.js: create a CNA org, create users, and grant an admin role.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "['baseUrl', 'apiUser', 'apiOrg', 'apiKey'].forEach((key) => {", + " if (!pm.environment.get(key)) {", + " throw new Error(`${key} must be set in the selected Postman environment.`)", + " }", + "})" + ] + } + } + ], + "item": [ + { + "name": "Create Registry Org", + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`.toLowerCase()", + "const orgShortName = `ptest_${runId}`.slice(0, 32)", + "const adminUsername = `admin_${runId}@postman-registry.example`", + "const userUsername = `user_${runId}@postman-registry.example`", + "const additionalUserUsername = `extra_${runId}@postman-registry.example`", + "", + "pm.collectionVariables.set('registryRunId', runId)", + "pm.collectionVariables.set('registryOrgShortName', orgShortName)", + "pm.collectionVariables.set('registryOrgLongName', `Postman Registry CNA ${runId} (fake)`)", + "pm.collectionVariables.set('registryAdminUsername', adminUsername)", + "pm.collectionVariables.set('registryAdminUsernameEncoded', encodeURIComponent(adminUsername))", + "pm.collectionVariables.set('registryUserUsername', userUsername)", + "pm.collectionVariables.set('registryUserUsernameEncoded', encodeURIComponent(userUsername))", + "pm.collectionVariables.set('registryAdditionalUserUsername', additionalUserUsername)", + "pm.collectionVariables.set('registryAdminUpdateWebsite', `https://postman-registry.example/admin-update/${runId}`)" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedShortName = pm.collectionVariables.get('registryOrgShortName')", + "const expectedLongName = pm.collectionVariables.get('registryOrgLongName')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.created && body.created.UUID && body.created.short_name === expectedShortName", + "postman.setNextRequest(canContinue ? 'Create Admin User' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes the created registry org', () => {", + " pm.expect(body).to.have.property('message', `${expectedShortName} organization was successfully created.`)", + " pm.expect(body).to.have.property('created')", + " pm.expect(body.created).to.have.property('UUID').that.is.a('string')", + " pm.expect(body.created).to.have.property('short_name', expectedShortName)", + " pm.expect(body.created).to.have.property('long_name', expectedLongName)", + " pm.expect(body.created).to.have.property('authority').that.includes('CNA')", + " pm.expect(body.created).to.have.property('id_quota', 500)", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryOrgUuid', body.created.UUID)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"partner_number\": \"CNA-POSTMAN-REGISTRY-{{registryRunId}}\",\n \"top_level_root\": \"MITRE TLR\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"program_data\": {\n \"status\": \"active\",\n \"partner_active_date\": \"2015-03-15\",\n \"cve_website_update_needed\": false,\n \"cve_website_update_date\": \"2024-11-01\"\n },\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org" + ] + }, + "description": "Creates a representative CNA org from the old test data shape." + } + }, + { + "name": "Create Admin User", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryAdminUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.created && body.created.UUID && body.created.username === expectedUsername && body.created.secret", + "postman.setNextRequest(canContinue ? 'Create Regular User' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes the created admin-pattern user', () => {", + " pm.expect(body).to.have.property('message', `${expectedUsername} was successfully created.`)", + " pm.expect(body).to.have.property('created')", + " pm.expect(body.created).to.have.property('UUID').that.is.a('string')", + " pm.expect(body.created).to.have.property('username', expectedUsername)", + " pm.expect(body.created).to.have.nested.property('name.first', 'Jane')", + " pm.expect(body.created).to.have.nested.property('name.last', 'Holloway')", + " pm.expect(body.created).to.have.property('status', 'active')", + " pm.expect(body.created).to.have.property('secret').that.is.a('string').and.is.not.empty", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryAdminUserUuid', body.created.UUID)", + " pm.collectionVariables.set('registryAdminOrg', pm.collectionVariables.get('registryOrgShortName'))", + " pm.collectionVariables.set('registryAdminApiKey', body.created.secret)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": {\n \"first\": \"Jane\",\n \"last\": \"Holloway\"\n },\n \"status\": \"active\",\n \"username\": \"{{registryAdminUsername}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user" + ] + }, + "description": "Creates the user that will receive the ADMIN role." + } + }, + { + "name": "Create Regular User", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryUserUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.created && body.created.UUID && body.created.username === expectedUsername && body.created.secret", + "postman.setNextRequest(canContinue ? 'Grant Admin Role' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes the created regular user', () => {", + " pm.expect(body).to.have.property('message', `${expectedUsername} was successfully created.`)", + " pm.expect(body).to.have.property('created')", + " pm.expect(body.created).to.have.property('UUID').that.is.a('string')", + " pm.expect(body.created).to.have.property('username', expectedUsername)", + " pm.expect(body.created).to.have.nested.property('name.first', 'Brian')", + " pm.expect(body.created).to.have.nested.property('name.last', 'Stokes')", + " pm.expect(body.created).to.have.property('status', 'active')", + " pm.expect(body.created).to.have.property('secret').that.is.a('string').and.is.not.empty", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryUserUuid', body.created.UUID)", + " pm.collectionVariables.set('registryUserOriginalApiKey', body.created.secret)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": {\n \"first\": \"Brian\",\n \"last\": \"Stokes\"\n },\n \"status\": \"active\",\n \"username\": \"{{registryUserUsername}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user" + ] + }, + "description": "Creates a non-admin user from the same representative org." + } + }, + { + "name": "Grant Admin Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryAdminUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const expectedMessage = `Role ADMIN granted to user ${expectedUsername}.`", + "const canContinue = pm.response.code === 200 && body.message === expectedMessage", + "postman.setNextRequest(canContinue ? 'Admin Cannot Set Secretariat-Only Org Fields' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response confirms the ADMIN grant', () => {", + " pm.expect(body).to.have.property('message', expectedMessage)", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"ADMIN\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryAdminUsernameEncoded}}/grant-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryAdminUsernameEncoded}}", + "grant-role" + ] + }, + "description": "Grants ADMIN to the admin-pattern registry user." + } + }, + { + "name": "Admin Cannot Set Secretariat-Only Org Fields", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 403 && body.error === 'SECRETARIAT_ONLY'", + "postman.setNextRequest(canContinue ? 'Registry Org Update Rejects Additional Key' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})", + "", + "pm.test('response identifies secretariat-only fields', () => {", + " pm.expect(body).to.have.property('error', 'SECRETARIAT_ONLY')", + " pm.expect(body.message || '').to.include('program_data')", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"id_quota\": 500,\n \"program_data\": {\n \"status\": \"active\"\n }\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}" + ] + }, + "description": "Verifies an org admin cannot update fields reserved for Secretariat users." + } + }, + { + "name": "Registry Org Update Rejects Additional Key", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const firstError = body.errors && body.errors[0]", + "const canContinue = pm.response.code === 400 && body.message === 'Parameters were invalid' && firstError && firstError.message === 'must NOT have additional properties'", + "postman.setNextRequest(canContinue ? 'Registry User Create Rejects Additional Key' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects the additional org key', () => {", + " pm.expect(body).to.have.property('message', 'Parameters were invalid')", + " pm.expect(firstError).to.have.property('message', 'must NOT have additional properties')", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{apiUser}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{apiOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{apiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\",\n \"test\": \"additional key not in schema\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}" + ] + }, + "description": "Verifies registry org updates reject keys outside the schema." + } + }, + { + "name": "Registry User Create Rejects Additional Key", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const firstError = body.errors && body.errors[0]", + "const canContinue = pm.response.code === 400 && body.message === 'Parameters were invalid' && firstError && firstError.message === 'must NOT have additional properties'", + "postman.setNextRequest(canContinue ? 'Update Own Registry Org as Admin' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects the additional user key', () => {", + " pm.expect(body).to.have.property('message', 'Parameters were invalid')", + " pm.expect(firstError).to.have.property('message', 'must NOT have additional properties')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": {\n \"first\": \"Extra\",\n \"last\": \"User\"\n },\n \"status\": \"active\",\n \"username\": \"{{registryAdditionalUserUsername}}\",\n \"test\": \"additional key not in schema\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user" + ] + }, + "description": "Verifies registry user creation rejects keys outside the schema." + } + }, + { + "name": "Update Own Registry Org as Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedShortName = pm.collectionVariables.get('registryOrgShortName')", + "const expectedWebsite = pm.collectionVariables.get('registryAdminUpdateWebsite')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const updatedWebsites = body.updated && body.updated.contact_info && body.updated.contact_info.websites", + "const canContinue = pm.response.code === 200 && body.updated && body.updated.short_name === expectedShortName && Array.isArray(updatedWebsites) && updatedWebsites.includes(expectedWebsite)", + "postman.setNextRequest(canContinue ? 'Reset Regular User Secret as Admin' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('admin-auth update targets the created registry org', () => {", + " pm.expect(body).to.have.property('updated')", + " pm.expect(body.updated).to.have.property('short_name', expectedShortName)", + "})", + "", + "pm.test('admin-auth update includes the requested website', () => {", + " pm.expect(body.updated).to.have.nested.property('contact_info.websites')", + " pm.expect(body.updated.contact_info.websites).to.include(expectedWebsite)", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"{{registryAdminUpdateWebsite}}\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}" + ] + }, + "description": "Uses the created admin user's credentials to update that user's own organization." + } + }, + { + "name": "Reset Regular User Secret as Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body['API-secret']", + "postman.setNextRequest(canContinue ? 'Old Regular User Key Is Rejected' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes a new API secret', () => {", + " pm.expect(body).to.have.property('API-secret').that.is.a('string').and.is.not.empty", + "})", + "", + "if (pm.response.code === 200 && body['API-secret']) {", + " pm.collectionVariables.set('registryUserApiKey', body['API-secret'])", + "}" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}/reset_secret", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}", + "reset_secret" + ] + }, + "description": "Uses the created admin user's credentials to reset the regular user's API secret." + } + }, + { + "name": "Old Regular User Key Is Rejected", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 401", + "postman.setNextRequest(canContinue ? 'Use Reset Regular User Key' : null)", + "", + "pm.test('status is 401', () => {", + " pm.response.to.have.status(401)", + "})", + "", + "pm.test('response rejects the old API key', () => {", + " pm.expect(body.error || 'UNAUTHORIZED').to.equal('UNAUTHORIZED')", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserOriginalApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}" + ] + }, + "description": "Verifies the regular user's original API key no longer works after reset_secret." + } + }, + { + "name": "Use Reset Regular User Key", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedUsername = pm.collectionVariables.get('registryUserUsername')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.username === expectedUsername", + "postman.setNextRequest(canContinue ? 'Regular User Cannot Reset Admin Secret' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('regular user can authenticate with the reset API key', () => {", + " pm.expect(body).to.have.property('username', expectedUsername)", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}" + ] + }, + "description": "Uses the reset regular user's credentials to retrieve that user's registry profile." + } + }, + { + "name": "Regular User Cannot Reset Admin Secret", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const canContinue = pm.response.code === 403", + "postman.setNextRequest(canContinue ? 'Regular User Cannot Grant Admin Role' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryAdminUsernameEncoded}}/reset_secret", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryAdminUsernameEncoded}}", + "reset_secret" + ] + }, + "description": "Verifies a regular user cannot reset the admin user's API secret." + } + }, + { + "name": "Regular User Cannot Grant Admin Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 403 && body.error === 'NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE'", + "postman.setNextRequest(canContinue ? 'Regular User Cannot Deactivate Self' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})", + "", + "pm.test('response identifies missing admin privileges', () => {", + " pm.expect(body).to.have.property('error', 'NOT_ORG_ADMIN_OR_SECRETARIAT_UPDATE')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"ADMIN\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}/grant-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}", + "grant-role" + ] + }, + "description": "Verifies a regular user cannot grant themselves the ADMIN role." + } + }, + { + "name": "Regular User Cannot Deactivate Self", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 400 && body.error === 'NOT_ALLOWED_TO_CHANGE_FIELD'", + "postman.setNextRequest(canContinue ? 'Created Admin Cannot Revoke Own Admin Role' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects regular-user status changes', () => {", + " pm.expect(body).to.have.property('error', 'NOT_ALLOWED_TO_CHANGE_FIELD')", + "})" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryUserUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryOrgShortName}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryUserApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"inactive\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}" + ] + }, + "description": "Verifies a regular user cannot deactivate their own account." + } + }, + { + "name": "Created Admin Cannot Revoke Own Admin Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 403 && body.error === 'NOT_ALLOWED_TO_SELF_DEMOTE'", + "postman.setNextRequest(canContinue ? 'Created Admin Cannot Grant Invalid Role' : null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})", + "", + "pm.test('response rejects admin self-demotion', () => {", + " pm.expect(body).to.have.property('error', 'NOT_ALLOWED_TO_SELF_DEMOTE')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"ADMIN\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryAdminUsernameEncoded}}/revoke-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryAdminUsernameEncoded}}", + "revoke-role" + ] + }, + "description": "Verifies the created admin cannot revoke their own ADMIN role." + } + }, + { + "name": "Created Admin Cannot Grant Invalid Role", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 400 && body.error === 'BAD_INPUT'", + "postman.setNextRequest(canContinue ? 'Reserve CVE-ID as Created Admin' : null)", + "", + "pm.test('status is 400', () => {", + " pm.response.to.have.status(400)", + "})", + "", + "pm.test('response rejects unsupported roles', () => {", + " pm.expect(body).to.have.property('error', 'BAD_INPUT')", + " pm.expect(body.message || '').to.include('Invalid role request')", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"MAGNANIMOUS\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}/user/{{registryUserUsernameEncoded}}/grant-role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "registry", + "org", + "{{registryOrgShortName}}", + "user", + "{{registryUserUsernameEncoded}}", + "grant-role" + ] + }, + "description": "Verifies role grants reject unsupported role names." + } + }, + { + "name": "Reserve CVE-ID as Created Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedYear = String(pm.collectionVariables.get('registryCveYear'))", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const reservedCveId = body.cve_ids && body.cve_ids[0] && body.cve_ids[0].cve_id", + "const canContinue = pm.response.code === 200 && reservedCveId", + "postman.setNextRequest(canContinue ? 'Get Reserved CVE-ID as Created Admin' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('response includes one reserved CVE-ID', () => {", + " pm.expect(body).to.have.property('cve_ids').that.is.an('array').with.lengthOf(1)", + " pm.expect(reservedCveId).to.match(new RegExp(`^CVE-${expectedYear}-[0-9]{4,}$`))", + "})", + "", + "if (canContinue) {", + " pm.collectionVariables.set('registryReservedCveId', reservedCveId)", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/cve-id?amount=1&cve_year={{registryCveYear}}&short_name={{registryOrgShortName}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "cve-id" + ], + "query": [ + { + "key": "amount", + "value": "1" + }, + { + "key": "cve_year", + "value": "{{registryCveYear}}" + }, + { + "key": "short_name", + "value": "{{registryOrgShortName}}" + } + ] + }, + "description": "Uses the created admin user's credentials to reserve one CVE-ID for that user's own organization." + } + }, + { + "name": "Get Reserved CVE-ID as Created Admin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const expectedCveId = pm.collectionVariables.get('registryReservedCveId')", + "const expectedOrg = pm.collectionVariables.get('registryOrgShortName')", + "let body = {}", + "", + "try {", + " body = pm.response.json()", + "} catch (error) {}", + "", + "const canContinue = pm.response.code === 200 && body.cve_id === expectedCveId && body.owning_cna === expectedOrg", + "postman.setNextRequest(canContinue ? 'Created Admin Cannot Reserve CVE-ID for Another Org' : null)", + "", + "pm.test('status is 200', () => {", + " pm.response.to.have.status(200)", + "})", + "", + "pm.test('owning admin can see reserved CVE-ID details', () => {", + " pm.expect(body).to.have.property('cve_id', expectedCveId)", + " pm.expect(body).to.have.property('state', 'RESERVED')", + " pm.expect(body).to.have.property('owning_cna', expectedOrg)", + "})" + ] + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/cve-id/{{registryReservedCveId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "cve-id", + "{{registryReservedCveId}}" + ] + }, + "description": "Retrieves the reserved CVE-ID as the owning organization admin." + } + }, + { + "name": "Created Admin Cannot Reserve CVE-ID for Another Org", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "postman.setNextRequest(null)", + "", + "pm.test('status is 403', () => {", + " pm.response.to.have.status(403)", + "})" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "CVE-API-USER", + "value": "{{registryAdminUsername}}" + }, + { + "key": "CVE-API-ORG", + "value": "{{registryAdminOrg}}" + }, + { + "key": "CVE-API-KEY", + "value": "{{registryAdminApiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/cve-id?amount=1&cve_year={{registryCveYear}}&short_name={{apiOrg}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "cve-id" + ], + "query": [ + { + "key": "amount", + "value": "1" + }, + { + "key": "cve_year", + "value": "{{registryCveYear}}" + }, + { + "key": "short_name", + "value": "{{apiOrg}}" + } + ] + }, + "description": "Verifies the created admin cannot reserve CVE-IDs for another organization." + } + } + ], + "variable": [ + { + "key": "registryRunId", + "value": "" + }, + { + "key": "registryOrgShortName", + "value": "" + }, + { + "key": "registryOrgLongName", + "value": "" + }, + { + "key": "registryOrgUuid", + "value": "" + }, + { + "key": "registryAdminUsername", + "value": "" + }, + { + "key": "registryAdminUsernameEncoded", + "value": "" + }, + { + "key": "registryAdminOrg", + "value": "" + }, + { + "key": "registryAdminApiKey", + "value": "" + }, + { + "key": "registryAdminUserUuid", + "value": "" + }, + { + "key": "registryUserUsername", + "value": "" + }, + { + "key": "registryUserUsernameEncoded", + "value": "" + }, + { + "key": "registryAdditionalUserUsername", + "value": "" + }, + { + "key": "registryUserUuid", + "value": "" + }, + { + "key": "registryUserApiKey", + "value": "" + }, + { + "key": "registryUserOriginalApiKey", + "value": "" + }, + { + "key": "registryAdminUpdateWebsite", + "value": "" + }, + { + "key": "registryCveYear", + "value": "2023" + }, + { + "key": "registryReservedCveId", + "value": "" + } + ] +} diff --git a/src/scripts/test_data/postman/cve-services.postman_environment.json b/src/scripts/test_data/postman/cve-services.postman_environment.json new file mode 100644 index 000000000..edc0dc48e --- /dev/null +++ b/src/scripts/test_data/postman/cve-services.postman_environment.json @@ -0,0 +1,30 @@ +{ + "name": "CVE Services Registry", + "values": [ + { + "key": "baseUrl", + "value": "http://localhost:3000", + "type": "default", + "enabled": true + }, + { + "key": "apiUser", + "value": "test_secretariat_0@mitre.org", + "type": "default", + "enabled": true + }, + { + "key": "apiOrg", + "value": "mitre", + "type": "default", + "enabled": true + }, + { + "key": "apiKey", + "value": "", + "type": "secret", + "enabled": true + } + ], + "_postman_variable_scope": "environment" +} From 1dbc06a011f91e59593ebc779bbdd32d19ea4f42 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 2 Jul 2026 09:27:07 -0400 Subject: [PATCH 05/23] Fix user UUID lookup option forwarding --- src/repositories/userRepository.js | 2 +- test/unit-tests/user/userRepositoryTest.js | 28 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test/unit-tests/user/userRepositoryTest.js diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js index 9cd152b28..fd04eeb6f 100644 --- a/src/repositories/userRepository.js +++ b/src/repositories/userRepository.js @@ -8,7 +8,7 @@ class UserRepository extends BaseRepository { async getUserUUID (userName, orgUUID, options = {}) { const utils = require('../utils/utils') - return utils.getUserUUID(userName, orgUUID, options) + return utils.getUserUUID(userName, orgUUID, false, options) } async isAdmin (username, shortname, options = {}) { diff --git a/test/unit-tests/user/userRepositoryTest.js b/test/unit-tests/user/userRepositoryTest.js new file mode 100644 index 000000000..2f283e5e2 --- /dev/null +++ b/test/unit-tests/user/userRepositoryTest.js @@ -0,0 +1,28 @@ +const { expect } = require('chai') +const sinon = require('sinon') + +const UserRepository = require('../../../src/repositories/userRepository') +const utils = require('../../../src/utils/utils') + +describe('Testing UserRepository', () => { + afterEach(() => { + sinon.restore() + }) + + it('Should forward options as the fourth argument when getting a user UUID', async () => { + const userRepo = new UserRepository() + const options = { session: 'mock-session' } + const getUserUUID = sinon.stub(utils, 'getUserUUID').resolves('user-uuid') + + const result = await userRepo.getUserUUID('user@example.org', 'org-uuid', options) + + expect(result).to.equal('user-uuid') + expect(getUserUUID.calledOnce).to.equal(true) + expect(getUserUUID.firstCall.args).to.deep.equal([ + 'user@example.org', + 'org-uuid', + false, + options + ]) + }) +}) From ddfce24a0f76bbb3b1c66d51a61cc0ada9c6d93c Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 2 Jul 2026 09:48:58 -0400 Subject: [PATCH 06/23] Fix auth context prototype method detection --- src/utils/authContext.js | 10 +-- test/unit-tests/org/orgGetSingleTest.js | 8 ++- test/unit-tests/user/userResetSecretTest.js | 10 ++- test/unit-tests/utils/authContextTest.js | 72 +++++++++++++++++++++ 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/src/utils/authContext.js b/src/utils/authContext.js index 17d5f2799..036c12a3d 100644 --- a/src/utils/authContext.js +++ b/src/utils/authContext.js @@ -40,8 +40,8 @@ function isBaseUserRepository (userRepo) { typeof userRepo?.findOneByUsernameAndOrgShortname === 'function' } -function hasOwnMethod (obj, methodName) { - return Object.prototype.hasOwnProperty.call(obj || {}, methodName) && typeof obj[methodName] === 'function' +function hasMethod (obj, methodName) { + return obj != null && typeof obj[methodName] === 'function' } function isAuthenticatedRequest (req) { @@ -289,7 +289,7 @@ async function isRequesterSecretariat (req, orgRepo, options = {}, returnLegacyF return false } - if (hasOwnMethod(orgRepo, 'isSecretariatByShortName')) { + if (hasMethod(orgRepo, 'isSecretariatByShortName')) { return orgRepo.isSecretariatByShortName(req.ctx.org, options, returnLegacyFormat) } @@ -400,11 +400,11 @@ async function isRequesterAdminOfOrg (req, userRepo, orgRepo, targetOrgOrShortNa } if (!req.ctx.orgUUID && !req.ctx.userUUID) { - if (hasOwnMethod(userRepo, 'isAdminOrSecretariat')) { + if (hasMethod(userRepo, 'isAdminOrSecretariat')) { return userRepo.isAdminOrSecretariat(fallbackTargetShortName, req.ctx.user, req.ctx.org, options, isRegistryObject) } - if (hasOwnMethod(userRepo, 'isAdmin')) { + if (hasMethod(userRepo, 'isAdmin')) { return userRepo.isAdmin(req.ctx.user, fallbackTargetShortName, options, isRegistryObject) } } diff --git a/test/unit-tests/org/orgGetSingleTest.js b/test/unit-tests/org/orgGetSingleTest.js index 06821217f..e3e8ead08 100644 --- a/test/unit-tests/org/orgGetSingleTest.js +++ b/test/unit-tests/org/orgGetSingleTest.js @@ -41,7 +41,7 @@ const fakeRegularOrgDocument = new BaseOrg(orgFixtures.regularOrg) const fakeTargetOrgDocument = new BaseOrg(orgFixtures.targetOrg) describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { - let status, json, res, next, mockSession, baseOrgRepo, getBaseOrgRepository, req + let status, json, res, next, mockSession, baseOrgRepo, getBaseOrgRepository, req, isSecretariatByShortName beforeEach(() => { status = sinon.stub() @@ -61,6 +61,7 @@ describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { baseOrgRepo = new BaseOrgRepository() getBaseOrgRepository = sinon.stub().returns(baseOrgRepo) + isSecretariatByShortName = sinon.stub(baseOrgRepo, 'isSecretariatByShortName') req = { ctx: { @@ -85,6 +86,7 @@ describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { it('Org does not exist', async () => { req.ctx.params.identifier = 'nonexistent-org' sinon.stub(baseOrgRepo, 'findOneByShortName').resolves(fakeSecretariatOrgDocument) + isSecretariatByShortName.resolves(true) sinon.stub(baseOrgRepo, 'isSecretariat').resolves(true) sinon.stub(baseOrgRepo, 'getOrg').resolves(null) @@ -100,6 +102,7 @@ describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { req.ctx.org = orgFixtures.regularOrg.short_name // Regular org req.ctx.params.identifier = orgFixtures.targetOrg.short_name sinon.stub(baseOrgRepo, 'findOneByShortName').resolves(fakeRegularOrgDocument) + isSecretariatByShortName.resolves(false) sinon.stub(baseOrgRepo, 'isSecretariat').resolves(false) await ORG_SINGLE(req, res, next) @@ -114,6 +117,7 @@ describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { // UUID format is invalid and will cause the controller to search by short name req.ctx.params.identifier = 'invalid-uuid-123' sinon.stub(baseOrgRepo, 'findOneByShortName').resolves(fakeSecretariatOrgDocument) + isSecretariatByShortName.resolves(true) sinon.stub(baseOrgRepo, 'isSecretariat').resolves(true) sinon.stub(baseOrgRepo, 'getOrg').resolves(null) @@ -128,6 +132,7 @@ describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { it('Secretariat can access any org by shortname', async () => { // Org exists and requester is secretariat sinon.stub(baseOrgRepo, 'findOneByShortName').resolves(fakeSecretariatOrgDocument) + isSecretariatByShortName.resolves(true) sinon.stub(baseOrgRepo, 'isSecretariat').resolves(true) sinon.stub(baseOrgRepo, 'getOrg').resolves(orgFixtures.targetOrg) @@ -176,6 +181,7 @@ describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { it('Secretariat can access an org by UUID', async () => { req.ctx.params.identifier = orgFixtures.targetOrg.UUID sinon.stub(baseOrgRepo, 'findOneByShortName').resolves(fakeSecretariatOrgDocument) + isSecretariatByShortName.resolves(true) sinon.stub(baseOrgRepo, 'isSecretariat').resolves(true) sinon.stub(baseOrgRepo, 'getOrg').resolves(orgFixtures.targetOrg) diff --git a/test/unit-tests/user/userResetSecretTest.js b/test/unit-tests/user/userResetSecretTest.js index 872f554f3..a7b87d680 100644 --- a/test/unit-tests/user/userResetSecretTest.js +++ b/test/unit-tests/user/userResetSecretTest.js @@ -22,7 +22,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', let status, json, res, next, getOrgRepository, orgRepo, getUserRepository, userRepo, mockSession, orgUUIDStub, regOrgUUIDStub, userUUIDStub, regUserUUIDStub, isSecretariatStub, isAdminStub, findOneUserStub, updateUserStub, - isRegSecretariatStub, isRegAdminStub, baseOrgRepo, getBaseOrgRepository, baseUserRepo, getBaseUserRepository, isSecretariatByShortName + isRegSecretariatStub, isRegAdminStub, isRegAdminOrSecretariatStub, baseOrgRepo, getBaseOrgRepository, baseUserRepo, getBaseUserRepository, isSecretariatByShortName beforeEach(() => { // Mock Express response objects @@ -65,6 +65,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', isRegSecretariatStub = sinon.stub(baseOrgRepo, 'isSecretariat') isSecretariatByShortName = sinon.stub(baseOrgRepo, 'isSecretariatByShortName') isRegAdminStub = sinon.stub(baseUserRepo, 'isAdmin') + isRegAdminOrSecretariatStub = sinon.stub(baseUserRepo, 'isAdminOrSecretariat') regOrgUUIDStub = sinon.stub(baseOrgRepo, 'getOrgUUID') regUserUUIDStub = sinon.stub(baseUserRepo, 'getUserUUID') }) @@ -136,6 +137,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', isSecretariatByShortName.resolves(false) isRegSecretariatStub.resolves(false) isRegAdminStub.resolves(false) + isRegAdminOrSecretariatStub.resolves(false) regUserUUIDStub.onFirstCall().resolves(userFixtures.existentUser.UUID) regUserUUIDStub.onSecondCall().resolves('FakeUUID') @@ -275,9 +277,15 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', isRegSecretariatStub.resolves(false) isAdminStub.resolves(true) isRegAdminStub.resolves(true) +<<<<<<< HEAD regUserUUIDStub.resolves(userFixtures.userC.UUID) sinon.stub(baseUserRepo, 'isUserAdminOfOrgUUID').resolves(true) sinon.stub(baseOrgRepo, 'findOneByUUID').resolves({ ...userFixtures.existentOrgDummy, admins: [userFixtures.userD.UUID] }) +======= + isRegAdminOrSecretariatStub.resolves(true) + regUserUUIDStub.onFirstCall().resolves(userFixtures.userC.UUID) + regUserUUIDStub.onSecondCall().resolves(userFixtures.userA.UUID) +>>>>>>> 82149d3d (Fix auth context prototype method detection) sinon.stub(baseUserRepo, 'resetSecret').resolves('ANEWUUID') const req = { diff --git a/test/unit-tests/utils/authContextTest.js b/test/unit-tests/utils/authContextTest.js index e59d13863..d8e62f10b 100644 --- a/test/unit-tests/utils/authContextTest.js +++ b/test/unit-tests/utils/authContextTest.js @@ -101,4 +101,76 @@ describe('Testing authContext requester helpers', () => { expect(await authContext.isRequesterAdmin(req, userRepo, orgRepo)).to.equal(true) expect(await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, { UUID: 'target-org-uuid', admins: ['user-uuid'] })).to.equal(true) }) + + it('Should detect Secretariat lookup methods defined on an ES6 class prototype', async () => { + req.ctx.authenticationChecked = false + const options = { session: 'mock-session' } + + class PrototypeOrgRepo { + constructor () { + this.calls = [] + } + + async isSecretariatByShortName (...args) { + this.calls.push(args) + return true + } + } + + const prototypeOrgRepo = new PrototypeOrgRepo() + + expect(Object.prototype.hasOwnProperty.call(prototypeOrgRepo, 'isSecretariatByShortName')).to.equal(false) + expect(await authContext.isRequesterSecretariat(req, prototypeOrgRepo, options, true)).to.equal(true) + expect(prototypeOrgRepo.calls).to.deep.equal([ + ['mitre', options, true] + ]) + }) + + it('Should detect admin-or-Secretariat lookup methods defined on an ES6 class prototype', async () => { + req.ctx.authenticationChecked = false + const options = { session: 'mock-session' } + + class PrototypeUserRepo { + constructor () { + this.calls = [] + } + + async isAdminOrSecretariat (...args) { + this.calls.push(args) + return true + } + } + + const prototypeUserRepo = new PrototypeUserRepo() + + expect(Object.prototype.hasOwnProperty.call(prototypeUserRepo, 'isAdminOrSecretariat')).to.equal(false) + expect(await authContext.isRequesterAdminOfOrg(req, prototypeUserRepo, orgRepo, 'target-org', options, true)).to.equal(true) + expect(prototypeUserRepo.calls).to.deep.equal([ + ['target-org', 'admin-user', 'mitre', options, true] + ]) + }) + + it('Should detect admin lookup fallback methods defined on an ES6 class prototype', async () => { + req.ctx.authenticationChecked = false + const options = { session: 'mock-session' } + + class PrototypeUserRepo { + constructor () { + this.calls = [] + } + + async isAdmin (...args) { + this.calls.push(args) + return true + } + } + + const prototypeUserRepo = new PrototypeUserRepo() + + expect(Object.prototype.hasOwnProperty.call(prototypeUserRepo, 'isAdmin')).to.equal(false) + expect(await authContext.isRequesterAdminOfOrg(req, prototypeUserRepo, orgRepo, 'target-org', options, true)).to.equal(true) + expect(prototypeUserRepo.calls).to.deep.equal([ + ['admin-user', 'target-org', options, true] + ]) + }) }) From 40b7c342c4e5a94e218e962ecc20dde2534a39d9 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 2 Jul 2026 10:00:13 -0400 Subject: [PATCH 07/23] Fix legacy org selected short name lookup --- src/repositories/baseOrgRepository.js | 3 ++- test/unit-tests/org/baseOrgRepositoryTest.js | 21 +++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index f155a1c01..5591e8e96 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -179,7 +179,8 @@ class BaseOrgRepository extends BaseRepository { */ async findOneByShortNameWithSelect (shortName, select, options = {}, returnLegacyFormat = false) { const OrgRepository = require('./orgRepository') - if (returnLegacyFormat) return await OrgRepository.findOneByShortName(shortName, options) + const legacyOrgRepo = new OrgRepository() + if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options, select) return await BaseOrgModel.findOne({ short_name: shortName }, null, options).select(select) } diff --git a/test/unit-tests/org/baseOrgRepositoryTest.js b/test/unit-tests/org/baseOrgRepositoryTest.js index e5e578e71..cad1297d7 100644 --- a/test/unit-tests/org/baseOrgRepositoryTest.js +++ b/test/unit-tests/org/baseOrgRepositoryTest.js @@ -3,8 +3,9 @@ const sinon = require('sinon') const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository') const BaseOrgModel = require('../../../src/model/baseorg') +const OrgRepository = require('../../../src/repositories/orgRepository') -describe('Testing BaseOrgRepository lookup queries', () => { +describe('Testing BaseOrgRepository', () => { afterEach(() => { sinon.restore() }) @@ -50,4 +51,22 @@ describe('Testing BaseOrgRepository lookup queries', () => { expect(query.$and[0].short_name.$not).to.be.instanceOf(RegExp) expect(query.$and[0].short_name.$not.source).to.equal('^Existing\\.Org$') }) + + it('Should use the legacy OrgRepository instance for selected short name lookups in legacy format', async () => { + const baseOrgRepo = new BaseOrgRepository() + const options = { session: 'mock-session' } + const select = 'UUID short_name' + const org = { UUID: 'org-uuid', short_name: 'mitre' } + const findOneByShortName = sinon.stub(OrgRepository.prototype, 'findOneByShortName').resolves(org) + + const result = await baseOrgRepo.findOneByShortNameWithSelect('mitre', select, options, true) + + expect(result).to.equal(org) + expect(findOneByShortName.calledOnce).to.equal(true) + expect(findOneByShortName.firstCall.args).to.deep.equal([ + 'mitre', + options, + select + ]) + }) }) From 12b44e0841d667be0e84f8626e366e6706d0bd0e Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 2 Jul 2026 10:13:27 -0400 Subject: [PATCH 08/23] Fix CVE rejection test requester lookup --- test/unit-tests/cve/cveRecordRejectionTest.js | 9 ++++++++- test/unit-tests/user/userResetSecretTest.js | 6 ------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/unit-tests/cve/cveRecordRejectionTest.js b/test/unit-tests/cve/cveRecordRejectionTest.js index 0f3637e46..a38444317 100644 --- a/test/unit-tests/cve/cveRecordRejectionTest.js +++ b/test/unit-tests/cve/cveRecordRejectionTest.js @@ -43,13 +43,20 @@ class MyOrg { async getOrgUUID (shortName) { if (shortName === cveFixtures.regularOrg.short_name) { return cveFixtures.regularOrg.UUID + } else if (shortName === cveFixtures.secretariatOrg.short_name) { + return cveFixtures.secretariatOrg.UUID } return null } } class MyUser { - async getUserUUID () { + async getUserUUID (username, orgUUID) { + if (username === cveFixtures.regularUser.username && orgUUID === cveFixtures.regularOrg.UUID) { + return cveFixtures.regularUser.UUID + } else if (username === cveFixtures.secretariatUser.username && orgUUID === cveFixtures.secretariatOrg.UUID) { + return cveFixtures.secretariatUser.UUID + } return null } } diff --git a/test/unit-tests/user/userResetSecretTest.js b/test/unit-tests/user/userResetSecretTest.js index a7b87d680..a0e500bb8 100644 --- a/test/unit-tests/user/userResetSecretTest.js +++ b/test/unit-tests/user/userResetSecretTest.js @@ -277,15 +277,9 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', isRegSecretariatStub.resolves(false) isAdminStub.resolves(true) isRegAdminStub.resolves(true) -<<<<<<< HEAD regUserUUIDStub.resolves(userFixtures.userC.UUID) sinon.stub(baseUserRepo, 'isUserAdminOfOrgUUID').resolves(true) sinon.stub(baseOrgRepo, 'findOneByUUID').resolves({ ...userFixtures.existentOrgDummy, admins: [userFixtures.userD.UUID] }) -======= - isRegAdminOrSecretariatStub.resolves(true) - regUserUUIDStub.onFirstCall().resolves(userFixtures.userC.UUID) - regUserUUIDStub.onSecondCall().resolves(userFixtures.userA.UUID) ->>>>>>> 82149d3d (Fix auth context prototype method detection) sinon.stub(baseUserRepo, 'resetSecret').resolves('ANEWUUID') const req = { From abdd85195ae7ac570c7521dc6eb4b43bee1addf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:48:22 +0000 Subject: [PATCH 09/23] Bump morgan from 1.10.1 to 1.11.0 Bumps [morgan](https://github.com/expressjs/morgan) from 1.10.1 to 1.11.0. - [Release notes](https://github.com/expressjs/morgan/releases) - [Changelog](https://github.com/expressjs/morgan/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/morgan/compare/1.10.1...1.11.0) --- updated-dependencies: - dependency-name: morgan dependency-version: 1.11.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 28 ++++++++++------------------ package.json | 2 +- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 088d21fea..0fe7e1a0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "mongo-cursor-pagination": "^8.1.3", "mongoose": "^8.9.5", "mongoose-aggregate-paginate-v2": "1.0.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "node-dev": "^7.4.3", "packageurl-js": "^2.0.1", "prompt-sync": "^4.2.0", @@ -5824,19 +5824,23 @@ } }, "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", - "on-finished": "~2.3.0", + "on-finished": "~2.4.1", "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/morgan/node_modules/debug": { @@ -5854,18 +5858,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -10119,4 +10111,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 318a4bcec..494ec3980 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "mongo-cursor-pagination": "^8.1.3", "mongoose": "^8.9.5", "mongoose-aggregate-paginate-v2": "1.0.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "node-dev": "^7.4.3", "packageurl-js": "^2.0.1", "prompt-sync": "^4.2.0", From c4f3d5871cbb7190c5c4352ce618b017a02c0a1c Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Wed, 15 Jul 2026 11:10:09 -0400 Subject: [PATCH 10/23] Protect existing production by only using MONGO_CONN_STRING in development. I did check the task definition and MONGO_CONN_STRING was not set, so it should have been safe anyway --- src/utils/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/db.js b/src/utils/db.js index 9d358aed0..bdcf52204 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -7,7 +7,7 @@ const logger = require('../middleware/logger') */ function getMongoConnectionString () { const appEnv = process.env.NODE_ENV - if (process.env.MONGO_CONN_STRING) { + if (process.env.MONGO_CONN_STRING && process.env.NODE_ENV === 'development') { logger.info(`Using NODE_ENV '${process.env.NODE_ENV}' and app environment '${appEnv}'`) logger.info('Using MONGO_CONN_STRING override') return process.env.MONGO_CONN_STRING From 08f91d628459e24ef88733d1142f7f818117a9db Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Wed, 15 Jul 2026 12:11:04 -0400 Subject: [PATCH 11/23] Better env check --- src/utils/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/db.js b/src/utils/db.js index bdcf52204..6cfb6ec84 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -7,7 +7,7 @@ const logger = require('../middleware/logger') */ function getMongoConnectionString () { const appEnv = process.env.NODE_ENV - if (process.env.MONGO_CONN_STRING && process.env.NODE_ENV === 'development') { + if (process.env.MONGO_CONN_STRING && process.env.NODE_ENV !== 'production') { logger.info(`Using NODE_ENV '${process.env.NODE_ENV}' and app environment '${appEnv}'`) logger.info('Using MONGO_CONN_STRING override') return process.env.MONGO_CONN_STRING From c609aaa4a64414b234a92a466cc009d3e2136a4b Mon Sep 17 00:00:00 2001 From: Chenyang Li Date: Thu, 16 Jul 2026 11:03:11 -0400 Subject: [PATCH 12/23] Assert getAllOrgs and getAllUsers are called with the expected field projections --- test/unit-tests/cve-id/cveIdGetAllTest.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/unit-tests/cve-id/cveIdGetAllTest.js b/test/unit-tests/cve-id/cveIdGetAllTest.js index e30098a7f..2946f871c 100644 --- a/test/unit-tests/cve-id/cveIdGetAllTest.js +++ b/test/unit-tests/cve-id/cveIdGetAllTest.js @@ -143,6 +143,12 @@ describe('Testing getFilteredCveId function', () => { expect(cveIdRepo.aggregatePaginate.args[0][0][0].$match).to.deep.equal(builtQuery) }) + it('Should request only the fields needed to build the org and user maps', async () => { + await cveIdController.CVEID_GET_FILTER(req, res, next) + expect(orgRepo.getAllOrgs.calledOnceWith({}, { UUID: 1, short_name: 1, _id: 0 })).to.equal(true) + expect(userRepo.getAllUsers.calledOnceWith({}, { UUID: 1, username: 1, org_UUID: 1, _id: 0 })).to.equal(true) + }) + it('Should swap UUIDs for names in Cve-ids', async () => { await cveIdController.CVEID_GET_FILTER(req, res, next) expect(res.status.args[0][0]).to.equal(200) From e0e601bc84b41f63b5d176d1140f41369731b81a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 20 Jul 2026 14:32:17 -0400 Subject: [PATCH 13/23] remove legacy registry=true support --- api-docs/openapi.json | 47 +--- src/controller/org.controller/index.js | 4 +- .../org.controller/org.controller.js | 157 +++++------- .../org.controller/org.middleware.js | 231 +----------------- src/controller/user.controller/index.js | 17 +- .../user.controller/user.controller.js | 4 +- .../user.controller/user.middleware.js | 2 +- src/middleware/middleware.js | 6 - src/swagger.js | 9 - .../org/regularUsersTestRegistry.js | 10 - test/integration-tests/user/getUsersTest.js | 6 +- test/unit-tests/org/orgCreateTest.js | 3 +- 12 files changed, 73 insertions(+), 423 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index f9fbbb5bd..d925207de 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "https://cveawg-dev.mitre.org/api" + "url": "urlplaceholder" } ], "paths": { @@ -3334,13 +3334,6 @@ "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all organizations

", "operationId": "orgAll", "parameters": [ - { - "name": "registry", - "in": "query", - "schema": { - "type": "string" - } - }, { "$ref": "#/components/parameters/pageQuery" }, @@ -3633,13 +3626,6 @@ }, "description": "The shortname of the organization" }, - { - "name": "registry", - "in": "query", - "schema": { - "type": "string" - } - }, { "$ref": "#/components/parameters/id_quota" }, @@ -3839,13 +3825,6 @@ }, "description": "The shortname of the organization" }, - { - "name": "registry", - "in": "query", - "schema": { - "type": "string" - } - }, { "$ref": "#/components/parameters/pageQuery" }, @@ -4365,9 +4344,6 @@ { "$ref": "#/components/parameters/pageQuery" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -4454,9 +4430,6 @@ { "$ref": "#/components/parameters/pageQuery" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -4473,14 +4446,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/user/list-users-response.json" - }, - { - "$ref": "../schemas/registry-user/list-registry-users-response.json" - } - ] + "$ref": "../schemas/user/list-users-response.json" } } } @@ -6104,15 +6070,6 @@ "minimum": 1 } }, - "registry": { - "in": "query", - "name": "registry", - "description": "When set to true, the endpoint will expect request data to conform to the applicable User Registry schema, and will provide response data conforming to the applicable User Registry schema. Defaults to false.", - "required": false, - "schema": { - "type": "boolean" - } - }, "short_name": { "in": "query", "name": "short_name", diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 8e362cf2f..d3b472d1b 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -9,7 +9,7 @@ const { body, param, query } = require('express-validator') const { parseGetParams, parsePostParams, parsePutParams, parseError, isUserRole, isValidUsername, isOrgRole, validateUpdateOrgParameters, shortCircuitLegacyCpsMitreOrgParameters } = require('./org.middleware') // Only God and Javascript know swhy its saying it is not used when it is..... // eslint-disable-next-line no-unused-vars -const { toUpperCaseArray, isFlatStringArray, handleRegistryParameter } = require('../../middleware/middleware') +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../../src/constants').getConstants const CONSTANTS = getConstants() @@ -1268,7 +1268,6 @@ router.get('/org', } } */ - mw.handleRegistryParameter, mw.validateUser, mw.onlySecretariat, query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), @@ -1670,7 +1669,6 @@ router.get('/org/:shortname/users', } } */ - mw.handleRegistryParameter, mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index cdadb284e..9bff904f0 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -8,6 +8,11 @@ const validateUUID = require('uuid').validate const _ = require('lodash') const authContext = require('../../utils/authContext') +const LEGACY_ORG_FORMAT = true +const REGISTRY_ORG_FORMAT = false +const LEGACY_USER_OBJECT = false +const REGISTRY_USER_OBJECT = true + /** * Get the details of all orgs. * Called by GET /api/org @@ -32,7 +37,7 @@ async function getOrgs (req, res, next) { options.sort = { short_name: 'asc' } options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value - const returnValue = await repo.getAllOrgs({ ...options }, true) + const returnValue = await repo.getAllOrgs({ ...options }, LEGACY_ORG_FORMAT) logger.info({ uuid: req.ctx.uuid, message: 'The orgs were sent to the user.' }) return res.status(200).json(returnValue) @@ -58,21 +63,20 @@ async function getOrg (req, res, next) { const requesterOrgShortName = req.ctx.org const identifier = req.ctx.params.identifier const identifierIsUUID = validateUUID(identifier) - const returnLegacyFormat = true let returnValue try { - const requesterOrg = await authContext.getRequesterOrg(req, repo, {}, returnLegacyFormat) + const requesterOrg = await authContext.getRequesterOrg(req, repo, {}, LEGACY_ORG_FORMAT) // Ensure requester org exists if (!requesterOrg) { return res.status(404).json(error.orgDne(requesterOrgShortName, 'requesterOrgShortName', 'header')) } - const isSecretariat = await authContext.isRequesterSecretariat(req, repo, {}, returnLegacyFormat) + const isSecretariat = await authContext.isRequesterSecretariat(req, repo, {}, LEGACY_ORG_FORMAT) const isRequesterSameOrg = identifierIsUUID ? requesterOrg.UUID === identifier - : await authContext.isRequesterSameOrg(req, repo, identifier, {}, returnLegacyFormat) + : await authContext.isRequesterSameOrg(req, repo, identifier, {}, LEGACY_ORG_FORMAT) // Ensure that if the requester is not Secretariat, they can't view orgs other than their own if (!isRequesterSameOrg && !isSecretariat) { @@ -80,7 +84,7 @@ async function getOrg (req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, returnLegacyFormat) + returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, LEGACY_ORG_FORMAT) } catch (err) { // Handle the specific error thrown by BaseOrgRepository.getOrg if (err.message && err.message.includes('Unknown Org type requested')) { @@ -139,13 +143,13 @@ async function getUsers (req, res, next) { return res.status(404).json(error.orgDnePathParam(orgShortName)) } - const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, !req.useRegistry) + const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, LEGACY_ORG_FORMAT) if (!isSameOrg && !isSecretariat) { logger.info({ uuid: req.ctx.uuid, message: orgShortName + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } - const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, !!req.useRegistry) + const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, LEGACY_USER_OBJECT) logger.info({ uuid: req.ctx.uuid, message: `The users of ${orgShortName} organization were sent to the user.` }) return res.status(200).json(payload) @@ -169,10 +173,10 @@ async function getUser (req, res, next) { const orgShortName = req.ctx.params.shortname const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, !req.useRegistry) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, LEGACY_ORG_FORMAT) const orgUUID = await orgRepo.getOrgUUID(orgShortName) - const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, !req.useRegistry) + const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: orgUUID, short_name: orgShortName }, {}, LEGACY_ORG_FORMAT) if (!isSameOrg && !isSecretariat) { logger.info({ uuid: req.ctx.uuid, message: req.ctx.org + ' organization can only be viewed by that organization\'s users or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) @@ -185,7 +189,7 @@ async function getUser (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() // This is simple, we can just call our function - const result = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName, {}, !!req.useRegistry) + const result = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName, {}, LEGACY_USER_OBJECT) if (!result) { logger.info({ uuid: req.ctx.uuid, message: username + ' does not exist.' }) @@ -218,22 +222,24 @@ async function getOrgIdQuota (req, res, next) { try { const orgRepo = req.ctx.repositories.getBaseOrgRepository() const shortName = req.ctx.params.shortname + const isRegistry = req.useRegistry === true + const returnLegacyFormat = isRegistry ? REGISTRY_ORG_FORMAT : LEGACY_ORG_FORMAT - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, !req.useRegistry) - const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, shortName, {}, !req.useRegistry) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, {}, returnLegacyFormat) + const isSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, shortName, {}, returnLegacyFormat) if (!isSameOrg && !isSecretariat) { logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization id quota can only be viewed by the users of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } - const org = await orgRepo.getOrg(shortName, false, {}, !req.useRegistry) + const org = await orgRepo.getOrg(shortName, false, {}, returnLegacyFormat) if (!org) { // a null org can only happen if the requestor is the Secretariat logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization does not exist.' }) return res.status(404).json(error.orgDnePathParam(shortName)) } - const returnPayload = await orgRepo.getOrgIdQuota(org, !req.useRegistry) + const returnPayload = await orgRepo.getOrgIdQuota(org, returnLegacyFormat) logger.info({ uuid: req.ctx.uuid, message: 'The organization\'s id quota was returned to the user.', details: returnPayload }) return res.status(200).json(returnPayload) } catch (err) { @@ -264,21 +270,8 @@ async function createOrg (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - if (req.useRegistry) { - // If we are creating an org via the registry flag, we can do a full validation. - const result = await repo.validateOrg(body, { session }) - if (!result.isValid) { - logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) - await session.abortTransaction() - if (!Array.isArray(body?.authority) || body?.authority.some(item => typeof item !== 'string')) { - return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) - } - return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', errors: result.errors }) - } - } - // Check to see if the org already exits - if (await repo.orgExists(body?.short_name, { session }, !req.useRegistry)) { + if (await repo.orgExists(body?.short_name, { session }, LEGACY_ORG_FORMAT)) { logger.info({ uuid: req.ctx.uuid, message: body?.short_name + ' organization was not created because it already exists.' }) await session.abortTransaction() return res.status(400).json(error.orgExists(body?.short_name)) @@ -295,9 +288,9 @@ async function createOrg (req, res, next) { return res.status(400).json(error.aliasCollision(collisionString)) } const userRepo = req.ctx.repositories.getBaseUserRepository() - const isSecretariat = await authContext.isRequesterSecretariat(req, repo, { session }, !req.useRegistry) - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, repo, { session }, !!req.useRegistry) - returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, !req.useRegistry, requestingUserUUID, isSecretariat) + const isSecretariat = await authContext.isRequesterSecretariat(req, repo, { session }, LEGACY_ORG_FORMAT) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, repo, { session }, LEGACY_USER_OBJECT) + returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, LEGACY_ORG_FORMAT, requestingUserUUID, isSecretariat) await session.commitTransaction() } catch (error) { @@ -351,23 +344,6 @@ async function updateOrg (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - // TODO: Check to see if this check is needed for both options - if (req.useRegistry) { - if (queryParametersJson['active_roles.add']) { - if (!Array.isArray(queryParametersJson.active_roles?.add) || queryParametersJson.active_roles?.add.some(item => typeof item !== 'string')) { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) - } - } - - if (queryParametersJson['active_roles.remove']) { - if (!Array.isArray(queryParametersJson.active_roles?.remove) || queryParametersJson.active_roles?.remove.some(item => typeof item !== 'string')) { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) - } - } - } - if (!(await orgRepository.orgExists(shortNameUrlParameter, { session }))) { logger.info({ uuid: req.ctx.uuid, message: `Organization ${shortNameUrlParameter} not found.` }) await session.abortTransaction() @@ -392,9 +368,9 @@ async function updateOrg (req, res, next) { } const userRepo = req.ctx.repositories.getBaseUserRepository() - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepository, { session }, !!req.useRegistry) - const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepository, { session }, !req.useRegistry) - const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepository, { session }, !!req.useRegistry) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepository, { session }, LEGACY_USER_OBJECT) + const isSecretariat = await authContext.isRequesterSecretariat(req, orgRepository, { session }, LEGACY_ORG_FORMAT) + const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepository, { session }, LEGACY_USER_OBJECT) if (!isSecretariat) { const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS @@ -405,7 +381,7 @@ async function updateOrg (req, res, next) { return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) } } - const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, !req.useRegistry, requestingUserUUID, isAdmin, isSecretariat) + const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, LEGACY_ORG_FORMAT, requestingUserUUID, isAdmin, isSecretariat) responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message payload = { @@ -433,7 +409,7 @@ async function updateOrg (req, res, next) { /** * Creates a user only if the org exists and the user does not exist for the specified shortname and username. - * Called by POST /api/registry/org/{shortname}/user, POST /api/org/{shortname}/user + * Called by POST /api/org/{shortname}/user * * @param {Object} req - The request object * @param {Object} res - The response object @@ -446,7 +422,6 @@ async function createUser (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() const orgShortName = req.ctx.params.shortname - const constants = getConstants() let returnValue // Check to make sure Org Exists first @@ -469,30 +444,13 @@ async function createUser (req, res, next) { try { session.startTransaction({ readPreference: 'primary' }) - if (req.useRegistry) { - const result = await userRepo.validateUser(body) - if (body?.role && typeof body?.role !== 'string') { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) - } - if (body?.role && !constants.USER_ROLES.includes(body?.role)) { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: `Role must be one of the following: ${constants.USER_ROLES}` }] }) - } - if (!result.isValid) { - logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) - } - } else { - if (!body?.username || typeof body?.username !== 'string') { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'username', msg: 'Parameter must be a non empty string' }] }) - } + if (!body?.username || typeof body?.username !== 'string') { + await session.abortTransaction() + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'username', msg: 'Parameter must be a non empty string' }] }) } // Ask repo if user already exists - if (await userRepo.orgHasUser(orgShortName, body?.username, { session }, !!req.useRegistry)) { + if (await userRepo.orgHasUser(orgShortName, body?.username, { session }, LEGACY_USER_OBJECT)) { logger.info({ uuid: req.ctx.uuid, message: `${body?.username} user was not created because it already exists.` }) await session.abortTransaction() return res.status(400).json(error.userExists(body?.username)) @@ -500,10 +458,10 @@ async function createUser (req, res, next) { let isRequesterAdminOrSecretariat if (!req.ctx.authenticated && !req.ctx.orgUUID && typeof userRepo.isAdminOrSecretariat === 'function') { - isRequesterAdminOrSecretariat = await userRepo.isAdminOrSecretariat(orgShortName, req.ctx.user, req.ctx.org, { session }, !!req.useRegistry) + isRequesterAdminOrSecretariat = await userRepo.isAdminOrSecretariat(orgShortName, req.ctx.user, req.ctx.org, { session }, LEGACY_USER_OBJECT) } else { - const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, !req.useRegistry) - const isRequesterAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, orgShortName, { session }, !!req.useRegistry) + const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, LEGACY_ORG_FORMAT) + const isRequesterAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, orgShortName, { session }, LEGACY_USER_OBJECT) isRequesterAdminOrSecretariat = isRequesterSecretariat || isRequesterAdminOfTargetOrg } @@ -518,8 +476,8 @@ async function createUser (req, res, next) { return res.status(400).json(error.userLimitReached()) } - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, !!req.useRegistry) - returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, !!req.useRegistry, requestingUserUUID) + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, LEGACY_USER_OBJECT) + returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, LEGACY_USER_OBJECT, requestingUserUUID) await session.commitTransaction() } catch (error) { await session.abortTransaction() @@ -552,7 +510,7 @@ async function createUser (req, res, next) { /** * Updates a user only if the user exist for the specified username. * If no user exists, it does not create the user. - * Called by PUT /org/{shortname}/user/{username}, PUT /org/{shortname}/user/{username} + * Called by PUT /org/{shortname}/user/{username} * * @param {Object} req - The request object * @param {Object} res - The response object @@ -575,26 +533,20 @@ async function updateUser (req, res, next) { const queryParametersJson = req.ctx.query // Get requester UUID for later - const requesterUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, !!req.useRegistry) - const targetUserUUID = await userRepo.getUserUUID(usernameParams, shortNameParams, { session }, !!req.useRegistry) + const requesterUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, LEGACY_USER_OBJECT) + const targetUserUUID = await userRepo.getUserUUID(usernameParams, shortNameParams, { session }, LEGACY_USER_OBJECT) - const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, !req.useRegistry) - const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepo, { session }, !!req.useRegistry) + const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, LEGACY_ORG_FORMAT) + const isAdmin = await authContext.isRequesterAdmin(req, userRepo, orgRepo, { session }, LEGACY_USER_OBJECT) const targetOrgUUID = await orgRepo.getOrgUUID(shortNameParams, { session }) - // if (req.useRegistry) { - // if (body?.role && typeof body?.role !== 'string') { - // return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) - // } - // } - if (!targetOrgUUID) { logger.info({ uuid: req.ctx.uuid, message: `Target organization ${shortNameParams} does not exist.` }) await session.abortTransaction() return res.status(404).json(error.orgDnePathParam(shortNameParams)) } - const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: targetOrgUUID, short_name: shortNameParams }, { session }, !req.useRegistry) + const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, { UUID: targetOrgUUID, short_name: shortNameParams }, { session }, LEGACY_ORG_FORMAT) if (!requesterSameOrg && !isRequesterSecretariat) { logger.info({ uuid: req.ctx.uuid, message: `${shortNameParams} organization data can only be modified by users of the same organization or the Secretariat.` }) await session.abortTransaction() @@ -694,7 +646,7 @@ async function updateUser (req, res, next) { } } - const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, !!req.useRegistry, requesterUUID) + const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, LEGACY_USER_OBJECT, requesterUUID) await session.commitTransaction() return res.status(200).json({ message: `${usernameParams} was successfully updated.`, updated: payload }) } catch (err) { @@ -730,12 +682,15 @@ async function resetSecret (req, res, next) { const orgRepo = req.ctx.repositories.getBaseOrgRepository() const userRepo = req.ctx.repositories.getBaseUserRepository() + const isRegistry = req.useRegistry === true + const returnLegacyFormat = isRegistry ? REGISTRY_ORG_FORMAT : LEGACY_ORG_FORMAT + const isRegistryUserObject = isRegistry ? REGISTRY_USER_OBJECT : LEGACY_USER_OBJECT try { session.startTransaction({ readPreference: 'primary' }) // Check if target org exists - const targetOrgUUID = await orgRepo.getOrgUUID(targetOrgShortName, { session }, !req.useRegistry) + const targetOrgUUID = await orgRepo.getOrgUUID(targetOrgShortName, { session }, returnLegacyFormat) if (!targetOrgUUID) { logger.info({ uuid: req.ctx.uuid, message: 'Org DNE' }) await session.abortTransaction() @@ -743,11 +698,11 @@ async function resetSecret (req, res, next) { } const targetOrg = { UUID: targetOrgUUID, short_name: targetOrgShortName } - const requesterUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, !!req.useRegistry) - const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, !req.useRegistry) + const requesterUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }, isRegistryUserObject) + const isRequesterSecretariat = await authContext.isRequesterSecretariat(req, orgRepo, { session }, returnLegacyFormat) if (!isRequesterSecretariat) { - const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg, { session }, !req.useRegistry) + const requesterSameOrg = await authContext.isRequesterSameOrg(req, orgRepo, targetOrg, { session }, returnLegacyFormat) if (!requesterSameOrg) { logger.info({ uuid: req.ctx.uuid, message: 'The api secret can only be reset by the Secretariat, an Org admin or if the requester is the user.' }) await session.abortTransaction() @@ -756,7 +711,7 @@ async function resetSecret (req, res, next) { } // Check if target user exists in target org - const targetUserUUID = await userRepo.getUserUUID(targetUsername, targetOrgShortName, { session }, !!req.useRegistry) + const targetUserUUID = await userRepo.getUserUUID(targetUsername, targetOrgShortName, { session }, isRegistryUserObject) if (!targetUserUUID) { logger.info({ uuid: req.ctx.uuid, message: 'User DNE' }) await session.abortTransaction() @@ -768,7 +723,7 @@ async function resetSecret (req, res, next) { // 1. WE are not the same user if (requesterUserUUID !== targetUserUUID) { // Check to see if we are the admin of the target organization - const isAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg, { session }, !!req.useRegistry) + const isAdminOfTargetOrg = await authContext.isRequesterAdminOfOrg(req, userRepo, orgRepo, targetOrg, { session }, isRegistryUserObject) if (!isAdminOfTargetOrg) { logger.info({ uuid: req.ctx.uuid, message: 'The api secret can only be reset by the Secretariat, an Org admin or if the requester is the user.' }) @@ -778,7 +733,7 @@ async function resetSecret (req, res, next) { } } - const updatedSecret = await userRepo.resetSecret(targetUsername, targetOrgShortName, { session }, !!req.useRegistry) + const updatedSecret = await userRepo.resetSecret(targetUsername, targetOrgShortName, { session }, isRegistryUserObject) logger.info({ uuid: req.ctx.uuid, message: `The API secret was successfully reset and sent to ${targetUsername}` }) const payload = { diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index d8331ecc9..e8f642899 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -2,7 +2,7 @@ const getConstants = require('../../constants').getConstants const { validationResult } = require('express-validator') const errors = require('./error') const error = new errors.OrgControllerError() -const { body, param, query } = require('express-validator') +const { param, query } = require('express-validator') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const CONSTANTS = getConstants() const errorMsgs = require('../../middleware/errorMessages') @@ -22,186 +22,8 @@ function isOrgRole (val) { return true } -function validateCreateOrgParameters () { - return async (req, res, next) => { - const useRegistry = req.query.registry === 'true' - let validations = [] - if (useRegistry) { - // Not allowed - // users, , in_use, created, last_updated - const orgOptions = ['CNA', 'Secretariat', 'Bulk Download', 'ADP'] - validations = [ - body(['short_name']).isString() - .trim() - .notEmpty() - .isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['long_name']).isString() - .trim() - .notEmpty(), - body(['cve_program_org_function']) - .default('CNA') - .isString() - .isIn(orgOptions), - body(['oversees']).default([]) - .isArray(), - body(['top_level_root']).default('') - .isString(), - body(['advisory_locations']) - .default([]) - .custom(isFlatStringArray), - body(['advisory_location_require_credentials']) - .default(false) - .isBoolean(), - body(['vulnerability_advisory_location_for_web_scraping']) - .default([]) - .custom(isFlatStringArray), - body(['tl_root_start_date']) - .default(null) - .isDate(), - body(['is_cna_discussion_list']) - .default(false) - .isBoolean(), - body([ - 'program_data.cve_website_update_date', - 'program_data.partner_active_date', - 'program_data.partner_inactive_date' - ]) - .optional({ nullable: true }) - .isDate(), - body(['program_data.cve_website_update_needed']) - .optional() - .isBoolean(), - body( - [ - 'charter_or_scope', - 'disclosure_policy', - 'product_list', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info.phone', - '', - '', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.status', - 'industry' - ]) - .default('') - .isString(), - body(['authority.active_roles']) - .default([CONSTANTS.AUTH_ROLE_ENUM.CNA]) - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole), - body(['id_quota']) - .default(CONSTANTS.DEFAULT_ID_QUOTA) - .not() - .isArray() - .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) - .withMessage(errorMsgs.ID_QUOTA), - ...isNotAllowed('reports_to', 'name', 'users', '', 'in_use', 'created', 'last_updated', 'policies.id_quota') - ] - } else { - validations = [ - body(['short_name']).isString() - .trim() - .notEmpty() - .isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['name']).isString() - .trim() - .notEmpty(), - body(['authority.active_roles']) - .default([CONSTANTS.AUTH_ROLE_ENUM.CNA]) - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole), - body(['policies.id_quota']) - .default(CONSTANTS.DEFAULT_ID_QUOTA) - .not() - .isArray() - .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) - .withMessage(errorMsgs.ID_QUOTA), - ...isNotAllowed( - 'oversees', - 'long_name', - 'cve_program_org_function', - 'in_use', - 'created', - 'top_level_root', - 'aliases', - 'id_quota', - 'contact_info.phone', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info', - 'users', - 'charter_or_scope', - 'disclosure_policy', - 'product_list', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info.phone', - 'private_contacts', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.cve_website_update_date', - 'program_data.cve_website_update_needed', - 'program_data.status', - 'advisory_locations', - 'advisory_location_require_credentials', - 'vulnerability_advisory_location_for_web_scraping', - 'industry', - 'tl_root_start_date', - 'is_cna_discussion_list') - ] - } - - const results = [] - for (const validation of validations) { - const result = await validation.run(req) - if (!result.isEmpty()) { - results.push(...result.errors) - } - } - if (results.length > 0) { - return res.status(400).json({ message: 'Parameters were invalid', details: results }) - } - next() - } -} - -function validateUserIdOrUsername () { - return async (req, res, next) => { - const useRegistry = req.query.registry === 'true' - const validations = [] - if (useRegistry) { - validations.push( - body('user_id') // Condition to run validation - .isString() - .trim() - .notEmpty(isValidUsername)) - } else { - validations.push(body('username').isString().trim().notEmpty(isValidUsername)) - } - const results = [] - for (const validation of validations) { - const result = await validation.run(req) - if (!result.isEmpty()) { - results.push(...result.errors) - } - } - if (results.length > 0) { - return res.status(400).json({ message: 'Parameters were invalid', details: results }) - } - next() - } -} - function validateUpdateOrgParameters () { return async (req, res, next) => { - const useRegistry = req.query.registry === 'true' const allowedParams = [...QUERY_PARAMETERS.shared] const registryParametersOnly = [...QUERY_PARAMETERS.registryOnly] @@ -218,43 +40,8 @@ function validateUpdateOrgParameters () { .custom(isFlatStringArray) .customSanitizer(toUpperCaseArray), // Path parameter validation - param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH })] - if (useRegistry) { - validations.push( - query(['oversees']).optional().isArray(), - query(['top_level_root']).optional().isString(), - query([ - 'charter_or_scope', - 'disclosure_policy', - 'product_list', - 'contact_info.websites', - 'contact_info.emails', - 'contact_info.phone', - '', - '', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.cve_website_update_date', - 'program_data.cve_website_update_needed', - 'program_data.status', - 'advisory_location_require_credentials', - 'vulnerability_advisory_location_for_web_scraping', - 'advisory_locations', - 'industry', - 'tl_root_start_date', - 'is_cna_discussion_list' - ]) - .optional() - .isString() - .trim() - ) - } else { - validations.push( - // Block registry-only parameters - ...isNotAllowedQuery(...registryParametersOnly) - ) - } + param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + ...isNotAllowedQuery(...registryParametersOnly)] const results = [] for (const validation of validations) { @@ -270,16 +57,6 @@ function validateUpdateOrgParameters () { } } -function isNotAllowed (...fields) { - return fields.map(field => - body(field) - .if((value, { req }) => _.has(req.body, field)) - .custom(() => { - throw new Error(`${field} must not be present`) - }) - ) -} - function isNotAllowedQuery (...fields) { return fields.map(field => query(field) @@ -417,8 +194,6 @@ module.exports = { isOrgRole, isUserRole, isValidUsername, - validateCreateOrgParameters, validateUpdateOrgParameters, - validateUserIdOrUsername, shortCircuitLegacyCpsMitreOrgParameters } diff --git a/src/controller/user.controller/index.js b/src/controller/user.controller/index.js index c7d4a73d8..ddbb44823 100644 --- a/src/controller/user.controller/index.js +++ b/src/controller/user.controller/index.js @@ -1,13 +1,10 @@ const express = require('express') const router = express.Router() const mw = require('../../middleware/middleware') -const { query, param } = require('express-validator') +const { query } = require('express-validator') const controller = require('./user.controller') const registryUserController = require('../registry-user.controller/registry-user.controller.js') const { parseGetParams, parseError } = require('./user.middleware') -// Only God and Javascript know why its saying it is not used when it is..... -// eslint-disable-next-line no-unused-vars -const { handleRegistryParameter } = require('../../middleware/middleware') const getConstants = require('../../constants').getConstants const CONSTANTS = getConstants() @@ -23,7 +20,6 @@ router.get('/registry/users',

Secretariat: Retrieves information about all users for all organizations

" #swagger.parameters['$ref'] = [ '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -102,7 +98,6 @@ router.get('/users',

Secretariat: Retrieves information about all users for all organizations

" #swagger.parameters['$ref'] = [ '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -112,10 +107,7 @@ router.get('/users', content:{ "application/json":{ schema: { - oneOf: [ - { $ref: '../schemas/user/list-users-response.json' }, - { $ref: '../schemas/registry-user/list-registry-users-response.json' } - ] + $ref: '../schemas/user/list-users-response.json' } } } @@ -161,12 +153,11 @@ router.get('/users', } } */ - param(['registry']).optional().isBoolean(), - mw.handleRegistryParameter, mw.validateUser, mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - query(['page', 'registry']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), parseError, parseGetParams, controller.ALL_USERS) diff --git a/src/controller/user.controller/user.controller.js b/src/controller/user.controller/user.controller.js index 3d78e4a43..0b9fc31d5 100644 --- a/src/controller/user.controller/user.controller.js +++ b/src/controller/user.controller/user.controller.js @@ -3,6 +3,8 @@ require('dotenv').config() const logger = require('../../middleware/logger') const getConstants = require('../../constants').getConstants +const LEGACY_USER_OBJECT = false + /** * Get the details of all users * Called by GET /api/users @@ -22,7 +24,7 @@ async function getAllUsers (req, res, next) { options.sort = { username: 'asc' } options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value - const returnValue = await repo.getAllUsers(options, !!req.useRegistry) + const returnValue = await repo.getAllUsers(options, LEGACY_USER_OBJECT) logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) return res.status(200).json(returnValue) diff --git a/src/controller/user.controller/user.middleware.js b/src/controller/user.controller/user.middleware.js index e9477fb70..95a900313 100644 --- a/src/controller/user.controller/user.middleware.js +++ b/src/controller/user.controller/user.middleware.js @@ -4,7 +4,7 @@ const error = new errors.UserControllerError() const utils = require('../../utils/utils') function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'query', ['page', 'registry']) + utils.reqCtxMapping(req, 'query', ['page']) next() } diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index aa32a66b6..29fd01910 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -96,11 +96,6 @@ async function optionallyValidateUser (req, res, next) { } } -const handleRegistryParameter = (req, res, next) => { - req.useRegistry = req.query.registry === 'true' - next() -} - const useRegistry = () => { return (req, res, next) => { req.useRegistry = true @@ -572,7 +567,6 @@ module.exports = { setCacheControl, optionallyValidateUser, validateUser, - handleRegistryParameter, useRegistry, onlySecretariat, onlySecretariatOrBulkDownload, diff --git a/src/swagger.js b/src/swagger.js index 6f491125f..4b18a646f 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -479,15 +479,6 @@ const doc = { minimum: 1 } }, - registry: { - in: 'query', - name: 'registry', - description: 'When set to true, the endpoint will expect request data to conform to the applicable User Registry schema, and will provide response data conforming to the applicable User Registry schema. Defaults to false.', - required: false, - schema: { - type: 'boolean' - } - }, short_name: { in: 'query', name: 'short_name', diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js index bdcb58b80..252122f48 100644 --- a/test/integration-tests/org/regularUsersTestRegistry.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -318,16 +318,6 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with expect(res).to.have.status(403) expect(res.body.error).to.contain('NOT_SAME_USER_OR_SECRETARIAT') }) - /* Commenting out since authority.active_roles are not returned in the GET request response for registry=true */ - // await chai.request(app) - // .get(`/api/org/${org}/user/${user}?registry=true`) - // .set(constants.nonSecretariatUserHeaders2) - // .send({ - // }) - // .then((res) => { - // expect(res).to.have.status(200) - // console.log(res.body) - // }) }) }) }) diff --git a/test/integration-tests/user/getUsersTest.js b/test/integration-tests/user/getUsersTest.js index 10f4a5401..ff140f85e 100644 --- a/test/integration-tests/user/getUsersTest.js +++ b/test/integration-tests/user/getUsersTest.js @@ -33,14 +33,12 @@ describe('Testing global user list endpoints', () => { }) }) - it('Should get all registry users from /users with registry query as Secretariat', async () => { + it('Should reject registry query parameter on /users', async () => { await chai.request(app) .get('/api/users?registry=true') .set(secretariatHeaders) .then((res) => { - expect(res).to.have.status(200) - expect(res.body).to.have.property('users') - expect(res.body.users).to.be.an('array').that.is.not.empty + expect(res).to.have.status(400) }) }) }) diff --git a/test/unit-tests/org/orgCreateTest.js b/test/unit-tests/org/orgCreateTest.js index bb0ed99dc..a2d28ff7a 100644 --- a/test/unit-tests/org/orgCreateTest.js +++ b/test/unit-tests/org/orgCreateTest.js @@ -178,8 +178,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { uuid: faker.datatype.uuid(), repositories: { getOrgRepository, getBaseOrgRepository, getUserRepository, getBaseUserRepository }, body: testOrgPayload - }, - query: { registry: 'false' } + } } await orgController.ORG_CREATE_SINGLE(req, res, next) From 9643c41d27161f0b0d4af4817467c9843290c234 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 20 Jul 2026 16:22:54 -0400 Subject: [PATCH 14/23] Clean up of unused endpoints --- api-docs/openapi.json | 1211 ++++++++------ .../delete-registry-org-response.json | 17 + .../delete-registry-user-response.json | 17 + src/controller/org.controller/index.js | 1188 -------------- .../org.controller/org.controller.js | 4 +- .../org.controller/org.middleware.js | 6 + .../registry-org.controller/index.js | 552 ------- .../registry-org.middleware.js | 74 - .../registry-user.controller/index.js | 397 ----- .../registry-user.middleware.js | 42 - src/controller/registry.controller/index.js | 1430 +++++++++++++++++ .../org.error.js} | 1 + .../org.registry.controller.js} | 21 +- .../user.registry.controller.js} | 102 +- src/controller/user.controller/index.js | 79 - src/routes.config.js | 7 +- src/swagger.js | 3 +- .../registry-org/createUserByOrgTest.js | 12 +- .../registry-org/registryOrgCRUDTest.js | 18 +- .../registryOrgDiscriminatorAuthorityTest.js | 2 +- .../registryOrgWithJointReviewTest.js | 10 +- .../registry-org/verifyDeepRemoveEmpty.js | 4 +- .../registry-user/registryUserCRUDTest.js | 48 +- .../registry/registryRouteMigrationTest.js | 39 + .../review-object/reviewObjectTest.js | 2 +- .../controllerSessionCleanupTest.js | 47 +- .../org/registryOrgControllerTest.js | 2 +- .../org/registryOrgGetSingleTest.js | 2 +- .../registry-org/registryOrgPayloadTest.js | 4 +- .../user/registryUserControllerTest.js | 134 +- 30 files changed, 2308 insertions(+), 3167 deletions(-) create mode 100644 schemas/registry-org/delete-registry-org-response.json create mode 100644 schemas/registry-user/delete-registry-user-response.json delete mode 100644 src/controller/registry-org.controller/index.js delete mode 100644 src/controller/registry-org.controller/registry-org.middleware.js delete mode 100644 src/controller/registry-user.controller/index.js delete mode 100644 src/controller/registry-user.controller/registry-user.middleware.js create mode 100644 src/controller/registry.controller/index.js rename src/controller/{registry-org.controller/error.js => registry.controller/org.error.js} (98%) rename src/controller/{registry-org.controller/registry-org.controller.js => registry.controller/org.registry.controller.js} (98%) rename src/controller/{registry-user.controller/registry-user.controller.js => registry.controller/user.registry.controller.js} (87%) create mode 100644 test/integration-tests/registry/registryRouteMigrationTest.js diff --git a/api-docs/openapi.json b/api-docs/openapi.json index d925207de..efd583b28 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "urlplaceholder" + "url": "https://cveawg-dev.mitre.org/api" } ], "paths": { @@ -1893,14 +1893,14 @@ } } }, - "/registry/org": { + "/org": { "get": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Retrieves all registry organizations (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all registry organizations

", - "operationId": "registryOrgAll", + "summary": "Retrieves all organizations (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all organizations

", + "operationId": "orgAll", "parameters": [ { "$ref": "#/components/parameters/pageQuery" @@ -1917,11 +1917,18 @@ ], "responses": { "200": { - "description": "Returns information about all registry organizations, along with pagination fields if results span multiple pages of data", + "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + "oneOf": [ + { + "$ref": "../schemas/org/list-orgs-response.json" + }, + { + "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + } + ] } } } @@ -1980,10 +1987,10 @@ }, "post": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Creates an organization (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates a new organization

", + "summary": "Creates an organization as specified in the request body (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates an organization

", "operationId": "orgCreateSingle", "parameters": [ { @@ -1998,11 +2005,18 @@ ], "responses": { "200": { - "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", + "description": "Returns information about the organization created", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + "oneOf": [ + { + "$ref": "../schemas/org/create-org-response.json" + }, + { + "$ref": "../schemas/registry-org/create-registry-org-response.json" + } + ] } } } @@ -2063,54 +2077,30 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "../schemas/registry-org/SecretariatOrg.json" - }, - { - "$ref": "../schemas/registry-org/CNAOrg.json" - }, - { - "$ref": "../schemas/registry-org/ADPOrg.json" - }, - { - "$ref": "../schemas/registry-org/BulkDownloadOrg.json" - } - ] - }, - "example": { - "short_name": "fake_company", - "long_name": "Fake Company", - "id_quota": 1000, - "authority": [ - "CNA" - ] + "$ref": "../schemas/org/create-org-request.json" } } } } } }, - "/registry/org/{shortname}/users": { + "/org/{identifier}": { "get": { "tags": [ - "Registry User" + "Organization" ], - "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", - "operationId": "userOrgAll", + "summary": "Retrieves information about the organization specified by short name or UUID (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any organization

", + "operationId": "orgSingle", "parameters": [ { - "name": "shortname", + "name": "identifier", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The shortname of the organization" - }, - { - "$ref": "#/components/parameters/pageQuery" + "description": "The shortname or UUID of the organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2124,33 +2114,11 @@ ], "responses": { "200": { - "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", + "description": "Returns the organization information", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/list-registry-users-response.json" - }, - "example": { - "totalCount": 1, - "itemsPerPage": 100, - "pageCount": 1, - "currentPage": 1, - "prevPage": null, - "nextPage": null, - "users": [ - { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "role": "ADMIN", - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - ] + "$ref": "../schemas/org/get-org-response.json" } } } @@ -2208,14 +2176,14 @@ } } }, - "/registry/org/{shortname}/id_quota": { - "get": { + "/org/{shortname}": { + "put": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", - "operationId": "orgIdQuota", + "summary": "Updates information about the organization specified by short name (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Updates any organization's information

", + "operationId": "orgUpdateSingle", "parameters": [ { "name": "shortname", @@ -2226,6 +2194,21 @@ }, "description": "The shortname of the organization" }, + { + "$ref": "#/components/parameters/id_quota" + }, + { + "$ref": "#/components/parameters/name" + }, + { + "$ref": "#/components/parameters/newShortname" + }, + { + "$ref": "#/components/parameters/active_roles_add" + }, + { + "$ref": "#/components/parameters/active_roles_remove" + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2238,11 +2221,11 @@ ], "responses": { "200": { - "description": "Returns the CVE ID quota for an organization", + "description": "Returns information about the organization updated", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/get-registry-org-quota-response.json" + "$ref": "../schemas/org/update-org-response.json" } } } @@ -2300,47 +2283,23 @@ } } }, - "/registry/org/{identifier}": { + "/org/{shortname}/id_quota": { "get": { "tags": [ - "Registry Organization" + "Organization" ], - "summary": "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any registry organization

", - "operationId": "registryOrgSingle", + "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", + "operationId": "orgIdQuota", "parameters": [ { - "name": "identifier", + "name": "shortname", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The shortname or UUID of the registry organization" - }, - { - "name": "expand", - "in": "query", - "description": "Optional expanded related data. Accepted value: users.", - "required": false, - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "string" - }, - "enum": { - "type": "array", - "example": [ - "users" - ], - "items": { - "type": "string" - } - } - } - } + "description": "The shortname of the organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2354,11 +2313,11 @@ ], "responses": { "200": { - "description": "Returns the registry organization information", + "description": "Returns the CVE ID quota for an organization", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/get-registry-org-response.json" + "$ref": "../schemas/org/get-org-quota-response.json" } } } @@ -2416,14 +2375,14 @@ } } }, - "/registry/org/{shortname}/user/{username}": { + "/org/{shortname}/users": { "get": { "tags": [ - "Registry User" + "Users" ], - "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

Secretariat: Retrieves any registry user's information

", - "operationId": "registryUserSingle", + "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", + "operationId": "userOrgAll", "parameters": [ { "name": "shortname", @@ -2431,15 +2390,11 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "The shortname of the organization" }, { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2453,11 +2408,11 @@ ], "responses": { "200": { - "description": "Returns information about the specified registry user", + "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/get-registry-user-response.json" + "$ref": "../schemas/user/list-users-response.json" } } } @@ -2513,14 +2468,16 @@ } } } - }, - "put": { + } + }, + "/org/{shortname}/user": { + "post": { "tags": [ - "Registry User" + "Users" ], - "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", - "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", - "operationId": "registryUserUpdateSingle", + "summary": "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Creates a user for the Admin's organization

Secretariat: Creates a user for any organization

", + "operationId": "userCreateSingle", "parameters": [ { "name": "shortname", @@ -2531,21 +2488,6 @@ }, "description": "The shortname of the organization" }, - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The username of the user" - }, - { - "$ref": "#/components/parameters/active" - }, - { - "$ref": "#/components/parameters/orgShortname" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2558,25 +2500,11 @@ ], "responses": { "200": { - "description": "Returns the updated user information", + "description": "Returns the new user information (with the secret)", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/update-registry-user-response.json" - }, - "example": { - "message": "jdoe was successfully updated.", - "updated": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } + "$ref": "../schemas/user/create-user-response.json" } } } @@ -2631,17 +2559,27 @@ } } } - } - } - }, - "/registry/org/{shortname}": { - "put": { - "tags": [ - "Registry Organization" - ], - "summary": "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

  • short_name
  • long_name
  • authority
  • aliases
  • oversees
  • top_level_root
  • charter_or_scope
  • product_list
  • disclosure_policy
  • contact_info.websites
  • contact_info.emails
  • contact_info.phone
  • partner_role_type
  • partner_country
  • advisory_locations
  • advisory_location_require_credentials
  • vulnerability_advisory_location_for_web_scraping
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", - "operationId": "orgUpdateSingle", + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-request.json" + } + } + } + } + } + }, + "/org/{shortname}/user/{username}": { + "get": { + "tags": [ + "Users" + ], + "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a user in the same organization

Secretariat: Retrieves any user's information

", + "operationId": "userSingle", "parameters": [ { "name": "shortname", @@ -2652,6 +2590,15 @@ }, "description": "The shortname of the organization" }, + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2664,11 +2611,11 @@ ], "responses": { "200": { - "description": "Returns information about the organization updated", + "description": "Returns information about the specified user", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-org/update-registry-org-response.json" + "$ref": "../schemas/user/get-user-response.json" } } } @@ -2723,35 +2670,15 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/registry-org/update-registry-org-request.json" - }, - "example": { - "short_name": "fake_company", - "long_name": "Fake Company", - "id_quota": 1000, - "authority": [ - "CNA" - ] - } - } - } } - } - }, - "/registry/org/{shortname}/user": { - "post": { + }, + "put": { "tags": [ - "Registry User" + "Users" ], - "summary": "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Creates a user for the Admin's organization

Secretariat: Creates a user for any organization

", - "operationId": "registryUserCreateSingle", + "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", + "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", + "operationId": "userUpdateSingle", "parameters": [ { "name": "shortname", @@ -2762,6 +2689,42 @@ }, "description": "The shortname of the organization" }, + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" + }, + { + "$ref": "#/components/parameters/active" + }, + { + "$ref": "#/components/parameters/activeUserRolesAdd" + }, + { + "$ref": "#/components/parameters/activeUserRolesRemove" + }, + { + "$ref": "#/components/parameters/nameFirst" + }, + { + "$ref": "#/components/parameters/nameLast" + }, + { + "$ref": "#/components/parameters/nameMiddle" + }, + { + "$ref": "#/components/parameters/nameSuffix" + }, + { + "$ref": "#/components/parameters/newUsername" + }, + { + "$ref": "#/components/parameters/orgShortname" + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2774,26 +2737,11 @@ ], "responses": { "200": { - "description": "Returns the new user information (with the secret)", + "description": "Returns the updated user information", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/create-registry-user-response.json" - } - }, - "example": { - "message": "jdoe was successfully created.", - "created": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "secret": "12345-abcde-67890", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" + "$ref": "../schemas/user/update-user-response.json" } } } @@ -2848,31 +2796,13 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/registry-user/create-registry-user-request.json" - } - }, - "example": { - "username": "jdoe", - "status": "active", - "name": { - "first": "John", - "last": "Doe" - } - } - } } } }, - "/registry/org/{shortname}/user/{username}/reset_secret": { + "/org/{shortname}/user/{username}/reset_secret": { "put": { "tags": [ - "Registry User" + "Users" ], "summary": "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)", "description": "

Access Control

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

Expected Behavior

Regular User: Resets user's own API secret

Admin User: Resets any user's API secret in the Admin's organization

Secretariat: Resets any user's API secret

", @@ -2970,32 +2900,118 @@ } } }, - "/registry/org/{shortname}/user/{username}/grant-role": { - "post": { + "/users": { + "get": { "tags": [ - "Registry User" + "Users" ], - "summary": "Grants a role to a user (accessible to Secretariat or Org Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Grants a role to a user in the Admin's organization

Secretariat: Grants a role to a user in any organization

", - "operationId": "registryUserGrantRole", + "summary": "Retrieves information about all registered users (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all users for all organizations

", + "operationId": "userAll", "parameters": [ { - "name": "shortname", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The shortname of the organization" + "$ref": "#/components/parameters/pageQuery" }, { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The username of the user" + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns all users, along with pagination fields if results span multiple pages of data.", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/list-users-response.json" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "401": { + "description": "Not Authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + } + }, + "/health-check": { + "get": { + "tags": [ + "Utilities" + ], + "summary": "Checks that the system is running (accessible to all users)", + "description": "

Access Control

Endpoint is accessible to all

Expected Behavior

Returns a 200 response code when CVE Services are running

", + "operationId": "healthCheck", + "responses": { + "200": { + "description": "Returns a 200 response code" + } + } + } + }, + "/registry/org": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Retrieves all registry organizations (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all registry organizations

", + "operationId": "registryOrgAll", + "parameters": [ + { + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3009,16 +3025,92 @@ ], "responses": { "200": { - "description": "Role granted successfully", + "description": "Returns information about all registry organizations, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } + "$ref": "../schemas/registry-org/list-registry-orgs-response.json" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "401": { + "description": "Not Authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + }, + "post": { + "tags": [ + "Registry Organization" + ], + "summary": "Creates an organization (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates a new organization

", + "operationId": "registryOrgCreateSingle", + "parameters": [ + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/list-registry-orgs-response.json" } } } @@ -3079,17 +3171,27 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "role": { - "type": "string", - "enum": [ - "ADMIN" - ] + "anyOf": [ + { + "$ref": "../schemas/registry-org/SecretariatOrg.json" + }, + { + "$ref": "../schemas/registry-org/CNAOrg.json" + }, + { + "$ref": "../schemas/registry-org/ADPOrg.json" + }, + { + "$ref": "../schemas/registry-org/BulkDownloadOrg.json" } - }, - "required": [ - "role" + ] + }, + "example": { + "short_name": "fake_company", + "long_name": "Fake Company", + "id_quota": 1000, + "authority": [ + "CNA" ] } } @@ -3097,14 +3199,14 @@ } } }, - "/registry/org/{shortname}/user/{username}/revoke-role": { - "post": { + "/registry/org/{shortname}/users": { + "get": { "tags": [ "Registry User" ], - "summary": "Revokes a role from a user (accessible to Secretariat or Org Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Revokes a role from a user in the Admin's organization

Secretariat: Revokes a role from a user in any organization

", - "operationId": "registryUserRevokeRole", + "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", + "operationId": "registryOrgUsersAll", "parameters": [ { "name": "shortname", @@ -3116,13 +3218,7 @@ "description": "The shortname of the organization" }, { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The username of the user" + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3136,16 +3232,33 @@ ], "responses": { "200": { - "description": "Role revoked successfully", + "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" + "$ref": "../schemas/registry-user/list-registry-users-response.json" + }, + "example": { + "totalCount": 1, + "itemsPerPage": 100, + "pageCount": 1, + "currentPage": 1, + "prevPage": null, + "nextPage": null, + "users": [ + { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "role": "ADMIN", + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" } - } + ] } } } @@ -3200,38 +3313,17 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "role": { - "type": "string", - "enum": [ - "ADMIN" - ] - } - }, - "required": [ - "role" - ] - } - } - } } } }, - "/registry/org/{shortname}/conversation/{index}": { - "put": { + "/registry/org/{shortname}/id_quota": { + "get": { "tags": [ "Registry Organization" ], - "summary": "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

Expected Behavior

Original Author: Allowed to update only the message body of a conversation posted by them

Secretariat: Allowed to update the message body and/or visibility of any conversation

", - "operationId": "registryUserUpdateConversation", + "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", + "operationId": "registryOrgIdQuota", "parameters": [ { "name": "shortname", @@ -3242,15 +3334,6 @@ }, "description": "The shortname of the organization" }, - { - "name": "index", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "The index of the conversation to update" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3263,11 +3346,11 @@ ], "responses": { "200": { - "description": "Returns the updated conversation", + "description": "Returns the CVE ID quota for an organization", "content": { "application/json": { "schema": { - "$ref": "../schemas/conversation/update-conversation-response.json" + "$ref": "../schemas/registry-org/get-registry-org-quota-response.json" } } } @@ -3325,17 +3408,47 @@ } } }, - "/org": { + "/registry/org/{identifier}": { "get": { "tags": [ - "Organization" + "Registry Organization" ], - "summary": "Retrieves all organizations (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all organizations

", - "operationId": "orgAll", + "summary": "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any registry organization

", + "operationId": "registryOrgSingle", "parameters": [ { - "$ref": "#/components/parameters/pageQuery" + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname or UUID of the registry organization" + }, + { + "name": "expand", + "in": "query", + "description": "Optional expanded related data. Accepted value: users.", + "required": false, + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "string" + }, + "enum": { + "type": "array", + "example": [ + "users" + ], + "items": { + "type": "string" + } + } + } + } }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3349,18 +3462,11 @@ ], "responses": { "200": { - "description": "Returns information about all organizations, along with pagination fields if results span multiple pages of data", + "description": "Returns the registry organization information", "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/list-orgs-response.json" - }, - { - "$ref": "../schemas/registry-org/list-registry-orgs-response.json" - } - ] + "$ref": "../schemas/registry-org/get-registry-org-response.json" } } } @@ -3416,15 +3522,33 @@ } } } - }, - "post": { + } + }, + "/registry/org/{shortname}/user/{username}": { + "get": { "tags": [ - "Organization" + "Registry User" ], - "summary": "Creates an organization as specified in the request body (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates an organization

", - "operationId": "orgCreateSingle", + "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", + "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

Secretariat: Retrieves any registry user's information

", + "operationId": "registryUserSingle", "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3437,18 +3561,11 @@ ], "responses": { "200": { - "description": "Returns information about the organization created", + "description": "Returns information about the specified registry user", "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/create-org-response.json" - }, - { - "$ref": "../schemas/registry-org/create-registry-org-response.json" - } - ] + "$ref": "../schemas/registry-user/get-registry-user-response.json" } } } @@ -3503,36 +3620,39 @@ } } } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/org/create-org-request.json" - } - } - } } - } - }, - "/org/{identifier}": { - "get": { + }, + "put": { "tags": [ - "Organization" + "Registry User" ], - "summary": "Retrieves information about the organization specified by short name or UUID (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves organization record for the specified shortname or UUID if it is the user's organization

Secretariat: Retrieves information about any organization

", - "operationId": "orgSingle", + "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", + "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", + "operationId": "registryUserUpdateSingle", "parameters": [ { - "name": "identifier", + "name": "shortname", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The shortname or UUID of the organization" + "description": "The shortname of the organization" + }, + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" + }, + { + "$ref": "#/components/parameters/active" + }, + { + "$ref": "#/components/parameters/orgShortname" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3546,11 +3666,25 @@ ], "responses": { "200": { - "description": "Returns the organization information", + "description": "Returns the updated user information", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/get-org-response.json" + "$ref": "../schemas/registry-user/update-registry-user-response.json" + }, + "example": { + "message": "jdoe was successfully updated.", + "updated": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } } } } @@ -3606,16 +3740,14 @@ } } } - } - }, - "/org/{shortname}": { - "put": { + }, + "delete": { "tags": [ - "Organization" + "Registry User" ], - "summary": "Updates information about the organization specified by short name (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Updates any organization's information

", - "operationId": "orgUpdateSingle", + "summary": "Deletes the registry user specified by organization and username (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Deletes the specified user from the specified organization

", + "operationId": "registryUserDeleteSingle", "parameters": [ { "name": "shortname", @@ -3627,19 +3759,13 @@ "description": "The shortname of the organization" }, { - "$ref": "#/components/parameters/id_quota" - }, - { - "$ref": "#/components/parameters/name" - }, - { - "$ref": "#/components/parameters/newShortname" - }, - { - "$ref": "#/components/parameters/active_roles_add" - }, - { - "$ref": "#/components/parameters/active_roles_remove" + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The username of the user" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3653,11 +3779,11 @@ ], "responses": { "200": { - "description": "Returns information about the organization updated", + "description": "Confirms deletion of the registry user", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/update-org-response.json" + "$ref": "../schemas/registry-user/delete-registry-user-response.json" } } } @@ -3715,14 +3841,14 @@ } } }, - "/org/{shortname}/id_quota": { - "get": { + "/registry/org/{shortname}": { + "put": { "tags": [ - "Organization" - ], - "summary": "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

Secretariat: Retrieves the CVE ID quota for any organization

", - "operationId": "orgIdQuota", + "Registry Organization" + ], + "summary": "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

  • short_name
  • long_name
  • authority
  • aliases
  • oversees
  • top_level_root
  • charter_or_scope
  • product_list
  • disclosure_policy
  • contact_info.websites
  • contact_info.emails
  • contact_info.phone
  • partner_role_type
  • partner_country
  • advisory_locations
  • advisory_location_require_credentials
  • vulnerability_advisory_location_for_web_scraping
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", + "operationId": "registryOrgUpdateSingle", "parameters": [ { "name": "shortname", @@ -3745,11 +3871,11 @@ ], "responses": { "200": { - "description": "Returns the CVE ID quota for an organization", + "description": "Returns information about the organization updated", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/get-org-quota-response.json" + "$ref": "../schemas/registry-org/update-registry-org-response.json" } } } @@ -3804,17 +3930,33 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/update-registry-org-request.json" + }, + "example": { + "short_name": "fake_company", + "long_name": "Fake Company", + "id_quota": 1000, + "authority": [ + "CNA" + ] + } + } + } } - } - }, - "/org/{shortname}/users": { - "get": { + }, + "delete": { "tags": [ - "Users" + "Registry Organization" ], - "summary": "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about users in the same organization

Secretariat: Retrieves all user information for any organization

", - "operationId": "userOrgAll", + "summary": "Deletes the registry organization specified by short name (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Deletes the specified registry organization

", + "operationId": "registryOrgDeleteSingle", "parameters": [ { "name": "shortname", @@ -3823,10 +3965,7 @@ "schema": { "type": "string" }, - "description": "The shortname of the organization" - }, - { - "$ref": "#/components/parameters/pageQuery" + "description": "The shortname of the registry organization" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3840,11 +3979,11 @@ ], "responses": { "200": { - "description": "Returns all users for the organization, along with pagination fields if results span multiple pages of data", + "description": "Confirms deletion of the registry organization", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/list-users-response.json" + "$ref": "../schemas/registry-org/delete-registry-org-response.json" } } } @@ -3902,14 +4041,14 @@ } } }, - "/org/{shortname}/user": { + "/registry/org/{shortname}/user": { "post": { "tags": [ - "Users" + "Registry User" ], "summary": "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)", "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Creates a user for the Admin's organization

Secretariat: Creates a user for any organization

", - "operationId": "userCreateSingle", + "operationId": "registryUserCreateSingle", "parameters": [ { "name": "shortname", @@ -3936,7 +4075,22 @@ "content": { "application/json": { "schema": { - "$ref": "../schemas/user/create-user-response.json" + "$ref": "../schemas/registry-user/create-registry-user-response.json" + } + }, + "example": { + "message": "jdoe was successfully created.", + "created": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "status": "active", + "secret": "12345-abcde-67890", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" } } } @@ -3997,21 +4151,29 @@ "content": { "application/json": { "schema": { - "$ref": "../schemas/user/create-user-request.json" + "$ref": "../schemas/registry-user/create-registry-user-request.json" + } + }, + "example": { + "username": "jdoe", + "status": "active", + "name": { + "first": "John", + "last": "Doe" } } } } } }, - "/org/{shortname}/user/{username}": { - "get": { + "/registry/org/{shortname}/user/{username}/reset_secret": { + "put": { "tags": [ - "Users" + "Registry User" ], - "summary": "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)", - "description": "

Access Control

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

Expected Behavior

Regular, CNA & Admin Users: Retrieves information about a user in the same organization

Secretariat: Retrieves any user's information

", - "operationId": "userSingle", + "summary": "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)", + "description": "

Access Control

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

Expected Behavior

Regular User: Resets user's own API secret

Admin User: Resets any user's API secret in the Admin's organization

Secretariat: Resets any user's API secret

", + "operationId": "registryUserResetSecret", "parameters": [ { "name": "shortname", @@ -4043,11 +4205,11 @@ ], "responses": { "200": { - "description": "Returns information about the specified user", + "description": "Returns the new API key", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/get-user-response.json" + "$ref": "../schemas/user/reset-secret-response.json" } } } @@ -4103,14 +4265,16 @@ } } } - }, - "put": { + } + }, + "/registry/org/{shortname}/user/{username}/grant-role": { + "post": { "tags": [ - "Users" + "Registry User" ], - "summary": "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)", - "description": "

Access Control

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

Expected Behavior

Regular User: Updates the user's own information. Only name fields may be changed.

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

", - "operationId": "userUpdateSingle", + "summary": "Grants a role to a user (accessible to Secretariat or Org Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Grants a role to a user in the Admin's organization

Secretariat: Grants a role to a user in any organization

", + "operationId": "registryUserGrantRole", "parameters": [ { "name": "shortname", @@ -4130,33 +4294,6 @@ }, "description": "The username of the user" }, - { - "$ref": "#/components/parameters/active" - }, - { - "$ref": "#/components/parameters/activeUserRolesAdd" - }, - { - "$ref": "#/components/parameters/activeUserRolesRemove" - }, - { - "$ref": "#/components/parameters/nameFirst" - }, - { - "$ref": "#/components/parameters/nameLast" - }, - { - "$ref": "#/components/parameters/nameMiddle" - }, - { - "$ref": "#/components/parameters/nameSuffix" - }, - { - "$ref": "#/components/parameters/newUsername" - }, - { - "$ref": "#/components/parameters/orgShortname" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -4169,11 +4306,16 @@ ], "responses": { "200": { - "description": "Returns the updated user information", + "description": "Role granted successfully", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/update-user-response.json" + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } } } @@ -4228,17 +4370,38 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "ADMIN" + ] + } + }, + "required": [ + "role" + ] + } + } + } } } }, - "/org/{shortname}/user/{username}/reset_secret": { - "put": { + "/registry/org/{shortname}/user/{username}/revoke-role": { + "post": { "tags": [ - "Users" + "Registry User" ], - "summary": "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)", - "description": "

Access Control

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

Expected Behavior

Regular User: Resets user's own API secret

Admin User: Resets any user's API secret in the Admin's organization

Secretariat: Resets any user's API secret

", - "operationId": "userResetSecret", + "summary": "Revokes a role from a user (accessible to Secretariat or Org Admin)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the target organization

Expected Behavior

Admin User: Revokes a role from a user in the Admin's organization

Secretariat: Revokes a role from a user in any organization

", + "operationId": "registryUserRevokeRole", "parameters": [ { "name": "shortname", @@ -4270,11 +4433,16 @@ ], "responses": { "200": { - "description": "Returns the new API key", + "description": "Role revoked successfully", "content": { "application/json": { "schema": { - "$ref": "../schemas/user/reset-secret-response.json" + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } } } @@ -4329,20 +4497,56 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "ADMIN" + ] + } + }, + "required": [ + "role" + ] + } + } + } } } }, - "/registry/users": { - "get": { + "/registry/org/{shortname}/conversation/{index}": { + "put": { "tags": [ - "Registry User" + "Registry Organization" ], - "summary": "Retrieves information about all registered users (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all users for all organizations

", - "operationId": "userAll", + "summary": "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

Expected Behavior

Original Author: Allowed to update only the message body of a conversation posted by them

Secretariat: Allowed to update the message body and/or visibility of any conversation

", + "operationId": "registryOrgUpdateConversation", "parameters": [ { - "$ref": "#/components/parameters/pageQuery" + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" + }, + { + "name": "index", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The index of the conversation to update" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -4356,11 +4560,11 @@ ], "responses": { "200": { - "description": "Returns all users, along with pagination fields if results span multiple pages of data.", + "description": "Returns the updated conversation", "content": { "application/json": { "schema": { - "$ref": "../schemas/registry-user/list-registry-users-response.json" + "$ref": "../schemas/conversation/update-conversation-response.json" } } } @@ -4418,14 +4622,14 @@ } } }, - "/users": { + "/registry/users": { "get": { "tags": [ - "Users" + "Registry User" ], "summary": "Retrieves information about all registered users (accessible to Secretariat)", "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all users for all organizations

", - "operationId": "userAll", + "operationId": "registryUserAll", "parameters": [ { "$ref": "#/components/parameters/pageQuery" @@ -4446,7 +4650,7 @@ "content": { "application/json": { "schema": { - "$ref": "../schemas/user/list-users-response.json" + "$ref": "../schemas/registry-user/list-registry-users-response.json" } } } @@ -4504,21 +4708,6 @@ } } }, - "/health-check": { - "get": { - "tags": [ - "Utilities" - ], - "summary": "Checks that the system is running (accessible to all users)", - "description": "

Access Control

Endpoint is accessible to all

Expected Behavior

Returns a 200 response code when CVE Services are running

", - "operationId": "healthCheck", - "responses": { - "200": { - "description": "Returns a 200 response code" - } - } - } - }, "/conversation": { "get": { "tags": [ diff --git a/schemas/registry-org/delete-registry-org-response.json b/schemas/registry-org/delete-registry-org-response.json new file mode 100644 index 000000000..f35e807f1 --- /dev/null +++ b/schemas/registry-org/delete-registry-org-response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/registry-org/delete-registry-org-response.json", + "type": "object", + "title": "CVE Delete Registry Org Response", + "description": "JSON Schema for a successful registry organization deletion response", + "properties": { + "message": { + "type": "string", + "description": "Confirmation that the registry organization was deleted" + } + }, + "required": [ + "message" + ], + "additionalProperties": false +} diff --git a/schemas/registry-user/delete-registry-user-response.json b/schemas/registry-user/delete-registry-user-response.json new file mode 100644 index 000000000..caf60671d --- /dev/null +++ b/schemas/registry-user/delete-registry-user-response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/registry-user/delete-registry-user-response.json", + "type": "object", + "title": "CVE Delete Registry User Response", + "description": "JSON Schema for a successful registry user deletion response", + "properties": { + "message": { + "type": "string", + "description": "Confirmation that the registry user was deleted" + } + }, + "required": [ + "message" + ], + "additionalProperties": false +} diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index d3b472d1b..52e4c4c77 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -3,8 +3,6 @@ const router = express.Router() const mw = require('../../middleware/middleware') const errorMsgs = require('../../middleware/errorMessages') const controller = require('./org.controller') -const registryOrgController = require('../registry-org.controller/registry-org.controller.js') -const registryUserController = require('../registry-user.controller/registry-user.controller.js') const { body, param, query } = require('express-validator') const { parseGetParams, parsePostParams, parsePutParams, parseError, isUserRole, isValidUsername, isOrgRole, validateUpdateOrgParameters, shortCircuitLegacyCpsMitreOrgParameters } = require('./org.middleware') // Only God and Javascript know swhy its saying it is not used when it is..... @@ -12,1192 +10,6 @@ const { parseGetParams, parsePostParams, parsePutParams, parseError, isUserRole, const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../../src/constants').getConstants const CONSTANTS = getConstants() - -router.get('/registry/org', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'registryOrgAll' - #swagger.summary = "Retrieves all registry organizations (accessible to Secretariat)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role

-

Expected Behavior

-

Secretariat: Retrieves information about all registry organizations

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns information about all registry organizations, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/list-registry-orgs-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - parseError, - parseGetParams, - registryOrgController.ALL_ORGS -) - -router.get('/registry/org/:shortname/users', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'userOrgAll' - #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

-

Expected Behavior

-

Regular, CNA & Admin Users: Retrieves information about users in the same organization

-

Secretariat: Retrieves all user information for any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns all users for the organization, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-user/list-registry-users-response.json' - }, - example: { - "totalCount": 1, - "itemsPerPage": 100, - "pageCount": 1, - "currentPage": 1, - "prevPage": null, - "nextPage": null, - "users": [ - { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "role": "ADMIN", - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - ] - } - } - } - } - #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.useRegistry(), - mw.validateUser, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - parseError, - parseGetParams, - registryOrgController.USER_ALL) - -router.get('/registry/org/:shortname/id_quota', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'orgIdQuota' - #swagger.summary = "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

-

Expected Behavior

-

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

-

Secretariat: Retrieves the CVE ID quota for any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the CVE ID quota for an organization', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/get-registry-org-quota-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.useRegistry(), - mw.validateUser, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), - parseError, - parseGetParams, - controller.ORG_ID_QUOTA) - -router.get('/registry/org/:identifier', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'registryOrgSingle' - #swagger.summary = "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

-

Expected Behavior

-

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

-

Secretariat: Retrieves information about any registry organization

" - #swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' } - #swagger.parameters['expand'] = { - in: 'query', - description: 'Optional expanded related data. Accepted value: users.', - required: false, - schema: { - type: 'string', - enum: ['users'] - } - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the registry organization information', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/get-registry-org-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.useRegistry(), - mw.validateUser, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['expand']) }), - query(['expand']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['expand']).optional().isIn(['users']), - parseError, - parseGetParams, - registryOrgController.SINGLE_ORG -) - -router.get('/registry/org/:shortname/user/:username', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserSingle' - #swagger.summary = "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

-

Expected Behavior

-

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

-

Secretariat: Retrieves any registry user's information

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.parameters['shortname'] = { - description: 'The shortname of the organization' - } - #swagger.parameters['username'] = { - description: 'The username of the registry user', - schema: { - type: 'string', - pattern: '^[a-zA-Z0-9._@-]+$' - } - } - #swagger.responses[200] = { - description: 'Returns information about the specified registry user', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/get-registry-user-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.useRegistry(), - mw.validateUser, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - param(['username']).isString().trim().notEmpty().custom(isValidUsername), - query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), - parseError, - parseGetParams, - registryUserController.SINGLE_USER -) - -router.post('/registry/org', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'orgCreateSingle' - #swagger.summary = "Creates an organization (accessible to Secretariat)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role

-

Expected Behavior

-

Secretariat: Creates a new organization

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { - anyOf: [ - { $ref: '../schemas/registry-org/SecretariatOrg.json' }, - { $ref: '../schemas/registry-org/CNAOrg.json' }, - { $ref: '../schemas/registry-org/ADPOrg.json' }, - { $ref: '../schemas/registry-org/BulkDownloadOrg.json' } - ] - }, - example: { - short_name: 'fake_company', - long_name: 'Fake Company', - id_quota: 1000, - authority: ['CNA'] - } - } - } - } - #swagger.responses[200] = { - description: 'Returns information about all organizations, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/list-registry-orgs-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), - parsePostParams, - parseError, - registryOrgController.CREATE_ORG -) - -router.put('/registry/org/:shortname', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'orgUpdateSingle' - #swagger.summary = "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

-

With Joint Approval required for the following fields:

-

Expected Behavior

- This endpoint expects a full organization object in the request body. -

Secretariat: Updates any organization's information

-

Organization Admin: Requests changes to its organization's information

-
    -
  • short_name
  • -
  • long_name
  • -
  • authority
  • -
  • aliases
  • -
  • oversees
  • -
  • top_level_root
  • -
  • charter_or_scope
  • -
  • product_list
  • -
  • disclosure_policy
  • -
  • contact_info.websites
  • -
  • contact_info.emails
  • -
  • contact_info.phone
  • -
  • partner_role_type
  • -
  • partner_country
  • -
  • advisory_locations
  • -
  • advisory_location_require_credentials
  • -
  • vulnerability_advisory_location_for_web_scraping
  • -
  • industry
  • -
  • tl_root_start_date
  • -
  • is_cna_discussion_list
  • -
" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { - $ref: '../schemas/registry-org/update-registry-org-request.json' - }, - example: { - short_name: 'fake_company', - long_name: 'Fake Company', - id_quota: 1000, - authority: ['CNA'] - } - } - } - } - #swagger.responses[200] = { - description: 'Returns information about the organization updated', - content: { - "application/json": { - schema: { - $ref: '../schemas/registry-org/update-registry-org-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.useRegistry(), - mw.validateUser, - // mw.onlySecretariat, - parseError, - parsePutParams, - registryOrgController.UPDATE_ORG -) - -router.post('/registry/org/:shortname/user', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserCreateSingle' - #swagger.summary = "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the target organization

-

Expected Behavior

-

Admin User: Creates a user for the Admin's organization

-

Secretariat: Creates a user for any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: - { $ref: '../schemas/registry-user/create-registry-user-request.json' } - }, - example: { - "username": "jdoe", - "status": "active", - "name": { - "first": "John", - "last": "Doe" - } - } - } - } - #swagger.responses[200] = { - description: 'Returns the new user information (with the secret)', - content: { - "application/json": { - schema: - { $ref: '../schemas/registry-user/create-registry-user-response.json' } - }, - example: { - "message": "jdoe was successfully created.", - "created": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "secret": "12345-abcde-67890", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - } - } - } - #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.useRegistry(), - mw.validateUser, - mw.onlySecretariatOrAdmin, - mw.onlyOrgWithPartnerRole, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['org_uuid']).optional().isString().trim(), - body(['uuid']).optional().isString().trim(), - body(['name.first']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_FIRSTNAME_LENGTH }).withMessage(errorMsgs.FIRSTNAME_LENGTH), - body(['name.last']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_LASTNAME_LENGTH }).withMessage(errorMsgs.LASTNAME_LENGTH), - body(['name.middle']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_MIDDLENAME_LENGTH }).withMessage(errorMsgs.MIDDLENAME_LENGTH), - body(['name.suffix']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_SUFFIX_LENGTH }).withMessage(errorMsgs.SUFFIX_LENGTH), - body(['authority.active_roles']).optional() - .custom(mw.isFlatStringArray) - .bail() - .customSanitizer(toUpperCaseArray) - .custom(isUserRole), - parseError, - parsePostParams, - registryOrgController.USER_CREATE_SINGLE -) - -router.put('/registry/org/:shortname/user/:username', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserUpdateSingle' - #swagger.summary = "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

-

Expected Behavior

-

Regular User: Updates the user's own information. Only name fields may be changed.

-

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

-

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/active', - '#/components/parameters/orgShortname', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the updated user information', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/update-registry-user-response.json' }, - example: { - "message": "jdoe was successfully updated.", - "updated": { - "UUID": "fe566221-6a2c-4279-8800-4d3795325997", - "username": "jdoe", - "name": { - "first": "John", - "last": "Doe" - }, - "status": "active", - "created": "2021-02-12T17:15:37.382Z", - "last_updated": "2021-02-12T17:15:37.382Z" - } - } - } - } - } - #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.useRegistry(), - mw.validateUser, - mw.onlyOrgWithPartnerRole, - parseError, - parsePutParams, - registryUserController.UPDATE_USER) - -router.put('/registry/org/:shortname/user/:username/reset_secret', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'userResetSecret' - #swagger.summary = "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)" - #swagger.description = " -

Access Control

-

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

-

Expected Behavior

-

Regular User: Resets user's own API secret

-

Admin User: Resets any user's API secret in the Admin's organization

-

Secretariat: Resets any user's API secret

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the new API key', - content: { - "application/json": { - schema: { $ref: '../schemas/user/reset-secret-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.useRegistry(), - mw.validateUser, - mw.onlyOrgWithPartnerRole, - parseError, - parsePostParams, - controller.USER_RESET_SECRET -) - -router.post('/registry/org/:shortname/user/:username/grant-role', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserGrantRole' - #swagger.summary = "Grants a role to a user (accessible to Secretariat or Org Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the target organization

-

Expected Behavior

-

Admin User: Grants a role to a user in the Admin's organization

-

Secretariat: Grants a role to a user in any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - role: { - type: 'string', - enum: ['ADMIN'] - } - }, - required: ['role'] - } - } - } - } - #swagger.responses[200] = { - description: 'Role granted successfully', - content: { - "application/json": { - schema: { type: 'object', properties: { message: { type: 'string' } } } - } - } - } - #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.useRegistry(), - mw.validateUser, - // mw.onlyOrgWithPartnerRole, // This might be too restrictive if we want Secretariat to do it for any org type - parseError, - parsePostParams, - registryUserController.GRANT_ROLE -) - -router.post('/registry/org/:shortname/user/:username/revoke-role', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserRevokeRole' - #swagger.summary = "Revokes a role from a user (accessible to Secretariat or Org Admin)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be an Admin of the target organization

-

Expected Behavior

-

Admin User: Revokes a role from a user in the Admin's organization

-

Secretariat: Revokes a role from a user in any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['username'] = { description: 'The username of the user' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - role: { - type: 'string', - enum: ['ADMIN'] - } - }, - required: ['role'] - } - } - } - } - #swagger.responses[200] = { - description: 'Role revoked successfully', - content: { - "application/json": { - schema: { type: 'object', properties: { message: { type: 'string' } } } - } - } - } - #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.useRegistry(), - mw.validateUser, - // mw.onlyOrgWithPartnerRole, - parseError, - parsePostParams, - registryUserController.REVOKE_ROLE -) - -router.put('/registry/org/:shortname/conversation/:index', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'registryUserUpdateConversation' - #swagger.summary = "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

-

Expected Behavior

-

Original Author: Allowed to update only the message body of a conversation posted by them

-

Secretariat: Allowed to update the message body and/or visibility of any conversation

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['index'] = { description: 'The index of the conversation to update' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns the updated conversation', - content: { - "application/json": { - schema: { $ref: '../schemas/conversation/update-conversation-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.useRegistry(), - mw.validateUser, - mw.onlyOrgWithPartnerRole, - parseError, - parsePostParams, - registryOrgController.EDIT_CONVERSATION -) - router.get('/org', /* #swagger.tags = ['Organization'] diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 9bff904f0..8fccc6b3b 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -510,7 +510,7 @@ async function createUser (req, res, next) { /** * Updates a user only if the user exist for the specified username. * If no user exists, it does not create the user. - * Called by PUT /org/{shortname}/user/{username} + * Called by PUT /api/org/{shortname}/user/{username} * * @param {Object} req - The request object * @param {Object} res - The response object @@ -667,7 +667,7 @@ async function updateUser (req, res, next) { /** * Resets API secret for specified user. - * Called by PUT /org/{shortname}/user/{username}/reset_secret, PUT /registry/org/{shortname}/user/{username}/reset_secret + * Called by PUT /api/org/{shortname}/user/{username}/reset_secret, PUT /api/registry/org/{shortname}/user/{username}/reset_secret * * @param {Object} req - The request object * @param {Object} res - The response object diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index e8f642899..fdff50735 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -168,6 +168,11 @@ function parseGetParams (req, res, next) { next() } +function parseDeleteParams (req, res, next) { + utils.reqCtxMapping(req, 'params', ['shortname', 'username']) + next() +} + function parseError (req, res, next) { const err = validationResult(req).formatWith(({ location, msg, param, value, nestedErrors }) => { return { msg: msg, param: param, location: location } @@ -190,6 +195,7 @@ module.exports = { parsePutParams, parsePostParams, parseGetParams, + parseDeleteParams, parseError, isOrgRole, isUserRole, diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js deleted file mode 100644 index aa9acc93b..000000000 --- a/src/controller/registry-org.controller/index.js +++ /dev/null @@ -1,552 +0,0 @@ -const express = require('express') -const router = express.Router() -const mw = require('../../middleware/middleware') -const { param, query, body } = require('express-validator') -const controller = require('./registry-org.controller') -const { parseGetParams, parsePostParams, parseDeleteParams, parseError } = require('./registry-org.middleware') -const getConstants = require('../../constants').getConstants -const CONSTANTS = getConstants() - -router.get('/registryOrg', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'getAllRegistryOrgs' - #swagger.ignore = true - #swagger.summary = "Retrieves information about all registry organizations (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Retrieves a list of all registry organizations

- #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'A list of all registry organizations, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-org/list-registry-orgs-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, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - parseError, - parseGetParams, - controller.ALL_ORGS -) - -router.get('/registryOrg/:identifier', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'getSingleRegistryOrg' - #swagger.ignore = true - #swagger.summary = "Retrieves information about a specific registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Retrieves information about the specified registry organization

- #swagger.parameters['identifier'] = { - in: 'path', - description: 'The identifier of the registry organization', - required: true, - type: 'string' - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'The requested registry organization information is returned', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-org/get-registry-org-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[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(['identifier']).isString().trim(), - parseError, - parseGetParams, - controller.SINGLE_ORG -) - -router.post('/registryOrg', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'createRegistryOrg' - #swagger.ignore = true - #swagger.summary = "Creates a new registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Creates a new registry organization

- #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { $ref: '../schemas/registry-org/create-registry-org-request.json' } - } - } - } - #swagger.responses[201] = { - description: 'The registry organization was successfully created', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-org/create-registry-org-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - body(['reports_to']).not().exists().withMessage('reports_to must not be present'), - parseError, - parsePostParams, - controller.CREATE_ORG -) - -router.put('/registryOrg/:shortname', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'updateRegistryOrg' - #swagger.ignore = true - #swagger.summary = "Updates an existing registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Updates an existing registry organization

- #swagger.parameters['shortname'] = { - in: 'path', - description: 'The Shortname of the registry organization to update', - required: true, - type: 'string' - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { $ref: '../schemas/registry-org/update-registry-org-request.json' } - } - } - } - #swagger.responses[200] = { - description: 'The registry organization was successfully updated', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-org/update-registry-org-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - param(['shortname']).isString().trim(), - body(['reports_to']).not().exists().withMessage('reports_to must not be present'), - parseError, - parsePostParams, - controller.UPDATE_ORG -) - -router.delete( - '/registryOrg/:identifier', - /* - #swagger.tags = ['Registry Organization'] - #swagger.operationId = 'deleteRegistryOrg' - #swagger.ignore = true - #swagger.summary = "Deletes an existing registry organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Deletes an existing registry organization

- #swagger.parameters['identifier'] = { - in: 'path', - description: 'The identifier of the registry organization to delete', - required: true, - type: 'string' - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'The registry organization was successfully deleted', - content: { - "application/json": { - schema: { - type: 'object', - properties: { - message: { - type: 'string', - description: 'Message describing successful deletion operation' - } - } - } - } - } - } - #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' } - } - } - } - */ - mw.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - param(['identifier']).isString().trim(), - parseError, - parseDeleteParams, - controller.DELETE_ORG -) - -router.get('/registryOrg/:shortname/users', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'registryUserOrgAll' - #swagger.ignore = true - #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Retrieves all user information for any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns all users for the organization, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/list-registry-users-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - parseError, - parseGetParams, - controller.USER_ALL) - -router.post('/registryOrg/:shortname/user', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'RegistryUserCreateSingle' - #swagger.ignore = true - #swagger.summary = "Create a user with the provided short name as the owning organization (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Creates a user for any organization

" - #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { $ref: '../schemas/registry-user/create-registry-user-request.json' }, - } - } - } - #swagger.responses[200] = { - description: 'Returns the new user information (with the secret)', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/create-registry-user-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - - parseError, - parsePostParams, - controller.USER_CREATE_SINGLE) - -module.exports = router diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js deleted file mode 100644 index 2d79faf6a..000000000 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ /dev/null @@ -1,74 +0,0 @@ -const utils = require('../../utils/utils') -const getConstants = require('../../constants').getConstants -const { validationResult } = require('express-validator') -const errors = require('./error') -const error = new errors.RegistryOrgControllerError() - -function parsePostParams (req, res, next) { - req.body = utils.deepRemoveEmpty(req.body) - utils.reqCtxMapping(req, 'body', []) - utils.reqCtxMapping(req, 'params', ['identifier', 'shortname']) - utils.reqCtxMapping(req, 'query', [ - 'long_name', 'short_name', 'aliases', - 'cve_program_org_function', 'authority.active_roles', - 'oversees', - 'top_level_root', 'users', - 'charter_or_scope', 'disclosure_policy', 'product_list', - 'id_quota', - 'private_contacts', 'contact_info.websites', 'contact_info.emails', 'contact_info.phone', - 'partner_role_type', - 'partner_number', - 'partner_country', - 'program_data.cve_website_update_date', - 'program_data.cve_website_update_needed', - 'program_data.status', - 'advisory_locations', - 'advisory_location_require_credentials', - 'vulnerability_advisory_location_for_web_scraping', - 'industry', - 'tl_root_start_date', - 'is_cna_discussion_list' - ]) - next() -} - -function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'params', ['identifier', 'shortname']) - utils.reqCtxMapping(req, 'query', ['page']) - next() -} - -function parseDeleteParams (req, res, next) { - utils.reqCtxMapping(req, 'params', ['identifier']) - next() -} - -function isOrgRole (val) { - const CONSTANTS = getConstants() - - val.forEach(role => { - if (!CONSTANTS.ORG_ROLES.includes(role)) { - throw new Error('Organization role does not exist.') - } - }) - - return true -} - -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, - parseError, - parseDeleteParams, - isOrgRole -} diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js deleted file mode 100644 index 179987a83..000000000 --- a/src/controller/registry-user.controller/index.js +++ /dev/null @@ -1,397 +0,0 @@ -const express = require('express') -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, parseError } = require('./registry-user.middleware') -const getConstants = require('../../constants').getConstants -const CONSTANTS = getConstants() - -router.get('/registryUser', - /* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'getAllRegistryUsers' - #swagger.ignore = true - #swagger.summary = "Retrieves information about all registry users (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Retrieves a list of all registry users

- #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'A list of all registry users, along with pagination fields if results span multiple pages of data', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/list-registry-users-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, - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - parseError, - parseGetParams, - controller.ALL_USERS -) - -router.get('/registryUser/:identifier', -/* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'getSingleRegistryUser' - #swagger.ignore = true - #swagger.summary = "Retrieves information about a specific registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Retrieves information about the specified registry user

- #swagger.parameters['identifier'] = { - in: 'path', - description: 'The identifier of the registry user', - required: true, - type: 'string' - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'The requested registry user information is returned', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/get-registry-user-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[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(['identifier']).isString().trim(), - parseError, - parseGetParams, - controller.SINGLE_USER -) - -router.post('/registryUser/:shortname', - /* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'createRegistryUser' - #swagger.ignore = true - #swagger.summary = "Creates a new registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Creates a new registry user

- #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { $ref: '../schemas/registry-user/create-registry-user-request.json' } - } - } - } - #swagger.responses[201] = { - description: 'The registry user was successfully created', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/create-registry-user-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, - param(['shortname']).isString().trim(), - parseError, - parsePostParams, - controller.CREATE_USER -) - -router.put('/registryUser/:identifier', - /* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'updateRegistryUser' - #swagger.ignore = true - #swagger.summary = "Updates an existing registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Updates an existing registry user

- #swagger.parameters['identifier'] = { - in: 'path', - description: 'The identifier of the registry user to update', - required: true, - type: 'string' - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.requestBody = { - required: true, - content: { - 'application/json': { - schema: { $ref: '../schemas/registry-user/update-registry-user-request.json' } - } - } - } - #swagger.responses[200] = { - description: 'The registry user was successfully updated', - content: { - "application/json": { - schema: { $ref: '../schemas/registry-user/update-registry-user-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, - param(['identifier']).isString().trim(), - // TODO: do more validation here - parseError, - parsePostParams, - controller.UPDATE_USER -) - -router.delete( - '/registryUser/:identifier', - /* - #swagger.tags = ['Secretariat Only Utility Endpoints'] - #swagger.operationId = 'deleteRegistryUser' - #swagger.ignore = true - #swagger.summary = "Deletes an existing registry user (accessible to Secretariat only)" - #swagger.description = " -

Access Control

-

Only users with Secretariat role can access this endpoint

-

Expected Behavior

-

Secretariat: Deletes an existing registry user

- #swagger.parameters['identifier'] = { - in: 'path', - description: 'The identifier of the registry user to delete', - required: true, - type: 'string' - } - #swagger.parameters['$ref'] = [ - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'The registry user was successfully deleted', - content: { - "application/json": { - schema: { - type: 'object', - properties: { - message: { - type: 'string', - description: 'Message describing successful deletion operation' - } - } - } - } - } - } - #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(['identifier']).isString().trim(), - parseError, - parseDeleteParams, - controller.DELETE_USER -) - -module.exports = router diff --git a/src/controller/registry-user.controller/registry-user.middleware.js b/src/controller/registry-user.controller/registry-user.middleware.js deleted file mode 100644 index cffcb7e6f..000000000 --- a/src/controller/registry-user.controller/registry-user.middleware.js +++ /dev/null @@ -1,42 +0,0 @@ -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', 'shortname']) - utils.reqCtxMapping(req, 'query', [ - 'new_username', - 'name.first', 'name.last', 'name.middle', 'name.suffix' - ]) - next() -} - -function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'params', ['identifier']) - utils.reqCtxMapping(req, 'query', ['page']) - next() -} - -function parseDeleteParams (req, res, next) { - utils.reqCtxMapping(req, 'params', ['identifier']) - 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, - parseError -} diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js new file mode 100644 index 000000000..36874272e --- /dev/null +++ b/src/controller/registry.controller/index.js @@ -0,0 +1,1430 @@ +const express = require('express') +const router = express.Router() +const mw = require('../../middleware/middleware') +const errorMsgs = require('../../middleware/errorMessages') +const controller = require('../org.controller/org.controller') +const registryOrgController = require('./org.registry.controller') +const registryUserController = require('./user.registry.controller') +const { body, param, query } = require('express-validator') +const { parseGetParams, parsePostParams, parsePutParams, parseDeleteParams, parseError, isUserRole, isValidUsername } = require('../org.controller/org.middleware') +// Only God and Javascript know swhy its saying it is not used when it is..... +// eslint-disable-next-line no-unused-vars +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') +const getConstants = require('../../../src/constants').getConstants +const CONSTANTS = getConstants() +const { parseGetParams: parseUserGetParams, parseError: parseUserError } = require('../user.controller/user.middleware') + +router.get('/registry/org', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgAll' + #swagger.summary = "Retrieves all registry organizations (accessible to Secretariat)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves information about all registry organizations

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns information about all registry organizations, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/list-registry-orgs-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.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + parseError, + parseGetParams, + registryOrgController.ALL_ORGS +) + +router.get('/registry/org/:shortname/users', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryOrgUsersAll' + #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

+

Expected Behavior

+

Regular, CNA & Admin Users: Retrieves information about users in the same organization

+

Secretariat: Retrieves all user information for any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns all users for the organization, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-user/list-registry-users-response.json' + }, + example: { + "totalCount": 1, + "itemsPerPage": 100, + "pageCount": 1, + "currentPage": 1, + "prevPage": null, + "nextPage": null, + "users": [ + { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "role": "ADMIN", + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } + ] + } + } + } + } + #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.useRegistry(), + mw.validateUser, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + parseError, + parseGetParams, + registryOrgController.USER_ALL) + +router.get('/registry/org/:shortname/id_quota', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgIdQuota' + #swagger.summary = "Retrieves an organization's CVE ID quota (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

+

Expected Behavior

+

Regular, CNA & Admin Users: Retrieves the CVE ID quota for the user's organization

+

Secretariat: Retrieves the CVE ID quota for any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the CVE ID quota for an organization', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/get-registry-org-quota-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.useRegistry(), + mw.validateUser, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), + parseError, + parseGetParams, + controller.ORG_ID_QUOTA) + +router.get('/registry/org/:identifier', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgSingle' + #swagger.summary = "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.

+

Expected Behavior

+

Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization

+

Secretariat: Retrieves information about any registry organization

" + #swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' } + #swagger.parameters['expand'] = { + in: 'query', + description: 'Optional expanded related data. Accepted value: users.', + required: false, + schema: { + type: 'string', + enum: ['users'] + } + } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the registry organization information', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/get-registry-org-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.useRegistry(), + mw.validateUser, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['expand']) }), + query(['expand']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['expand']).optional().isIn(['users']), + parseError, + parseGetParams, + registryOrgController.SINGLE_ORG +) + +router.get('/registry/org/:shortname/user/:username', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserSingle' + #swagger.summary = "Retrieves information about a user for the specified username and organization short name (accessible to same-organization users or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can access this endpoint only for users in their own organization. Secretariat users can access any user.

+

Expected Behavior

+

Regular, CNA & Admin Users: Retrieves information about a registry user in the same organization

+

Secretariat: Retrieves any registry user's information

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.parameters['shortname'] = { + description: 'The shortname of the organization' + } + #swagger.parameters['username'] = { + description: 'The username of the registry user', + schema: { + type: 'string', + pattern: '^[a-zA-Z0-9._@-]+$' + } + } + #swagger.responses[200] = { + description: 'Returns information about the specified registry user', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-user/get-registry-user-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.useRegistry(), + mw.validateUser, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), + parseError, + parseGetParams, + registryUserController.SINGLE_USER +) + +router.post('/registry/org', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgCreateSingle' + #swagger.summary = "Creates an organization (accessible to Secretariat)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Creates a new organization

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + anyOf: [ + { $ref: '../schemas/registry-org/SecretariatOrg.json' }, + { $ref: '../schemas/registry-org/CNAOrg.json' }, + { $ref: '../schemas/registry-org/ADPOrg.json' }, + { $ref: '../schemas/registry-org/BulkDownloadOrg.json' } + ] + }, + example: { + short_name: 'fake_company', + long_name: 'Fake Company', + id_quota: 1000, + authority: ['CNA'] + } + } + } + } + #swagger.responses[200] = { + description: 'Returns information about all organizations, along with pagination fields if results span multiple pages of data', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/list-registry-orgs-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.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), + parsePostParams, + parseError, + registryOrgController.CREATE_ORG +) + +router.put('/registry/org/:shortname', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgUpdateSingle' + #swagger.summary = "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

+

With Joint Approval required for the following fields:

+

Expected Behavior

+ This endpoint expects a full organization object in the request body. +

Secretariat: Updates any organization's information

+

Organization Admin: Requests changes to its organization's information

+
    +
  • short_name
  • +
  • long_name
  • +
  • authority
  • +
  • aliases
  • +
  • oversees
  • +
  • top_level_root
  • +
  • charter_or_scope
  • +
  • product_list
  • +
  • disclosure_policy
  • +
  • contact_info.websites
  • +
  • contact_info.emails
  • +
  • contact_info.phone
  • +
  • partner_role_type
  • +
  • partner_country
  • +
  • advisory_locations
  • +
  • advisory_location_require_credentials
  • +
  • vulnerability_advisory_location_for_web_scraping
  • +
  • industry
  • +
  • tl_root_start_date
  • +
  • is_cna_discussion_list
  • +
" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/registry-org/update-registry-org-request.json' + }, + example: { + short_name: 'fake_company', + long_name: 'Fake Company', + id_quota: 1000, + authority: ['CNA'] + } + } + } + } + #swagger.responses[200] = { + description: 'Returns information about the organization updated', + content: { + "application/json": { + schema: { + $ref: '../schemas/registry-org/update-registry-org-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.useRegistry(), + mw.validateUser, + // mw.onlySecretariat, + parseError, + parsePutParams, + registryOrgController.UPDATE_ORG +) + +router.delete('/registry/org/:shortname', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgDeleteSingle' + #swagger.summary = "Deletes the registry organization specified by short name (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Deletes the specified registry organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the registry organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Confirms deletion of the registry organization', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-org/delete-registry-org-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.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + parseError, + parseDeleteParams, + registryOrgController.DELETE_ORG +) + +router.post('/registry/org/:shortname/user', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserCreateSingle' + #swagger.summary = "Create a user with the provided short name as the owning organization (accessible to Secretariat or target organization Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the target organization

+

Expected Behavior

+

Admin User: Creates a user for the Admin's organization

+

Secretariat: Creates a user for any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: + { $ref: '../schemas/registry-user/create-registry-user-request.json' } + }, + example: { + "username": "jdoe", + "status": "active", + "name": { + "first": "John", + "last": "Doe" + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the new user information (with the secret)', + content: { + "application/json": { + schema: + { $ref: '../schemas/registry-user/create-registry-user-response.json' } + }, + example: { + "message": "jdoe was successfully created.", + "created": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "status": "active", + "secret": "12345-abcde-67890", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } + } + } + } + #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.useRegistry(), + mw.validateUser, + mw.onlySecretariatOrAdmin, + mw.onlyOrgWithPartnerRole, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['org_uuid']).optional().isString().trim(), + body(['uuid']).optional().isString().trim(), + body(['name.first']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_FIRSTNAME_LENGTH }).withMessage(errorMsgs.FIRSTNAME_LENGTH), + body(['name.last']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_LASTNAME_LENGTH }).withMessage(errorMsgs.LASTNAME_LENGTH), + body(['name.middle']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_MIDDLENAME_LENGTH }).withMessage(errorMsgs.MIDDLENAME_LENGTH), + body(['name.suffix']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_SUFFIX_LENGTH }).withMessage(errorMsgs.SUFFIX_LENGTH), + body(['authority.active_roles']).optional() + .custom(mw.isFlatStringArray) + .bail() + .customSanitizer(toUpperCaseArray) + .custom(isUserRole), + parseError, + parsePostParams, + registryOrgController.USER_CREATE_SINGLE +) + +router.put('/registry/org/:shortname/user/:username', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserUpdateSingle' + #swagger.summary = "Updates information about a user for the specified username and organization shortname (accessible to self, same-organization Admins, or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can update their own name fields. Organization admins can update users in their organization. Secretariat users can update users in any organization.

+

Expected Behavior

+

Regular User: Updates the user's own information. Only name fields may be changed.

+

Admin User: Updates information about a user in the Admin's organization. Allowed to change all fields except org_short_name.

+

Secretariat: Updates information about a user in any organization. Allowed to change all fields.

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/active', + '#/components/parameters/orgShortname', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the updated user information', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-user/update-registry-user-response.json' }, + example: { + "message": "jdoe was successfully updated.", + "updated": { + "UUID": "fe566221-6a2c-4279-8800-4d3795325997", + "username": "jdoe", + "name": { + "first": "John", + "last": "Doe" + }, + "status": "active", + "created": "2021-02-12T17:15:37.382Z", + "last_updated": "2021-02-12T17:15:37.382Z" + } + } + } + } + } + #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.useRegistry(), + mw.validateUser, + mw.onlyOrgWithPartnerRole, + parseError, + parsePutParams, + registryUserController.UPDATE_USER) + +router.delete('/registry/org/:shortname/user/:username', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserDeleteSingle' + #swagger.summary = "Deletes the registry user specified by organization and username (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Deletes the specified user from the specified organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Confirms deletion of the registry user', + content: { + "application/json": { + schema: { $ref: '../schemas/registry-user/delete-registry-user-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.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + param(['username']).isString().trim().notEmpty().custom(isValidUsername), + parseError, + parseDeleteParams, + registryUserController.DELETE_USER +) + +router.put('/registry/org/:shortname/user/:username/reset_secret', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserResetSecret' + #swagger.summary = "Reset the API key for a user (accessible to self, same-organization Admins, or Secretariat)" + #swagger.description = " +

Access Control

+

Authenticated users can reset their own API secret. Organization admins can reset users in their organization. Secretariat users can reset any user's API secret.

+

Expected Behavior

+

Regular User: Resets user's own API secret

+

Admin User: Resets any user's API secret in the Admin's organization

+

Secretariat: Resets any user's API secret

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the new API key', + content: { + "application/json": { + schema: { $ref: '../schemas/user/reset-secret-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.useRegistry(), + mw.validateUser, + mw.onlyOrgWithPartnerRole, + parseError, + parsePostParams, + controller.USER_RESET_SECRET +) + +router.post('/registry/org/:shortname/user/:username/grant-role', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserGrantRole' + #swagger.summary = "Grants a role to a user (accessible to Secretariat or Org Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the target organization

+

Expected Behavior

+

Admin User: Grants a role to a user in the Admin's organization

+

Secretariat: Grants a role to a user in any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + role: { + type: 'string', + enum: ['ADMIN'] + } + }, + required: ['role'] + } + } + } + } + #swagger.responses[200] = { + description: 'Role granted successfully', + content: { + "application/json": { + schema: { type: 'object', properties: { message: { type: 'string' } } } + } + } + } + #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.useRegistry(), + mw.validateUser, + // mw.onlyOrgWithPartnerRole, // This might be too restrictive if we want Secretariat to do it for any org type + parseError, + parsePostParams, + registryUserController.GRANT_ROLE +) + +router.post('/registry/org/:shortname/user/:username/revoke-role', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserRevokeRole' + #swagger.summary = "Revokes a role from a user (accessible to Secretariat or Org Admin)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the target organization

+

Expected Behavior

+

Admin User: Revokes a role from a user in the Admin's organization

+

Secretariat: Revokes a role from a user in any organization

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['username'] = { description: 'The username of the user' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + role: { + type: 'string', + enum: ['ADMIN'] + } + }, + required: ['role'] + } + } + } + } + #swagger.responses[200] = { + description: 'Role revoked successfully', + content: { + "application/json": { + schema: { type: 'object', properties: { message: { type: 'string' } } } + } + } + } + #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.useRegistry(), + mw.validateUser, + // mw.onlyOrgWithPartnerRole, + parseError, + parsePostParams, + registryUserController.REVOKE_ROLE +) + +router.put('/registry/org/:shortname/conversation/:index', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgUpdateConversation' + #swagger.summary = "Update the conversation at the given index for the given organization (accessible to Secretariat or original same-organization author)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be the original author of the conversation in the same organization

+

Expected Behavior

+

Original Author: Allowed to update only the message body of a conversation posted by them

+

Secretariat: Allowed to update the message body and/or visibility of any conversation

" + #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } + #swagger.parameters['index'] = { description: 'The index of the conversation to update' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the updated conversation', + content: { + "application/json": { + schema: { $ref: '../schemas/conversation/update-conversation-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.useRegistry(), + mw.validateUser, + mw.onlyOrgWithPartnerRole, + parseError, + parsePostParams, + registryOrgController.EDIT_CONVERSATION +) + +router.get('/registry/users', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserAll' + #swagger.summary = "Retrieves information about all registered users (accessible to Secretariat)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves information about all users for all organizations

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/pageQuery', + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns all users, along with pagination fields if results span multiple pages of data.', + content:{ + "application/json":{ + schema: { + $ref: '../schemas/registry-user/list-registry-users-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.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + parseUserError, + parseUserGetParams, + registryUserController.ALL_USERS +) + +module.exports = router diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry.controller/org.error.js similarity index 98% rename from src/controller/registry-org.controller/error.js rename to src/controller/registry.controller/org.error.js index 1b350b417..c47c26a9f 100644 --- a/src/controller/registry-org.controller/error.js +++ b/src/controller/registry.controller/org.error.js @@ -1,5 +1,6 @@ const idrErr = require('../../utils/error') +/** Registry organization controller errors. */ class RegistryOrgControllerError extends idrErr.IDRError { orgDnePathParam (shortname) { // org const err = {} diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry.controller/org.registry.controller.js similarity index 98% rename from src/controller/registry-org.controller/registry-org.controller.js rename to src/controller/registry.controller/org.registry.controller.js index cf739a819..deb83fc3c 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry.controller/org.registry.controller.js @@ -1,8 +1,9 @@ +/** Registry organization route handlers. */ const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') const _ = require('lodash') -const errors = require('./error') +const errors = require('./org.error') const error = new errors.RegistryOrgControllerError() const conversationErrors = require('../conversation.controller/error') const convoError = new conversationErrors.ConversationControllerError() @@ -106,7 +107,7 @@ function removeAdditionalContactUUIDFields (org) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It retrieves a list of all registry organizations. - * Called by GET /api/registryOrg + * Called by GET /api/registry/org */ async function getAllOrgs (req, res, next) { try { @@ -158,7 +159,7 @@ async function getAllOrgs (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It retrieves information about the specified registry organization. - * Called by GET /api/registryOrg/:identifier + * Called by GET /api/registry/org/:identifier */ async function getOrg (req, res, next) { try { @@ -243,7 +244,7 @@ async function getOrg (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It creates a new registry organization. - * Called by POST /api/registryOrg + * Called by POST /api/registry/org */ async function createOrg (req, res, next) { try { @@ -409,7 +410,7 @@ async function validateRequestedShortName (req, repo, body, shortName, session) * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It updates an existing registry organization. - * Called by PUT /api/registryOrg/:shortname + * Called by PUT /api/registry/org/:shortname */ async function updateOrg (req, res, next) { try { @@ -608,18 +609,18 @@ async function updateOrg (req, res, next) { * * @async * @function deleteOrg - * @param {object} req - The Express request object, containing the organization identifier in `req.ctx.params.identifier`. + * @param {object} req - The Express request object, containing the organization short name in `req.ctx.params.shortname`. * @param {object} res - The Express response object. * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It deletes an existing registry organization. - * Called by DELETE /api/registryOrg/:identifier + * Called by DELETE /api/registry/org/:shortname */ async function deleteOrg (req, res, next) { try { const session = await mongoose.startSession({ causalConsistency: false }) const repo = req.ctx.repositories.getBaseOrgRepository() - const shortName = req.ctx.params.identifier + const shortName = req.ctx.params.shortname let targetOrgUUID try { @@ -668,7 +669,7 @@ async function deleteOrg (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins. * @description This endpoint is accessible to Secretariat only. It retrieves user information for any organization. - * Called by GET /api/registryOrg/:shortname/users + * Called by GET /api/registry/org/:shortname/users */ async function getUsers (req, res, next) { try { @@ -726,7 +727,7 @@ async function getUsers (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. * @description This endpoint is accessible to Secretariat only. It creates a user for any organization. - * Called by POST /api/registryOrg/:shortname/user + * Called by POST /api/registry/org/:shortname/user */ async function createUserByOrg (req, res, next) { try { diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry.controller/user.registry.controller.js similarity index 87% rename from src/controller/registry-user.controller/registry-user.controller.js rename to src/controller/registry.controller/user.registry.controller.js index 7502322f0..788e7c8c6 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry.controller/user.registry.controller.js @@ -1,3 +1,4 @@ +/** Registry user controller route handlers. */ const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') @@ -25,7 +26,7 @@ function removeImmutableUpdateFields (body) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins. * @description This endpoint is accessible to Secretariat only. It retrieves a list of all registry users. - * Called by GET /api/registryUser + * Called by GET /api/registry/users */ async function getAllUsers (req, res, next) { try { @@ -85,7 +86,7 @@ async function getAllUsers (req, res, next) { * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. Response body includes 'role' field for admins. * @description This endpoint is accessible to Secretariat only. It retrieves information about the specified registry user. - * Called by GET /api/registryUser/:identifier + * Called by GET /api/registry/org/:shortname/user/:username */ async function getUser (req, res, next) { /* @@ -174,79 +175,6 @@ async function getUser (req, res, next) { } } -async function createUser (req, res, next) { - try { - const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const userRepo = req.ctx.repositories.getBaseUserRepository() - const body = req.ctx.body - const orgShortName = req.ctx.params.shortname - let returnValue - - const orgUUID = await orgRepo.getOrgUUID(orgShortName) - if (!orgUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'The user could not be created because ' + orgShortName + ' organization does not exist.' }) - return res.status(404).json(error.orgDnePathParam(orgShortName)) - } - - // Do not allow the user to pass in a UUID - if ((body?.UUID ?? null) || (body?.uuid ?? null)) { - return res.status(400).json(error.uuidProvided('user')) - } - - if ((body?.org_UUID ?? null) || (body?.org_uuid ?? null)) { - return res.status(400).json(error.uuidProvided('org')) - } - - const session = await mongoose.startSession({ causalConsistency: false }) - - try { - session.startTransaction({ readPreference: 'primary' }) - - const result = await userRepo.validateUser(body) - if (body?.role && typeof body?.role !== 'string') { - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) - } - if (!result.isValid) { - logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) - await session.abortTransaction() - return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) - } - - // Ask repo if user already exists - if (await userRepo.orgHasUser(orgShortName, body?.username, { session })) { - logger.info({ uuid: req.ctx.uuid, message: `${body?.username} user was not created because it already exists.` }) - await session.abortTransaction() - return res.status(400).json(error.userExists(body?.username)) - } - - const users = await userRepo.findUsersByOrgShortname(orgShortName, { session }) - if (users.length >= 100) { - await session.abortTransaction() - return res.status(400).json(error.userLimitReached()) - } - - const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }) - returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true, requestingUserUUID) - await session.commitTransaction() - } catch (error) { - await session.abortTransaction() - throw error - } finally { - await session.endSession() - } - - const responseMessage = { - message: `${body?.username} was successfully created.`, - created: returnValue - } - - return res.status(200).json(responseMessage) - } catch (err) { - next(err) - } -} - async function updateUser (req, res, next) { /* This function is a little bit overloaded ATM until future releases of CVE-Services @@ -454,19 +382,36 @@ async function updateUser (req, res, next) { } } +/** + * Deletes a registry user from the specified organization. + * Called by DELETE /api/registry/org/{shortname}/user/{username} + * + * @param {Object} req - The request object + * @param {Object} res - The response object + * @param {Function} next - The next middleware function + * @returns {Promise} + */ async function deleteUser (req, res, next) { try { const userRepo = req.ctx.repositories.getBaseUserRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const userUUID = req.ctx.params.identifier + const orgShortName = req.ctx.params.shortname + const username = req.ctx.params.username + const org = await orgRepo.findOneByShortName(orgShortName) - const user = await userRepo.findUserByUUID(userUUID) + if (!org) { + logger.info({ uuid: req.ctx.uuid, message: 'Org DNE' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + const user = await userRepo.findOneByUsernameAndOrgShortname(username, orgShortName) if (!user) { logger.info({ uuid: req.ctx.uuid, message: 'User DNE' }) - return res.status(404).json(error.userDne(userUUID)) + return res.status(404).json(error.userDne(username)) } + const userUUID = user.UUID const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo) await userRepo.deleteUserByUUID(userUUID, {}, requestingUserUUID) @@ -628,7 +573,6 @@ async function revokeRole (req, res, next) { module.exports = { ALL_USERS: getAllUsers, SINGLE_USER: getUser, - CREATE_USER: createUser, UPDATE_USER: updateUser, DELETE_USER: deleteUser, GRANT_ROLE: grantRole, diff --git a/src/controller/user.controller/index.js b/src/controller/user.controller/index.js index ddbb44823..c118669c0 100644 --- a/src/controller/user.controller/index.js +++ b/src/controller/user.controller/index.js @@ -3,89 +3,10 @@ const router = express.Router() const mw = require('../../middleware/middleware') const { query } = require('express-validator') const controller = require('./user.controller') -const registryUserController = require('../registry-user.controller/registry-user.controller.js') const { parseGetParams, parseError } = require('./user.middleware') const getConstants = require('../../constants').getConstants const CONSTANTS = getConstants() -router.get('/registry/users', - /* - #swagger.tags = ['Registry User'] - #swagger.operationId = 'userAll' - #swagger.summary = "Retrieves information about all registered users (accessible to Secretariat)" - #swagger.description = " -

Access Control

-

User must belong to an organization with the Secretariat role

-

Expected Behavior

-

Secretariat: Retrieves information about all users for all organizations

" - #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/apiEntityHeader', - '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' - ] - #swagger.responses[200] = { - description: 'Returns all users, along with pagination fields if results span multiple pages of data.', - content:{ - "application/json":{ - schema: { - $ref: '../schemas/registry-user/list-registry-users-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.useRegistry(), - mw.validateUser, - mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - parseError, - parseGetParams, - registryUserController.ALL_USERS -) - router.get('/users', /* #swagger.tags = ['Users'] diff --git a/src/routes.config.js b/src/routes.config.js index 9cf95cdc3..b914e243c 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -7,8 +7,7 @@ const CveIdController = require('./controller/cve-id.controller') const SchemasController = require('./controller/schemas.controller') const SystemController = require('./controller/system.controller') const UserController = require('./controller/user.controller') -const RegistryUserController = require('./controller/registry-user.controller') -const RegistryOrgController = require('./controller/registry-org.controller') +const RegistryController = require('./controller/registry.controller') const AuditController = require('./controller/audit.controller') const ConversationController = require('./controller/conversation.controller') const ReviewObjectController = require('./controller/review-object.controller') @@ -36,9 +35,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', CveIdController) app.use('/api/', SystemController) app.use('/api/', UserController) - // At this time, we have moved the crud operations to mirror the cve legacy endpoint just with /registry/ in them. In the future we may want these. - app.use('/api/', RegistryUserController) - app.use('/api/', RegistryOrgController) + app.use('/api/', RegistryController) app.use('/api/', ConversationController) app.use('/api/', ReviewObjectController) app.use('/api/', GlossaryController) diff --git a/src/swagger.js b/src/swagger.js index 4b18a646f..f038c1a6b 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -6,8 +6,7 @@ const endpointsFiles = [ 'src/controller/org.controller/index.js', 'src/controller/user.controller/index.js', 'src/controller/system.controller/index.js', - 'src/controller/registry-org.controller/index.js', - 'src/controller/registry-user.controller/index.js', + 'src/controller/registry.controller/index.js', 'src/controller/conversation.controller/index.js', 'src/controller/review-object.controller/index.js' ] diff --git a/test/integration-tests/registry-org/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js index 14f3a64cf..be9a94d9c 100644 --- a/test/integration-tests/registry-org/createUserByOrgTest.js +++ b/test/integration-tests/registry-org/createUserByOrgTest.js @@ -8,7 +8,7 @@ const expect = chai.expect const constants = require('../constants.js') const app = require('../../../src/index.js') -describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { +describe('Testing POST /api/registry/org/:shortname/user endpoint', () => { context('Positive Tests', () => { it('Should create a new user in an organization', (done) => { const orgShortName = 'mitre' @@ -22,7 +22,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { role: 'ADMIN' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(newUser) .end((err, res) => { @@ -48,7 +48,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { status: 'active' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(newUser) .end((err, res) => { @@ -69,7 +69,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { status: 'active' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(existingUser) .end((err, res) => { @@ -89,7 +89,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { } } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(invalidUser) .end((err, res) => { @@ -111,7 +111,7 @@ describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { test: 'additional key not in schema' } chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) + .post(`/api/registry/org/${orgShortName}/user`) .set(constants.headers) .send(existingUser) .then((res) => { diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 1ebd05f80..46fb6286c 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -36,8 +36,8 @@ function expectConversationWithoutAuthorId (conversations, body) { expect(convo).to.not.have.property('author_id') } -describe('Testing /registryOrg endpoints', () => { - context('Testing POST /registryOrg endpoint', () => { +describe('Testing /registry/org endpoints', () => { + context('Testing POST /registry/org endpoint', () => { context('Positive Tests', () => { it('Creates a new registry org', async () => { await chai.request(app) @@ -340,7 +340,7 @@ describe('Testing /registryOrg endpoints', () => { }) }) }) - context('Testing GET /registryOrg endpoints', () => { + context('Testing GET /registry/org endpoints', () => { context('Positive Tests', () => { it('Gets a list of all registry organizations', async () => { await chai.request(app) @@ -659,7 +659,7 @@ describe('Testing /registryOrg endpoints', () => { }) }) }) - context('Testing PUT /registryOrg endpoint', () => { + context('Testing PUT /registry/org endpoint', () => { context('Positive Tests', () => { it('Updates a registry organization providing a full organization object', async () => { await chai.request(app) @@ -933,7 +933,7 @@ describe('Testing /registryOrg endpoints', () => { // Cleanup sub org await chai.request(app) - .delete(`/api/registryOrg/${subOrg.short_name}`) + .delete(`/api/registry/org/${subOrg.short_name}`) .set(secretariatHeaders) }) it('Preserves inUse and in_use properties across updates', async () => { @@ -975,7 +975,7 @@ describe('Testing /registryOrg endpoints', () => { // Cleanup await chai.request(app) - .delete(`/api/registryOrg/${tempOrg.short_name}`) + .delete(`/api/registry/org/${tempOrg.short_name}`) .set(secretariatHeaders) }) }) @@ -1152,11 +1152,11 @@ describe('Testing /registryOrg endpoints', () => { }) }) }) - context('Testing DELETE /registryOrg endpoint', () => { + context('Testing DELETE /registry/org endpoint', () => { context('Positive Tests', () => { it('Deletes a registry organization with the provided short name', async () => { await chai.request(app) - .delete('/api/registryOrg/registry_org_test') + .delete('/api/registry/org/registry_org_test') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -1167,7 +1167,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to delete a registry organization that does not exist', async () => { await chai.request(app) - .delete('/api/registryOrg/registry_org_test2') + .delete('/api/registry/org/registry_org_test2') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) diff --git a/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js b/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js index 7fa072f43..ed3ea8b70 100644 --- a/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js +++ b/test/integration-tests/registry-org/registryOrgDiscriminatorAuthorityTest.js @@ -16,7 +16,7 @@ describe('Testing Registry Org Discriminator Authority inheritance', () => { const shortName = orgsToCleanup.pop() try { await chai.request(app) - .delete(`/api/registryOrg/${shortName}`) + .delete(`/api/registry/org/${shortName}`) .set(secretariatHeaders) } catch (err) { // ignore errors during cleanup diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index d891e91c4..28cd37220 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -197,7 +197,7 @@ describe('Testing Joint approval', () => { }) it('Check to see if the org was partially updated', async () => { await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined @@ -217,7 +217,7 @@ describe('Testing Joint approval', () => { }) // Verify that the org was updated with the new body values await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined @@ -233,7 +233,7 @@ describe('Testing Joint approval', () => { let reviewUUID it('Create an org to use for testing', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testRegistryOrgForReviewWithComments) .then((res, err) => { @@ -300,7 +300,7 @@ describe('Testing Joint approval', () => { }) it('Check to see if the org was partially updated', async () => { await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined @@ -373,7 +373,7 @@ describe('Testing Joint approval', () => { }) it('Check to see if the org was fully updated', async () => { await chai.request(app) - .get(`/api/registryOrg/${orgUUID}`) + .get(`/api/registry/org/${orgUUID}`) .set(secretariatHeaders) .then((res, err) => { expect(err).to.be.undefined diff --git a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js index c6abde3a3..6884a6784 100644 --- a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js +++ b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js @@ -22,7 +22,7 @@ describe('Testing Deep Remove Empty in Create Org', () => { context('Positive Tests', () => { it('Creates a registry org and verifies null values are removed', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testNullRemovalOrg) .then((res, err) => { @@ -56,7 +56,7 @@ describe('Testing Deep Remove Empty in Create Org', () => { after(async () => { // Cleanup: Delete the created org await chai.request(app) - .delete('/api/registryOrg/test_null_removal') + .delete('/api/registry/org/test_null_removal') .set(secretariatHeaders) }) }) diff --git a/test/integration-tests/registry-user/registryUserCRUDTest.js b/test/integration-tests/registry-user/registryUserCRUDTest.js index 01c089c98..b696e01d2 100644 --- a/test/integration-tests/registry-user/registryUserCRUDTest.js +++ b/test/integration-tests/registry-user/registryUserCRUDTest.js @@ -40,7 +40,7 @@ const postNewOrg = async (shortName) => { const postNewUser = async (orgShortName, username) => { return chai.request(app) - .post(`/api/registryUser/${orgShortName}`) + .post(`/api/registry/org/${orgShortName}/user`) .set(secretariatHeaders) .send({ username, @@ -71,11 +71,11 @@ const createRegistryUser = async () => { return { orgShortName, createdUser } } -describe('Testing /registryUser endpoints', () => { +describe('Testing canonical registry user endpoints', () => { context('Positive Tests', () => { it('Gets a list of all registry users', async () => { await chai.request(app) - .get('/api/registryUser') + .get('/api/registry/users') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -84,7 +84,7 @@ describe('Testing /registryUser endpoints', () => { }) }) - it('Gets a registry user by UUID', async () => { + it('Gets a registry user by organization and username', async () => { let user await chai.request(app) .get('/api/registry/org/win_5/user/jasminesmith@win_5.com') @@ -95,7 +95,7 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .get(`/api/registryUser/${user.UUID}`) + .get(`/api/registry/org/win_5/user/${user.username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -105,12 +105,11 @@ describe('Testing /registryUser endpoints', () => { }) }) - it('Creates, updates, and deletes a registry user by UUID', async () => { + it('Creates, updates, and deletes a registry user by organization and username', async () => { const username = `${uuidv4()}@registry-user.test` - let userUUID await chai.request(app) - .post('/api/registryUser/range_4') + .post('/api/registry/org/range_4/user') .set(secretariatHeaders) .send({ username, @@ -125,12 +124,11 @@ describe('Testing /registryUser endpoints', () => { expect(res.body).to.have.property('created') expect(res.body.created).to.have.property('UUID') expect(res.body.created).to.have.property('username', username) - userUUID = res.body.created.UUID }) let user await chai.request(app) - .get(`/api/registryUser/${userUUID}`) + .get(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -138,7 +136,7 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .put(`/api/registryUser/${userUUID}`) + .put(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .send({ ...user, @@ -153,7 +151,7 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .delete(`/api/registryUser/${userUUID}`) + .delete(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -161,20 +159,20 @@ describe('Testing /registryUser endpoints', () => { }) await chai.request(app) - .get(`/api/registryUser/${userUUID}`) + .get(`/api/registry/org/range_4/user/${username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) }) }) - it('Logs the updated user UUID when updating a registry user by identifier', async () => { - const { createdUser } = await createRegistryUser() + it('Logs the updated user UUID when updating a registry user by canonical identity', async () => { + const { orgShortName, createdUser } = await createRegistryUser() const loggerInfoStub = sinon.stub(logger, 'info') try { await chai.request(app) - .put(`/api/registryUser/${createdUser.UUID}`) + .put(`/api/registry/org/${orgShortName}/user/${createdUser.username}`) .set(secretariatHeaders) .send({ UUID: createdUser.UUID, @@ -198,13 +196,13 @@ describe('Testing /registryUser endpoints', () => { } }) - it('Logs the deleted user UUID when deleting a registry user by identifier', async () => { - const { createdUser } = await createRegistryUser() + it('Logs the deleted user UUID when deleting a registry user by canonical identity', async () => { + const { orgShortName, createdUser } = await createRegistryUser() const loggerInfoStub = sinon.stub(logger, 'info') try { await chai.request(app) - .delete(`/api/registryUser/${createdUser.UUID}`) + .delete(`/api/registry/org/${orgShortName}/user/${createdUser.username}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -221,7 +219,7 @@ describe('Testing /registryUser endpoints', () => { context('Negative Tests', () => { it('Fails when page query parameter is not an integer', async () => { await chai.request(app) - .get('/api/registryUser') + .get('/api/registry/users') .set(secretariatHeaders) // Must be secretariat to reach validation .query({ page: 'not-a-number' }) // Invalid data .then((res) => { @@ -232,7 +230,7 @@ describe('Testing /registryUser endpoints', () => { it('Fails when page query parameter is below the minimum', async () => { await chai.request(app) - .get('/api/registryUser') + .get('/api/registry/users') .set(secretariatHeaders) .query({ page: 0 }) // Assuming min is 1 .then((res) => { @@ -240,9 +238,9 @@ describe('Testing /registryUser endpoints', () => { }) }) - it('Fails when identifier contains invalid characters', async () => { + it('Fails when username contains invalid characters', async () => { await chai.request(app) - .get('/api/registryUser/uuid