From 4aa8529bd8c0616a0908c67d01274adb0681e931 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 4 Dec 2024 13:30:59 -0500 Subject: [PATCH 01/35] updating version to 2.5.0 --- api-docs/openapi.json | 2 +- src/swagger.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 0169e993e..0f88bb0dc 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.4.0", + "version": "2.5.0", "title": "CVE Services API", "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:

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

Contact the CVE Services team", "contact": { diff --git a/src/swagger.js b/src/swagger.js index 29cdbc7f4..fc8bcab90 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -18,7 +18,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re /* eslint-disable no-multi-str */ const doc = { info: { - version: '2.4.0', + version: '2.5.0', title: 'CVE Services API', description: "The CVE Services API supports automation tooling for the CVE Program. Credentials are \ required for most service endpoints. Representatives of \ From e3993da487232438b956c8e59af27384ae67b124 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 12 May 2025 12:57:48 -0400 Subject: [PATCH 02/35] re-wrote the for loop to correctly terminate on the return --- .../org.controller/org.controller.js | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index d1a094b57..c37fea1b2 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -450,34 +450,42 @@ async function createUser (req, res, next) { return res.status(400).json(error.userLimitReached()) } - Object.keys(req.ctx.body).forEach(k => { - const key = k.toLowerCase() + const body = req.ctx.body + const keys = Object.keys(body) - if (key === 'username') { - newUser.username = req.ctx.body.username - } else if (key === 'authority') { - if (req.ctx.body.authority.active_roles) { - newUser.authority.active_roles = [...new Set(req.ctx.body.authority.active_roles)] // Removes any duplicate strings from array - } - } else if (key === 'name') { - if (req.ctx.body.name.first) { - newUser.name.first = req.ctx.body.name.first - } - if (req.ctx.body.name.last) { - newUser.name.last = req.ctx.body.name.last - } - if (req.ctx.body.name.middle) { - newUser.name.middle = req.ctx.body.name.middle - } - if (req.ctx.body.name.suffix) { - newUser.name.suffix = req.ctx.body.name.suffix - } - } else if (key === 'org_uuid') { - return res.status(400).json(error.uuidProvided('org')) - } else if (key === 'uuid') { + for (const keyRaw of keys) { + const key = keyRaw.toLowerCase() + + if (key === 'uuid') { return res.status(400).json(error.uuidProvided('user')) } - }) + + if (key === 'org_uuid') { + return res.status(400).json(error.uuidProvided('org')) + } + + const handlers = { + username: () => { + newUser.username = body.username + }, + authority: () => { + if (body.authority?.active_roles) { + newUser.authority.active_roles = [...new Set(body.authority.active_roles)] + } + }, + name: () => { + const name = body.name || {} + if (name.first) newUser.name.first = name.first + if (name.last) newUser.name.last = name.last + if (name.middle) newUser.name.middle = name.middle + if (name.suffix) newUser.name.suffix = name.suffix + } + } + + if (handlers[key]) { + handlers[key]() // execute the appropriate handler + } + } const requesterOrgUUID = await orgRepo.getOrgUUID(requesterShortName) const isSecretariat = await orgRepo.isSecretariatUUID(requesterOrgUUID) From 33f53e93757ab8cef28ef8fd87e0a17d6639e878 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 12 May 2025 13:55:08 -0400 Subject: [PATCH 03/35] Update mongoose usage to no longer use n --- src/controller/org.controller/org.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index c37fea1b2..02cc4dc56 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -395,7 +395,7 @@ async function updateOrg (req, res, next) { // update org let result = await orgRepo.updateByOrgUUID(org.UUID, newOrg) - if (result.n === 0) { + if (result.matchedCount === 0) { logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) return res.status(404).json(error.orgDnePathParam(shortName)) } @@ -719,7 +719,7 @@ async function updateUser (req, res, next) { newUser.authority.active_roles = duplicateCheckedRoles let result = await userRepo.updateByUserNameAndOrgUUID(username, orgUUID, newUser) - if (result.n === 0) { + if (result.matchedCount === 0) { logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + shortName + ' organization.' }) return res.status(404).json(error.userDne(username)) } @@ -794,7 +794,7 @@ async function resetSecret (req, res, next) { const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) oldUser.secret = await argon2.hash(randomKey) // store in db const user = await userRepo.updateByUserNameAndOrgUUID(oldUser.username, orgUUID, oldUser) - if (user.n === 0) { + if (user.matchedCount === 0) { logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + orgShortName + ' organization.' }) return res.status(404).json(error.userDne(username)) } From b557e8a186a5f72fa9c173596fb9e05a058ec750 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 14 May 2025 10:59:03 -0400 Subject: [PATCH 04/35] version number updates --- api-docs/openapi.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/swagger.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 43e1887e7..9b0a0b3b8 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.5.3", + "version": "2.5.4", "title": "CVE Services API", "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:
  • If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials
  • Contact your Root (Google, INCIBE, JPCERT/CC, or Red Hat) or Top-Level Root (CISA ICS or MITRE) to request credentials

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

Contact the CVE Services team", "contact": { diff --git a/package-lock.json b/package-lock.json index 981474310..8d7454c3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.5.3", + "version": "2.5.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.5.3", + "version": "2.5.4", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", diff --git a/package.json b/package.json index cbd1617ce..cbb81d10f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cve-services", "author": "Automation Working Group", - "version": "2.5.3", + "version": "2.5.4", "license": "(CC0)", "devDependencies": { "@faker-js/faker": "^7.6.0", diff --git a/src/swagger.js b/src/swagger.js index af5d04700..0aaa44171 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -18,7 +18,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re /* eslint-disable no-multi-str */ const doc = { info: { - version: '2.5.3', + version: '2.5.4', title: 'CVE Services API', description: "The CVE Services API supports automation tooling for the CVE Program. Credentials are \ required for most service endpoints. Representatives of \ From da156ee6177998d1df359d09943ed49e0d0dda26 Mon Sep 17 00:00:00 2001 From: rbrittonMitre <72146575+rbrittonMitre@users.noreply.github.com> Date: Wed, 28 May 2025 09:21:49 -0400 Subject: [PATCH 05/35] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5da891939..fe75fd04b 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,9 @@ When you start your local development server using `npm run start:dev` the speci You can use `npm run swagger-autogen` to generate a new specification file. +### CVE Record Submission Validation Rules + +As part of the submission processing, CVE Services "validates" that specific requirements are met prior to accepting the submission and posting the CVE Record to the CVE List. Validation rules for CVE Record Submission are noted [here](https://github.com/CVEProject/automation-working-group/blob/master/meeting-notes/files/CVERules.md). ### Unit Testing From 9bd7b05d441a3ae7ef655a7efafad560a220fb44 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 17 Mar 2025 15:32:02 -0400 Subject: [PATCH 06/35] Added user and org mongoose models for user registry --- src/model/registry-org.js | 60 ++++++++++++++++++++++++++++++++++++++ src/model/registry-user.js | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/model/registry-org.js create mode 100644 src/model/registry-user.js diff --git a/src/model/registry-org.js b/src/model/registry-org.js new file mode 100644 index 000000000..4df938eea --- /dev/null +++ b/src/model/registry-org.js @@ -0,0 +1,60 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose; +const aggregatePaginate = require('mongoose-aggregate-paginate-v2') +const MongoPaging = require('mongo-cursor-pagination') + +const schema = { + _id: false, + UUID: String, + long_name: String, + short_name: String, + aliases: [String], + cve_program_org_function: { + type: String, + enum: ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download'] + }, + authority: { + active_roles: [String] + }, + reports_to: { type: Schema.Types.ObjectId, ref: 'RegistryOrg' }, + oversees: [{ type: Schema.Types.ObjectId, ref: 'RegistryOrg' }], + root_or_tlr: Boolean, + users: [{ type: Schema.Types.ObjectId, ref: 'RegistryUser' }], + charter_or_scope: String, + disclosure_policy: String, + product_list: String, + soft_quota: Number, + hard_quota: Number, + contact_info: { + additional_contact_users: [{ type: Schema.Types.ObjectId, ref: 'RegistryUser' }], + poc: String, + poc_email: String, + poc_phone: String, + admins: [{ type: Schema.Types.ObjectId, ref: 'RegistryUser' }], + org_email: String, + website: String + }, + in_use: Boolean, + created: Date, + last_updated: Date +}; + +const RegistryOrgSchema = new mongoose.Schema(schema, { collection: 'RegistryOrg', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) + +RegistryOrgSchema.query.byShortName = function (shortName) { + return this.where({ short_name: shortName }) +} + +RegistryOrgSchema.query.byUUID = function (uuid) { + return this.where({ UUID: uuid }) +} + +RegistryOrgSchema.index({ UUID: 1 }) +RegistryOrgSchema.index({ 'authority.active_roles': 1 }) + +RegistryOrgSchema.plugin(aggregatePaginate) + +// Cursor pagination +RegistryOrgSchema.plugin(MongoPaging.mongoosePlugin) +const RegistryOrg = mongoose.model('RegistryOrg', RegistryOrgSchema) +module.exports = RegistryOrg diff --git a/src/model/registry-user.js b/src/model/registry-user.js new file mode 100644 index 000000000..2003d3a76 --- /dev/null +++ b/src/model/registry-user.js @@ -0,0 +1,58 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose; +const aggregatePaginate = require('mongoose-aggregate-paginate-v2') +const MongoPaging = require('mongo-cursor-pagination') + +const schema = { + _id: false, + UUID: String, + user_id: String, + secret: String, + name: { + first: String, + last: String, + middle: String, + suffix: String + }, + org_affiliations: [{ + org_id: { type: Schema.Types.ObjectId, ref: 'RegistryOrg' }, + email: String, + phone: String + }], + cve_program_org_membership: [{ + program_org: { type: Schema.Types.ObjectId, ref: 'RegistryOrg' }, + role: { + type: String, + enum: ['Chair', 'Member', 'Admin'] + }, + status: { + type: String, + enum: ['active', 'inactive'] + } + }], + created: Date, + created_by: { type: Schema.Types.ObjectId, ref: 'RegistryUser' }, + last_updated: Date, + deactivation_date: Date, + last_active: Date +} + +const RegistryUserSchema = new mongoose.Schema(schema, { collection: 'RegistryUser', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }); + +RegistryUserSchema.query.byUserID = function (userID) { + return this.where({ user_id: userID }); +} + +RegistryUserSchema.query.byUUID = function (uuid) { + return this.where({ UUID: uuid }); +} + +RegistryUserSchema.index({ UUID: 1 }); +RegistryUserSchema.index({ user_id: 1 }); + +RegistryUserSchema.plugin(aggregatePaginate) + +// Cursor pagination +RegistryUserSchema.plugin(MongoPaging.mongoosePlugin) +const RegistryUser = mongoose.model('RegistryUser', RegistryUserSchema) +module.exports = RegistryUser \ No newline at end of file From 3279406482a88a91cd66c9d49d0c9f015a129b7f Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 7 Apr 2025 12:17:13 -0400 Subject: [PATCH 07/35] [WIP] Progress on registry user endpoints --- .../registry-user.controller/index.js | 52 ++++ .../registry-user.controller.js | 278 ++++++++++++++++++ .../registry-user.middleware.js | 30 ++ src/repositories/registryUserRepository.js | 27 ++ src/repositories/repositoryFactory.js | 12 + src/routes.config.js | 2 + 6 files changed, 401 insertions(+) create mode 100644 src/controller/registry-user.controller/index.js create mode 100644 src/controller/registry-user.controller/registry-user.controller.js create mode 100644 src/controller/registry-user.controller/registry-user.middleware.js create mode 100644 src/repositories/registryUserRepository.js diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js new file mode 100644 index 000000000..669664eab --- /dev/null +++ b/src/controller/registry-user.controller/index.js @@ -0,0 +1,52 @@ +const express = require('express') +const router = express.Router() +const mw = require('../../middleware/middleware') +const { body, 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', + 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', + mw.validateUser, + param(['identifier']).isString().trim(), + // parseError, + parseGetParams, + controller.SINGLE_USER +); + +router.post('/registryUser', + mw.validateUser, + // mw.onlySecretariat, // TODO: permissions + // parseError, + parsePostParams, + controller.CREATE_USER +); + +router.put('/registryUser/:identifier', + mw.validateUser, + param(['identifier']).isString().trim(), + // parseError, + parsePostParams, + controller.UPDATE_USER +) + +router.delete('/registryUser/:identifier', + mw.validateUser, + param(['identifier']).isString().trim(), + // parseError, + parseDeleteParams, + controller.DELETE_USER +); + +module.exports = router; \ No newline at end of file diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js new file mode 100644 index 000000000..c31b16202 --- /dev/null +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -0,0 +1,278 @@ +const argon2 = require('argon2'); +const cryptoRandomString = require('crypto-random-string'); +const uuid = require('uuid'); +const logger = require('../../middleware/logger'); +const { getConstants } = require('../../constants'); +const RegistryUser = require('../../model/registry-user'); + +async function getAllUsers(req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT + } + + const options = CONSTANTS.PAGINATOR_OPTIONS + 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 repo = req.ctx.repositories.getRegistryUserRepository() + + const agt = setAggregateUserObj({}) + const pg = await repo.aggregatePaginate(agt, options) + const payload = { users: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } +} + +async function getUser(req, res, next) { + try { + const repo = req.ctx.repositories.getRegistryUserRepository(); + const identifier = req.ctx.params.identifier; + const agt = setAggregateUserObj({ UUID: identifier }); + let result = await repo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + logger.info({ uuid: req.ctx.uuid, message: identifier + ' user was sent to the user.', user: result }) + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +async function createUser(req, res, next) { + try { + // const requesterUsername = req.ctx.user + // const requesterShortName = req.ctx.org + const orgRepo = req.ctx.repositories.getOrgRepository() + const userRepo = req.ctx.repositories.getUserRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + const body = req.ctx.body; + + // TODO: check if affiliated orgs and program orgs exist, and if their membership limit is reached + + const newUser = new RegistryUser(); + Object.keys(body).map(k => k.toLowerCase()).forEach(k => { + if (k === 'user_id' || k === 'username') { + newUser.user_id = body[k]; + } else if (k === 'name') { + newUser.name = { + first: '', + last: '', + middle: '', + suffix: '', + ...body.name + }; + } else if (k === 'org_affiliations') { + // TODO: dedupe + } else if (k === 'cve_program_org_membership') { + // TODO: dedupe + } else if (k === 'uuid') { + return res.status(400).json(error.uuidProvided('user')); + } + }); + + // TODO: check that requesting user is admin of org for new user + + newUser.UUID = uuid.v4(); + const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }); + newUser.secret = await argon2.hash(randomKey); + newUser.last_active = null; + newUser.deactivation_date = null; + + await registryUserRepo.updateByUUID(newUser.UUID, newUser, { upsert: true }); + const agt = setAggregateUserObj({ UUID: newUser.UUID }); + let result = await registryUserRepo.aggregate(agt); + result = result.length > 0 ? result[0] : null; + + const payload = { + action: 'create_registry_user', + change: result.user_id + ' was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + result.secret = randomKey + const responseMessage = { + message: result.user_id + ' was successfully created.', + created: result + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +async function updateUser(req, res, next) { + try { + const requesterShortName = req.ctx.org + const requesterUsername = req.ctx.user + // const username = req.ctx.params.username + // const shortName = req.ctx.params.shortname + const userUUID = req.ctx.params.identifier; + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository(); + // const orgUUID = await orgRepo.getOrgUUID(shortName) + const isSecretariat = await orgRepo.isSecretariat(requesterShortName) + const isAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName) // Check if requester is Admin of the designated user's org + + const user = await registryUserRepo.findOneByUUID(userUUID); + const newUser = new RegistryUser(); + + // Sets the name values to what currently exists in the database, this ensures data is retained during partial name updates + newUser.name.first = user.name.first + newUser.name.last = user.name.last + newUser.name.middle = user.name.middle + newUser.name.suffix = user.name.suffix + + const queryParameterPermissions = { + new_user_id: true, + 'name.first': false, + 'name.last': false, + 'name.middle': false, + 'name.suffix': false, + 'org_affiliations.add': false, + 'org_affiliations.remove': false, + 'cve_program_org_membership.add': false, + 'cve_program_org_membership.remove': false, + } + + // TODO: check permissions + // Check to ensure that the user has the right permissions to edit the fields tha they are requesting to edit, and fail fast if they do not. + // if (Object.keys(req.ctx.query).length > 0 && Object.keys(req.ctx.query).some((key) => { return queryParameterPermissions[key] }) && !(isAdmin || isSecretariat)) { + // logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + requesterUsername + ' user is not Org Admin or Secretariat to modify these fields.' }) + // return res.status(403).json(error.notOrgAdminOrSecretariatUpdate()) + // } + + for (const k in req.ctx.query) { + const key = k.toLowerCase() + + if (key === 'new_user_id') { + newUser.user_id = req.ctx.query.new_user_id + } else if (key === 'name.first') { + newUser.name.first = req.ctx.query['name.first'] + } else if (key === 'name.last') { + newUser.name.last = req.ctx.query['name.last'] + } else if (key === 'name.middle') { + newUser.name.middle = req.ctx.query['name.middle'] + } else if (key === 'name.suffix') { + newUser.name.suffix = req.ctx.query['name.suffix'] + } + + // TODO: process org affiliations and program org membership updates + } + + await registryUserRepo.updateByUUID(userUUID, newUser); + const agt = setAggregateUserObj({ UUID: userUUID }); + let result = await registryUserRepo.aggregate(agt); + result = result.length > 0 ? result[0] : null; + + const payload = { + action: 'update_registry_user', + change: result.user_id + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + let msgStr = '' + if (Object.keys(req.ctx.query).length > 0) { + msgStr = result.user_id + ' was successfully updated.' + } else { + msgStr = 'No updates were specified for ' + result.user_id + '.' + } + const responseMessage = { + message: msgStr, + updated: result + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +async function deleteUser(req, res, next) { + try { + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository(); + const userUUID = req.ctx.params.identifier; + + const user = await registryUserRepo.findOneByUUID(userUUID); + + // TODO: check permissions + + await registryUserRepo.deleteByUUID(userUUID); + + const payload = { + action: 'delete_registry_user', + change: user.user_id + ' was successfully deleted.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org) + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: user.user_id + ' was successfully deleted.' + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +function setAggregateUserObj(query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} + +module.exports = { + ALL_USERS: getAllUsers, + SINGLE_USER: getUser, + CREATE_USER: createUser, + UPDATE_USER: updateUser, + DELETE_USER: deleteUser +}; \ No newline at end of file diff --git a/src/controller/registry-user.controller/registry-user.middleware.js b/src/controller/registry-user.controller/registry-user.middleware.js new file mode 100644 index 000000000..2a9f02983 --- /dev/null +++ b/src/controller/registry-user.controller/registry-user.middleware.js @@ -0,0 +1,30 @@ +const utils = require('../../utils/utils') + +function parsePostParams (req, res, next) { + utils.reqCtxMapping(req, 'body', []) + utils.reqCtxMapping(req, 'params', ['identifier']) + utils.reqCtxMapping(req, 'query', [ + 'new_user_id', + 'name.first', 'name.last', 'name.middle', 'name.suffix', + 'org_affiliations.add', 'org_affiliations.remove', + 'cve_program_org_membership.add', 'cve_program_org_membership.remove' + ]) + 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() +} + +module.exports = { + parsePostParams, + parseGetParams, + parseDeleteParams +} \ No newline at end of file diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js new file mode 100644 index 000000000..4ae309df6 --- /dev/null +++ b/src/repositories/registryUserRepository.js @@ -0,0 +1,27 @@ +const BaseRepository = require('./baseRepository') +const RegistryUser = require('../model/registry-user') +const utils = require('../utils/utils') + +class RegistryUserRepository extends BaseRepository { + constructor () { + super(RegistryUser) + } + + async findOneByUUID (UUID) { + return this.collection.findOne().byUUID(UUID) + } + + async getAllUsers () { + return this.collection.find() + } + + async updateByUUID (uuid, user, options = {}) { + return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(user).setOptions(options); + } + + async deleteByUUID (uuid) { + return this.collection.deleteOne({ UUID: uuid }); + } +} + +module.exports = RegistryUserRepository; \ No newline at end of file diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 2fb251d7b..2253bd9a1 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -3,6 +3,8 @@ const CveRepository = require('./cveRepository') const CveIdRepository = require('./cveIdRepository') const CveIdRangeRepository = require('./cveIdRangeRepository') const UserRepository = require('./userRepository') +const RegistryUserRepository = require('./registryUserRepository') +// const RegistryOrgRepository = require('./registryOrgRepository') class RepositoryFactory { getOrgRepository () { @@ -29,6 +31,16 @@ class RepositoryFactory { const repo = new UserRepository() return repo } + + getRegistryUserRepository () { + const repo = new RegistryUserRepository() + return repo + } + + // getRegistryOrgRepository () { + // const repo = new RegistryOrgRepository() + // return repo + // } } module.exports = RepositoryFactory diff --git a/src/routes.config.js b/src/routes.config.js index e1fc27835..a96ff76a2 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -7,6 +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') var options = { swaggerOptions: { @@ -30,6 +31,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', CveIdController) app.use('/api/', SystemController) app.use('/api/', UserController) + app.use('/api/', RegistryUserController) app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)) app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions)) app.use('/schemas/', SchemasController) From 0e014778c7e19633eafc20802060cdaed7daa0dc Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 21 Apr 2025 09:58:52 -0400 Subject: [PATCH 08/35] [WIP] added routes for registry orgs --- .../registry-org.controller/index.js | 66 ++++ .../registry-org.controller.js | 320 ++++++++++++++++++ .../registry-org.middleware.js | 48 +++ .../registry-user.controller/index.js | 2 + src/repositories/registryOrgRepository.js | 27 ++ src/repositories/repositoryFactory.js | 10 +- src/routes.config.js | 2 + 7 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 src/controller/registry-org.controller/index.js create mode 100644 src/controller/registry-org.controller/registry-org.controller.js create mode 100644 src/controller/registry-org.controller/registry-org.middleware.js create mode 100644 src/repositories/registryOrgRepository.js diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js new file mode 100644 index 000000000..f455e9d2c --- /dev/null +++ b/src/controller/registry-org.controller/index.js @@ -0,0 +1,66 @@ +const express = require('express') +const router = express.Router() +const mw = require('../../middleware/middleware') +const errorMsgs = require('../../middleware/errorMessages') +const { body, param, query } = require('express-validator') +const controller = require('./registry-org.controller') +const { parseGetParams, parsePostParams, parseDeleteParams, parseError, isOrgRole } = require('./registry-org.middleware') +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') +const getConstants = require('../../constants').getConstants +const CONSTANTS = getConstants() + +router.get('/registryOrg', + 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', + mw.validateUser, + param(['identifier']).isString().trim(), + // parseError, + parseGetParams, + controller.SINGLE_ORG +); + +router.post('/registryOrg', + mw.validateUser, + mw.onlySecretariat, + body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['long_name']).isString().trim().notEmpty(), + body(['authority.active_roles']).optional() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole), + body(['soft_quota']).optional().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), + body(['hard_quota']).optional().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), + // TODO: more validation needed? + // parseError, + parsePostParams, + controller.CREATE_ORG +); + +router.put('/registryOrg/:identifier', + mw.validateUser, + param(['identifier']).isString().trim(), + // TODO: do more validation here + // parseError, + parsePostParams, + controller.UPDATE_ORG +) + +router.delete('/registryOrg/:identifier', + mw.validateUser, + // TODO: permissions + param(['identifier']).isString().trim(), + // parseError, + parseDeleteParams, + controller.DELETE_ORG +); + +module.exports = router; \ No newline at end of file diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js new file mode 100644 index 000000000..bd2633177 --- /dev/null +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -0,0 +1,320 @@ +const uuid = require('uuid'); +const logger = require('../../middleware/logger'); +const { getConstants } = require('../../constants'); +const RegistryOrg = require('../../model/registry-org'); + +async function getAllOrgs(req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT + } + + const options = CONSTANTS.PAGINATOR_OPTIONS + 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 repo = req.ctx.repositories.getRegistryOrgRepository() + + const agt = setAggregateOrgObj({}) + const pg = await repo.aggregatePaginate(agt, options) + const payload = { orgs: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: 'The org information was sent to the secretariat user.' }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } +} + +async function getOrg(req, res, next) { + try { + const repo = req.ctx.repositories.getRegistryOrgRepository(); + const identifier = req.ctx.params.identifier; + const agt = setAggregateOrgObj({ UUID: identifier }); + let result = await repo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + logger.info({ uuid: req.ctx.uuid, message: identifier + ' org was sent to the user.', org: result }) + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +async function createOrg(req, res, next) { + try { + const CONSTANTS = getConstants() + const orgRepo = req.ctx.repositories.getOrgRepository(); + const userRepo = req.ctx.repositories.getUserRepository(); + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository(); + const body = req.ctx.body; + + const newOrg = new RegistryOrg(); + Object.keys(body).map(k => k.toLowerCase()).forEach(k => { + if (k === 'long_name') { + newOrg.long_name = body[k]; + } else if (k === 'short_name') { + newOrg.short_name = body[k]; + } else if (k === 'aliases') { + newOrg.aliases = [...new Set(body[k].active_roles)]; + } else if (k === 'cve_program_org_function') { + newOrg.cve_program_org_function = body[k]; + } else if (k === 'authority') { + if ('active_roles' in body[k]) { + newOrg.authority.active_roles = [...new Set(body[k].active_roles)]; + } + } else if (k === 'reports_to') { + // TODO: org check logic? + } else if (k === 'oversees') { + // TODO: org check logic? + } else if (k === 'root_or_tlr') { + newOrg.root_or_tlr = body[k]; + } else if (k === 'users') { + // TODO: users logic? + } else if (k === 'charter_or_scope') { + newOrg.charter_or_scope = body[k]; + } else if (k === 'disclosure_policy') { + newOrg.disclosure_policy = body[k]; + } else if (k === 'product_list') { + newOrg.product_list = body[k]; + } else if (k === 'soft_quota') { + newOrg.soft_quota = body[k]; + } else if (k === 'hard_quota') { + newOrg.hard_quota = body[k]; + } else if (k === 'contact_info') { + const {additional_contact_users, admins, ...contactInfo} = body[k]; + newOrg.contact_info = { + additional_contact_users: [...(additional_contact_users || [])], + poc: '', + poc_email: '', + poc_phone: '', + admins: [...(admins || [])], + org_email: '', + website: '', + ...contactInfo + }; + } else if (k === 'uuid') { + return res.status(400).json(error.uuidProvided('org')); + } + }); + + if (newOrg.reports_to === undefined) { + // TODO: throw error if no reports_to and not root_or_tlr? + } + if (newOrg.root_or_tlr === undefined) { + newOrg.root_or_tlr = false; + } + if (newOrg.soft_quota === undefined) { // set to default quota if none is specified + newOrg.soft_quota = CONSTANTS.DEFAULT_ID_QUOTA + } + if (newOrg.hard_quota === undefined) { // set to default quota if none is specified + newOrg.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA + } + if (newOrg.authority.active_roles.length === 1 && newOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + newOrg.soft_quota = 0 + newOrg.hard_quota = 0 + } + + newOrg.in_use = false; + newOrg.UUID = uuid.v4(); + + await registryOrgRepo.updateByUUID(newOrg.UUID, newOrg, { upsert: true }); + const agt = setAggregateOrgObj({ UUID: newOrg.UUID }); + let result = await registryOrgRepo.aggregate(agt); + result = result.length > 0 ? result[0] : null; + + const payload = { + action: 'create_registry_org', + change: result.short_name + ' was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + org: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: result.short_name + ' was successfully created.', + created: result + } + return res.status(200).json(responseMessage) + } catch (err) { + next(err); + } +} + +async function updateOrg(req, res, next) { + try { + const orgUUID = req.ctx.params.identifier; + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository(); + + const org = await registryOrgRepo.findOneByUUID(orgUUID); + const newOrg = new RegistryOrg(); + newOrg.contact_info = {...org.contact_info}; + + for (const k in req.ctx.query) { + const key = k.toLowerCase() + + if (key === 'long_name') { + newOrg.long_name = req.ctx.query.long_name + } else if (key === 'short_name') { + newOrg.short_name = req.ctx.query.short_name + } else if (key === 'aliases') { + // TODO: handle aliases + } else if (key === 'cve_program_org_function') { + newOrg.cve_program_org_function = req.ctx.query.cve_program_org_function; + // TODO: validate against enum? + } else if (key === 'authority') { + // TODO: handle active_roles + } else if (key === 'reports_to') { + // TODO: validate org + } else if (key === 'oversees') { + // TODO: validate orgs + } else if (key === 'root_or_tlr') { + newOrg.root_or_tlr = req.ctx.query.root_or_tlr; + } else if (key === 'users') { + // TODO: validate users + } else if (key === 'charter_or_scope') { + newOrg.charter_or_scope = req.ctx.query.charter_or_scope; + } else if (key === 'disclosure_policy') { + newOrg.disclosure_policy = req.ctx.query.disclosure_policy; + } else if (key === 'product_list') { + newOrg.product_list = req.ctx.query.product_list; + } else if (key === 'soft_quota') { + newOrg.soft_quota = req.ctx.query.soft_quota; + } else if (key === 'hard_quota') { + newOrg.hard_quota = req.ctx.query.hard_quota; + } else if (key === 'contact_info.additional_contact_users') { + // TODO: validate users + } else if (key === 'contact_info.poc') { + newOrg.contact_info.poc = req.ctx.query['contact_info.poc']; + } else if (key === 'contact_info.poc_email') { + newOrg.contact_info.poc_email = req.ctx.query['contact_info.poc_email']; + } else if (key === 'contact_info.poc_phone') { + newOrg.contact_info.poc_phone = req.ctx.query['contact_info.poc_phone']; + } else if (key === 'contact_info.admins') { + // TODO: validate admins + } else if (key === 'contact_info.org_email') { + newOrg.contact_info.org_email = req.ctx.query['contact_info.org_email']; + } else if (key === 'contact_info.website') { + newOrg.contact_info.website = req.ctx.query['contact_info.website']; + } + } + + await registryOrgRepo.updateByUUID(orgUUID, newOrg); + const agt = setAggregateOrgObj({ UUID: orgUUID }); + let result = await registryOrgRepo.aggregate(agt); + result = result.length > 0 ? result[0] : null; + + const payload = { + action: 'update_registry_org', + change: result.short_name + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + let msgStr = '' + if (Object.keys(req.ctx.query).length > 0) { + msgStr = result.short_name + ' was successfully updated.' + } else { + msgStr = 'No updates were specified for ' + result.short_name + '.' + } + const responseMessage = { + message: msgStr, + updated: result + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err); + } +} + +async function deleteOrg(req, res, next) { + try { + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository(); + const orgUUID = req.ctx.params.identifier; + + const org = await registryOrgRepo.findOneByUUID(orgUUID); + + // TODO: check permissions + + await registryOrgRepo.deleteByUUID(orgUUID); + + const payload = { + action: 'delete_registry_org', + change: org.short_name + ' was successfully deleted.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org) + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: org.short_name + ' was successfully deleted.' + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } +} + +function setAggregateOrgObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + long_name: true, + short_name: true, + aliases: true, + cve_program_org_function: true, + authority: true, + reports_to: true, + oversees: true, + root_or_tlr: true, + users: true, + charter_or_scope: true, + disclosure_policy: true, + product_list: true, + soft_quota: true, + hard_quota: true, + contact_info: true, + in_use: true, + created: true, + last_updated: true + } + } + ] +} + +module.exports = { + ALL_ORGS: getAllOrgs, + SINGLE_ORG: getOrg, + CREATE_ORG: createOrg, + UPDATE_ORG: updateOrg, + DELETE_ORG: deleteOrg +}; \ No newline at end of file diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js new file mode 100644 index 000000000..d24a5a1f3 --- /dev/null +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -0,0 +1,48 @@ +const utils = require('../../utils/utils') +const getConstants = require('../../constants').getConstants + +function parsePostParams (req, res, next) { + utils.reqCtxMapping(req, 'body', []) + utils.reqCtxMapping(req, 'params', ['identifier']) + utils.reqCtxMapping(req, 'query', [ + 'long_name', 'short_name', 'aliases', + 'cve_program_org_function', 'authority.active_roles', + 'reports_to', 'oversees', + 'root_or_tlr', 'users', + 'charter_or_scope', 'disclosure_policy', 'product_list', + 'soft_quota', 'hard_quota', + 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', + 'contact_info.admins', 'contact_info.org_email', 'contact_info.website' + ]) + 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 isOrgRole (val) { + const CONSTANTS = getConstants() + + val.forEach(role => { + if (!CONSTANTS.ORG_ROLES.includes(role)) { + throw new Error('Organization role does not exist.') + } + }) + + return true +} + +module.exports = { + parsePostParams, + parseGetParams, + parseDeleteParams, + isOrgRole +} \ No newline at end of file diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 669664eab..6e0367bf4 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -28,6 +28,7 @@ router.get('/registryUser/:identifier', router.post('/registryUser', mw.validateUser, // mw.onlySecretariat, // TODO: permissions + // TODO: validation // parseError, parsePostParams, controller.CREATE_USER @@ -36,6 +37,7 @@ router.post('/registryUser', router.put('/registryUser/:identifier', mw.validateUser, param(['identifier']).isString().trim(), + // TODO: do more validation here // parseError, parsePostParams, controller.UPDATE_USER diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js new file mode 100644 index 000000000..6ec53c27c --- /dev/null +++ b/src/repositories/registryOrgRepository.js @@ -0,0 +1,27 @@ +const BaseRepository = require('./baseRepository') +const RegistryOrg = require('../model/registry-org') +const utils = require('../utils/utils') + +class RegistryOrgRepository extends BaseRepository { + constructor () { + super(RegistryOrg) + } + + async findOneByUUID (UUID) { + return this.collection.findOne().byUUID(UUID) + } + + async getAllOrgs () { + return this.collection.find() + } + + async updateByUUID (uuid, org, options = {}) { + return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(org).setOptions(options); + } + + async deleteByUUID (uuid) { + return this.collection.deleteOne({ UUID: uuid }); + } +} + +module.exports = RegistryOrgRepository; \ No newline at end of file diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 2253bd9a1..5ba558a69 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -4,7 +4,7 @@ const CveIdRepository = require('./cveIdRepository') const CveIdRangeRepository = require('./cveIdRangeRepository') const UserRepository = require('./userRepository') const RegistryUserRepository = require('./registryUserRepository') -// const RegistryOrgRepository = require('./registryOrgRepository') +const RegistryOrgRepository = require('./registryOrgRepository') class RepositoryFactory { getOrgRepository () { @@ -37,10 +37,10 @@ class RepositoryFactory { return repo } - // getRegistryOrgRepository () { - // const repo = new RegistryOrgRepository() - // return repo - // } + getRegistryOrgRepository () { + const repo = new RegistryOrgRepository() + return repo + } } module.exports = RepositoryFactory diff --git a/src/routes.config.js b/src/routes.config.js index a96ff76a2..7ce0fb508 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -8,6 +8,7 @@ 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') var options = { swaggerOptions: { @@ -32,6 +33,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', SystemController) app.use('/api/', UserController) app.use('/api/', RegistryUserController) + app.use('/api/', RegistryOrgController) app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)) app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions)) app.use('/schemas/', SchemasController) From e4d2c97b21259cef897ffcdc0587f9956ed8ff62 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:20:03 -0400 Subject: [PATCH 09/35] Updated registries to use uuids --- src/model/registry-org.js | 86 +++++++++++++++++++++++--------------- src/model/registry-user.js | 84 ++++++++++++++++++++++--------------- 2 files changed, 104 insertions(+), 66 deletions(-) diff --git a/src/model/registry-org.js b/src/model/registry-org.js index 4df938eea..53dad7d7a 100644 --- a/src/model/registry-org.js +++ b/src/model/registry-org.js @@ -1,43 +1,43 @@ const mongoose = require('mongoose') -const { Schema } = mongoose; +const { Schema } = mongoose const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const schema = { - _id: false, - UUID: String, - long_name: String, - short_name: String, - aliases: [String], - cve_program_org_function: { - type: String, - enum: ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download'] - }, - authority: { + _id: false, + UUID: String, + long_name: String, + short_name: String, + aliases: [String], + cve_program_org_function: { + type: String, + enum: ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download'] + }, + authority: { active_roles: [String] }, - reports_to: { type: Schema.Types.ObjectId, ref: 'RegistryOrg' }, - oversees: [{ type: Schema.Types.ObjectId, ref: 'RegistryOrg' }], - root_or_tlr: Boolean, - users: [{ type: Schema.Types.ObjectId, ref: 'RegistryUser' }], - charter_or_scope: String, - disclosure_policy: String, - product_list: String, - soft_quota: Number, - hard_quota: Number, - contact_info: { - additional_contact_users: [{ type: Schema.Types.ObjectId, ref: 'RegistryUser' }], - poc: String, - poc_email: String, - poc_phone: String, - admins: [{ type: Schema.Types.ObjectId, ref: 'RegistryUser' }], - org_email: String, - website: String - }, - in_use: Boolean, - created: Date, - last_updated: Date -}; + reports_to: String, + oversees: [String], + root_or_tlr: Boolean, + users: [String], + charter_or_scope: String, + disclosure_policy: String, + product_list: String, + soft_quota: Number, + hard_quota: Number, + contact_info: { + additional_contact_users: [String], + poc: String, + poc_email: String, + poc_phone: String, + admins: [String], + org_email: String, + website: String + }, + in_use: Boolean, + created: Date, + last_updated: Date +} const RegistryOrgSchema = new mongoose.Schema(schema, { collection: 'RegistryOrg', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) @@ -49,6 +49,26 @@ RegistryOrgSchema.query.byUUID = function (uuid) { return this.where({ UUID: uuid }) } +RegistryOrgSchema.statics.populateOverseesAndReportsTo = async function (items) { // Assuming the model name is 'RegistryUser' + for (const item of items) { + if (item.oversees.length > 0) { + const populatedOversees = await Promise.all( + item.oversees.map(async (uuid) => { + const org = await RegistryOrg.findOne({ UUID: uuid }) + return org ? org.toObject() : uuid // Return the user object if found, otherwise return the UUID + }) + ) + item.oversees = populatedOversees + } + if (item.reports_to) { + const org = await RegistryOrg.findOne({ UUID: item.reports_to }) + item.reports_to = org ? org.toObject() : item.reports_to // Return the org object if found, otherwise return the UUID + } + } + + return this +} + RegistryOrgSchema.index({ UUID: 1 }) RegistryOrgSchema.index({ 'authority.active_roles': 1 }) diff --git a/src/model/registry-user.js b/src/model/registry-user.js index 2003d3a76..bd8fe484c 100644 --- a/src/model/registry-user.js +++ b/src/model/registry-user.js @@ -1,58 +1,76 @@ const mongoose = require('mongoose') -const { Schema } = mongoose; +const { Schema } = mongoose const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const schema = { - _id: false, - UUID: String, - user_id: String, - secret: String, - name: { + _id: false, + UUID: String, + user_id: String, + secret: String, + name: { first: String, last: String, middle: String, suffix: String }, - org_affiliations: [{ - org_id: { type: Schema.Types.ObjectId, ref: 'RegistryOrg' }, - email: String, - phone: String - }], - cve_program_org_membership: [{ - program_org: { type: Schema.Types.ObjectId, ref: 'RegistryOrg' }, - role: { - type: String, - enum: ['Chair', 'Member', 'Admin'] - }, - status: { - type: String, - enum: ['active', 'inactive'] - } - }], - created: Date, - created_by: { type: Schema.Types.ObjectId, ref: 'RegistryUser' }, - last_updated: Date, - deactivation_date: Date, - last_active: Date + org_affiliations: [{ + org_id: String, + email: String, + phone: String + }], + cve_program_org_membership: [{ + program_org: String, + role: { + type: String, + enum: ['Chair', 'Member', 'Admin'] + }, + status: { + type: String, + enum: ['active', 'inactive'] + } + }], + created: Date, + created_by: String, + last_updated: Date, + deactivation_date: Date, + last_active: Date } -const RegistryUserSchema = new mongoose.Schema(schema, { collection: 'RegistryUser', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }); +const userPrivate = '-secret -_id -org_affiliations._id -cve_program_org_membership._id -created_by -created -last_updated -last_active -__v' +const userSecretariat = '-secret' +const RegistryUserSchema = new mongoose.Schema(schema, { collection: 'RegistryUser', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) RegistryUserSchema.query.byUserID = function (userID) { - return this.where({ user_id: userID }); + return this.where({ user_id: userID }) } RegistryUserSchema.query.byUUID = function (uuid) { - return this.where({ UUID: uuid }); + return this.where({ UUID: uuid }) } -RegistryUserSchema.index({ UUID: 1 }); -RegistryUserSchema.index({ user_id: 1 }); +RegistryUserSchema.statics.populateAdmins = async function (items) { // Assuming the model name is 'RegistryUser' + for (const item of items) { + if (item.contact_info && item.contact_info.admins && item.contact_info.admins.length > 0) { + const populatedAdmins = await Promise.all( + item.contact_info.admins.map(async (uuid) => { + const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields) + return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID + }) + ) + item.contact_info.admins = populatedAdmins + } + } + + return this +} + +RegistryUserSchema.index({ UUID: 1 }) +RegistryUserSchema.index({ user_id: 1 }) RegistryUserSchema.plugin(aggregatePaginate) // Cursor pagination RegistryUserSchema.plugin(MongoPaging.mongoosePlugin) const RegistryUser = mongoose.model('RegistryUser', RegistryUserSchema) -module.exports = RegistryUser \ No newline at end of file +module.exports = RegistryUser From ea1bf04ed382a8ed4c8b5dade0733bbfe1fed22e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:21:03 -0400 Subject: [PATCH 10/35] Updated controller and middleware for orgs --- .../registry-org.controller.js | 612 +++++++++--------- .../registry-org.middleware.js | 2 +- 2 files changed, 310 insertions(+), 304 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index bd2633177..1d819a389 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -1,320 +1,326 @@ -const uuid = require('uuid'); -const logger = require('../../middleware/logger'); -const { getConstants } = require('../../constants'); -const RegistryOrg = require('../../model/registry-org'); - -async function getAllOrgs(req, res, next) { - try { - const CONSTANTS = getConstants() - - // temporary measure to allow tests to work after fixing #920 - // tests required changing the global limit to force pagination - if (req.TEST_PAGINATOR_LIMIT) { - CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT - } - - const options = CONSTANTS.PAGINATOR_OPTIONS - 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 repo = req.ctx.repositories.getRegistryOrgRepository() - - const agt = setAggregateOrgObj({}) - const pg = await repo.aggregatePaginate(agt, options) - const payload = { orgs: pg.itemsList } - - if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { - payload.totalCount = pg.itemCount - payload.itemsPerPage = pg.itemsPerPage - payload.pageCount = pg.pageCount - payload.currentPage = pg.currentPage - payload.prevPage = pg.prevPage - payload.nextPage = pg.nextPage - } - - logger.info({ uuid: req.ctx.uuid, message: 'The org information was sent to the secretariat user.' }) - return res.status(200).json(payload) - } catch (err) { - next(err) - } +const uuid = require('uuid') +const logger = require('../../middleware/logger') +const { getConstants } = require('../../constants') +const RegistryOrg = require('../../model/registry-org') +const RegistryUser = require('../../model/registry-user') + +async function getAllOrgs (req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT + } + + const options = CONSTANTS.PAGINATOR_OPTIONS + 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 repo = req.ctx.repositories.getRegistryOrgRepository() + + const agt = setAggregateOrgObj({}) + const pg = await repo.aggregatePaginate(agt, options) + + await RegistryOrg.populateOverseesAndReportsTo(pg.itemsList) + await RegistryUser.populateAdmins(pg.itemsList) + // Update UUIDS to objects + + const payload = { orgs: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: 'The org information was sent to the secretariat user.' }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } } -async function getOrg(req, res, next) { - try { - const repo = req.ctx.repositories.getRegistryOrgRepository(); - const identifier = req.ctx.params.identifier; - const agt = setAggregateOrgObj({ UUID: identifier }); - let result = await repo.aggregate(agt) +async function getOrg (req, res, next) { + try { + const repo = req.ctx.repositories.getRegistryOrgRepository() + const identifier = req.ctx.params.identifier + const agt = setAggregateOrgObj({ UUID: identifier }) + let result = await repo.aggregate(agt) result = result.length > 0 ? result[0] : null - logger.info({ uuid: req.ctx.uuid, message: identifier + ' org was sent to the user.', org: result }) + logger.info({ uuid: req.ctx.uuid, message: identifier + ' org was sent to the user.', org: result }) return res.status(200).json(result) - } catch (err) { - next(err) - } + } catch (err) { + next(err) + } } -async function createOrg(req, res, next) { - try { - const CONSTANTS = getConstants() - const orgRepo = req.ctx.repositories.getOrgRepository(); - const userRepo = req.ctx.repositories.getUserRepository(); - const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository(); - const body = req.ctx.body; - - const newOrg = new RegistryOrg(); - Object.keys(body).map(k => k.toLowerCase()).forEach(k => { - if (k === 'long_name') { - newOrg.long_name = body[k]; - } else if (k === 'short_name') { - newOrg.short_name = body[k]; - } else if (k === 'aliases') { - newOrg.aliases = [...new Set(body[k].active_roles)]; - } else if (k === 'cve_program_org_function') { - newOrg.cve_program_org_function = body[k]; - } else if (k === 'authority') { - if ('active_roles' in body[k]) { - newOrg.authority.active_roles = [...new Set(body[k].active_roles)]; - } - } else if (k === 'reports_to') { - // TODO: org check logic? - } else if (k === 'oversees') { - // TODO: org check logic? - } else if (k === 'root_or_tlr') { - newOrg.root_or_tlr = body[k]; - } else if (k === 'users') { - // TODO: users logic? - } else if (k === 'charter_or_scope') { - newOrg.charter_or_scope = body[k]; - } else if (k === 'disclosure_policy') { - newOrg.disclosure_policy = body[k]; - } else if (k === 'product_list') { - newOrg.product_list = body[k]; - } else if (k === 'soft_quota') { - newOrg.soft_quota = body[k]; - } else if (k === 'hard_quota') { - newOrg.hard_quota = body[k]; - } else if (k === 'contact_info') { - const {additional_contact_users, admins, ...contactInfo} = body[k]; - newOrg.contact_info = { - additional_contact_users: [...(additional_contact_users || [])], - poc: '', - poc_email: '', - poc_phone: '', - admins: [...(admins || [])], - org_email: '', - website: '', - ...contactInfo - }; - } else if (k === 'uuid') { - return res.status(400).json(error.uuidProvided('org')); - } - }); - - if (newOrg.reports_to === undefined) { - // TODO: throw error if no reports_to and not root_or_tlr? - } - if (newOrg.root_or_tlr === undefined) { - newOrg.root_or_tlr = false; - } - if (newOrg.soft_quota === undefined) { // set to default quota if none is specified - newOrg.soft_quota = CONSTANTS.DEFAULT_ID_QUOTA - } - if (newOrg.hard_quota === undefined) { // set to default quota if none is specified - newOrg.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA - } - if (newOrg.authority.active_roles.length === 1 && newOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 - newOrg.soft_quota = 0 - newOrg.hard_quota = 0 - } - - newOrg.in_use = false; - newOrg.UUID = uuid.v4(); - - await registryOrgRepo.updateByUUID(newOrg.UUID, newOrg, { upsert: true }); - const agt = setAggregateOrgObj({ UUID: newOrg.UUID }); - let result = await registryOrgRepo.aggregate(agt); - result = result.length > 0 ? result[0] : null; - - const payload = { - action: 'create_registry_org', - change: result.short_name + ' was successfully created.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - org: result - } - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) - logger.info(JSON.stringify(payload)) - - const responseMessage = { - message: result.short_name + ' was successfully created.', - created: result - } - return res.status(200).json(responseMessage) - } catch (err) { - next(err); - } +async function createOrg (req, res, next) { + try { + const CONSTANTS = getConstants() + const orgRepo = req.ctx.repositories.getOrgRepository() + const userRepo = req.ctx.repositories.getUserRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + const body = req.ctx.body + + const newOrg = new RegistryOrg() + Object.keys(body).map(k => k.toLowerCase()).forEach(k => { + if (k === 'long_name') { + newOrg.long_name = body[k] + } else if (k === 'short_name') { + newOrg.short_name = body[k] + } else if (k === 'aliases') { + newOrg.aliases = [...new Set(body[k].active_roles)] + } else if (k === 'cve_program_org_function') { + newOrg.cve_program_org_function = body[k] + } else if (k === 'authority') { + if ('active_roles' in body[k]) { + newOrg.authority.active_roles = [...new Set(body[k].active_roles)] + } + } else if (k === 'reports_to') { + // TODO: org check logic? + } else if (k === 'oversees') { + // TODO: org check logic? + } else if (k === 'root_or_tlr') { + newOrg.root_or_tlr = body[k] + } else if (k === 'users') { + // TODO: users logic? + } else if (k === 'charter_or_scope') { + newOrg.charter_or_scope = body[k] + } else if (k === 'disclosure_policy') { + newOrg.disclosure_policy = body[k] + } else if (k === 'product_list') { + newOrg.product_list = body[k] + } else if (k === 'soft_quota') { + newOrg.soft_quota = body[k] + } else if (k === 'hard_quota') { + newOrg.hard_quota = body[k] + } else if (k === 'contact_info') { + const { additional_contact_users, admins, ...contactInfo } = body[k] + newOrg.contact_info = { + additional_contact_users: [...(additional_contact_users || [])], + poc: '', + poc_email: '', + poc_phone: '', + admins: [...(admins || [])], + org_email: '', + website: '', + ...contactInfo + } + } else if (k === 'uuid') { + return res.status(400).json(error.uuidProvided('org')) + } + }) + + if (newOrg.reports_to === undefined) { + // TODO: throw error if no reports_to and not root_or_tlr? + } + if (newOrg.root_or_tlr === undefined) { + newOrg.root_or_tlr = false + } + if (newOrg.soft_quota === undefined) { // set to default quota if none is specified + newOrg.soft_quota = CONSTANTS.DEFAULT_ID_QUOTA + } + if (newOrg.hard_quota === undefined) { // set to default quota if none is specified + newOrg.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA + } + if (newOrg.authority.active_roles.length === 1 && newOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + newOrg.soft_quota = 0 + newOrg.hard_quota = 0 + } + + newOrg.in_use = false + newOrg.UUID = uuid.v4() + + await registryOrgRepo.updateByUUID(newOrg.UUID, newOrg, { upsert: true }) + const agt = setAggregateOrgObj({ UUID: newOrg.UUID }) + let result = await registryOrgRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'create_registry_org', + change: result.short_name + ' was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + org: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: result.short_name + ' was successfully created.', + created: result + } + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } } -async function updateOrg(req, res, next) { - try { - const orgUUID = req.ctx.params.identifier; - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() - const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository(); - - const org = await registryOrgRepo.findOneByUUID(orgUUID); - const newOrg = new RegistryOrg(); - newOrg.contact_info = {...org.contact_info}; - - for (const k in req.ctx.query) { - const key = k.toLowerCase() - - if (key === 'long_name') { - newOrg.long_name = req.ctx.query.long_name - } else if (key === 'short_name') { - newOrg.short_name = req.ctx.query.short_name - } else if (key === 'aliases') { - // TODO: handle aliases - } else if (key === 'cve_program_org_function') { - newOrg.cve_program_org_function = req.ctx.query.cve_program_org_function; - // TODO: validate against enum? - } else if (key === 'authority') { - // TODO: handle active_roles - } else if (key === 'reports_to') { - // TODO: validate org - } else if (key === 'oversees') { - // TODO: validate orgs - } else if (key === 'root_or_tlr') { - newOrg.root_or_tlr = req.ctx.query.root_or_tlr; - } else if (key === 'users') { - // TODO: validate users - } else if (key === 'charter_or_scope') { - newOrg.charter_or_scope = req.ctx.query.charter_or_scope; - } else if (key === 'disclosure_policy') { - newOrg.disclosure_policy = req.ctx.query.disclosure_policy; - } else if (key === 'product_list') { - newOrg.product_list = req.ctx.query.product_list; - } else if (key === 'soft_quota') { - newOrg.soft_quota = req.ctx.query.soft_quota; - } else if (key === 'hard_quota') { - newOrg.hard_quota = req.ctx.query.hard_quota; - } else if (key === 'contact_info.additional_contact_users') { - // TODO: validate users - } else if (key === 'contact_info.poc') { - newOrg.contact_info.poc = req.ctx.query['contact_info.poc']; - } else if (key === 'contact_info.poc_email') { - newOrg.contact_info.poc_email = req.ctx.query['contact_info.poc_email']; - } else if (key === 'contact_info.poc_phone') { - newOrg.contact_info.poc_phone = req.ctx.query['contact_info.poc_phone']; - } else if (key === 'contact_info.admins') { - // TODO: validate admins - } else if (key === 'contact_info.org_email') { - newOrg.contact_info.org_email = req.ctx.query['contact_info.org_email']; - } else if (key === 'contact_info.website') { - newOrg.contact_info.website = req.ctx.query['contact_info.website']; - } - } - - await registryOrgRepo.updateByUUID(orgUUID, newOrg); - const agt = setAggregateOrgObj({ UUID: orgUUID }); - let result = await registryOrgRepo.aggregate(agt); - result = result.length > 0 ? result[0] : null; - - const payload = { - action: 'update_registry_org', - change: result.short_name + ' was successfully updated.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - user: result - } - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) - logger.info(JSON.stringify(payload)) - - let msgStr = '' - if (Object.keys(req.ctx.query).length > 0) { - msgStr = result.short_name + ' was successfully updated.' - } else { - msgStr = 'No updates were specified for ' + result.short_name + '.' - } - const responseMessage = { - message: msgStr, - updated: result - } - - return res.status(200).json(responseMessage) - } catch (err) { - next(err); - } +async function updateOrg (req, res, next) { + try { + const orgUUID = req.ctx.params.identifier + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + + const org = await registryOrgRepo.findOneByUUID(orgUUID) + const newOrg = new RegistryOrg() + newOrg.contact_info = { ...org.contact_info } + + for (const k in req.ctx.query) { + const key = k.toLowerCase() + + if (key === 'long_name') { + newOrg.long_name = req.ctx.query.long_name + } else if (key === 'short_name') { + newOrg.short_name = req.ctx.query.short_name + } else if (key === 'aliases') { + // TODO: handle aliases + } else if (key === 'cve_program_org_function') { + newOrg.cve_program_org_function = req.ctx.query.cve_program_org_function + // TODO: validate against enum? + } else if (key === 'authority') { + // TODO: handle active_roles + } else if (key === 'reports_to') { + // TODO: validate org + } else if (key === 'oversees') { + // TODO: validate orgs + } else if (key === 'root_or_tlr') { + newOrg.root_or_tlr = req.ctx.query.root_or_tlr + } else if (key === 'users') { + // TODO: validate users + } else if (key === 'charter_or_scope') { + newOrg.charter_or_scope = req.ctx.query.charter_or_scope + } else if (key === 'disclosure_policy') { + newOrg.disclosure_policy = req.ctx.query.disclosure_policy + } else if (key === 'product_list') { + newOrg.product_list = req.ctx.query.product_list + } else if (key === 'soft_quota') { + newOrg.soft_quota = req.ctx.query.soft_quota + } else if (key === 'hard_quota') { + newOrg.hard_quota = req.ctx.query.hard_quota + } else if (key === 'contact_info.additional_contact_users') { + // TODO: validate users + } else if (key === 'contact_info.poc') { + newOrg.contact_info.poc = req.ctx.query['contact_info.poc'] + } else if (key === 'contact_info.poc_email') { + newOrg.contact_info.poc_email = req.ctx.query['contact_info.poc_email'] + } else if (key === 'contact_info.poc_phone') { + newOrg.contact_info.poc_phone = req.ctx.query['contact_info.poc_phone'] + } else if (key === 'contact_info.admins') { + // TODO: validate admins + } else if (key === 'contact_info.org_email') { + newOrg.contact_info.org_email = req.ctx.query['contact_info.org_email'] + } else if (key === 'contact_info.website') { + newOrg.contact_info.website = req.ctx.query['contact_info.website'] + } + } + + await registryOrgRepo.updateByUUID(orgUUID, newOrg) + const agt = setAggregateOrgObj({ UUID: orgUUID }) + let result = await registryOrgRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'update_registry_org', + change: result.short_name + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + let msgStr = '' + if (Object.keys(req.ctx.query).length > 0) { + msgStr = result.short_name + ' was successfully updated.' + } else { + msgStr = 'No updates were specified for ' + result.short_name + '.' + } + const responseMessage = { + message: msgStr, + updated: result + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } } -async function deleteOrg(req, res, next) { - try { - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() - const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository(); - const orgUUID = req.ctx.params.identifier; - - const org = await registryOrgRepo.findOneByUUID(orgUUID); - - // TODO: check permissions - - await registryOrgRepo.deleteByUUID(orgUUID); - - const payload = { - action: 'delete_registry_org', - change: org.short_name + ' was successfully deleted.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org) - } - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) - logger.info(JSON.stringify(payload)) - - const responseMessage = { - message: org.short_name + ' was successfully deleted.' - } - - return res.status(200).json(responseMessage) - } catch (err) { - next(err) - } +async function deleteOrg (req, res, next) { + try { + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + const orgUUID = req.ctx.params.identifier + + const org = await registryOrgRepo.findOneByUUID(orgUUID) + + // TODO: check permissions + + await registryOrgRepo.deleteByUUID(orgUUID) + + const payload = { + action: 'delete_registry_org', + change: org.short_name + ' was successfully deleted.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org) + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: org.short_name + ' was successfully deleted.' + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } } function setAggregateOrgObj (query) { - return [ - { - $match: query - }, - { - $project: { - _id: false, - UUID: true, - long_name: true, - short_name: true, - aliases: true, - cve_program_org_function: true, - authority: true, - reports_to: true, - oversees: true, - root_or_tlr: true, - users: true, - charter_or_scope: true, - disclosure_policy: true, - product_list: true, - soft_quota: true, - hard_quota: true, - contact_info: true, - in_use: true, - created: true, - last_updated: true - } - } - ] + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + long_name: true, + short_name: true, + aliases: true, + cve_program_org_function: true, + authority: true, + reports_to: true, + oversees: true, + root_or_tlr: true, + users: true, + charter_or_scope: true, + disclosure_policy: true, + product_list: true, + soft_quota: true, + hard_quota: true, + contact_info: true, + in_use: true, + created: true, + last_updated: true + } + } + ] } module.exports = { - ALL_ORGS: getAllOrgs, - SINGLE_ORG: getOrg, - CREATE_ORG: createOrg, - UPDATE_ORG: updateOrg, - DELETE_ORG: deleteOrg -}; \ No newline at end of file + ALL_ORGS: getAllOrgs, + SINGLE_ORG: getOrg, + CREATE_ORG: createOrg, + UPDATE_ORG: updateOrg, + DELETE_ORG: deleteOrg +} diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index d24a5a1f3..1f921b78a 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -45,4 +45,4 @@ module.exports = { parseGetParams, parseDeleteParams, isOrgRole -} \ No newline at end of file +} From 63d87e8401f2359a7a3ae8a499f3883d6c5b9ce3 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:21:30 -0400 Subject: [PATCH 11/35] Added new populate script and test data for new collections --- datadump/pre-population/registry-orgs.json | 110 ++++++++++++++++++++ datadump/pre-population/registry-users.json | 89 ++++++++++++++++ src/scripts/populate.js | 20 +++- 3 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 datadump/pre-population/registry-orgs.json create mode 100644 datadump/pre-population/registry-users.json diff --git a/datadump/pre-population/registry-orgs.json b/datadump/pre-population/registry-orgs.json new file mode 100644 index 000000000..ced7e8ed6 --- /dev/null +++ b/datadump/pre-population/registry-orgs.json @@ -0,0 +1,110 @@ +[ + { + "UUID": "org-uuid-1", + "long_name": "Test Organization One", + "short_name": "TestOrg1", + "aliases": [ + "TO1", + "Test1" + ], + "cve_program_org_function": "CNA", + "authority": { + "active_roles": [ + "CNA" + ] + }, + "reports_to": null, + "oversees": ["org-uuid-2"], + "root_or_tlr": true, + "users": ["user-uuid-1"], + "charter_or_scope": "Responsible for technology sector vulnerabilities", + "disclosure_policy": "90-day disclosure policy", + "product_list": "Product A, Product B, Product C", + "soft_quota": 100, + "hard_quota": 150, + "contact_info": { + "additional_contact_users": [], + "poc": "John Doe", + "poc_email": "john.doe@testorg1.com", + "poc_phone": "+1-555-001-1001", + "admins": ["user-uuid-1"], + "org_email": "contact@testorg1.com", + "website": "https://www.testorg1.com" + }, + "in_use": true, + "created": "2023-06-01T00:00:00.000Z", + "last_updated": "2023-06-01T00:00:00.000Z" + }, + { + "UUID": "org-uuid-2", + "long_name": "Security Solutions Inc.", + "short_name": "SecSol", + "aliases": [ + "SSI", + "SecInc" + ], + "cve_program_org_function": "CNA", + "authority": { + "active_roles": [ + "CNA" + ] + }, + "reports_to": "org-uuid-1", + "oversees": [], + "root_or_tlr": true, + "users": ["user-uuid-2"], + "charter_or_scope": "Focused on cybersecurity software vulnerabilities", + "disclosure_policy": "60-day responsible disclosure policy", + "product_list": "SecureShield, CyberGuard, DataDefender", + "soft_quota": 75, + "hard_quota": 100, + "contact_info": { + "additional_contact_users": [], + "poc": "Jane Smith", + "poc_email": "jane.smith@secsol.com", + "poc_phone": "+1-555-002-2002", + "admins": ["user-uuid-2"], + "org_email": "info@secsol.com", + "website": "https://www.secsol.com" + }, + "in_use": true, + "created": "2023-06-02T00:00:00.000Z", + "last_updated": "2023-06-02T00:00:00.000Z" + }, + { + "UUID": "org-uuid-3", + "long_name": "Global Network Systems", + "short_name": "GNS", + "aliases": [ + "GlobalNet", + "NetSys" + ], + "cve_program_org_function": "CNA", + "authority": { + "active_roles": [ + "CNA" + ] + }, + "reports_to": null, + "oversees": [], + "root_or_tlr": false, + "users": ["user-uuid-3"], + "charter_or_scope": "Specializing in network infrastructure vulnerabilities", + "disclosure_policy": "45-day coordinated disclosure policy", + "product_list": "NetRouter, CloudConnect, SecureSwitch", + "soft_quota": 120, + "hard_quota": 180, + "contact_info": { + "additional_contact_users": [], + "poc": "Michael Johnson", + "poc_email": "michael.johnson@gns.com", + "poc_phone": "+1-555-003-3003", + "admins": ["user-uuid-3"], + "org_email": "contact@gns.com", + "website": "https://www.gns.com" + }, + "in_use": true, + "created": "2023-06-03T00:00:00.000Z", + "last_updated": "2023-06-03T00:00:00.000Z" + } +] \ No newline at end of file diff --git a/datadump/pre-population/registry-users.json b/datadump/pre-population/registry-users.json new file mode 100644 index 000000000..67b1da391 --- /dev/null +++ b/datadump/pre-population/registry-users.json @@ -0,0 +1,89 @@ +[ + { + "UUID": "user-uuid-1", + "user_id": "user1@testorg1.com", + "secret": "secretKey1", + "name": { + "first": "John", + "last": "Doe", + "middle": "A", + "suffix": "Jr" + }, + "org_affiliations": [ + { + "org_id": "org-uuid-1", + "email": "john.doe@testorg1.com", + "phone": "+1-555-001-1001" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-uuid-1", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-01T00:00:00.000Z", + "created_by": "system", + "last_updated": "2023-06-01T00:00:00.000Z", + "last_active": "2023-06-01T00:00:00.000Z" + }, + { + "UUID": "user-uuid-2", + "user_id": "jane.smith@secsol.com", + "secret": "secretKey2", + "name": { + "first": "Jane", + "last": "Smith", + "middle": "B", + "suffix": "" + }, + "org_affiliations": [ + { + "org_id": "org-uuid-2", + "email": "jane.smith@secsol.com", + "phone": "+1-555-002-2002" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-uuid-2", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-02T00:00:00.000Z", + "created_by": "system", + "last_updated": "2023-06-02T00:00:00.000Z", + "last_active": "2023-06-02T00:00:00.000Z" + }, + { + "UUID": "user-uuid-3", + "user_id": "michael.johnson@gns.com", + "secret": "secretKey3", + "name": { + "first": "Michael", + "last": "Johnson", + "middle": "C", + "suffix": "" + }, + "org_affiliations": [ + { + "org_id": "org-uuid-3", + "email": "michael.johnson@gns.com", + "phone": "+1-555-003-3003" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-uuid-3", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-03T00:00:00.000Z", + "created_by": "system", + "last_updated": "2023-06-03T00:00:00.000Z", + "last_active": "2023-06-03T00:00:00.000Z" + } +] \ No newline at end of file diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 22b3ee9f8..8c8d07822 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -16,6 +16,8 @@ const CveId = require('../model/cve-id') const Cve = require('../model/cve') const Org = require('../model/org') const User = require('../model/user') +const RegistryOrg = require('../model/registry-org') +const RegistryUser = require('../model/registry-user') const error = new errors.IDRError() @@ -24,7 +26,9 @@ const populateTheseCollections = { 'Cve-Id-Range': CveIdRange, 'Cve-Id': CveId, User: User, - Org: Org + Org: Org, + RegistryOrg: RegistryOrg, + RegistryUser: RegistryUser } const indexesToCreate = { @@ -89,14 +93,18 @@ db.once('open', async () => { names.push(collection.name) }) - if (!names.includes('Cve-Id-Range') && !names.includes('Cve-Id') && !names.includes('Cve') && - !names.includes('Org') && !names.includes('User')) { + if (!names.includes('Cve-Id-Range') && !names.includes('Cve-Id') && !names.includes('Cve') && !names.includes('Org') && !names.includes('User')) { // Org await dataUtils.populateCollection( './datadump/pre-population/orgs.json', Org, dataUtils.newOrgTransform ) + await dataUtils.populateCollection( + './datadump/pre-population/registry-orgs.json', + RegistryOrg + ) + // User, depends on Org const hash = await dataUtils.preprocessUserSecrets() await dataUtils.populateCollection( @@ -104,6 +112,12 @@ db.once('open', async () => { User, dataUtils.newUserTransform, hash ) + // const registryUserHash = await dataUtils.preprocessUserSecrets() + await dataUtils.populateCollection( + './datadump/pre-population/registry-users.json', + RegistryUser + ) + const populatePromises = [] // CVE ID Range From 940cf6a36e64f611c21c2d6e3b83e3a5c15dc9da Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 16:15:32 -0400 Subject: [PATCH 12/35] Swagger --- api-docs/openapi.json | 848 ++++++++++++++++++ .../get-registry-org-response.json | 157 ++++ .../get-registry-users-response.json | 120 +++ .../registry-org.controller/index.js | 382 +++++++- .../registry-org.controller.js | 1 - .../registry-user.controller/index.js | 338 ++++++- .../registry-user.controller.js | 498 +++++----- src/controller/schemas.controller/index.js | 6 + .../schemas.controller/schemas.controller.js | 16 +- src/swagger.js | 4 +- 10 files changed, 2080 insertions(+), 290 deletions(-) create mode 100644 schemas/registry-org/get-registry-org-response.json create mode 100644 schemas/registry-user/get-registry-users-response.json diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 8f4ed585e..415a4ce0c 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2981,6 +2981,854 @@ } } } + }, + "/registryOrg": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Retrieves information about all registry organizations (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Retrieves a list of all registry organizations

", + "operationId": "getAllRegistryOrgs", + "parameters": [ + { + "$ref": "#/components/parameters/pageQuery" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "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/get-registry-org-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + }, + "post": { + "tags": [ + "Registry Organization" + ], + "summary": "Creates a new registry organization (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Creates a new registry organization

", + "operationId": "createRegistryOrg", + "parameters": [ + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "201": { + "description": "The registry organization was successfully created", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgPayload" + } + } + } + } + } + }, + "/registryOrg/{identifier}": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Retrieves information about a specific registry organization", + "description": "

Access Control

All authenticated users can access this endpoint

Expected Behavior

All Users: Retrieves information about the specified registry organization

", + "operationId": "getSingleRegistryOrg", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry organization" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The requested registry organization information is returned", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/get-org-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" + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Registry Organization" + ], + "summary": "Updates an existing registry organization (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Updates an existing registry organization

", + "operationId": "updateRegistryOrg", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry organization to update" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry organization was successfully updated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/update-org-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" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgPayload" + } + } + } + } + }, + "delete": { + "tags": [ + "Registry Organization" + ], + "summary": "Deletes an existing registry organization (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Deletes an existing registry organization

", + "operationId": "deleteRegistryOrg", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry organization to delete" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry organization was successfully deleted", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/delete-org-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" + } + } + } + } + } + } + }, + "/registryUser": { + "get": { + "tags": [ + "Registry User" + ], + "summary": "Retrieves information about all registry users (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Retrieves a list of all registry users

", + "operationId": "getAllRegistryUsers", + "parameters": [ + { + "$ref": "#/components/parameters/pageQuery" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "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/get-registry-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + }, + "post": { + "tags": [ + "Registry User" + ], + "summary": "Creates a new registry user (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Creates a new registry user

", + "operationId": "createRegistryUser", + "parameters": [ + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "201": { + "description": "The registry user was successfully created", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserPayload" + } + } + } + } + } + }, + "/registryUser/{identifier}": { + "get": { + "tags": [ + "Registry User" + ], + "summary": "Retrieves information about a specific registry user", + "description": "

Access Control

All authenticated users can access this endpoint

Expected Behavior

All Users: Retrieves information about the specified registry user

", + "operationId": "getSingleRegistryUser", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry user" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The requested registry user information is returned", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/get-user-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" + } + } + } + }, + "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" + } + } + } + } + } + }, + "put": { + "tags": [ + "Registry User" + ], + "summary": "Updates an existing registry user (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Updates an existing registry user

", + "operationId": "updateRegistryUser", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry user to update" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry user was successfully updated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/update-user-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" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserPayload" + } + } + } + } + }, + "delete": { + "tags": [ + "Registry User" + ], + "summary": "Deletes an existing registry user (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Deletes an existing registry user

", + "operationId": "deleteRegistryUser", + "parameters": [ + { + "name": "identifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The identifier of the registry user to delete" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "The registry user was successfully deleted", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/delete-user-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" + } + } + } + } + } + } } }, "components": { diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json new file mode 100644 index 000000000..839f24d92 --- /dev/null +++ b/schemas/registry-org/get-registry-org-response.json @@ -0,0 +1,157 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/org/organization.json", + "type": "object", + "title": "CVE Organization", + "description": "JSON Schema for CVE Organization data", + "properties": { + "UUID": { + "type": "string", + "description": "Unique identifier for the organization" + }, + "long_name": { + "type": "string", + "description": "Full name of the organization" + }, + "short_name": { + "type": "string", + "description": "Short name or acronym of the organization" + }, + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Alternative names or aliases for the organization" + }, + "cve_program_org_function": { + "type": "string", + "enum": ["CNA", "ADP", "Root", "Secretariat"], + "description": "The organization's function within the CVE program" + }, + "authority": { + "type": "object", + "properties": { + "active_roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["CNA", "ADP", "Root", "Secretariat"] + } + } + }, + "required": ["active_roles"] + }, + "reports_to": { + "type": ["string", "null"], + "description": "UUID of the parent organization, if any" + }, + "oversees": { + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of organizations overseen by this organization" + }, + "root_or_tlr": { + "type": "boolean", + "description": "Indicates if the organization is a root or top-level root" + }, + "users": { + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of users associated with this organization" + }, + "charter_or_scope": { + "type": "string", + "description": "Description of the organization's charter or scope" + }, + "disclosure_policy": { + "type": "string", + "description": "The organization's disclosure policy" + }, + "product_list": { + "type": "string", + "description": "List of products associated with the organization" + }, + "soft_quota": { + "type": "integer", + "description": "Soft quota for CVE IDs" + }, + "hard_quota": { + "type": "integer", + "description": "Hard quota for CVE IDs" + }, + "contact_info": { + "type": "object", + "properties": { + "additional_contact_users": { + "type": "array", + "items": { + "type": "string" + } + }, + "poc": { + "type": "string", + "description": "Point of contact name" + }, + "poc_email": { + "type": "string", + "format": "email", + "description": "Point of contact email" + }, + "poc_phone": { + "type": "string", + "description": "Point of contact phone number" + }, + "admins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of admin users" + }, + "org_email": { + "type": "string", + "format": "email", + "description": "Organization's email address" + }, + "website": { + "type": "string", + "format": "uri", + "description": "Organization's website URL" + } + }, + "required": ["poc", "poc_email", "admins", "org_email"] + }, + "in_use": { + "type": "boolean", + "description": "Indicates if the organization is currently active" + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the organization was created" + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "Timestamp of the last update to the organization data" + } + }, + "required": [ + "UUID", + "long_name", + "short_name", + "cve_program_org_function", + "authority", + "root_or_tlr", + "users", + "contact_info", + "in_use", + "created", + "last_updated" + ] +} \ No newline at end of file diff --git a/schemas/registry-user/get-registry-users-response.json b/schemas/registry-user/get-registry-users-response.json new file mode 100644 index 000000000..d753e14ee --- /dev/null +++ b/schemas/registry-user/get-registry-users-response.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cve.mitre.org/schema/user/registry-user.json", + "type": "object", + "title": "CVE Registry User", + "description": "JSON Schema for CVE Registry User data", + "properties": { + "UUID": { + "type": "string", + "description": "Unique identifier for the user" + }, + "user_id": { + "type": "string", + "description": "User's identifier or username" + }, + "name": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "User's first name" + }, + "last": { + "type": "string", + "description": "User's last name" + }, + "middle": { + "type": "string", + "description": "User's middle name" + }, + "suffix": { + "type": "string", + "description": "User's name suffix" + } + }, + "required": ["first", "last"] + }, + "org_affiliations": { + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of organizations the user is affiliated with" + }, + "cve_program_org_membership": { + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of CVE program organizations the user is a member of" + }, + "authority": { + "type": "object", + "properties": { + "active_roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["ADMIN", "PUBLISHER"] + } + } + }, + "required": ["active_roles"] + }, + "secret": { + "type": "string", + "description": "Hashed secret for user authentication" + }, + "last_active": { + "type": ["string", "null"], + "format": "date-time", + "description": "Timestamp of the user's last activity" + }, + "deactivation_date": { + "type": ["string", "null"], + "format": "date-time", + "description": "Timestamp of when the user was deactivated, if applicable" + }, + "contact_info": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "User's email address" + }, + "phone": { + "type": "string", + "description": "User's phone number" + } + }, + "required": ["email"] + }, + "in_use": { + "type": "boolean", + "description": "Indicates if the user account is currently active" + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the user account was created" + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "Timestamp of the last update to the user data" + } + }, + "required": [ + "UUID", + "user_id", + "name", + "authority", + "secret", + "contact_info", + "in_use", + "created", + "last_updated" + ] +} \ No newline at end of file diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index f455e9d2c..48c0e51d9 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -10,42 +10,299 @@ const getConstants = require('../../constants').getConstants const CONSTANTS = getConstants() router.get('/registryOrg', - mw.validateUser, - mw.onlySecretariat, + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'getAllRegistryOrgs' + #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/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[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', - mw.validateUser, - param(['identifier']).isString().trim(), - // parseError, - parseGetParams, - controller.SINGLE_ORG -); + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'getSingleRegistryOrg' + #swagger.summary = "Retrieves information about a specific registry organization" + #swagger.description = " +

Access Control

+

All authenticated users can access this endpoint

+

Expected Behavior

+

All Users: 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/org/get-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, + param(['identifier']).isString().trim(), + // parseError, + parseGetParams, + controller.SINGLE_ORG +) router.post('/registryOrg', - mw.validateUser, - mw.onlySecretariat, - body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['long_name']).isString().trim().notEmpty(), - body(['authority.active_roles']).optional() - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole), - body(['soft_quota']).optional().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), - body(['hard_quota']).optional().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), - // TODO: more validation needed? - // parseError, - parsePostParams, - controller.CREATE_ORG -); + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'createRegistryOrg' + #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: '#/components/schemas/CreateOrgPayload' } + } + } + } + #swagger.responses[201] = { + description: 'The registry organization was successfully created', + content: { + "application/json": { + schema: { $ref: '../schemas/org/create-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.validateUser, + mw.onlySecretariat, + body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['long_name']).isString().trim().notEmpty(), + body(['authority.active_roles']).optional() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole), + body(['soft_quota']).optional().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), + body(['hard_quota']).optional().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), + // TODO: more validation needed? + // parseError, + parsePostParams, + controller.CREATE_ORG +) router.put('/registryOrg/:identifier', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'updateRegistryOrg' + #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['identifier'] = { + in: 'path', + description: 'The identifier 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: '#/components/schemas/UpdateOrgPayload' } + } + } + } + #swagger.responses[200] = { + description: 'The registry organization was successfully updated', + content: { + "application/json": { + schema: { $ref: '../schemas/org/update-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.validateUser, param(['identifier']).isString().trim(), // TODO: do more validation here @@ -55,12 +312,73 @@ router.put('/registryOrg/:identifier', ) router.delete('/registryOrg/:identifier', - mw.validateUser, - // TODO: permissions - param(['identifier']).isString().trim(), - // parseError, - parseDeleteParams, - controller.DELETE_ORG -); + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'deleteRegistryOrg' + #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: { $ref: '../schemas/org/delete-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' } + } + } + } + */ + mw.validateUser, + // TODO: permissions + param(['identifier']).isString().trim(), + // parseError, + parseDeleteParams, + controller.DELETE_ORG +) -module.exports = router; \ No newline at end of file +module.exports = router diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 1d819a389..549c6deaa 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -18,7 +18,6 @@ async function getAllOrgs (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 repo = req.ctx.repositories.getRegistryOrgRepository() - const agt = setAggregateOrgObj({}) const pg = await repo.aggregatePaginate(agt, options) diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 6e0367bf4..7c578fda9 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -8,33 +8,290 @@ const getConstants = require('../../constants').getConstants const CONSTANTS = getConstants() router.get('/registryUser', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'getAllRegistryUsers' + #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/get-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 -); + controller.ALL_USERS +) router.get('/registryUser/:identifier', +/* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'getSingleRegistryUser' + #swagger.summary = "Retrieves information about a specific registry user" + #swagger.description = " +

Access Control

+

All authenticated users can access this endpoint

+

Expected Behavior

+

All Users: 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/user/get-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, param(['identifier']).isString().trim(), // parseError, parseGetParams, controller.SINGLE_USER -); +) router.post('/registryUser', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'createRegistryUser' + #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: '#/components/schemas/CreateUserPayload' } + } + } + } + #swagger.responses[201] = { + description: 'The registry user was successfully created', + content: { + "application/json": { + schema: { $ref: '../schemas/user/create-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, // TODO: permissions // TODO: validation // parseError, parsePostParams, controller.CREATE_USER -); +) router.put('/registryUser/:identifier', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'updateRegistryUser' + #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: '#/components/schemas/UpdateUserPayload' } + } + } + } + #swagger.responses[200] = { + description: 'The registry user was successfully updated', + content: { + "application/json": { + schema: { $ref: '../schemas/user/update-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, param(['identifier']).isString().trim(), // TODO: do more validation here @@ -44,11 +301,80 @@ router.put('/registryUser/:identifier', ) router.delete('/registryUser/:identifier', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'deleteRegistryUser' + #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: { $ref: '../schemas/user/delete-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, param(['identifier']).isString().trim(), // parseError, parseDeleteParams, controller.DELETE_USER -); +) -module.exports = router; \ No newline at end of file +module.exports = router diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index c31b16202..58c6d16d5 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -1,278 +1,278 @@ -const argon2 = require('argon2'); -const cryptoRandomString = require('crypto-random-string'); -const uuid = require('uuid'); -const logger = require('../../middleware/logger'); -const { getConstants } = require('../../constants'); -const RegistryUser = require('../../model/registry-user'); - -async function getAllUsers(req, res, next) { - try { - const CONSTANTS = getConstants() - - // temporary measure to allow tests to work after fixing #920 - // tests required changing the global limit to force pagination - if (req.TEST_PAGINATOR_LIMIT) { - CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT - } - - const options = CONSTANTS.PAGINATOR_OPTIONS - 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 repo = req.ctx.repositories.getRegistryUserRepository() - - const agt = setAggregateUserObj({}) - const pg = await repo.aggregatePaginate(agt, options) - const payload = { users: pg.itemsList } - - if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { - payload.totalCount = pg.itemCount - payload.itemsPerPage = pg.itemsPerPage - payload.pageCount = pg.pageCount - payload.currentPage = pg.currentPage - payload.prevPage = pg.prevPage - payload.nextPage = pg.nextPage - } - - logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) - return res.status(200).json(payload) - } catch (err) { - next(err) +const argon2 = require('argon2') +const cryptoRandomString = require('crypto-random-string') +const uuid = require('uuid') +const logger = require('../../middleware/logger') +const { getConstants } = require('../../constants') +const RegistryUser = require('../../model/registry-user') + +async function getAllUsers (req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT } + + const options = CONSTANTS.PAGINATOR_OPTIONS + 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 repo = req.ctx.repositories.getRegistryUserRepository() + + const agt = setAggregateUserObj({}) + const pg = await repo.aggregatePaginate(agt, options) + const payload = { users: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } } -async function getUser(req, res, next) { - try { - const repo = req.ctx.repositories.getRegistryUserRepository(); - const identifier = req.ctx.params.identifier; - const agt = setAggregateUserObj({ UUID: identifier }); - let result = await repo.aggregate(agt) +async function getUser (req, res, next) { + try { + const repo = req.ctx.repositories.getRegistryUserRepository() + const identifier = req.ctx.params.identifier + const agt = setAggregateUserObj({ UUID: identifier }) + let result = await repo.aggregate(agt) result = result.length > 0 ? result[0] : null - logger.info({ uuid: req.ctx.uuid, message: identifier + ' user was sent to the user.', user: result }) + logger.info({ uuid: req.ctx.uuid, message: identifier + ' user was sent to the user.', user: result }) return res.status(200).json(result) - } catch (err) { - next(err) - } + } catch (err) { + next(err) + } } -async function createUser(req, res, next) { - try { - // const requesterUsername = req.ctx.user - // const requesterShortName = req.ctx.org - const orgRepo = req.ctx.repositories.getOrgRepository() - const userRepo = req.ctx.repositories.getUserRepository() - const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() - const body = req.ctx.body; - - // TODO: check if affiliated orgs and program orgs exist, and if their membership limit is reached - - const newUser = new RegistryUser(); - Object.keys(body).map(k => k.toLowerCase()).forEach(k => { - if (k === 'user_id' || k === 'username') { - newUser.user_id = body[k]; - } else if (k === 'name') { - newUser.name = { - first: '', - last: '', - middle: '', - suffix: '', - ...body.name - }; - } else if (k === 'org_affiliations') { - // TODO: dedupe - } else if (k === 'cve_program_org_membership') { - // TODO: dedupe - } else if (k === 'uuid') { - return res.status(400).json(error.uuidProvided('user')); - } - }); - - // TODO: check that requesting user is admin of org for new user - - newUser.UUID = uuid.v4(); - const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }); - newUser.secret = await argon2.hash(randomKey); - newUser.last_active = null; - newUser.deactivation_date = null; - - await registryUserRepo.updateByUUID(newUser.UUID, newUser, { upsert: true }); - const agt = setAggregateUserObj({ UUID: newUser.UUID }); - let result = await registryUserRepo.aggregate(agt); - result = result.length > 0 ? result[0] : null; - - const payload = { - action: 'create_registry_user', - change: result.user_id + ' was successfully created.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - user: result - } - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) - logger.info(JSON.stringify(payload)) - - result.secret = randomKey - const responseMessage = { - message: result.user_id + ' was successfully created.', - created: result - } - - return res.status(200).json(responseMessage) - } catch (err) { - next(err) - } +async function createUser (req, res, next) { + try { + // const requesterUsername = req.ctx.user + // const requesterShortName = req.ctx.org + const orgRepo = req.ctx.repositories.getOrgRepository() + const userRepo = req.ctx.repositories.getUserRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + const body = req.ctx.body + + // TODO: check if affiliated orgs and program orgs exist, and if their membership limit is reached + + const newUser = new RegistryUser() + Object.keys(body).map(k => k.toLowerCase()).forEach(k => { + if (k === 'user_id' || k === 'username') { + newUser.user_id = body[k] + } else if (k === 'name') { + newUser.name = { + first: '', + last: '', + middle: '', + suffix: '', + ...body.name + } + } else if (k === 'org_affiliations') { + // TODO: dedupe + } else if (k === 'cve_program_org_membership') { + // TODO: dedupe + } else if (k === 'uuid') { + return res.status(400).json(error.uuidProvided('user')) + } + }) + + // TODO: check that requesting user is admin of org for new user + + newUser.UUID = uuid.v4() + const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) + newUser.secret = await argon2.hash(randomKey) + newUser.last_active = null + newUser.deactivation_date = null + + await registryUserRepo.updateByUUID(newUser.UUID, newUser, { upsert: true }) + const agt = setAggregateUserObj({ UUID: newUser.UUID }) + let result = await registryUserRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'create_registry_user', + change: result.user_id + ' was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + result.secret = randomKey + const responseMessage = { + message: result.user_id + ' was successfully created.', + created: result + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } } -async function updateUser(req, res, next) { - try { - const requesterShortName = req.ctx.org - const requesterUsername = req.ctx.user - // const username = req.ctx.params.username - // const shortName = req.ctx.params.shortname - const userUUID = req.ctx.params.identifier; - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() - const registryUserRepo = req.ctx.repositories.getRegistryUserRepository(); - // const orgUUID = await orgRepo.getOrgUUID(shortName) - const isSecretariat = await orgRepo.isSecretariat(requesterShortName) - const isAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName) // Check if requester is Admin of the designated user's org - - const user = await registryUserRepo.findOneByUUID(userUUID); - const newUser = new RegistryUser(); - - // Sets the name values to what currently exists in the database, this ensures data is retained during partial name updates - newUser.name.first = user.name.first - newUser.name.last = user.name.last - newUser.name.middle = user.name.middle - newUser.name.suffix = user.name.suffix - - const queryParameterPermissions = { - new_user_id: true, - 'name.first': false, - 'name.last': false, - 'name.middle': false, - 'name.suffix': false, - 'org_affiliations.add': false, - 'org_affiliations.remove': false, - 'cve_program_org_membership.add': false, - 'cve_program_org_membership.remove': false, - } - - // TODO: check permissions - // Check to ensure that the user has the right permissions to edit the fields tha they are requesting to edit, and fail fast if they do not. - // if (Object.keys(req.ctx.query).length > 0 && Object.keys(req.ctx.query).some((key) => { return queryParameterPermissions[key] }) && !(isAdmin || isSecretariat)) { - // logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + requesterUsername + ' user is not Org Admin or Secretariat to modify these fields.' }) - // return res.status(403).json(error.notOrgAdminOrSecretariatUpdate()) - // } - - for (const k in req.ctx.query) { - const key = k.toLowerCase() - - if (key === 'new_user_id') { +async function updateUser (req, res, next) { + try { + const requesterShortName = req.ctx.org + const requesterUsername = req.ctx.user + // const username = req.ctx.params.username + // const shortName = req.ctx.params.shortname + const userUUID = req.ctx.params.identifier + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + // const orgUUID = await orgRepo.getOrgUUID(shortName) + const isSecretariat = await orgRepo.isSecretariat(requesterShortName) + const isAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName) // Check if requester is Admin of the designated user's org + + const user = await registryUserRepo.findOneByUUID(userUUID) + const newUser = new RegistryUser() + + // Sets the name values to what currently exists in the database, this ensures data is retained during partial name updates + newUser.name.first = user.name.first + newUser.name.last = user.name.last + newUser.name.middle = user.name.middle + newUser.name.suffix = user.name.suffix + + const queryParameterPermissions = { + new_user_id: true, + 'name.first': false, + 'name.last': false, + 'name.middle': false, + 'name.suffix': false, + 'org_affiliations.add': false, + 'org_affiliations.remove': false, + 'cve_program_org_membership.add': false, + 'cve_program_org_membership.remove': false + } + + // TODO: check permissions + // Check to ensure that the user has the right permissions to edit the fields tha they are requesting to edit, and fail fast if they do not. + // if (Object.keys(req.ctx.query).length > 0 && Object.keys(req.ctx.query).some((key) => { return queryParameterPermissions[key] }) && !(isAdmin || isSecretariat)) { + // logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + requesterUsername + ' user is not Org Admin or Secretariat to modify these fields.' }) + // return res.status(403).json(error.notOrgAdminOrSecretariatUpdate()) + // } + + for (const k in req.ctx.query) { + const key = k.toLowerCase() + + if (key === 'new_user_id') { newUser.user_id = req.ctx.query.new_user_id - } else if (key === 'name.first') { + } else if (key === 'name.first') { newUser.name.first = req.ctx.query['name.first'] - } else if (key === 'name.last') { + } else if (key === 'name.last') { newUser.name.last = req.ctx.query['name.last'] - } else if (key === 'name.middle') { + } else if (key === 'name.middle') { newUser.name.middle = req.ctx.query['name.middle'] - } else if (key === 'name.suffix') { + } else if (key === 'name.suffix') { newUser.name.suffix = req.ctx.query['name.suffix'] - } - - // TODO: process org affiliations and program org membership updates - } - - await registryUserRepo.updateByUUID(userUUID, newUser); - const agt = setAggregateUserObj({ UUID: userUUID }); - let result = await registryUserRepo.aggregate(agt); - result = result.length > 0 ? result[0] : null; - - const payload = { - action: 'update_registry_user', - change: result.user_id + ' was successfully updated.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - user: result - } - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) - logger.info(JSON.stringify(payload)) - - let msgStr = '' + } + + // TODO: process org affiliations and program org membership updates + } + + await registryUserRepo.updateByUUID(userUUID, newUser) + const agt = setAggregateUserObj({ UUID: userUUID }) + let result = await registryUserRepo.aggregate(agt) + result = result.length > 0 ? result[0] : null + + const payload = { + action: 'update_registry_user', + change: result.user_id + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + user: result + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + let msgStr = '' if (Object.keys(req.ctx.query).length > 0) { msgStr = result.user_id + ' was successfully updated.' } else { msgStr = 'No updates were specified for ' + result.user_id + '.' } const responseMessage = { - message: msgStr, - updated: result + message: msgStr, + updated: result } - - return res.status(200).json(responseMessage) - } catch (err) { - next(err) - } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } } -async function deleteUser(req, res, next) { - try { - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() - const registryUserRepo = req.ctx.repositories.getRegistryUserRepository(); - const userUUID = req.ctx.params.identifier; - - const user = await registryUserRepo.findOneByUUID(userUUID); - - // TODO: check permissions - - await registryUserRepo.deleteByUUID(userUUID); - - const payload = { - action: 'delete_registry_user', - change: user.user_id + ' was successfully deleted.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org) - } - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) - logger.info(JSON.stringify(payload)) - - const responseMessage = { - message: user.user_id + ' was successfully deleted.' - } - - return res.status(200).json(responseMessage) - } catch (err) { - next(err) - } +async function deleteUser (req, res, next) { + try { + const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = req.ctx.repositories.getOrgRepository() + const registryUserRepo = req.ctx.repositories.getRegistryUserRepository() + const userUUID = req.ctx.params.identifier + + const user = await registryUserRepo.findOneByUUID(userUUID) + + // TODO: check permissions + + await registryUserRepo.deleteByUUID(userUUID) + + const payload = { + action: 'delete_registry_user', + change: user.user_id + ' was successfully deleted.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org) + } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + logger.info(JSON.stringify(payload)) + + const responseMessage = { + message: user.user_id + ' was successfully deleted.' + } + + return res.status(200).json(responseMessage) + } catch (err) { + next(err) + } } -function setAggregateUserObj(query) { - return [ - { - $match: query - }, - { - $project: { - _id: false, - UUID: true, - user_id: true, - name: true, - org_affiliations: true, - cve_program_org_membership: true, - created: true, - created_by: true, - last_updated: true, - deactivation_date: true, - last_active: true - } - } - ] +function setAggregateUserObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] } module.exports = { - ALL_USERS: getAllUsers, - SINGLE_USER: getUser, - CREATE_USER: createUser, - UPDATE_USER: updateUser, - DELETE_USER: deleteUser -}; \ No newline at end of file + ALL_USERS: getAllUsers, + SINGLE_USER: getUser, + CREATE_USER: createUser, + UPDATE_USER: updateUser, + DELETE_USER: deleteUser +} diff --git a/src/controller/schemas.controller/index.js b/src/controller/schemas.controller/index.js index fe9cdd2b9..8e0491ece 100644 --- a/src/controller/schemas.controller/index.js +++ b/src/controller/schemas.controller/index.js @@ -49,4 +49,10 @@ router.get('/user/list-users-response.json', controller.getListUsersSchema) router.get('/user/reset-secret-response.json', controller.getResetSecretResponseSchema) router.get('/user/update-user-response.json', controller.getUpdateUserResponseSchema) +// Schemas relating to Registry-Org +router.get('/registry-org/get-registry-org-response.json', controller.getRegistryOrgResponseSchema) + +// Schemas relating to Registry-User +router.get('/registry-user/get-registry-user-response.json', controller.getRegistryUserResponseSchema) + module.exports = router diff --git a/src/controller/schemas.controller/schemas.controller.js b/src/controller/schemas.controller/schemas.controller.js index caf97cd28..f8dc2dc14 100644 --- a/src/controller/schemas.controller/schemas.controller.js +++ b/src/controller/schemas.controller/schemas.controller.js @@ -228,6 +228,18 @@ async function getCveCountResponseSchema (req, res) { res.status(200) } +async function getRegistryOrgResponseSchema (req, res) { + const registryOrgResponseSchema = require('../../../schemas/registry-org/get-registry-org-response.json') + res.json(registryOrgResponseSchema) + res.status(200) +} + +async function getRegistryUserResponseSchema (req, res) { + const registryUserResponseSchema = require('../../../schemas/registry-user/get-registry-user-response.json') + res.json(registryUserResponseSchema) + res.status(200) +} + module.exports = { getBadRequestSchema: getBadRequestSchema, getCreateCveRecordResponseSchema: getCreateCveRecordResponseSchema, @@ -265,5 +277,7 @@ module.exports = { getAdpFullSchema: getAdpFullSchema, getCnaSecretariatFullSchema: getCnaSecretariatFullSchema, getCnaMinSchema: getCnaMinSchema, - getCveCountResponseSchema: getCveCountResponseSchema + getCveCountResponseSchema: getCveCountResponseSchema, + getRegistryOrgResponseSchema: getRegistryOrgResponseSchema, + getRegistryUserResponseSchema: getRegistryUserResponseSchema } diff --git a/src/swagger.js b/src/swagger.js index 0aaa44171..55ad59a3e 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -5,7 +5,9 @@ const endpointsFiles = [ 'src/controller/cve.controller/index.js', 'src/controller/org.controller/index.js', 'src/controller/user.controller/index.js', - 'src/controller/system.controller/index.js' + 'src/controller/system.controller/index.js', + 'src/controller/registry-org.controller/index.js', + 'src/controller/registry-user.controller/index.js' ] const publishedCVERecord = require('../schemas/cve/published-cve-example.json') const rejectedCVERecord = require('../schemas/cve/rejected-cve-example.json') From 9b2a113a463eac23124f6d0b10701e0067bfaddb Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 10:17:48 -0400 Subject: [PATCH 13/35] I don't know how to spell --- api-docs/openapi.json | 2 +- .../get-registry-users-response.json | 28 +++++++++++++++---- .../registry-user.controller/index.js | 2 +- src/controller/schemas.controller/index.js | 2 +- .../schemas.controller/schemas.controller.js | 2 +- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 415a4ce0c..bb82d9fc6 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3425,7 +3425,7 @@ ], "responses": { "200": { - "description": "A list of all registry users, along with pagination fields if results span multiple pages of data", + "description": "A list of all registry organizations, along with pagination fields if results span multiple pages of data", "content": { "application/json": { "schema": { diff --git a/schemas/registry-user/get-registry-users-response.json b/schemas/registry-user/get-registry-users-response.json index d753e14ee..d5df13284 100644 --- a/schemas/registry-user/get-registry-users-response.json +++ b/schemas/registry-user/get-registry-users-response.json @@ -33,7 +33,10 @@ "description": "User's name suffix" } }, - "required": ["first", "last"] + "required": [ + "first", + "last" + ] }, "org_affiliations": { "type": "array", @@ -56,23 +59,34 @@ "type": "array", "items": { "type": "string", - "enum": ["ADMIN", "PUBLISHER"] + "enum": [ + "ADMIN", + "PUBLISHER" + ] } } }, - "required": ["active_roles"] + "required": [ + "active_roles" + ] }, "secret": { "type": "string", "description": "Hashed secret for user authentication" }, "last_active": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "format": "date-time", "description": "Timestamp of the user's last activity" }, "deactivation_date": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "format": "date-time", "description": "Timestamp of when the user was deactivated, if applicable" }, @@ -89,7 +103,9 @@ "description": "User's phone number" } }, - "required": ["email"] + "required": [ + "email" + ] }, "in_use": { "type": "boolean", diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 7c578fda9..756825b46 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -24,7 +24,7 @@ router.get('/registryUser', '#/components/parameters/apiSecretHeader' ] #swagger.responses[200] = { - description: 'A list of all registry users, along with pagination fields if results span multiple pages of data', + 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-user/get-registry-users-response.json' } diff --git a/src/controller/schemas.controller/index.js b/src/controller/schemas.controller/index.js index 8e0491ece..0d86380aa 100644 --- a/src/controller/schemas.controller/index.js +++ b/src/controller/schemas.controller/index.js @@ -53,6 +53,6 @@ router.get('/user/update-user-response.json', controller.getUpdateUserResponseSc router.get('/registry-org/get-registry-org-response.json', controller.getRegistryOrgResponseSchema) // Schemas relating to Registry-User -router.get('/registry-user/get-registry-user-response.json', controller.getRegistryUserResponseSchema) +router.get('/registry-user/get-registry-users-response.json', controller.getRegistryUserResponseSchema) module.exports = router diff --git a/src/controller/schemas.controller/schemas.controller.js b/src/controller/schemas.controller/schemas.controller.js index f8dc2dc14..2a87eb399 100644 --- a/src/controller/schemas.controller/schemas.controller.js +++ b/src/controller/schemas.controller/schemas.controller.js @@ -235,7 +235,7 @@ async function getRegistryOrgResponseSchema (req, res) { } async function getRegistryUserResponseSchema (req, res) { - const registryUserResponseSchema = require('../../../schemas/registry-user/get-registry-user-response.json') + const registryUserResponseSchema = require('../../../schemas/registry-user/get-registry-users-response.json') res.json(registryUserResponseSchema) res.status(200) } From a550da222e9d0601b9f279103a9a4e87192804cc Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 5 May 2025 16:40:43 -0400 Subject: [PATCH 14/35] Populate reference fields --- .../registry-org.controller.js | 2 + .../registry-user.controller.js | 5 ++ src/model/registry-org.js | 48 +++++++++++++++++-- src/model/registry-user.js | 36 +++++++++++++- 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 549c6deaa..fa7e49668 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -22,6 +22,8 @@ async function getAllOrgs (req, res, next) { const pg = await repo.aggregatePaginate(agt, options) await RegistryOrg.populateOverseesAndReportsTo(pg.itemsList) + await RegistryUser.populateUsers(pg.itemsList) + await RegistryUser.populateAdditionalContactUsers(pg.itemsList); await RegistryUser.populateAdmins(pg.itemsList) // Update UUIDS to objects diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 58c6d16d5..d94072def 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -4,6 +4,7 @@ const uuid = require('uuid') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') const RegistryUser = require('../../model/registry-user') +const RegistryOrg = require('../../model/registry-org') async function getAllUsers (req, res, next) { try { @@ -22,6 +23,10 @@ async function getAllUsers (req, res, next) { const agt = setAggregateUserObj({}) const pg = await repo.aggregatePaginate(agt, options) + + await RegistryOrg.populateOrgAffiliations(pg.itemsList); + await RegistryOrg.populateCVEProgramOrgMembership(pg.itemsList); + const payload = { users: pg.itemsList } if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { diff --git a/src/model/registry-org.js b/src/model/registry-org.js index 53dad7d7a..62a3f7fa3 100644 --- a/src/model/registry-org.js +++ b/src/model/registry-org.js @@ -39,6 +39,8 @@ const schema = { last_updated: Date } +const orgPrivate = '-_id -soft_quota -hard_quota -contact_info.admins -in_use -created -last_updated -__v'; +const orgSecretariat = ''; const RegistryOrgSchema = new mongoose.Schema(schema, { collection: 'RegistryOrg', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) RegistryOrgSchema.query.byShortName = function (shortName) { @@ -49,19 +51,19 @@ RegistryOrgSchema.query.byUUID = function (uuid) { return this.where({ UUID: uuid }) } -RegistryOrgSchema.statics.populateOverseesAndReportsTo = async function (items) { // Assuming the model name is 'RegistryUser' +RegistryOrgSchema.statics.populateOverseesAndReportsTo = async function (items) { // Assuming the model name is 'RegistryOrg' for (const item of items) { if (item.oversees.length > 0) { const populatedOversees = await Promise.all( item.oversees.map(async (uuid) => { - const org = await RegistryOrg.findOne({ UUID: uuid }) - return org ? org.toObject() : uuid // Return the user object if found, otherwise return the UUID + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate); + return org ? org.toObject() : uuid // Return the org object if found, otherwise return the UUID }) ) item.oversees = populatedOversees } if (item.reports_to) { - const org = await RegistryOrg.findOne({ UUID: item.reports_to }) + const org = await RegistryOrg.findOne({ UUID: item.reports_to }).select(orgPrivate); item.reports_to = org ? org.toObject() : item.reports_to // Return the org object if found, otherwise return the UUID } } @@ -69,6 +71,44 @@ RegistryOrgSchema.statics.populateOverseesAndReportsTo = async function (items) return this } +RegistryOrgSchema.statics.populateOrgAffiliations = async function (items) { // Assuming the model name is 'RegistryOrg' + for (const item of items) { + if (item.org_affiliations.length > 0) { + const populatedOrgs = await Promise.all( + item.org_affiliations.map(async ({ org_id: uuid, ...orgMeta }) => { + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate); + return { + org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID + ...orgMeta + }; + }) + ) + item.org_affiliations = populatedOrgs; + } + } + + return this +} + +RegistryOrgSchema.statics.populateCVEProgramOrgMembership = async function (items) { // Assuming the model name is 'RegistryOrg' + for (const item of items) { + if (item.cve_program_org_membership.length > 0) { + const populatedOrgs = await Promise.all( + item.cve_program_org_membership.map(async ({ program_org: uuid, ...orgMeta }) => { + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate); + return { + org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID + ...orgMeta + }; + }) + ) + item.cve_program_org_membership = populatedOrgs; + } + } + + return this +} + RegistryOrgSchema.index({ UUID: 1 }) RegistryOrgSchema.index({ 'authority.active_roles': 1 }) diff --git a/src/model/registry-user.js b/src/model/registry-user.js index bd8fe484c..e9eb097ba 100644 --- a/src/model/registry-user.js +++ b/src/model/registry-user.js @@ -15,7 +15,7 @@ const schema = { suffix: String }, org_affiliations: [{ - org_id: String, + org: String, email: String, phone: String }], @@ -54,7 +54,7 @@ RegistryUserSchema.statics.populateAdmins = async function (items) { // Assuming if (item.contact_info && item.contact_info.admins && item.contact_info.admins.length > 0) { const populatedAdmins = await Promise.all( item.contact_info.admins.map(async (uuid) => { - const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields) + const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID }) ) @@ -65,6 +65,38 @@ RegistryUserSchema.statics.populateAdmins = async function (items) { // Assuming return this } +RegistryUserSchema.statics.populateUsers = async function (items) { // Assuming the model name is 'RegistryUser' + for (const item of items) { + if (item.users && item.users.length > 0) { + const populatedUsers = await Promise.all( + item.users.map(async (uuid) => { + const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields + return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID + }) + ) + item.users = populatedUsers + } + } + + return this +} + +RegistryUserSchema.statics.populateAdditionalContactUsers = async function (items) { // Assuming the model name is 'RegistryUser' + for (const item of items) { + if (item.contact_info && item.contact_info.additional_contact_users && item.contact_info.additional_contact_users.length > 0) { + const populatedUsers = await Promise.all( + item.contact_info.additional_contact_users.map(async (uuid) => { + const user = await RegistryUser.findOne({ UUID: uuid }).select(userPrivate) // Only return necessary fields + return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID + }) + ) + item.users = populatedUsers + } + } + + return this +} + RegistryUserSchema.index({ UUID: 1 }) RegistryUserSchema.index({ user_id: 1 }) From f283619d8eb3ad68329a36aea7c53ec74e1d417e Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 5 May 2025 16:43:11 -0400 Subject: [PATCH 15/35] Fix org_affiliations field --- src/model/registry-user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/registry-user.js b/src/model/registry-user.js index e9eb097ba..c75855926 100644 --- a/src/model/registry-user.js +++ b/src/model/registry-user.js @@ -15,7 +15,7 @@ const schema = { suffix: String }, org_affiliations: [{ - org: String, + org_id: String, email: String, phone: String }], From 03677256057883c29f7b1e3810eed05c9c38896d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 14:26:37 -0400 Subject: [PATCH 16/35] An org can not be a root, and not report to anyone, however, we may want to set MITRE as the default for now? We will bring this up to AWG. --- .../registry-org.controller/registry-org.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index fa7e49668..7ac7d1a9b 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -118,7 +118,8 @@ async function createOrg (req, res, next) { }) if (newOrg.reports_to === undefined) { - // TODO: throw error if no reports_to and not root_or_tlr? + // TODO: This may need to be set to mitre, will ask the awg + newOrg.reports_to = null } if (newOrg.root_or_tlr === undefined) { newOrg.root_or_tlr = false From 85e99d41a790fbb72b81ec31f4bb309babac453b Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 15:13:44 -0400 Subject: [PATCH 17/35] Added secretariat data, so we can start building policy --- datadump/pre-population/registry-orgs.json | 36 +++++++++++++++++++ datadump/pre-population/registry-users.json | 31 ++++++++++++++-- .../registry-org.controller.js | 2 -- src/scripts/populate.js | 4 +-- src/utils/data.js | 9 +++++ 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/datadump/pre-population/registry-orgs.json b/datadump/pre-population/registry-orgs.json index ced7e8ed6..de5b23ec5 100644 --- a/datadump/pre-population/registry-orgs.json +++ b/datadump/pre-population/registry-orgs.json @@ -1,4 +1,40 @@ [ + { + "UUID": "org-secretariat", + "long_name": "Secretariat Org", + "short_name": "SecretariatOrg", + "aliases": [], + "cve_program_org_function": "Secretariat", + "authority": { + "active_roles": [ + "CNA", + "Top Level Root", + "CNA-LR", + "Bulk Download" + ] + }, + "reports_to": null, + "oversees": ["org-uuid-1", "org-uuid-3"], + "root_or_tlr": true, + "users": ["user-uuid-secretariat"], + "charter_or_scope": "All Things CVE", + "disclosure_policy": "When the time is right", + "product_list": "Product A, Product B, Product C", + "soft_quota": 100, + "hard_quota": 150, + "contact_info": { + "additional_contact_users": [], + "poc": "John Doe", + "poc_email": "john.doe@secretariat.com", + "poc_phone": "+1-555-001-1001", + "admins": [], + "org_email": "contact@secretariat.com", + "website": "https://www.cve.org" + }, + "in_use": true, + "created": "2023-06-01T00:00:00.000Z", + "last_updated": "2023-06-01T00:00:00.000Z" + }, { "UUID": "org-uuid-1", "long_name": "Test Organization One", diff --git a/datadump/pre-population/registry-users.json b/datadump/pre-population/registry-users.json index 67b1da391..636022c8c 100644 --- a/datadump/pre-population/registry-users.json +++ b/datadump/pre-population/registry-users.json @@ -1,8 +1,35 @@ [ + { + "UUID": "user-uuid-secretariat", + "user_id": "secretariat", + "name": { + "first": "David", + "last": "Rocca", + "middle": "T", + "suffix": "" + }, + "org_affiliations": [ + { + "org_id": "org-secretariat", + "email": "drocca@mitre.org", + "phone": "+1-555-001-1001" + } + ], + "cve_program_org_membership": [ + { + "program_org": "org-secretariat", + "role": "Admin", + "status": "active" + } + ], + "created": "2023-06-01T00:00:00.000Z", + "created_by": "drocca", + "last_updated": "2023-06-01T00:00:00.000Z", + "last_active": "2023-06-01T00:00:00.000Z" + }, { "UUID": "user-uuid-1", "user_id": "user1@testorg1.com", - "secret": "secretKey1", "name": { "first": "John", "last": "Doe", @@ -31,7 +58,6 @@ { "UUID": "user-uuid-2", "user_id": "jane.smith@secsol.com", - "secret": "secretKey2", "name": { "first": "Jane", "last": "Smith", @@ -60,7 +86,6 @@ { "UUID": "user-uuid-3", "user_id": "michael.johnson@gns.com", - "secret": "secretKey3", "name": { "first": "Michael", "last": "Johnson", diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 7ac7d1a9b..186fb6ad6 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -264,8 +264,6 @@ async function deleteOrg (req, res, next) { const org = await registryOrgRepo.findOneByUUID(orgUUID) - // TODO: check permissions - await registryOrgRepo.deleteByUUID(orgUUID) const payload = { diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 8c8d07822..1b1422d75 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -112,10 +112,10 @@ db.once('open', async () => { User, dataUtils.newUserTransform, hash ) - // const registryUserHash = await dataUtils.preprocessUserSecrets() + const registryUserHash = await dataUtils.preprocessUserSecrets() await dataUtils.populateCollection( './datadump/pre-population/registry-users.json', - RegistryUser + RegistryUser, dataUtils.newRegistryUserTransform, registryUserHash ) const populatePromises = [] diff --git a/src/utils/data.js b/src/utils/data.js index 4352c822b..0fdc10bab 100644 --- a/src/utils/data.js +++ b/src/utils/data.js @@ -86,6 +86,14 @@ async function newUserTransform (user, hash) { return user } +async function newRegistryUserTransform (user, hash) { + // shared secret key in development environments + if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { + user.secret = hash + } + return user +} + async function newCveIdTransform (cveId) { const tmpRequestingCnaUUID = await utils.getOrgUUID(cveId.requested_by.cna) const tmpOwningCnaUUID = await utils.getOrgUUID(cveId.owning_cna) @@ -162,6 +170,7 @@ module.exports = { newCveIdTransform, newOrgTransform, newUserTransform, + newRegistryUserTransform, newCveTransform, populateCollection, preprocessUserSecrets From 9c172b737520acf1199c91f1f128eba85aec5da4 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 12:30:53 -0400 Subject: [PATCH 18/35] Secretariat policy now is applied --- datadump/pre-population/registry-orgs.json | 3 +- .../registry-org.controller/index.js | 4 +- src/middleware/middleware.js | 145 ++++++++++++------ src/model/registry-user.js | 4 + src/repositories/registryOrgRepository.js | 14 +- src/repositories/registryUserRepository.js | 14 +- src/utils/utils.js | 28 +++- 7 files changed, 151 insertions(+), 61 deletions(-) diff --git a/datadump/pre-population/registry-orgs.json b/datadump/pre-population/registry-orgs.json index de5b23ec5..b3a8d03b9 100644 --- a/datadump/pre-population/registry-orgs.json +++ b/datadump/pre-population/registry-orgs.json @@ -10,7 +10,8 @@ "CNA", "Top Level Root", "CNA-LR", - "Bulk Download" + "Bulk Download", + "SECRETARIAT" ] }, "reports_to": null, diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index 48c0e51d9..2c6ad8441 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -66,8 +66,8 @@ router.get('/registryOrg', } } */ - mw.validateUser, - mw.onlySecretariat, + mw.validateUser(true), + mw.onlySecretariat(true), query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 5bfb60726..6084524de 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -83,55 +83,79 @@ async function optionallyValidateUser (req, res, next) { } } -async function validateUser (req, res, next) { - const org = req.ctx.org - const user = req.ctx.user - const key = req.ctx.key - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() - const CONSTANTS = getConstants() - - try { - if (!org) { - return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.ORG)) +function validateUser (useRegistry = false) { + return async (req, res, next) => { + const org = req.ctx.org + const user = req.ctx.user + const key = req.ctx.key + let userRepo = null + let orgRepo = null + if (useRegistry) { + userRepo = req.ctx.repositories.getRegistryUserRepository() + orgRepo = req.ctx.repositories.getRegistryOrgRepository() + } else { + userRepo = req.ctx.repositories.getUserRepository() + orgRepo = req.ctx.repositories.getOrgRepository() } - if (!user) { - return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.USER)) - } + const CONSTANTS = getConstants() - if (!key) { - return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.KEY)) - } + try { + if (!org) { + return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.ORG)) + } - logger.info({ uuid: req.ctx.uuid, message: 'Authenticating user: ' + user }) // userUUID may be null if user does not exist - const orgUUID = await orgRepo.getOrgUUID(org) - if (!orgUUID) { - logger.info({ uuid: req.ctx.uuid, message: org + ' organization does not exist. User authentication FAILED for ' + user }) - return res.status(401).json(error.unauthorized()) - } + if (!user) { + return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.USER)) + } - const result = await userRepo.findOneByUserNameAndOrgUUID(user, orgUUID) - if (!result) { - logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User not found. User authentication FAILED for ' + user })) - return res.status(401).json(error.unauthorized()) - } + if (!key) { + return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.KEY)) + } - if (!result.active) { - logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User deactivated. Authentication failed for ' + user })) - return res.status(401).json(error.unauthorized()) - } + logger.info({ uuid: req.ctx.uuid, message: 'Authenticating user: ' + user }) // userUUID may be null if user does not exist + const orgUUID = await orgRepo.getOrgUUID(org) + if (!orgUUID) { + logger.info({ uuid: req.ctx.uuid, message: org + ' organization does not exist. User authentication FAILED for ' + user }) + return res.status(401).json(error.unauthorized()) + } - const isPwd = await argon2.verify(result.secret, key) - if (!isPwd) { - logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'Incorrect apikey. User authentication FAILED for ' + user })) - return res.status(401).json(error.unauthorized()) - } + const result = await userRepo.findOneByUserNameAndOrgUUID(user, orgUUID) + if (!result) { + logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User not found. User authentication FAILED for ' + user })) + return res.status(401).json(error.unauthorized()) + } - logger.info({ uuid: req.ctx.uuid, message: 'SUCCESSFUL user authentication for ' + user }) - next() - } catch (err) { - next(err) + let activeInOrg = false + if (useRegistry) { + // Check if user has active status organization's registry org membership list + for (var organization of result.cve_program_org_membership) { + if (organization.program_org === orgUUID) { + if (organization.status === 'active') { + activeInOrg = true + } + break + } + } + } + + if ((!useRegistry && !result.active) || + (useRegistry && !activeInOrg)) { + logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User deactivated. Authentication failed for ' + user })) + return res.status(401).json(error.unauthorized()) + } + + const isPwd = await argon2.verify(result.secret, key) + if (!isPwd) { + logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'Incorrect apikey. User authentication FAILED for ' + user })) + return res.status(401).json(error.unauthorized()) + } + + logger.info({ uuid: req.ctx.uuid, message: 'SUCCESSFUL user authentication for ' + user }) + next() + } catch (err) { + next(err) + } } } @@ -160,26 +184,52 @@ async function onlySecretariatOrBulkDownload (req, res, next) { } } -// Checks that the requester belongs to an org that has the 'SECRETARIAT' role -async function onlySecretariat (req, res, next) { +async function onlySecretariatUserRegistry (req, res, next) { const org = req.ctx.org - const orgRepo = req.ctx.repositories.getOrgRepository() + const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() const CONSTANTS = getConstants() try { - const isSec = await orgRepo.isSecretariat(org) + const isSec = await registryOrgRepo.isSecretariat(org) if (!isSec) { logger.info({ uuid: req.ctx.uuid, message: org + ' is NOT a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) return res.status(403).json(error.secretariatOnly()) } - - logger.info({ uuid: req.ctx.uuid, message: 'Confirmed ' + org + ' as a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) + logger.info({ uuid: req.ctx.uuid, message: 'Confirmed ' + org + 'as a Secretariat' }) next() } catch (err) { next(err) } } +// Checks that the requester belongs to an org that has the 'SECRETARIAT' role + +function onlySecretariat (useRegistry = false) { + return async (req, res, next) => { + const org = req.ctx.org + let orgRepo = null + if (useRegistry) { + orgRepo = req.ctx.repositories.getRegistryOrgRepository() + } else { + orgRepo = req.ctx.repositories.getOrgRepository() + } + const CONSTANTS = getConstants() + + try { + const isSec = await orgRepo.isSecretariat(org) + if (!isSec) { + logger.info({ uuid: req.ctx.uuid, message: org + ' is NOT a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) + return res.status(403).json(error.secretariatOnly()) + } + + logger.info({ uuid: req.ctx.uuid, message: 'Confirmed ' + org + ' as a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) + next() + } catch (err) { + next(err) + } + } +} + // Checks that the requester belongs to an org that has the 'SECRETARIAT' role or is a user with the 'ADMIN' role async function onlySecretariatOrAdmin (req, res, next) { const org = req.ctx.org @@ -486,6 +536,7 @@ module.exports = { onlySecretariat, onlySecretariatOrBulkDownload, onlySecretariatOrAdmin, + onlySecretariatUserRegistry, onlyCnas, onlyAdps, onlyOrgWithPartnerRole, diff --git a/src/model/registry-user.js b/src/model/registry-user.js index c75855926..5f264e761 100644 --- a/src/model/registry-user.js +++ b/src/model/registry-user.js @@ -49,6 +49,10 @@ RegistryUserSchema.query.byUUID = function (uuid) { return this.where({ UUID: uuid }) } +RegistryUserSchema.query.byUserIdAndOrgUUID = function (userId, orgUUID) { + return this.where({ user_id: userId, 'org_affiliations.org_id': orgUUID }) +} + RegistryUserSchema.statics.populateAdmins = async function (items) { // Assuming the model name is 'RegistryUser' for (const item of items) { if (item.contact_info && item.contact_info.admins && item.contact_info.admins.length > 0) { diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js index 6ec53c27c..3c029d45d 100644 --- a/src/repositories/registryOrgRepository.js +++ b/src/repositories/registryOrgRepository.js @@ -11,17 +11,25 @@ class RegistryOrgRepository extends BaseRepository { return this.collection.findOne().byUUID(UUID) } + async getOrgUUID (shortName) { + return utils.getOrgUUID(shortName, true) // use registryOrgRepository to find org UUID + } + async getAllOrgs () { return this.collection.find() } + async isSecretariat (shortName) { + return utils.isSecretariat(shortName, true) + } + async updateByUUID (uuid, org, options = {}) { - return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(org).setOptions(options); + return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(org).setOptions(options) } async deleteByUUID (uuid) { - return this.collection.deleteOne({ UUID: uuid }); + return this.collection.deleteOne({ UUID: uuid }) } } -module.exports = RegistryOrgRepository; \ No newline at end of file +module.exports = RegistryOrgRepository diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js index 4ae309df6..f4de1620d 100644 --- a/src/repositories/registryUserRepository.js +++ b/src/repositories/registryUserRepository.js @@ -15,13 +15,21 @@ class RegistryUserRepository extends BaseRepository { return this.collection.find() } + async isSecretariat (org) { + return utils.isSecretariat(org, true) + } + async updateByUUID (uuid, user, options = {}) { - return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(user).setOptions(options); + return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(user).setOptions(options) + } + + async findOneByUserNameAndOrgUUID (userName, orgUUID) { + return this.collection.findOne().byUserIdAndOrgUUID(userName, orgUUID) } async deleteByUUID (uuid) { - return this.collection.deleteOne({ UUID: uuid }); + return this.collection.deleteOne({ UUID: uuid }) } } -module.exports = RegistryUserRepository; \ No newline at end of file +module.exports = RegistryUserRepository diff --git a/src/utils/utils.js b/src/utils/utils.js index 1ef6d63d5..730df79a5 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,11 +1,21 @@ const Org = require('../model/org') const User = require('../model/user') + +const RegistryOrg = require('../model/registry-org') +const RegistryUser = require('../model/registry-user') + const getConstants = require('../constants').getConstants const _ = require('lodash') const { DateTime } = require('luxon') -async function getOrgUUID (shortName) { - const org = await Org.findOne().byShortName(shortName) +async function getOrgUUID (shortName, useRegistry = false) { + let org = null + if (useRegistry) { + org = await RegistryOrg.findOne().byShortName(shortName) + } else { + org = await Org.findOne().byShortName(shortName) + } + let result = null if (org) { result = org.UUID @@ -22,11 +32,19 @@ async function getUserUUID (userName, orgUUID) { return result } -async function isSecretariat (shortName) { +async function isSecretariat (shortName, useRegistry = false) { let result = false + let orgUUID = null + let secretariats = [] + const CONSTANTS = getConstants() - const orgUUID = await getOrgUUID(shortName) // may be null if org does not exists - const secretariats = await Org.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) + if (useRegistry) { + orgUUID = await getOrgUUID(shortName, useRegistry) // may be null if org does not exists + secretariats = await RegistryOrg.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) + } else { + orgUUID = await getOrgUUID(shortName) // may be null if org does not exists + secretariats = await Org.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) + } if (orgUUID) { secretariats.forEach((obj) => { From 6e05b919abd447e5808835591c0c7a1014469354 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 14:02:16 -0400 Subject: [PATCH 19/35] single org now respects policy --- .../registry-org.controller/error.js | 98 +++++++++++++++++++ .../registry-org.controller/index.js | 2 +- .../registry-org.controller.js | 28 +++++- src/repositories/registryOrgRepository.js | 4 + 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/controller/registry-org.controller/error.js diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js new file mode 100644 index 000000000..dd5ffe352 --- /dev/null +++ b/src/controller/registry-org.controller/error.js @@ -0,0 +1,98 @@ +const idrErr = require('../../utils/error') + +class RegistryOrgControllerError extends idrErr.IDRError { + orgDnePathParam (shortname) { // org + const err = {} + err.error = 'ORG_DNE_PARAM' + err.message = `The '${shortname}' organization designated by the shortname path parameter does not exist.` + return err + } + + userDne (username) { // org + const err = {} + err.error = 'USER_DNE' + err.message = `The user '${username}' designated by the username parameter does not exist.` + return err + } + + notSameOrgOrSecretariat () { // org + const err = {} + err.error = 'NOT_SAME_ORG_OR_SECRETARIAT' + err.message = 'This information can only be viewed by the users of the same organization or the Secretariat.' + return err + } + + notAllowedToChangeOrganization () { + const err = {} + err.error = 'NOT_ALLOWED_TO_CHANGE_ORGANIZATION' + err.message = 'Only the Secretariat can change the organization for a user.' + return err + } + + orgExists (shortname) { // org + const err = {} + err.error = 'ORG_EXISTS' + err.message = `The '${shortname}' organization already exists.` + return err + } + + userExists (username) { // org + const err = {} + err.error = 'USER_EXISTS' + err.message = `The user '${username}' already exists.` + return err + } + + uuidProvided (creationType) { + const err = {} + err.error = 'UUID_PROVIDED' + err.message = `Providing UUIDs for ${creationType} creation or update is not allowed.` + return err + } + + duplicateUsername (shortname, username) { // org + const err = {} + err.error = 'DUPLICATE_USERNAME' + err.message = `The user could not be updated because the '${shortname}' organization contains another user with the username '${username}'.` + return err + } + + alreadyInOrg (shortname, username) { // org + const err = {} + err.error = 'USER_ALREADY_IN_ORG' + err.message = `The user could not be updated because the user '${username}' already belongs to the '${shortname}' organization.` + return err + } + + duplicateShortname (shortname) { // org + const err = {} + err.error = 'DUPLICATE_SHORTNAME' + err.message = `The organization cannot be renamed as '${shortname}' because this shortname is used by another organization.` + return err + } + + paramDne (param) { // org + const err = {} + err.error = 'BAD_PARAMETER_NAME' + err.message = `'${param}' is not a valid parameter.` + return err + } + + notAllowedToSelfDemote () { + const err = {} + err.error = 'NOT_ALLOWED_TO_SELF_DEMOTE' + err.message = 'Please have another admin user from your organization change your role.' + return err + } + + userLimitReached () { + const err = {} + err.error = 'NUMBER_OF_USERS_IN_ORG_LIMIT_REACHED' + err.message = 'The requested user can not be created and added to the organization because the organization has hit its limit of 100 users. Contact the Secretariat.' + return err + } +} + +module.exports = { + RegistryOrgControllerError +} diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index 2c6ad8441..bbed8ebd8 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -138,7 +138,7 @@ router.get('/registryOrg/:identifier', } } */ - mw.validateUser, + mw.validateUser(true), param(['identifier']).isString().trim(), // parseError, parseGetParams, diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 186fb6ad6..b4a635ee9 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -3,6 +3,9 @@ const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') const RegistryOrg = require('../../model/registry-org') const RegistryUser = require('../../model/registry-user') +const errors = require('./error') +const error = new errors.RegistryOrgControllerError() +const validateUUID = require('uuid').validate async function getAllOrgs (req, res, next) { try { @@ -23,7 +26,7 @@ async function getAllOrgs (req, res, next) { await RegistryOrg.populateOverseesAndReportsTo(pg.itemsList) await RegistryUser.populateUsers(pg.itemsList) - await RegistryUser.populateAdditionalContactUsers(pg.itemsList); + await RegistryUser.populateAdditionalContactUsers(pg.itemsList) await RegistryUser.populateAdmins(pg.itemsList) // Update UUIDS to objects @@ -49,9 +52,30 @@ async function getOrg (req, res, next) { try { const repo = req.ctx.repositories.getRegistryOrgRepository() const identifier = req.ctx.params.identifier - const agt = setAggregateOrgObj({ UUID: identifier }) + const orgShortName = req.ctx.org + const isSecretariat = await repo.isSecretariat(orgShortName) + const org = await repo.findOneByShortName(orgShortName) + let orgIdentifer = orgShortName + let agt = setAggregateOrgObj({ UUID: identifier }) + + if (validateUUID(identifier)) { + orgIdentifer = org.UUID + agt = setAggregateOrgObj({ UUID: identifier }) + } + + if (orgIdentifer !== identifier && !isSecretariat) { + logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) + return res.status(403).json(error.notSameOrgOrSecretariat()) + } + let result = await repo.aggregate(agt) result = result.length > 0 ? result[0] : null + // TODO: We need real error messages here pls and thanks + + if (!result) { + logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization does not exist.' }) + return res.status(404).json(error.orgDne(identifier, 'identifier', 'path')) + } logger.info({ uuid: req.ctx.uuid, message: identifier + ' org was sent to the user.', org: result }) return res.status(200).json(result) diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js index 3c029d45d..546035710 100644 --- a/src/repositories/registryOrgRepository.js +++ b/src/repositories/registryOrgRepository.js @@ -7,6 +7,10 @@ class RegistryOrgRepository extends BaseRepository { super(RegistryOrg) } + async findOneByShortName (shortName) { + return this.collection.findOne().byShortName(shortName) + } + async findOneByUUID (UUID) { return this.collection.findOne().byUUID(UUID) } From c5ed828ddea8da5c639382e9046411b4ede96d46 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 15:13:10 -0400 Subject: [PATCH 20/35] Post request for create org is now working --- src/controller/registry-org.controller/index.js | 4 ++-- .../registry-org.controller.js | 11 ++++++++--- src/repositories/registryUserRepository.js | 4 ++++ src/utils/utils.js | 10 ++++++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index bbed8ebd8..d95735d36 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -209,8 +209,8 @@ router.post('/registryOrg', } } */ - mw.validateUser, - mw.onlySecretariat, + mw.validateUser(true), + mw.onlySecretariat(true), body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), body(['long_name']).isString().trim().notEmpty(), body(['authority.active_roles']).optional() diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index b4a635ee9..518cbe8ae 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -87,8 +87,7 @@ async function getOrg (req, res, next) { async function createOrg (req, res, next) { try { const CONSTANTS = getConstants() - const orgRepo = req.ctx.repositories.getOrgRepository() - const userRepo = req.ctx.repositories.getUserRepository() + const userRepo = req.ctx.repositories.getRegistryUserRepository() const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() const body = req.ctx.body @@ -141,6 +140,12 @@ async function createOrg (req, res, next) { } }) + const doesExist = await registryOrgRepo.findOneByShortName(newOrg.short_name) + if (doesExist) { + logger.info({ uuid: req.ctx.uuid, message: newOrg.short_name + ' organization was not created because it already exists.' }) + return res.status(400).json(error.orgExists(newOrg.short_name)) + } + if (newOrg.reports_to === undefined) { // TODO: This may need to be set to mitre, will ask the awg newOrg.reports_to = null @@ -171,7 +176,7 @@ async function createOrg (req, res, next) { action: 'create_registry_org', change: result.short_name + ' was successfully created.', req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + org_UUID: await registryOrgRepo.getOrgUUID(req.ctx.org), org: result } payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js index f4de1620d..8af89bd5e 100644 --- a/src/repositories/registryUserRepository.js +++ b/src/repositories/registryUserRepository.js @@ -7,6 +7,10 @@ class RegistryUserRepository extends BaseRepository { super(RegistryUser) } + async getUserUUID (username, orgUUID) { + return utils.getUserUUID(username, orgUUID, true) + } + async findOneByUUID (UUID) { return this.collection.findOne().byUUID(UUID) } diff --git a/src/utils/utils.js b/src/utils/utils.js index 730df79a5..b71653d5f 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -23,8 +23,14 @@ async function getOrgUUID (shortName, useRegistry = false) { return result } -async function getUserUUID (userName, orgUUID) { - const user = await User.findOne().byUserNameAndOrgUUID(userName, orgUUID) +async function getUserUUID (userName, orgUUID, useRegistry = false) { + let user = null + if (useRegistry) { + user = await RegistryUser.findOne().byUserIdAndOrgUUID(userName, orgUUID) + } else { + user = await User.findOne().byUserNameAndOrgUUID(userName, orgUUID) + } + let result = null if (user) { result = user.UUID From ecbb4e039e6c4632510a928820292e96fef87142 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 15:50:36 -0400 Subject: [PATCH 21/35] Update org now works --- api-docs/openapi.json | 74 ++++++++++--------- .../registry-org.controller/index.js | 12 +-- .../registry-org.controller.js | 17 +++-- .../registry-org.middleware.js | 4 +- 4 files changed, 59 insertions(+), 48 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index bb82d9fc6..0149155cf 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3220,13 +3220,13 @@ } } }, - "put": { + "delete": { "tags": [ "Registry Organization" ], - "summary": "Updates an existing registry organization (accessible to Secretariat only)", - "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Updates an existing registry organization

", - "operationId": "updateRegistryOrg", + "summary": "Deletes an existing registry organization (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Deletes an existing registry organization

", + "operationId": "deleteRegistryOrg", "parameters": [ { "name": "identifier", @@ -3235,7 +3235,7 @@ "schema": { "type": "string" }, - "description": "The identifier of the registry organization to update" + "description": "The identifier of the registry organization to delete" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3249,11 +3249,11 @@ ], "responses": { "200": { - "description": "The registry organization was successfully updated", + "description": "The registry organization was successfully deleted", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/update-org-response.json" + "$ref": "../schemas/org/delete-org-response.json" } } } @@ -3297,45 +3297,27 @@ } } } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "$ref": "../schemas/errors/generic.json" - } - } - } - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrgPayload" - } - } } } - }, - "delete": { + } + }, + "/registryOrg/{shortname}": { + "put": { "tags": [ "Registry Organization" ], - "summary": "Deletes an existing registry organization (accessible to Secretariat only)", - "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Deletes an existing registry organization

", - "operationId": "deleteRegistryOrg", + "summary": "Updates an existing registry organization (accessible to Secretariat only)", + "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Updates an existing registry organization

", + "operationId": "updateRegistryOrg", "parameters": [ { - "name": "identifier", + "name": "shortname", "in": "path", "required": true, "schema": { "type": "string" }, - "description": "The identifier of the registry organization to delete" + "description": "The Shortname of the registry organization to update" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3349,11 +3331,11 @@ ], "responses": { "200": { - "description": "The registry organization was successfully deleted", + "description": "The registry organization was successfully updated", "content": { "application/json": { "schema": { - "$ref": "../schemas/org/delete-org-response.json" + "$ref": "../schemas/org/update-org-response.json" } } } @@ -3397,6 +3379,26 @@ } } } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgPayload" + } + } } } } diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index d95735d36..e03da2c24 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -225,7 +225,7 @@ router.post('/registryOrg', controller.CREATE_ORG ) -router.put('/registryOrg/:identifier', +router.put('/registryOrg/:shortname', /* #swagger.tags = ['Registry Organization'] #swagger.operationId = 'updateRegistryOrg' @@ -235,9 +235,9 @@ router.put('/registryOrg/:identifier',

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Updates an existing registry organization

- #swagger.parameters['identifier'] = { + #swagger.parameters['shortname'] = { in: 'path', - description: 'The identifier of the registry organization to update', + description: 'The Shortname of the registry organization to update', required: true, type: 'string' } @@ -303,11 +303,13 @@ router.put('/registryOrg/:identifier', } } */ - mw.validateUser, - param(['identifier']).isString().trim(), + mw.validateUser(true), + mw.onlySecretariat(true), + param(['shortname']).isString().trim(), // TODO: do more validation here // parseError, parsePostParams, + parseGetParams, controller.UPDATE_ORG ) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 518cbe8ae..9d333aa7d 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -194,12 +194,19 @@ async function createOrg (req, res, next) { async function updateOrg (req, res, next) { try { - const orgUUID = req.ctx.params.identifier - const userRepo = req.ctx.repositories.getUserRepository() - const orgRepo = req.ctx.repositories.getOrgRepository() + const shortName = req.ctx.params.shortname + const userRepo = req.ctx.repositories.getRegistryUserRepository() const registryOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + // const shortName = req.ctx.params.shortname + + const org = await registryOrgRepo.findOneByShortName(shortName) + if (!org) { + logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) + return res.status(404).json(error.orgDnePathParam(shortName)) + } + + const orgUUID = await registryOrgRepo.getOrgUUID(shortName) - const org = await registryOrgRepo.findOneByUUID(orgUUID) const newOrg = new RegistryOrg() newOrg.contact_info = { ...org.contact_info } @@ -261,7 +268,7 @@ async function updateOrg (req, res, next) { action: 'update_registry_org', change: result.short_name + ' was successfully updated.', req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + org_UUID: await registryOrgRepo.getOrgUUID(req.ctx.org), user: result } payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 1f921b78a..bd8834c4e 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -3,7 +3,7 @@ const getConstants = require('../../constants').getConstants function parsePostParams (req, res, next) { utils.reqCtxMapping(req, 'body', []) - utils.reqCtxMapping(req, 'params', ['identifier']) + utils.reqCtxMapping(req, 'params', ['identifier, shortname']) utils.reqCtxMapping(req, 'query', [ 'long_name', 'short_name', 'aliases', 'cve_program_org_function', 'authority.active_roles', @@ -18,7 +18,7 @@ function parsePostParams (req, res, next) { } function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'params', ['identifier']) + utils.reqCtxMapping(req, 'params', ['identifier', 'shortname']) utils.reqCtxMapping(req, 'query', ['page']) next() } From 15d44e270d46b4eb779f4a6d6426a14b5d23103e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 13 May 2025 14:11:08 -0400 Subject: [PATCH 22/35] Updated get/registryUsers to now handle new policy --- src/controller/registry-user.controller/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 756825b46..8a3392115 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -64,8 +64,8 @@ router.get('/registryUser', } } */ - mw.validateUser, - mw.onlySecretariat, + mw.validateUser(true), + mw.onlySecretariat(true), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), // parseError, From bff80155bf0cd6de9832ba15778a1997cd22dce5 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 13 May 2025 15:21:50 -0400 Subject: [PATCH 23/35] Added registryOrg shortname user get --- api-docs/openapi.json | 96 ++++++++++++++++++- .../registry-org.controller/index.js | 77 ++++++++++++++- .../registry-org.controller.js | 80 +++++++++++++++- .../registry-org.middleware.js | 14 +++ 4 files changed, 264 insertions(+), 3 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 0149155cf..acb594b52 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2974,7 +2974,6 @@ "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", - "parameters": [], "responses": { "200": { "description": "Returns a 200 response code" @@ -3403,6 +3402,101 @@ } } }, + "/registryOrg/{shortname}/users": { + "get": { + "tags": [ + "Registry User" + ], + "summary": "Retrieves all users for the organization with the specified short name (accessible to all registered users)", + "description": "

Access Control

All registered users can access this endpoint

Expected Behavior

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

Secretariat: Retrieves all user information for any organization

", + "operationId": "registryUserOrgAll", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" + }, + { + "$ref": "#/components/parameters/pageQuery" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "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/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" + } + } + } + } + } + } + }, "/registryUser": { "get": { "tags": [ diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index e03da2c24..79e0b1bc7 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -375,7 +375,7 @@ router.delete('/registryOrg/:identifier', } } */ - mw.validateUser, + mw.validateUser(true), // TODO: permissions param(['identifier']).isString().trim(), // parseError, @@ -383,4 +383,79 @@ router.delete('/registryOrg/:identifier', controller.DELETE_ORG ) +console.log(controller.USER_ALL) + +router.get('/registryOrg/:shortname/users', + /* + #swagger.tags = ['Registry User'] + #swagger.operationId = 'registryUserOrgAll' + #swagger.summary = "Retrieves all users for the organization with the specified short name (accessible to all registered users)" + #swagger.description = " +

Access Control

+

All registered users can access this endpoint

+

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/user/list-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.validateUser(true), + 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) module.exports = router diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 9d333aa7d..a2b8e5526 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -321,6 +321,83 @@ async function deleteOrg (req, res, next) { } } +/** + * Get the details of all users from an org given the specified shortname + * Called by GET /api/org/{shortname}/users + **/ +async function getUsers (req, res, next) { + try { + const CONSTANTS = getConstants() + + // temporary measure to allow tests to work after fixing #920 + // tests required changing the global limit to force pagination + if (req.TEST_PAGINATOR_LIMIT) { + CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT + } + + const options = CONSTANTS.PAGINATOR_OPTIONS + 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 shortName = req.ctx.org + const orgShortName = req.ctx.params.shortname + const orgRepo = req.ctx.repositories.getRegistryOrgRepository() + const userRepo = req.ctx.repositories.getRegistryUserRepository() + const orgUUID = await orgRepo.getOrgUUID(orgShortName) + const isSecretariat = await orgRepo.isSecretariat(shortName) + + if (!orgUUID) { + logger.info({ uuid: req.ctx.uuid, message: orgShortName + ' organization does not exist.' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + if (orgShortName !== shortName && !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 agt = setAggregateUserObj({ 'org_affiliations.org_id': orgUUID }) + const pg = await userRepo.aggregatePaginate(agt, options) + const payload = { users: pg.itemsList } + + if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { + payload.totalCount = pg.itemCount + payload.itemsPerPage = pg.itemsPerPage + payload.pageCount = pg.pageCount + payload.currentPage = pg.currentPage + payload.prevPage = pg.prevPage + payload.nextPage = pg.nextPage + } + + logger.info({ uuid: req.ctx.uuid, message: `The users of ${orgShortName} organization were sent to the user.` }) + return res.status(200).json(payload) + } catch (err) { + next(err) + } +} + +function setAggregateUserObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} + function setAggregateOrgObj (query) { return [ { @@ -358,5 +435,6 @@ module.exports = { SINGLE_ORG: getOrg, CREATE_ORG: createOrg, UPDATE_ORG: updateOrg, - DELETE_ORG: deleteOrg + DELETE_ORG: deleteOrg, + USER_ALL: getUsers } diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index bd8834c4e..bb3fb6cd9 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -1,5 +1,8 @@ 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) { utils.reqCtxMapping(req, 'body', []) @@ -40,9 +43,20 @@ function isOrgRole (val) { 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 } From b61de26950dab998a4c53e54d9975ceedb3b79fc Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 14 May 2025 10:55:15 -0400 Subject: [PATCH 24/35] More updates for user endpoints --- api-docs/openapi.json | 102 ++++++++++++++++++ .../registry-org.controller/index.js | 88 ++++++++++++++- .../registry-org.controller.js | 7 +- .../registry-org.middleware.js | 4 + src/middleware/middleware.js | 5 + 5 files changed, 204 insertions(+), 2 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index acb594b52..bc32a7ba7 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3497,6 +3497,108 @@ } } }, + "/registryOrg/{shortname}/user": { + "post": { + "tags": [ + "Registry User" + ], + "summary": "Create a user with the provided short name as the owning organization (accessible to Admins and Secretariats)", + "description": "

Access Control

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

Expected Behavior

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

Secretariat: Creates a user for any organization

", + "operationId": "RegistryUserCreateSingle", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the organization" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns the new user information (with the secret)", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-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" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/user/create-user-request.json" + } + } + } + } + } + }, "/registryUser": { "get": { "tags": [ diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index 79e0b1bc7..bf94c006a 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -4,7 +4,7 @@ const mw = require('../../middleware/middleware') const errorMsgs = require('../../middleware/errorMessages') const { body, param, query } = require('express-validator') const controller = require('./registry-org.controller') -const { parseGetParams, parsePostParams, parseDeleteParams, parseError, isOrgRole } = require('./registry-org.middleware') +const { parseGetParams, parsePostParams, parseDeleteParams, parseError, isOrgRole, isUserRole, isValidUsername } = require('./registry-org.middleware') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../constants').getConstants const CONSTANTS = getConstants() @@ -458,4 +458,90 @@ router.get('/registryOrg/:shortname/users', parseError, parseGetParams, controller.USER_ALL) + +router.post('/registryOrg/: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 Admins and Secretariats)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role or be an Admin of the 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/user/create-user-request.json' }, + } + } + } + #swagger.responses[200] = { + description: 'Returns the new user information (with the secret)', + content: { + "application/json": { + schema: { $ref: '../schemas/user/create-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(true), + // mw.onlySecretariatOrAdmin(true), + // // mw.onlyOrgWithPartnerRole, + param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['cve_program_org_membership']) + .optional() + .custom(mw.isCveProgramOrgMembershipObject), + parseError, + parsePostParams, + controller.USER_CREATE_SINGLE) + module.exports = router diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index a2b8e5526..5bbf18cfe 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -375,6 +375,10 @@ async function getUsers (req, res, next) { } } +function createUserByOrg (req, res, next) { + console.log('HERE') +} + function setAggregateUserObj (query) { return [ { @@ -436,5 +440,6 @@ module.exports = { CREATE_ORG: createOrg, UPDATE_ORG: updateOrg, DELETE_ORG: deleteOrg, - USER_ALL: getUsers + USER_ALL: getUsers, + USER_CREATE_SINGLE: createUserByOrg } diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index bb3fb6cd9..f266edb32 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -31,6 +31,10 @@ function parseDeleteParams (req, res, next) { next() } +function isUserRole (val) { + const constants = getConstants() +} + function isOrgRole (val) { const CONSTANTS = getConstants() diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 6084524de..73ad15d65 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -495,6 +495,10 @@ function isFlatStringArray (val) { return true } +function isCveProgramOrgMembershipObject (val) { + console.log(val) +} + /** * Recursively casts to strings and upper-cases all items in array * @@ -548,6 +552,7 @@ module.exports = { validateJsonSyntax, rateLimiter: limiter, isFlatStringArray, + isCveProgramOrgMembershipObject, toUpperCaseArray, containsNoInvalidCharacters, trimJSONWhitespace From 844d711185539d2ec899ccbaa26a04b291568873 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 15 May 2025 16:56:39 -0400 Subject: [PATCH 25/35] Holy damn, it works, kinda sorta --- api-docs/openapi.json | 315 ++++++++++++++++++ src/controller/org.controller/index.js | 12 +- .../org.controller/org.controller.js | 204 +++++++++--- .../org.controller/org.middleware.js | 34 +- .../registry-org.controller/index.js | 20 +- .../registry-user.controller/index.js | 4 +- src/middleware/middleware.js | 158 +++++---- 7 files changed, 600 insertions(+), 147 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index bc32a7ba7..a7dbaa6c2 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -24,6 +24,13 @@ "description": "

Access Control

All registered users can access this endpoint

Expected Behavior

Regular, CNA & Admin Users: Retrieves filtered CVE IDs owned by the user's organization

Secretariat: Retrieves filtered CVE IDs owned by any organization

", "operationId": "cveIdGetFiltered", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/cveIdGetFilteredState" }, @@ -126,6 +133,13 @@ "description": "

Access Control

User must belong to an organization with the CNA or Secretariat role

Expected Behavior

CNA: Reserves CVE IDs for the CNA

Secretariat: Reserves CVE IDs for any organization

", "operationId": "cveIdReserve", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/amount" }, @@ -522,6 +536,13 @@ }, "description": "The id of the CVE ID to update" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/org" }, @@ -620,6 +641,13 @@ }, "description": "The year of the CVE-ID-Range" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -905,6 +933,13 @@ }, "description": "The CVE ID for the record being submitted" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1005,6 +1040,13 @@ }, "description": "The CVE ID for the record being updated" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1098,6 +1140,13 @@ "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves all CVE records for all organizations

", "operationId": "cveGetFiltered", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/cveRecordFilteredTimeModifiedLt" }, @@ -1256,6 +1305,13 @@ "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves all CVE records for all organizations

", "operationId": "cveGetFilteredCursor", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/cveRecordFilteredTimeModifiedLt" }, @@ -1372,6 +1428,13 @@ }, "description": "The CVE ID for the record being created" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1484,6 +1547,13 @@ }, "description": "The CVE ID for which the record is being updated" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1598,6 +1668,13 @@ }, "description": "The CVE ID for the record being rejected" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1699,6 +1776,13 @@ }, "description": "The CVE ID for the record being rejected" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1802,6 +1886,13 @@ }, "description": "The CVE ID for which the record is being updated" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -1896,6 +1987,13 @@ "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" }, @@ -1980,6 +2078,13 @@ "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates an organization

", "operationId": "orgCreateSingle", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2082,6 +2187,13 @@ }, "description": "The shortname or UUID of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2153,6 +2265,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2174,6 +2296,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/id_quota" }, @@ -2260,6 +2389,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2281,6 +2420,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2352,6 +2498,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2373,6 +2529,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -2447,6 +2610,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2468,6 +2641,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2579,6 +2759,13 @@ }, "description": "The username of the user" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2650,6 +2837,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } }, "put": { @@ -2678,6 +2875,13 @@ }, "description": "The username of the user" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/active" }, @@ -2776,6 +2980,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2806,6 +3020,13 @@ }, "description": "The username of the user" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2877,6 +3098,16 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/org/create-org-request.json" + } + } + } } } }, @@ -2889,6 +3120,13 @@ "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": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -2990,6 +3228,13 @@ "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Retrieves a list of all registry organizations

", "operationId": "getAllRegistryOrgs", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -3064,6 +3309,13 @@ "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Creates a new registry organization

", "operationId": "createRegistryOrg", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3156,6 +3408,13 @@ }, "description": "The identifier of the registry organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3236,6 +3495,13 @@ }, "description": "The identifier of the registry organization to delete" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3318,6 +3584,13 @@ }, "description": "The Shortname of the registry organization to update" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3420,6 +3693,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -3608,6 +3888,13 @@ "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Retrieves a list of all registry users

", "operationId": "getAllRegistryUsers", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/pageQuery" }, @@ -3682,6 +3969,13 @@ "description": "

Access Control

Only users with Secretariat role can access this endpoint

Expected Behavior

Secretariat: Creates a new registry user

", "operationId": "createRegistryUser", "parameters": [ + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3774,6 +4068,13 @@ }, "description": "The identifier of the registry user" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3854,6 +4155,13 @@ }, "description": "The identifier of the registry user to update" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3954,6 +4262,13 @@ }, "description": "The identifier of the registry user to delete" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/apiEntityHeader" }, diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 15a160a62..584f6ae53 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -4,7 +4,7 @@ const mw = require('../../middleware/middleware') const errorMsgs = require('../../middleware/errorMessages') const controller = require('./org.controller') const { body, param, query } = require('express-validator') -const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername } = require('./org.middleware') +const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername, validateOrgParameters } = require('./org.middleware') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../../src/constants').getConstants const CONSTANTS = getConstants() @@ -82,6 +82,7 @@ router.get('/org', parseError, parseGetParams, controller.ORG_ALL) + router.post('/org', /* #swagger.tags = ['Organization'] @@ -155,15 +156,10 @@ router.post('/org', } } */ + param(['registry']).optional().isBoolean(), mw.validateUser, mw.onlySecretariat, - 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']).optional() - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole), - body(['policies.id_quota']).optional().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), + validateOrgParameters(), parseError, parsePostParams, controller.ORG_CREATE_SINGLE) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 02cc4dc56..9a8a9d40d 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -1,6 +1,9 @@ require('dotenv').config() +const mongoose = require('mongoose') const User = require('../../model/user') const Org = require('../../model/org') +const RegistryOrg = require('../../model/registry-org') +const RegistryUser = require('../../model/registry-user') const logger = require('../../middleware/logger') const argon2 = require('argon2') const getConstants = require('../../constants').getConstants @@ -235,80 +238,157 @@ async function getOrgIdQuota (req, res, next) { **/ async function createOrg (req, res, next) { const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' try { - const newOrg = new Org() - const orgRepo = req.ctx.repositories.getOrgRepository() + const legOrg = new Org() + const regOrg = new RegistryOrg() - for (const k in req.ctx.body) { - const key = k.toLowerCase() + const orgRepo = req.ctx.repositories.getOrgRepository() + const regOrgRepo = req.ctx.repositories.getRegistryOrgRepository() - switch (key) { - case 'short_name': - newOrg.short_name = req.ctx.body.short_name - break + const body = req.ctx.body + const keys = Object.keys(body) - case 'name': - newOrg.name = req.ctx.body.name - break + for (const keyRaw of keys) { + const key = keyRaw.toLowerCase() + if (key === 'uuid') { + return res.status(400).json(error.uuidProvided('user')) + } - case 'authority': + const handlers = { + name: () => { + legOrg.name = body.name + regOrg.long_name = body.name + }, + short_name: () => { + legOrg.short_name = body.short_name + regOrg.short_name = body.short_name + }, + authority: () => { if ('active_roles' in req.ctx.body.authority) { - newOrg.authority.active_roles = req.ctx.body.authority.active_roles + legOrg.authority.active_roles = req.ctx.body.authority.active_roles + regOrg.authority.active_roles = req.ctx.body.authority.active_roles } - break - - case 'policies': + }, + policies: () => { if ('id_quota' in req.ctx.body.policies) { - newOrg.policies.id_quota = req.ctx.body.policies.id_quota + legOrg.policies.id_quota = req.ctx.body.policies.id_quota + regOrg.hard_quota = req.ctx.body.policies.id_quota } - break + } - case 'uuid': - return res.status(400).json(error.uuidProvided('org')) } + if (handlers[key]) { + handlers[key]() + } + } + const session = await mongoose.startSession() + let legResult = null + let regResult = null + try { + session.startTransaction() + legResult = await orgRepo.findOneByShortName(legOrg.short_name) // Find org in MongoDB + regResult = await regOrgRepo.findOneByShortName(regOrg.short_name) // Find org in registry + } catch (error) { + await session.abortTransaction() + throw error + } finally { + session.endSession() } - let result = await orgRepo.findOneByShortName(newOrg.short_name) // Find org in MongoDB - if (result) { - logger.info({ uuid: req.ctx.uuid, message: newOrg.short_name + ' organization was not created because it already exists.' }) - return res.status(400).json(error.orgExists(newOrg.short_name)) + if (legResult && regResult) { + logger.info({ uuid: req.ctx.uuid, message: legResult.short_name + ' organization was not created because it already exists.' }) + return res.status(400).json(error.orgExists(legOrg.short_name)) } - newOrg.inUse = false - newOrg.UUID = uuid.v4() + legOrg.inUse = false + regOrg.inUse = false + const sharedUuid = uuid.v4() + legOrg.UUID = sharedUuid + regOrg.UUID = sharedUuid + + if (legOrg.authority.active_roles.length === 0) { // default is to make the Org a CNA if no role is specified + legOrg.authority.active_roles = [CONSTANTS.AUTH_ROLE_ENUM.CNA] + } + if (regOrg.authority.active_roles.length === 0) { // default is to make the Org a CNA if no role is specified + regOrg.authority.active_roles = [CONSTANTS.AUTH_ROLE_ENUM.CNA] + } - if (newOrg.authority.active_roles.length === 0) { // default is to make the Org a CNA if no role is specified - newOrg.authority.active_roles = [CONSTANTS.AUTH_ROLE_ENUM.CNA] + if (legOrg.policies.id_quota === undefined) { // set to default quota if none is specified + legOrg.policies.id_quota = CONSTANTS.DEFAULT_ID_QUOTA + } + if (regOrg.hard_quota === undefined) { // set to default quota if none is specified + regOrg.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA } - if (newOrg.policies.id_quota === undefined) { // set to default quota if none is specified - newOrg.policies.id_quota = CONSTANTS.DEFAULT_ID_QUOTA + if (legOrg.authority.active_roles.length === 1 && legOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + legOrg.policies.id_quota = 0 } - if (newOrg.authority.active_roles.length === 1 && newOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 - newOrg.policies.id_quota = 0 + if (regOrg.authority.active_roles.length === 1 && regOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + regOrg.hard_quota = 0 } - await orgRepo.updateByOrgUUID(newOrg.UUID, newOrg, { upsert: true }) // Create org in MongoDB if it doesn't exist - const agt = setAggregateOrgObj({ short_name: newOrg.short_name }) - result = await orgRepo.aggregate(agt) - result = result.length > 0 ? result[0] : null + try { + session.startTransaction() + await orgRepo.updateByOrgUUID(legOrg.UUID, legOrg, { upsert: true }) + await regOrgRepo.updateByUUID(regOrg.UUID, regOrg, { upsert: true }) - const responseMessage = { - message: newOrg.short_name + ' organization was successfully created.', - created: result + const legAgt = setAggregateOrgObj({ short_name: legOrg.short_name }) + const regAgt = setAggregateRegistryOrgObj({ short_name: regOrg.short_name }) + + legResult = await orgRepo.aggregate(legAgt) + legResult = legResult.length > 0 ? legResult[0] : null + + regResult = await regOrgRepo.aggregate(regAgt) + regResult = regResult.length > 0 ? regResult[0] : null + } catch (error) { + await session.abortTransaction() + throw error + } finally { + session.endSession() } - const payload = { - action: 'create_org', - change: newOrg.short_name + ' organization was successfully created.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - org: result + let responseMessage = null + let payload = null + + if (isRegistry) { + responseMessage = { + message: regOrg.short_name + ' organization was successfully created.', + created: regResult + } + + payload = { + action: 'create_org', + change: regOrg.short_name + ' organization was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await regOrgRepo.getOrgUUID(req.ctx.org), + org: regResult + } + } else { + responseMessage = { + message: legOrg.short_name + ' organization was successfully created.', + created: legResult + } + + payload = { + action: 'create_org', + change: legOrg.short_name + ' organization was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + org: legResult + } } + const userRepo = req.ctx.repositories.getUserRepository() - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() + if (isRegistry) { + payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, payload.org_UUID) + } else { + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + } + logger.info(JSON.stringify(payload)) return res.status(200).json(responseMessage) } catch (err) { @@ -833,6 +913,38 @@ function setAggregateOrgObj (query) { ] } +function setAggregateRegistryOrgObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + long_name: true, + short_name: true, + aliases: true, + cve_program_org_function: true, + authority: true, + reports_to: true, + oversees: true, + root_or_tlr: true, + users: true, + charter_or_scope: true, + disclosure_policy: true, + product_list: true, + soft_quota: true, + hard_quota: true, + contact_info: true, + in_use: true, + created: true, + last_updated: true + } + } + ] +} + function setAggregateUserObj (query) { return [ { diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 3913baeba..7e1364130 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -2,6 +2,10 @@ const getConstants = require('../../constants').getConstants const { validationResult } = require('express-validator') const errors = require('./error') const error = new errors.OrgControllerError() +const { body } = require('express-validator') +const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') +const CONSTANTS = getConstants() +const errorMsgs = require('../../middleware/errorMessages') const utils = require('../../utils/utils') function isOrgRole (val) { @@ -16,6 +20,33 @@ function isOrgRole (val) { return true } +function validateOrgParameters () { + return async (req, res, next) => { + const useRegistry = req.query.registry === 'true' + let validations = [] + if (useRegistry) { + // TODO: Implement registry validation + return res.status(400).json({ errors: 'failed successfully' }) + } 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']).optional() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole), + body(['policies.id_quota']).optional().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)] + } + + for (const validation of validations) { + const result = await validation.run(req) + if (!result.isEmpty()) { + return res.status(400).json({ errors: result.array() }) + } + } + next() + } +} + function isUserRole (val) { const CONSTANTS = getConstants() @@ -70,5 +101,6 @@ module.exports = { parseError, isOrgRole, isUserRole, - isValidUsername + isValidUsername, + validateOrgParameters } diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index bf94c006a..a21348fa1 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -66,8 +66,8 @@ router.get('/registryOrg', } } */ - mw.validateUser(true), - mw.onlySecretariat(true), + 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 }), @@ -138,7 +138,7 @@ router.get('/registryOrg/:identifier', } } */ - mw.validateUser(true), + mw.validateUser, param(['identifier']).isString().trim(), // parseError, parseGetParams, @@ -209,8 +209,8 @@ router.post('/registryOrg', } } */ - mw.validateUser(true), - mw.onlySecretariat(true), + mw.validateUser, + mw.onlySecretariat, body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), body(['long_name']).isString().trim().notEmpty(), body(['authority.active_roles']).optional() @@ -303,8 +303,8 @@ router.put('/registryOrg/:shortname', } } */ - mw.validateUser(true), - mw.onlySecretariat(true), + mw.validateUser, + mw.onlySecretariat, param(['shortname']).isString().trim(), // TODO: do more validation here // parseError, @@ -375,7 +375,7 @@ router.delete('/registryOrg/:identifier', } } */ - mw.validateUser(true), + mw.validateUser, // TODO: permissions param(['identifier']).isString().trim(), // parseError, @@ -452,7 +452,7 @@ router.get('/registryOrg/:shortname/users', } } */ - mw.validateUser(true), + mw.validateUser, 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, @@ -533,7 +533,7 @@ router.post('/registryOrg/:shortname/user', } } */ - // mw.validateUser(true), + // mw.validateUser, // mw.onlySecretariatOrAdmin(true), // // mw.onlyOrgWithPartnerRole, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 8a3392115..756825b46 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -64,8 +64,8 @@ router.get('/registryUser', } } */ - mw.validateUser(true), - mw.onlySecretariat(true), + mw.validateUser, + mw.onlySecretariat, query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), // parseError, diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 73ad15d65..9077654d0 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -83,79 +83,78 @@ async function optionallyValidateUser (req, res, next) { } } -function validateUser (useRegistry = false) { - return async (req, res, next) => { - const org = req.ctx.org - const user = req.ctx.user - const key = req.ctx.key - let userRepo = null - let orgRepo = null - if (useRegistry) { - userRepo = req.ctx.repositories.getRegistryUserRepository() - orgRepo = req.ctx.repositories.getRegistryOrgRepository() - } else { - userRepo = req.ctx.repositories.getUserRepository() - orgRepo = req.ctx.repositories.getOrgRepository() - } +async function validateUser (req, res, next) { + const org = req.ctx.org + const user = req.ctx.user + const key = req.ctx.key + let userRepo = null + let orgRepo = null + const useRegistry = req.query.registry === 'true' + if (useRegistry) { + userRepo = req.ctx.repositories.getRegistryUserRepository() + orgRepo = req.ctx.repositories.getRegistryOrgRepository() + } else { + userRepo = req.ctx.repositories.getUserRepository() + orgRepo = req.ctx.repositories.getOrgRepository() + } - const CONSTANTS = getConstants() + const CONSTANTS = getConstants() - try { - if (!org) { - return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.ORG)) - } + try { + if (!org) { + return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.ORG)) + } - if (!user) { - return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.USER)) - } + if (!user) { + return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.USER)) + } - if (!key) { - return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.KEY)) - } + if (!key) { + return res.status(400).json(error.badRequest(CONSTANTS.AUTH_HEADERS.KEY)) + } - logger.info({ uuid: req.ctx.uuid, message: 'Authenticating user: ' + user }) // userUUID may be null if user does not exist - const orgUUID = await orgRepo.getOrgUUID(org) - if (!orgUUID) { - logger.info({ uuid: req.ctx.uuid, message: org + ' organization does not exist. User authentication FAILED for ' + user }) - return res.status(401).json(error.unauthorized()) - } + logger.info({ uuid: req.ctx.uuid, message: 'Authenticating user: ' + user }) // userUUID may be null if user does not exist + const orgUUID = await orgRepo.getOrgUUID(org) + if (!orgUUID) { + logger.info({ uuid: req.ctx.uuid, message: org + ' organization does not exist. User authentication FAILED for ' + user }) + return res.status(401).json(error.unauthorized()) + } - const result = await userRepo.findOneByUserNameAndOrgUUID(user, orgUUID) - if (!result) { - logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User not found. User authentication FAILED for ' + user })) - return res.status(401).json(error.unauthorized()) - } + const result = await userRepo.findOneByUserNameAndOrgUUID(user, orgUUID) + if (!result) { + logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User not found. User authentication FAILED for ' + user })) + return res.status(401).json(error.unauthorized()) + } - let activeInOrg = false - if (useRegistry) { - // Check if user has active status organization's registry org membership list - for (var organization of result.cve_program_org_membership) { - if (organization.program_org === orgUUID) { - if (organization.status === 'active') { - activeInOrg = true - } - break + let activeInOrg = false + if (useRegistry) { + // Check if user has active status organization's registry org membership list + for (var organization of result.cve_program_org_membership) { + if (organization.program_org === orgUUID) { + if (organization.status === 'active') { + activeInOrg = true } + break } } + } - if ((!useRegistry && !result.active) || + if ((!useRegistry && !result.active) || (useRegistry && !activeInOrg)) { - logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User deactivated. Authentication failed for ' + user })) - return res.status(401).json(error.unauthorized()) - } - - const isPwd = await argon2.verify(result.secret, key) - if (!isPwd) { - logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'Incorrect apikey. User authentication FAILED for ' + user })) - return res.status(401).json(error.unauthorized()) - } + logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User deactivated. Authentication failed for ' + user })) + return res.status(401).json(error.unauthorized()) + } - logger.info({ uuid: req.ctx.uuid, message: 'SUCCESSFUL user authentication for ' + user }) - next() - } catch (err) { - next(err) + const isPwd = await argon2.verify(result.secret, key) + if (!isPwd) { + logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'Incorrect apikey. User authentication FAILED for ' + user })) + return res.status(401).json(error.unauthorized()) } + + logger.info({ uuid: req.ctx.uuid, message: 'SUCCESSFUL user authentication for ' + user }) + next() + } catch (err) { + next(err) } } @@ -204,29 +203,28 @@ async function onlySecretariatUserRegistry (req, res, next) { // Checks that the requester belongs to an org that has the 'SECRETARIAT' role -function onlySecretariat (useRegistry = false) { - return async (req, res, next) => { - const org = req.ctx.org - let orgRepo = null - if (useRegistry) { - orgRepo = req.ctx.repositories.getRegistryOrgRepository() - } else { - orgRepo = req.ctx.repositories.getOrgRepository() - } - const CONSTANTS = getConstants() - - try { - const isSec = await orgRepo.isSecretariat(org) - if (!isSec) { - logger.info({ uuid: req.ctx.uuid, message: org + ' is NOT a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) - return res.status(403).json(error.secretariatOnly()) - } +async function onlySecretariat (req, res, next) { + const org = req.ctx.org + let orgRepo = null + const useRegistry = req.query.registry === 'true' + if (useRegistry) { + orgRepo = req.ctx.repositories.getRegistryOrgRepository() + } else { + orgRepo = req.ctx.repositories.getOrgRepository() + } + const CONSTANTS = getConstants() - logger.info({ uuid: req.ctx.uuid, message: 'Confirmed ' + org + ' as a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) - next() - } catch (err) { - next(err) + try { + const isSec = await orgRepo.isSecretariat(org) + if (!isSec) { + logger.info({ uuid: req.ctx.uuid, message: org + ' is NOT a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) + return res.status(403).json(error.secretariatOnly()) } + + logger.info({ uuid: req.ctx.uuid, message: 'Confirmed ' + org + ' as a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT }) + next() + } catch (err) { + next(err) } } From c7a0f77db3ed91ad214d4fb5ec646f1a8fba47a9 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 19 May 2025 15:11:41 -0400 Subject: [PATCH 26/35] post /api/org is now backwards compatible --- .../org.controller/org.controller.js | 226 ++++++++++-------- .../org.controller/org.middleware.js | 107 ++++++++- src/model/registry-org.js | 22 +- 3 files changed, 239 insertions(+), 116 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 9a8a9d40d..aa1a855d2 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -237,9 +237,11 @@ async function getOrgIdQuota (req, res, next) { * Called by POST /api/org/ **/ async function createOrg (req, res, next) { - const CONSTANTS = getConstants() const isRegistry = req.query.registry === 'true' + let payload = null + let responseMessage = null + try { const legOrg = new Org() const regOrg = new RegistryOrg() @@ -250,35 +252,86 @@ async function createOrg (req, res, next) { const body = req.ctx.body const keys = Object.keys(body) + // Short name is handled the same in leg and reg + const handlers = { + short_name: () => { + legOrg.short_name = body.short_name + regOrg.short_name = body.short_name + } + } + + if (isRegistry) { + // Reg only handlers + handlers.long_name = () => { + legOrg.name = body.name + regOrg.long_name = body.name + } + + handlers.cve_program_org_function = () => { + regOrg.cve_program_org_function = body.cve_program_org_function + } + + handlers.oversees = () => { + regOrg.oversees = body.oversees + } + handlers.root_or_tlr = () => { + regOrg.root_or_tlr = body.root_or_tlr + } + handlers.charter_or_scope = () => { + regOrg.charter_or_scope = body.charter_or_scope + } + handlers.disclosure_policy = () => { + regOrg.disclosure_policy = body.disclosure_policy + } + handlers.product_list = () => { + regOrg.product_list = body.product_list + } + handlers.reports_to = () => { + regOrg.reports_to = body.reports_to + } + + handlers['contact_info.poc'] = () => { + regOrg.contact_info.poc = body.contact_info.poc + } + handlers['contact_info.poc_email'] = () => { + regOrg.contact_info.poc_email = body.contact_info.poc_email + } + handlers['contact_info.poc_phone'] = () => { + regOrg.contact_info.poc_phone = body.contact_info.poc_phone + } + handlers['contact_info.org_email'] = () => { + regOrg.contact_info.org_email = body.contact_info.org_email + } + handlers['contact_info.website'] = () => { + regOrg.contact_info.website = body.contact_info.website + } + } else { + // Leg only handlers + handlers.name = () => { + legOrg.name = body.name + regOrg.long_name = body.name + } + + handlers.authority = () => { + if ('active_roles' in req.ctx.body.authority) { + legOrg.authority.active_roles = req.ctx.body.authority.active_roles + regOrg.authority.active_roles = req.ctx.body.authority.active_roles + } + } + handlers.policies = () => { + if ('id_quota' in req.ctx.body.policies) { + legOrg.policies.id_quota = req.ctx.body.policies.id_quota + regOrg.hard_quota = req.ctx.body.policies.id_quota + } + } + } + for (const keyRaw of keys) { const key = keyRaw.toLowerCase() if (key === 'uuid') { return res.status(400).json(error.uuidProvided('user')) } - const handlers = { - name: () => { - legOrg.name = body.name - regOrg.long_name = body.name - }, - short_name: () => { - legOrg.short_name = body.short_name - regOrg.short_name = body.short_name - }, - authority: () => { - if ('active_roles' in req.ctx.body.authority) { - legOrg.authority.active_roles = req.ctx.body.authority.active_roles - regOrg.authority.active_roles = req.ctx.body.authority.active_roles - } - }, - policies: () => { - if ('id_quota' in req.ctx.body.policies) { - legOrg.policies.id_quota = req.ctx.body.policies.id_quota - regOrg.hard_quota = req.ctx.body.policies.id_quota - } - } - - } if (handlers[key]) { handlers[key]() } @@ -290,48 +343,26 @@ async function createOrg (req, res, next) { session.startTransaction() legResult = await orgRepo.findOneByShortName(legOrg.short_name) // Find org in MongoDB regResult = await regOrgRepo.findOneByShortName(regOrg.short_name) // Find org in registry - } catch (error) { - await session.abortTransaction() - throw error - } finally { - session.endSession() - } - - if (legResult && regResult) { - logger.info({ uuid: req.ctx.uuid, message: legResult.short_name + ' organization was not created because it already exists.' }) - return res.status(400).json(error.orgExists(legOrg.short_name)) - } - legOrg.inUse = false - regOrg.inUse = false - const sharedUuid = uuid.v4() - legOrg.UUID = sharedUuid - regOrg.UUID = sharedUuid + if (legResult && regResult) { + logger.info({ uuid: req.ctx.uuid, message: legResult.short_name + ' organization was not created because it already exists.' }) + return res.status(400).json(error.orgExists(legOrg.short_name)) + } - if (legOrg.authority.active_roles.length === 0) { // default is to make the Org a CNA if no role is specified - legOrg.authority.active_roles = [CONSTANTS.AUTH_ROLE_ENUM.CNA] - } - if (regOrg.authority.active_roles.length === 0) { // default is to make the Org a CNA if no role is specified - regOrg.authority.active_roles = [CONSTANTS.AUTH_ROLE_ENUM.CNA] - } + legOrg.inUse = false + regOrg.inUse = false + const sharedUuid = uuid.v4() + legOrg.UUID = sharedUuid + regOrg.UUID = sharedUuid - if (legOrg.policies.id_quota === undefined) { // set to default quota if none is specified - legOrg.policies.id_quota = CONSTANTS.DEFAULT_ID_QUOTA - } - if (regOrg.hard_quota === undefined) { // set to default quota if none is specified - regOrg.hard_quota = CONSTANTS.DEFAULT_ID_QUOTA - } - - if (legOrg.authority.active_roles.length === 1 && legOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 - legOrg.policies.id_quota = 0 - } + if (legOrg.authority.active_roles.length === 1 && legOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + legOrg.policies.id_quota = 0 + } - if (regOrg.authority.active_roles.length === 1 && regOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 - regOrg.hard_quota = 0 - } + if (regOrg.authority.active_roles.length === 1 && regOrg.authority.active_roles[0] === 'ADP') { // ADPs have quota of 0 + regOrg.hard_quota = 0 + } - try { - session.startTransaction() await orgRepo.updateByOrgUUID(legOrg.UUID, legOrg, { upsert: true }) await regOrgRepo.updateByUUID(regOrg.UUID, regOrg, { upsert: true }) @@ -343,50 +374,47 @@ async function createOrg (req, res, next) { regResult = await regOrgRepo.aggregate(regAgt) regResult = regResult.length > 0 ? regResult[0] : null - } catch (error) { - await session.abortTransaction() - throw error - } finally { - session.endSession() - } - let responseMessage = null - let payload = null + if (isRegistry) { + responseMessage = { + message: regOrg.short_name + ' organization was successfully created.', + created: regResult + } - if (isRegistry) { - responseMessage = { - message: regOrg.short_name + ' organization was successfully created.', - created: regResult - } + payload = { + action: 'create_org', + change: regOrg.short_name + ' organization was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await regOrgRepo.getOrgUUID(req.ctx.org), + org: regResult + } + } else { + responseMessage = { + message: legOrg.short_name + ' organization was successfully created.', + created: legResult + } - payload = { - action: 'create_org', - change: regOrg.short_name + ' organization was successfully created.', - req_UUID: req.ctx.uuid, - org_UUID: await regOrgRepo.getOrgUUID(req.ctx.org), - org: regResult - } - } else { - responseMessage = { - message: legOrg.short_name + ' organization was successfully created.', - created: legResult + payload = { + action: 'create_org', + change: legOrg.short_name + ' organization was successfully created.', + req_UUID: req.ctx.uuid, + org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + org: legResult + } } - payload = { - action: 'create_org', - change: legOrg.short_name + ' organization was successfully created.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - org: legResult + const userRepo = req.ctx.repositories.getUserRepository() + const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() + if (isRegistry) { + payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, payload.org_UUID) + } else { + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) } - } - - const userRepo = req.ctx.repositories.getUserRepository() - const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() - if (isRegistry) { - payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, payload.org_UUID) - } else { - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + } catch (error) { + await session.abortTransaction() + throw error + } finally { + session.endSession() } logger.info(JSON.stringify(payload)) diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 7e1364130..f99735242 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -7,6 +7,7 @@ const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middle const CONSTANTS = getConstants() const errorMsgs = require('../../middleware/errorMessages') const utils = require('../../utils/utils') +const _ = require('lodash') function isOrgRole (val) { const CONSTANTS = getConstants() @@ -25,16 +26,100 @@ function validateOrgParameters () { const useRegistry = req.query.registry === 'true' let validations = [] if (useRegistry) { - // TODO: Implement registry validation - return res.status(400).json({ errors: 'failed successfully' }) - } else { - validations = [body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - body(['name']).isString().trim().notEmpty(), + // Optional + // soft_quota, + // Not allowed + // users, contact_info.admins, in_use, created, last_updated + const orgOptions = ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', '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(['root_or_tlr']).default(false) + .isBoolean(), + body( + [ + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'reports_to', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'contact_info.website' + ]) + .default('') + .isString(), body(['authority.active_roles']).optional() + .default([CONSTANTS.AUTH_ROLE_ENUM.CNA]) .custom(isFlatStringArray) .customSanitizer(toUpperCaseArray) .custom(isOrgRole), - body(['policies.id_quota']).optional().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)] + body(['hard_quota']).optional() + .not() + .isArray() + .default(CONSTANTS.DEFAULT_ID_QUOTA) + .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) + .withMessage(errorMsgs.ID_QUOTA), + ...isNotAllowed('name', 'users', 'contact_info.admins', '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', + 'contact_info.admins', + 'in_use', + 'created', + 'root_or_tlr', + 'soft_quota', + 'aliases', + 'hard_quota', + 'contact_info.org_email', + 'contact_info.website', + 'contact_info', + 'users', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'reports_to', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'contact_info.additional_contact_users', + 'contact_info.website') + ] } for (const validation of validations) { @@ -47,6 +132,16 @@ function validateOrgParameters () { } } +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 isUserRole (val) { const CONSTANTS = getConstants() diff --git a/src/model/registry-org.js b/src/model/registry-org.js index 62a3f7fa3..b7b046e61 100644 --- a/src/model/registry-org.js +++ b/src/model/registry-org.js @@ -11,7 +11,7 @@ const schema = { aliases: [String], cve_program_org_function: { type: String, - enum: ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download'] + enum: ['Top Level Root', 'Root', 'CNA', 'CNA-LR', 'Secretariat', 'Board', 'AWG', 'TWG', 'SPWG', 'Bulk Download', 'ADP'] }, authority: { active_roles: [String] @@ -39,8 +39,8 @@ const schema = { last_updated: Date } -const orgPrivate = '-_id -soft_quota -hard_quota -contact_info.admins -in_use -created -last_updated -__v'; -const orgSecretariat = ''; +const orgPrivate = '-_id -soft_quota -hard_quota -contact_info.admins -in_use -created -last_updated -__v' +const orgSecretariat = '' const RegistryOrgSchema = new mongoose.Schema(schema, { collection: 'RegistryOrg', timestamps: { createdAt: 'created', updatedAt: 'last_updated' } }) RegistryOrgSchema.query.byShortName = function (shortName) { @@ -56,14 +56,14 @@ RegistryOrgSchema.statics.populateOverseesAndReportsTo = async function (items) if (item.oversees.length > 0) { const populatedOversees = await Promise.all( item.oversees.map(async (uuid) => { - const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate); + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) return org ? org.toObject() : uuid // Return the org object if found, otherwise return the UUID }) ) item.oversees = populatedOversees } if (item.reports_to) { - const org = await RegistryOrg.findOne({ UUID: item.reports_to }).select(orgPrivate); + const org = await RegistryOrg.findOne({ UUID: item.reports_to }).select(orgPrivate) item.reports_to = org ? org.toObject() : item.reports_to // Return the org object if found, otherwise return the UUID } } @@ -76,14 +76,14 @@ RegistryOrgSchema.statics.populateOrgAffiliations = async function (items) { // if (item.org_affiliations.length > 0) { const populatedOrgs = await Promise.all( item.org_affiliations.map(async ({ org_id: uuid, ...orgMeta }) => { - const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate); + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) return { org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID ...orgMeta - }; + } }) ) - item.org_affiliations = populatedOrgs; + item.org_affiliations = populatedOrgs } } @@ -95,14 +95,14 @@ RegistryOrgSchema.statics.populateCVEProgramOrgMembership = async function (item if (item.cve_program_org_membership.length > 0) { const populatedOrgs = await Promise.all( item.cve_program_org_membership.map(async ({ program_org: uuid, ...orgMeta }) => { - const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate); + const org = await RegistryOrg.findOne({ UUID: uuid }).select(orgPrivate) return { org: org ? org.toObject() : uuid, // Return the org object if found, otherwise return the UUID ...orgMeta - }; + } }) ) - item.cve_program_org_membership = populatedOrgs; + item.cve_program_org_membership = populatedOrgs } } From 33409007047538f9bf270092cbdfe6f18f16ad9f Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 10:12:31 -0400 Subject: [PATCH 27/35] working state --- src/controller/org.controller/index.js | 4 +- .../org.controller/org.controller.js | 294 +++++++++++++----- .../org.controller/org.middleware.js | 4 +- src/repositories/baseRepository.js | 19 +- src/repositories/orgRepository.js | 19 +- 5 files changed, 250 insertions(+), 90 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 584f6ae53..473275f7c 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -4,7 +4,7 @@ const mw = require('../../middleware/middleware') const errorMsgs = require('../../middleware/errorMessages') const controller = require('./org.controller') const { body, param, query } = require('express-validator') -const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername, validateOrgParameters } = require('./org.middleware') +const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername, validateCreateOrgParameters } = require('./org.middleware') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../../src/constants').getConstants const CONSTANTS = getConstants() @@ -159,7 +159,7 @@ router.post('/org', param(['registry']).optional().isBoolean(), mw.validateUser, mw.onlySecretariat, - validateOrgParameters(), + validateCreateOrgParameters(), parseError, parsePostParams, controller.ORG_CREATE_SINGLE) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index aa1a855d2..479649174 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -10,6 +10,7 @@ const getConstants = require('../../constants').getConstants const cryptoRandomString = require('crypto-random-string') const uuid = require('uuid') const errors = require('./error') +const RegistryOrgRepository = require('../../repositories/registryOrgRepository') const error = new errors.OrgControllerError() const validateUUID = require('uuid').validate const booleanIsTrue = require('../../utils/utils').booleanIsTrue @@ -18,7 +19,7 @@ const booleanIsTrue = require('../../utils/utils').booleanIsTrue * Get the details of all orgs * Called by GET /api/org **/ -async function getOrgs (req, res, next) { +async function getOrgs(req, res, next) { try { const CONSTANTS = getConstants() @@ -57,7 +58,7 @@ async function getOrgs (req, res, next) { * Get the details of a single org for the specified shortname * Called by GET /api/org/{identifier} **/ -async function getOrg (req, res, next) { +async function getOrg(req, res, next) { try { const orgShortName = req.ctx.org const identifier = req.ctx.params.identifier @@ -97,7 +98,7 @@ async function getOrg (req, res, next) { * Get the details of all users from an org given the specified shortname * Called by GET /api/org/{shortname}/users **/ -async function getUsers (req, res, next) { +async function getUsers(req, res, next) { try { const CONSTANTS = getConstants() @@ -151,7 +152,7 @@ async function getUsers (req, res, next) { * Get the details of a single user for the specified username * Called by GET /api/org/{shortname}/user/{username} **/ -async function getUser (req, res, next) { +async function getUser(req, res, next) { try { const shortName = req.ctx.org const username = req.ctx.params.username @@ -191,7 +192,7 @@ async function getUser (req, res, next) { * Get details on ID quota for an org with the specified org shortname * Called by GET /api/org/{shortname}/id_quota **/ -async function getOrgIdQuota (req, res, next) { +async function getOrgIdQuota(req, res, next) { try { const orgShortName = req.ctx.org const shortName = req.ctx.params.shortname @@ -236,7 +237,7 @@ async function getOrgIdQuota (req, res, next) { * If the org exists, we do not update the org. * Called by POST /api/org/ **/ -async function createOrg (req, res, next) { +async function createOrg(req, res, next) { const isRegistry = req.query.registry === 'true' let payload = null @@ -429,106 +430,234 @@ async function createOrg (req, res, next) { * If no org exists, we do not create the org. * Called by PUT /api/org/{shortname} **/ -async function updateOrg (req, res, next) { +async function updateOrg(req, res, next) { + const isRegistry = req.query.registry === 'true' + let responseMessage = null + let payload = null + + const session = await mongoose.startSession() // Start a Mongoose session for transaction + try { - const shortName = req.ctx.params.shortname - const newOrg = new Org() - const removeRoles = [] - const addRoles = [] + session.startTransaction() + + const shortNameParam = req.ctx.params.shortname // The short_name from the URL path + const orgRepo = req.ctx.repositories.getOrgRepository() - const org = await orgRepo.findOneByShortName(shortName) - let agt = setAggregateOrgObj({ short_name: shortName }) + const regOrgRepo = req.ctx.repositories.getRegistryOrgRepository() + const userRepo = req.ctx.repositories.getUserRepository() + const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() - // org doesn't exist - if (!org) { - logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) - return res.status(404).json(error.orgDnePathParam(shortName)) + // --- Unified Fetching Logic --- + const orgToUpdate = await orgRepo.findOneByShortName(shortNameParam, { session }) + + if (!orgToUpdate) { + logger.info({ uuid: req.ctx.uuid, message: `Organization ${shortNameParam} not found.` }) + await session.abortTransaction() + session.endSession() + return res.status(404).json(error.orgDnePathParam(shortNameParam)) } - Object.keys(req.ctx.query).forEach(k => { - const key = k.toLowerCase() + const regOrgToUpdate = await regOrgRepo.findOneByUUID(orgToUpdate.UUID, { session }) - if (key === 'new_short_name') { - newOrg.short_name = req.ctx.query.new_short_name - agt = setAggregateOrgObj({ short_name: newOrg.short_name }) - } else if (key === 'name') { - newOrg.name = req.ctx.query.name - } else if (key === 'id_quota') { - newOrg.policies.id_quota = req.ctx.query.id_quota - } else if (key === 'active_roles.add') { - if (Array.isArray(req.ctx.query['active_roles.add'])) { - req.ctx.query['active_roles.add'].forEach(r => { - addRoles.push(r) - }) - } - } else if (key === 'active_roles.remove') { - if (Array.isArray(req.ctx.query['active_roles.remove'])) { - req.ctx.query['active_roles.remove'].forEach(r => { - removeRoles.push(r) - }) - } + if (!regOrgToUpdate) { + // This indicates an inconsistent state, as an Org should have a corresponding RegistryOrg if created by the system + logger.error({ uuid: req.ctx.uuid, message: `Registry org counterpart for ${orgToUpdate.short_name} (UUID: ${orgToUpdate.UUID}) not found. Data inconsistency.` }) + await session.abortTransaction() + session.endSession() + return res.status(500).json(error.serverError('Inconsistent organization data: Registry counterpart missing.')) + } + + const newOrgUpdates = new Org() // For legacy org changes + const newRegOrgUpdates = new RegistryOrg() // For registry org changes + + const queryParams = req.ctx.query + const keys = Object.keys(queryParams) + // Initialize with the current short_name, will be updated if 'new_short_name' handler is called + let newShortNameForAggregation = orgToUpdate.short_name + + const addRolesCollector = [] + const removeRolesCollector = [] + + // Define handlers + const handlers = {} + + // --- Shared Handlers --- + handlers.new_short_name = () => { + const newShort = queryParams.new_short_name + if (newShort && typeof newShort === 'string' && newShort.trim() !== '') { // ensure newShort is valid + newOrgUpdates.short_name = newShort + newRegOrgUpdates.short_name = newShort + newShortNameForAggregation = newShort } - }) + } + + handlers['active_roles.add'] = () => { + const rolesFromQuery = queryParams['active_roles.add'] + if (rolesFromQuery) (Array.isArray(rolesFromQuery) ? rolesFromQuery : [rolesFromQuery]).forEach(r => addRolesCollector.push(r)) + } - // updating the org's roles - if (org) { - const roles = org.authority.active_roles + handlers['active_roles.remove'] = () => { + const rolesFromQuery = queryParams['active_roles.remove'] + if (rolesFromQuery) (Array.isArray(rolesFromQuery) ? rolesFromQuery : [rolesFromQuery]).forEach(r => removeRolesCollector.push(r)) + } - // adding roles - addRoles.forEach(role => { - if (!roles.includes(role)) { - roles.push(role) + // --- Conditional Handlers (controlled by isRegistry) --- + if (isRegistry) { + // Registry-focused updates + handlers.long_name = () => { + const value = queryParams.long_name + if (value !== undefined) { + newOrgUpdates.name = value + newRegOrgUpdates.long_name = value + } + } + handlers.cve_program_org_function = () => { + if (queryParams.cve_program_org_function !== undefined) newRegOrgUpdates.cve_program_org_function = queryParams.cve_program_org_function + } + handlers.oversees = () => { + if (queryParams.oversees !== undefined) newRegOrgUpdates.oversees = queryParams.oversees + } + handlers.root_or_tlr = () => { + if (queryParams.root_or_tlr !== undefined) newRegOrgUpdates.root_or_tlr = queryParams.root_or_tlr + } + handlers.charter_or_scope = () => { + if (queryParams.charter_or_scope !== undefined) newRegOrgUpdates.charter_or_scope = queryParams.charter_or_scope + } + handlers.disclosure_policy = () => { + if (queryParams.disclosure_policy !== undefined) newRegOrgUpdates.disclosure_policy = queryParams.disclosure_policy + } + handlers.product_list = () => { + if (queryParams.product_list !== undefined) newRegOrgUpdates.product_list = queryParams.product_list + } + handlers.reports_to = () => { + if (queryParams.reports_to !== undefined) newRegOrgUpdates.reports_to = queryParams.reports_to + }; + // Contact Info for Registry Org + ['contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website'].forEach(field => { + handlers[field] = () => { + const fieldKeys = field.split('.') + if (queryParams[field] !== undefined) { + if (!newRegOrgUpdates[fieldKeys[0]]) newRegOrgUpdates[fieldKeys[0]] = {} + newRegOrgUpdates[fieldKeys[0]][fieldKeys[1]] = queryParams[field] + } } }) + } else { + // Legacy-focused updates (some sync to registry org) + handlers.name = () => { + const value = queryParams.name + if (value !== undefined) { + newOrgUpdates.name = value + newRegOrgUpdates.long_name = value + } + } + handlers.id_quota = () => { + const value = queryParams.id_quota + if (value !== undefined) { + if (!newOrgUpdates.policies) newOrgUpdates.policies = {} + newOrgUpdates.policies.id_quota = value + newRegOrgUpdates.hard_quota = value + } + } + } - // removing roles - removeRoles.forEach(role => { - const index = roles.indexOf(role) + for (const keyRaw of keys) { + const key = keyRaw.toLowerCase() + if (handlers[key]) { + handlers[key]() + } + } - if (index > -1) { - roles.splice(index, 1) - } + // Process collected role changes and sync them + if (addRolesCollector.length > 0 || removeRolesCollector.length > 0) { + const baseRoles = orgToUpdate.authority && orgToUpdate.authority.active_roles ? [...orgToUpdate.authority.active_roles] : [] + + addRolesCollector.forEach(role => { + if (!baseRoles.includes(role)) baseRoles.push(role) }) + const finalRoles = baseRoles.filter(role => !removeRolesCollector.includes(role)) - newOrg.authority.active_roles = roles + if (!newOrgUpdates.authority) newOrgUpdates.authority = {} + newOrgUpdates.authority.active_roles = finalRoles + if (!newRegOrgUpdates.authority) newRegOrgUpdates.authority = {} + newRegOrgUpdates.authority.active_roles = finalRoles // Sync roles } - if (newOrg.short_name) { - const result = await orgRepo.findOneByShortName(newOrg.short_name) + // ADP Quota override logic + if (newOrgUpdates.authority && newOrgUpdates.authority.active_roles) { // Check if roles were potentially modified + if (newOrgUpdates.authority.active_roles.length === 1 && newOrgUpdates.authority.active_roles[0] === 'ADP') { + if (!newOrgUpdates.policies) newOrgUpdates.policies = {} + newOrgUpdates.policies.id_quota = 0 + newRegOrgUpdates.hard_quota = 0 // Sync ADP quota + } + } - if (result) { - return res.status(403).json(error.duplicateShortname(newOrg.short_name)) + // Check for duplicate short_name if it's being changed + if (newOrgUpdates.short_name && newOrgUpdates.short_name !== orgToUpdate.short_name) { + const existingLegOrg = await orgRepo.findOneByShortName(newOrgUpdates.short_name, { session }) + if (existingLegOrg && existingLegOrg.UUID !== orgToUpdate.UUID) { + await session.abortTransaction(); session.endSession() + return res.status(403).json(error.duplicateShortname(newOrgUpdates.short_name)) + } + const existingRegOrg = await regOrgRepo.findOneByShortName(newRegOrgUpdates.short_name, { session }) + if (existingRegOrg && existingRegOrg.UUID !== regOrgToUpdate.UUID) { + await session.abortTransaction(); session.endSession() + return res.status(403).json(error.duplicateShortname(newRegOrgUpdates.short_name)) } } - // update org - let result = await orgRepo.updateByOrgUUID(org.UUID, newOrg) - if (result.matchedCount === 0) { - logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) - return res.status(404).json(error.orgDnePathParam(shortName)) + // Helper to check if an update object has actual data to set + const hasChanges = (updateObj) => { + if (!updateObj) return false + const topLevelKeys = Object.keys(updateObj).filter(k => typeof updateObj[k] !== 'object' || updateObj[k] === null) + if (topLevelKeys.length > 0) return true + if (updateObj.policies && Object.keys(updateObj.policies).length > 0) return true + if (updateObj.authority && Object.keys(updateObj.authority).length > 0) return true + if (updateObj.contact_info && Object.keys(updateObj.contact_info).length > 0) return true + // Add checks for other nested objects if any + return false } - result = await orgRepo.aggregate(agt) - result = result.length > 0 ? result[0] : null + if (hasChanges(newOrgUpdates)) { + console.log('DEBUG: Session ID object before update:', JSON.stringify(session.id)) + await orgRepo.updateByOrgUUID(orgToUpdate.UUID, newOrgUpdates, { session, upsert: false }) + } - const responseMessage = { - message: shortName + ' organization was successfully updated.', - updated: result + if (hasChanges(newRegOrgUpdates)) { + await regOrgRepo.updateByUUID(regOrgToUpdate.UUID, newRegOrgUpdates, { session, upsert: false }) } - const payload = { - action: 'update_org', - change: shortName + ' organization was successfully updated.', - req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), - org: result + let finalOrgState + // Response shaping controlled by isRegistry + if (isRegistry) { + const regAgt = setAggregateRegistryOrgObj({ short_name: newShortNameForAggregation }) + finalOrgState = (await regOrgRepo.aggregate(regAgt, { session }))[0] || null + responseMessage = { message: `${orgToUpdate.short_name} (Registry View) was successfully updated.`, updated: finalOrgState } // Clarify message + payload = { action: 'update_org', change: `${orgToUpdate.short_name} (Registry View) was successfully updated.`, org: finalOrgState } + payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, regOrgToUpdate.UUID, { session }) + payload.org_UUID = regOrgToUpdate.UUID + } else { + const legAgt = setAggregateOrgObj({ short_name: newShortNameForAggregation }) + console.log('DEBUG: Session ID object before aggregate update:', JSON.stringify(session.id)) + finalOrgState = (await orgRepo.aggregate(legAgt, { session }))[0] || null + responseMessage = { message: `${orgToUpdate.short_name} (Legacy View) was successfully updated.`, updated: finalOrgState } // Clarify message + payload = { action: 'update_org', change: `${orgToUpdate.short_name} (Legacy View) was successfully updated.`, org: finalOrgState } + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, orgToUpdate.UUID, { session }) + payload.org_UUID = orgToUpdate.UUID } - const userRepo = req.ctx.repositories.getUserRepository() - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + + payload.req_UUID = req.ctx.uuid + + await session.commitTransaction() logger.info(JSON.stringify(payload)) return res.status(200).json(responseMessage) } catch (err) { + if (session.inTransaction()) { + await session.abortTransaction() + } next(err) + } finally { + session.endSession() } } @@ -537,7 +666,7 @@ async function updateOrg (req, res, next) { * the user does not exist for the specified shortname and username * Called by POST /api/org/{shortname}/user **/ -async function createUser (req, res, next) { +async function createUser(req, res, next) { try { const orgShortName = req.ctx.params.shortname const requesterUsername = req.ctx.user @@ -652,7 +781,7 @@ async function createUser (req, res, next) { * If no user exists, it does not create the user. * Called by PUT /org/{shortname}/user/{username} **/ -async function updateUser (req, res, next) { +async function updateUser(req, res, next) { try { const requesterShortName = req.ctx.org const requesterUsername = req.ctx.user @@ -863,7 +992,7 @@ async function updateUser (req, res, next) { } // Called by PUT /org/{shortname}/user/{username}/reset_secret -async function resetSecret (req, res, next) { +async function resetSecret(req, res, next) { try { const requesterShortName = req.ctx.org const requesterUsername = req.ctx.user @@ -922,7 +1051,8 @@ async function resetSecret (req, res, next) { } } -function setAggregateOrgObj (query) { +function setAggregateOrgObj(query) { + console.log('CRITICAL DEBUG: Query object received by setAggregateOrgObj:', JSON.stringify(query)) return [ { $match: query @@ -941,7 +1071,7 @@ function setAggregateOrgObj (query) { ] } -function setAggregateRegistryOrgObj (query) { +function setAggregateRegistryOrgObj(query) { return [ { $match: query @@ -973,7 +1103,7 @@ function setAggregateRegistryOrgObj (query) { ] } -function setAggregateUserObj (query) { +function setAggregateUserObj(query) { return [ { $match: query @@ -993,7 +1123,7 @@ function setAggregateUserObj (query) { ] } -function parseUserName (newUser) { +function parseUserName(newUser) { if (newUser.name) { if (!newUser.name.first) { newUser.name.first = '' diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index f99735242..a31987b98 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -21,7 +21,7 @@ function isOrgRole (val) { return true } -function validateOrgParameters () { +function validateCreateOrgParameters () { return async (req, res, next) => { const useRegistry = req.query.registry === 'true' let validations = [] @@ -197,5 +197,5 @@ module.exports = { isOrgRole, isUserRole, isValidUsername, - validateOrgParameters + validateCreateOrgParameters } diff --git a/src/repositories/baseRepository.js b/src/repositories/baseRepository.js index 1e468faeb..1a179d157 100644 --- a/src/repositories/baseRepository.js +++ b/src/repositories/baseRepository.js @@ -11,8 +11,23 @@ class BaseRepository { } } - async aggregate (aggregation) { - return this.collection.aggregate(aggregation) + async aggregate (pipeline, options = {}) { + const aggQuery = this.collection.aggregate(pipeline) + + // Check if a session is provided in the options and apply it + if (options.session) { + aggQuery.session(options.session) + } + + // You can also pass other Mongoose aggregate options if needed from 'options' + // if (options.readConcern) { + // aggQuery.readConcern(options.readConcern); + // } + // if (options.collation) { + // aggQuery.collation(options.collation); + // } + + return aggQuery.exec() } async aggregatePaginate (aggregation, options) { diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js index 84d47fb7c..378ee64d4 100644 --- a/src/repositories/orgRepository.js +++ b/src/repositories/orgRepository.js @@ -19,8 +19,23 @@ class OrgRepository extends BaseRepository { return utils.getOrgUUID(shortName) } - async updateByOrgUUID (orgUUID, org, options = {}) { - return this.collection.findOneAndUpdate().byUUID(orgUUID).updateOne(org).setOptions(options) + async updateByOrgUUID (orgUUID, updateData, executeOptions = {}) { + // The filter to find the document + const filter = { UUID: orgUUID } + + // The update to apply. Using $set is generally safer for partial updates. + // If updateData contains operators like $inc, $push, etc., you might not need $set. + // Mongoose often infers $set for flat objects, but being explicit is good. + const updatePayload = { $set: updateData } + // Or, if updateData might contain MongoDB update operators ($inc, $unset, etc.): + // const updatePayload = updateData; + + // The executeOptions should include { session, upsert: false, new: true (if you want the new doc returned by this func) } + // Crucially, the 'session' object from the caller is in executeOptions. + + // Perform the findOneAndUpdate operation + // The third argument is where options like 'session', 'upsert', 'new' go. + return this.collection.findOneAndUpdate(filter, updatePayload, executeOptions) } async isSecretariat (shortName) { From 3632ef943d79d11e3f92870900dc7cdd476bee3c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:22:01 -0400 Subject: [PATCH 28/35] Actually use sessions in create org, fixing my previous mistake --- .../org.controller/org.controller.js | 18 +++--- src/repositories/orgRepository.js | 21 ++----- src/repositories/registryOrgRepository.js | 11 +++- src/repositories/registryUserRepository.js | 4 +- src/repositories/userRepository.js | 6 +- src/utils/utils.js | 55 ++++++++++++------- 6 files changed, 61 insertions(+), 54 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 479649174..d3893ec86 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -342,8 +342,8 @@ async function createOrg(req, res, next) { let regResult = null try { session.startTransaction() - legResult = await orgRepo.findOneByShortName(legOrg.short_name) // Find org in MongoDB - regResult = await regOrgRepo.findOneByShortName(regOrg.short_name) // Find org in registry + legResult = await orgRepo.findOneByShortName(legOrg.short_name, { session }) // Find org in MongoDB + regResult = await regOrgRepo.findOneByShortName(regOrg.short_name, { session }) // Find org in registry if (legResult && regResult) { logger.info({ uuid: req.ctx.uuid, message: legResult.short_name + ' organization was not created because it already exists.' }) @@ -364,16 +364,16 @@ async function createOrg(req, res, next) { regOrg.hard_quota = 0 } - await orgRepo.updateByOrgUUID(legOrg.UUID, legOrg, { upsert: true }) - await regOrgRepo.updateByUUID(regOrg.UUID, regOrg, { upsert: true }) + await orgRepo.updateByOrgUUID(legOrg.UUID, legOrg, { session, upsert: true }) + await regOrgRepo.updateByUUID(regOrg.UUID, regOrg, { session, upsert: true }) const legAgt = setAggregateOrgObj({ short_name: legOrg.short_name }) const regAgt = setAggregateRegistryOrgObj({ short_name: regOrg.short_name }) - legResult = await orgRepo.aggregate(legAgt) + legResult = await orgRepo.aggregate(legAgt, { session }) legResult = legResult.length > 0 ? legResult[0] : null - regResult = await regOrgRepo.aggregate(regAgt) + regResult = await regOrgRepo.aggregate(regAgt, { session }) regResult = regResult.length > 0 ? regResult[0] : null if (isRegistry) { @@ -399,7 +399,7 @@ async function createOrg(req, res, next) { action: 'create_org', change: legOrg.short_name + ' organization was successfully created.', req_UUID: req.ctx.uuid, - org_UUID: await orgRepo.getOrgUUID(req.ctx.org), + org_UUID: await orgRepo.getOrgUUID(req.ctx.org, { session }), org: legResult } } @@ -407,9 +407,9 @@ async function createOrg(req, res, next) { const userRepo = req.ctx.repositories.getUserRepository() const userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() if (isRegistry) { - payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, payload.org_UUID) + payload.user_UUID = await userRegistryRepo.getUserUUID(req.ctx.user, payload.org_UUID, { session }) } else { - payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID) + payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID, { session }) } } catch (error) { await session.abortTransaction() diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js index 378ee64d4..acd6c0715 100644 --- a/src/repositories/orgRepository.js +++ b/src/repositories/orgRepository.js @@ -7,34 +7,23 @@ class OrgRepository extends BaseRepository { super(Org) } - async findOneByShortName (shortName) { - return this.collection.findOne().byShortName(shortName) + async findOneByShortName (shortName, options = {}) { + const query = { short_name: shortName } + return this.collection.findOne(query, null, options) } async findOneByUUID (UUID) { return this.collection.findOne().byUUID(UUID) } - async getOrgUUID (shortName) { - return utils.getOrgUUID(shortName) + async getOrgUUID (shortName, options = {}) { + return utils.getOrgUUID(shortName, false, options) } async updateByOrgUUID (orgUUID, updateData, executeOptions = {}) { // The filter to find the document const filter = { UUID: orgUUID } - - // The update to apply. Using $set is generally safer for partial updates. - // If updateData contains operators like $inc, $push, etc., you might not need $set. - // Mongoose often infers $set for flat objects, but being explicit is good. const updatePayload = { $set: updateData } - // Or, if updateData might contain MongoDB update operators ($inc, $unset, etc.): - // const updatePayload = updateData; - - // The executeOptions should include { session, upsert: false, new: true (if you want the new doc returned by this func) } - // Crucially, the 'session' object from the caller is in executeOptions. - - // Perform the findOneAndUpdate operation - // The third argument is where options like 'session', 'upsert', 'new' go. return this.collection.findOneAndUpdate(filter, updatePayload, executeOptions) } diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js index 546035710..5c7266d32 100644 --- a/src/repositories/registryOrgRepository.js +++ b/src/repositories/registryOrgRepository.js @@ -7,8 +7,10 @@ class RegistryOrgRepository extends BaseRepository { super(RegistryOrg) } - async findOneByShortName (shortName) { - return this.collection.findOne().byShortName(shortName) + async findOneByShortName (shortName, options = {}) { + const query = { short_name: shortName } + // We are returning the whole object here, so no projection is needed + return this.collection.findOne(query, null, options) } async findOneByUUID (UUID) { @@ -28,7 +30,10 @@ class RegistryOrgRepository extends BaseRepository { } async updateByUUID (uuid, org, options = {}) { - return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(org).setOptions(options) + // The filter to find the document + const filter = { UUID: uuid } + const updatePayload = { $set: org } + return this.collection.findOneAndUpdate(filter, updatePayload, options) } async deleteByUUID (uuid) { diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js index 8af89bd5e..352ec99db 100644 --- a/src/repositories/registryUserRepository.js +++ b/src/repositories/registryUserRepository.js @@ -7,8 +7,8 @@ class RegistryUserRepository extends BaseRepository { super(RegistryUser) } - async getUserUUID (username, orgUUID) { - return utils.getUserUUID(username, orgUUID, true) + async getUserUUID (username, orgUUID, options = {}) { + return utils.getUserUUID(username, orgUUID, true, options) } async findOneByUUID (UUID) { diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js index 4aa255a42..0d1913658 100644 --- a/src/repositories/userRepository.js +++ b/src/repositories/userRepository.js @@ -7,8 +7,8 @@ class UserRepository extends BaseRepository { super(User) } - async getUserUUID (userName, orgUUID) { - return utils.getUserUUID(userName, orgUUID) + async getUserUUID (userName, orgUUID, options = {}) { + return utils.getUserUUID(userName, orgUUID, options) } async isAdmin (username, shortname) { @@ -27,7 +27,7 @@ class UserRepository extends BaseRepository { return this.collection.find().byOrgUUID(orgUUID).countDocuments().exec() } - async findOneByUserNameAndOrgUUID (userName, orgUUID) { + async findOneByUserNameAndOrgUUID (userName, orgUUID, projection = null, options = {}) { return this.collection.findOne().byUserNameAndOrgUUID(userName, orgUUID) } diff --git a/src/utils/utils.js b/src/utils/utils.js index b71653d5f..8aecdfe77 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -8,34 +8,47 @@ const getConstants = require('../constants').getConstants const _ = require('lodash') const { DateTime } = require('luxon') -async function getOrgUUID (shortName, useRegistry = false) { - let org = null - if (useRegistry) { - org = await RegistryOrg.findOne().byShortName(shortName) - } else { - org = await Org.findOne().byShortName(shortName) - } +async function getOrgUUID (shortName, useRegistry = false, options = {}) { + const ModelToQuery = useRegistry ? RegistryOrg : Org + const query = { short_name: shortName } + const projection = 'UUID' // We only need the UUID field - let result = null - if (org) { - result = org.UUID - } - return result + // It's often good practice to use .lean() for read-only operations + // if you don't need full Mongoose documents. + + const executionOptions = { ...options } + if (executionOptions.lean === undefined) executionOptions.lean = true + + const orgDocument = await ModelToQuery.findOne(query, projection, executionOptions) + + return orgDocument ? orgDocument.UUID : null } -async function getUserUUID (userName, orgUUID, useRegistry = false) { - let user = null +async function getUserUUID (userIdentifier, orgUUID, useRegistry = false, options = {}) { + const ModelToQuery = useRegistry ? RegistryUser : User + let query + if (useRegistry) { - user = await RegistryUser.findOne().byUserIdAndOrgUUID(userName, orgUUID) + // For RegistryUser, query by user_id and check within the org_affiliations array + query = { + user_id: userIdentifier, // Matches the 'user_id' field in RegistryUser schema + 'org_affiliations.org_id': orgUUID // Uses dot notation to query the array + } } else { - user = await User.findOne().byUserNameAndOrgUUID(userName, orgUUID) + // For User, query by username and org_UUID + query = { + username: userIdentifier, // Matches the 'username' field in User schema + org_UUID: orgUUID // Matches the 'org_UUID' field in User schema + } } - let result = null - if (user) { - result = user.UUID - } - return result + const projection = 'UUID' // We only need the user's UUID field + const executionOptions = { ...options } + if (executionOptions.lean === undefined) executionOptions.lean = true + + const userDocument = await ModelToQuery.findOne(query, projection, executionOptions) + + return userDocument ? userDocument.UUID : null } async function isSecretariat (shortName, useRegistry = false) { From 11b1b2cfbda713ce2aea239d1f5fb9d3f55cf07e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:26:34 -0400 Subject: [PATCH 29/35] Added script to create replica set --- src/scripts/set-replica-set.js | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/scripts/set-replica-set.js diff --git a/src/scripts/set-replica-set.js b/src/scripts/set-replica-set.js new file mode 100644 index 000000000..cf460021c --- /dev/null +++ b/src/scripts/set-replica-set.js @@ -0,0 +1,99 @@ +// Filename: initMongoReplicaSet.js +// Purpose: Initiates a MongoDB replica set on a mongod instance +// that was started with the --replSet option. +// Uses directConnection=true for the initial connection to simplify +// connecting to a node that is not yet part of an initialized replica set. + +const { MongoClient } = require('mongodb') + +// Configuration +const MONGODB_HOST_IP = '127.0.0.1' // Explicitly use 127.0.0.1 +const MONGODB_PORT = '27017' +// Added ?directConnection=true to the URI +const MONGODB_URI = `mongodb://${MONGODB_HOST_IP}:${MONGODB_PORT}/admin?directConnection=true` +const REPLICA_SET_NAME = 'rs0' // Must match the --replSet name used when starting mongod +const HOST_ADDRESS = `${MONGODB_HOST_IP}:${MONGODB_PORT}` // The address of this mongod instance +const SERVER_SELECTION_TIMEOUT_MS = 35000 // Slightly increased timeout + +async function initiateReplicaSet () { + // serverSelectionTimeoutMS might be less relevant with directConnection=true, but kept for consistency + const client = new MongoClient(MONGODB_URI, { serverSelectionTimeoutMS: SERVER_SELECTION_TIMEOUT_MS }) + + try { + console.log(`Attempting to connect to MongoDB at ${MONGODB_URI} ...`) + await client.connect() + console.log('Successfully connected to MongoDB (using directConnection).') + + const adminDb = client.db('admin') // Ensure we are using the admin database + + // Check current replica set status + let status + try { + console.log('Checking replica set status...') + // With directConnection, replSetGetStatus might behave differently or even fail if not on a replSet member. + // However, our mongod IS configured as a replSet member, just not initiated. + status = await adminDb.command({ replSetGetStatus: 1 }) + + if (status.ok && status.set === REPLICA_SET_NAME && status.myState === 1) { + console.log(`Replica set '${REPLICA_SET_NAME}' is already initialized and this node is PRIMARY.`) + return + } + if (status.ok && status.members && status.members.length > 0) { + console.log(`Replica set '${status.set || REPLICA_SET_NAME}' seems to be already configured or in a specific state.`) + console.log('Current status:', JSON.stringify(status, null, 2)) + return + } + if (status.ok === 0 && status.codeName !== 'NotYetInitialized' && !status.errmsg?.includes('no replset config')) { + console.warn('Replica set status check returned an unexpected error:', JSON.stringify(status, null, 2)) + } + } catch (err) { + if (err.codeName === 'NotYetInitialized' || err.message.includes('no replset config') || err.message.includes('NotYetInitialized')) { + console.log('Replica set not yet initialized (as expected from rs.status() in mongosh). Proceeding with initialization.') + } else if (err.code === 94 || err.message.includes('No replica set name has been specified')) { // Error code for replSetGetStatus on non-replset node + console.log('replSetGetStatus failed, likely because directConnection is on and it is not fully initialized. This is okay if mongod was started with --replSet. Proceeding with initiation attempt.') + } else { + console.warn(`Warning during replica set status check: ${err.message}. Attempting initialization anyway.`) + console.log('Error details:', JSON.stringify(err, null, 2)) + } + } + + // Define the replica set configuration + const replicaSetConfig = { + _id: REPLICA_SET_NAME, + members: [ + { _id: 0, host: HOST_ADDRESS } + ] + } + + console.log(`Attempting to initiate replica set '${REPLICA_SET_NAME}' with config:`, JSON.stringify(replicaSetConfig, null, 2)) + + const result = await adminDb.command({ replSetInitiate: replicaSetConfig }) + + if (result.ok === 1) { + console.log(`Replica set '${REPLICA_SET_NAME}' initiated successfully!`) + console.log('It might take a few moments for the node to become PRIMARY.') + console.log('You can verify with `mongosh` and `rs.status()`.') + } else { + console.error('Failed to initiate replica set.', result) + if (result.codeName === 'InvalidReplicaSetConfig') { + console.error('Detail: The replica set configuration was invalid. This can happen if the host address is not resolvable from the perspective of the mongod server itself.') + } else if (result.codeName === 'AlreadyInitialized') { + console.log(`Replica set '${REPLICA_SET_NAME}' is already initialized.`) + } + } + } catch (error) { + console.error('An error occurred during the process:', error) + if (error.message && error.message.includes('already initialized')) { + console.log(`It seems the replica set '${REPLICA_SET_NAME}' is already initialized.`) + } else if (error.codeName === 'ConfigurationInProgress') { + console.log('Replica set configuration is already in progress or node is recovering. Try again in a moment.') + } + } finally { + if (client) { + await client.close() + console.log('MongoDB connection closed.') + } + } +} + +initiateReplicaSet() From f5a1a9e16a9b29f009a26645d6a4f04b0457d8b7 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:49:59 -0400 Subject: [PATCH 30/35] Got sessions? Finally actually fix create Org to use sessions --- src/controller/org.controller/org.controller.js | 5 +++-- src/controller/org.controller/org.middleware.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index d3893ec86..aadbd8a4b 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -264,8 +264,8 @@ async function createOrg(req, res, next) { if (isRegistry) { // Reg only handlers handlers.long_name = () => { - legOrg.name = body.name - regOrg.long_name = body.name + legOrg.name = body.long_name + regOrg.long_name = body.long_name } handlers.cve_program_org_function = () => { @@ -411,6 +411,7 @@ async function createOrg(req, res, next) { } else { payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID, { session }) } + await session.commitTransaction() } catch (error) { await session.abortTransaction() throw error diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index a31987b98..08968d29a 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -61,15 +61,15 @@ function validateCreateOrgParameters () { ]) .default('') .isString(), - body(['authority.active_roles']).optional() + body(['authority.active_roles']) .default([CONSTANTS.AUTH_ROLE_ENUM.CNA]) .custom(isFlatStringArray) .customSanitizer(toUpperCaseArray) .custom(isOrgRole), - body(['hard_quota']).optional() + body(['hard_quota']) + .default(CONSTANTS.DEFAULT_ID_QUOTA) .not() .isArray() - .default(CONSTANTS.DEFAULT_ID_QUOTA) .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) .withMessage(errorMsgs.ID_QUOTA), ...isNotAllowed('name', 'users', 'contact_info.admins', 'in_use', 'created', 'last_updated', 'policies.id_quota') From c2f6a5d5892781052f3435a629de2edaedea08a8 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 13:52:50 -0400 Subject: [PATCH 31/35] We are rolling now, getOrg by identifier is now backwards compatible --- src/controller/org.controller/index.js | 22 ++-- .../org.controller/org.controller.js | 46 +++++--- .../org.controller/org.middleware.js | 101 +++++++++++++++++- 3 files changed, 136 insertions(+), 33 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 473275f7c..906331353 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -4,7 +4,7 @@ const mw = require('../../middleware/middleware') const errorMsgs = require('../../middleware/errorMessages') const controller = require('./org.controller') const { body, param, query } = require('express-validator') -const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername, validateCreateOrgParameters } = require('./org.middleware') +const { parseGetParams, parsePostParams, parseError, isOrgRole, isUserRole, isValidUsername, validateCreateOrgParameters, validateUpdateOrgParameters } = require('./org.middleware') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const getConstants = require('../../../src/constants').getConstants const CONSTANTS = getConstants() @@ -76,7 +76,8 @@ router.get('/org', */ mw.validateUser, mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + param(['registry']).optional().isBoolean(), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page', 'registry']) }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, @@ -230,6 +231,7 @@ router.get('/org/:identifier', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), param(['identifier']).isString().trim(), parseError, parseGetParams, @@ -304,22 +306,10 @@ router.put('/org/:shortname', } } */ + param(['registry']).optional().isBoolean(), mw.validateUser, mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['new_short_name', 'id_quota', 'name', 'active_roles.add', 'active_roles.remove']) }), - query(['new_short_name', 'id_quota', 'name', 'active_roles.add', 'active_roles.remove']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query(['new_short_name']).optional().isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), - query(['id_quota']).optional().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), - query(['name']).optional().isString().trim().notEmpty(), - query(['active_roles.add']).optional().toArray() - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), - query(['active_roles.remove']).optional().toArray() - .custom(isFlatStringArray) - .customSanitizer(toUpperCaseArray) - .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), + validateUpdateOrgParameters(), parseError, parsePostParams, controller.ORG_UPDATE_SINGLE) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index aadbd8a4b..5a8b81a25 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -22,6 +22,7 @@ const booleanIsTrue = require('../../utils/utils').booleanIsTrue async function getOrgs(req, res, next) { try { const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -32,9 +33,17 @@ async function getOrgs(req, res, next) { const options = CONSTANTS.PAGINATOR_OPTIONS 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 repo = req.ctx.repositories.getOrgRepository() - const agt = setAggregateOrgObj({}) + let agt + let repo + if (isRegistry) { + repo = req.ctx.repositories.getRegistryOrgRepository() + agt = setAggregateRegistryOrgObj({}) + } else { + repo = req.ctx.repositories.getOrgRepository() + agt = setAggregateOrgObj({}) + } + const pg = await repo.aggregatePaginate(agt, options) const payload = { organizations: pg.itemsList } @@ -60,18 +69,23 @@ async function getOrgs(req, res, next) { **/ async function getOrg(req, res, next) { try { + const isRegistry = req.query.registry === 'true' + const orgShortName = req.ctx.org const identifier = req.ctx.params.identifier - const repo = req.ctx.repositories.getOrgRepository() + + const repo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() + const isSecretariat = await repo.isSecretariat(orgShortName) const org = await repo.findOneByShortName(orgShortName) let orgIdentifer = orgShortName - let agt = setAggregateOrgObj({ short_name: identifier }) + + let agt = isRegistry ? setAggregateRegistryOrgObj({ short_name: identifier }) : setAggregateOrgObj({ short_name: identifier }) // check if identifier is uuid and if so, reassign agt and orgIdentifier if (validateUUID(identifier)) { orgIdentifer = org.UUID - agt = setAggregateOrgObj({ UUID: identifier }) + agt = isRegistry ? setAggregateRegistryOrgObj({ UUID: identifier }) : setAggregateOrgObj({ UUID: identifier }) } if (orgIdentifer !== identifier && !isSecretariat) { @@ -258,6 +272,12 @@ async function createOrg(req, res, next) { short_name: () => { legOrg.short_name = body.short_name regOrg.short_name = body.short_name + }, + authority: () => { + if ('active_roles' in req.ctx.body.authority) { + legOrg.authority.active_roles = req.ctx.body.authority.active_roles + regOrg.authority.active_roles = req.ctx.body.authority.active_roles + } } } @@ -275,6 +295,9 @@ async function createOrg(req, res, next) { handlers.oversees = () => { regOrg.oversees = body.oversees } + handlers.hard_quota = () => { + regOrg.hard_quota = body.hard_quota + } handlers.root_or_tlr = () => { regOrg.root_or_tlr = body.root_or_tlr } @@ -313,12 +336,6 @@ async function createOrg(req, res, next) { regOrg.long_name = body.name } - handlers.authority = () => { - if ('active_roles' in req.ctx.body.authority) { - legOrg.authority.active_roles = req.ctx.body.authority.active_roles - regOrg.authority.active_roles = req.ctx.body.authority.active_roles - } - } handlers.policies = () => { if ('id_quota' in req.ctx.body.policies) { legOrg.policies.id_quota = req.ctx.body.policies.id_quota @@ -505,6 +522,7 @@ async function updateOrg(req, res, next) { // --- Conditional Handlers (controlled by isRegistry) --- if (isRegistry) { // Registry-focused updates + // In general, these do not have a direct effect on Legacy Orgs, so they are handled separately handlers.long_name = () => { const value = queryParams.long_name if (value !== undefined) { @@ -512,6 +530,11 @@ async function updateOrg(req, res, next) { newRegOrgUpdates.long_name = value } } + handlers.hard_quota = () => { + const value = queryParams.hard_quota + newOrgUpdates.policies.id_quota = value + newRegOrgUpdates.hard_quota = value + } handlers.cve_program_org_function = () => { if (queryParams.cve_program_org_function !== undefined) newRegOrgUpdates.cve_program_org_function = queryParams.cve_program_org_function } @@ -639,7 +662,6 @@ async function updateOrg(req, res, next) { payload.org_UUID = regOrgToUpdate.UUID } else { const legAgt = setAggregateOrgObj({ short_name: newShortNameForAggregation }) - console.log('DEBUG: Session ID object before aggregate update:', JSON.stringify(session.id)) finalOrgState = (await orgRepo.aggregate(legAgt, { session }))[0] || null responseMessage = { message: `${orgToUpdate.short_name} (Legacy View) was successfully updated.`, updated: finalOrgState } // Clarify message payload = { action: 'update_org', change: `${orgToUpdate.short_name} (Legacy View) was successfully updated.`, org: finalOrgState } diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 08968d29a..79341fd56 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -2,11 +2,12 @@ const getConstants = require('../../constants').getConstants const { validationResult } = require('express-validator') const errors = require('./error') const error = new errors.OrgControllerError() -const { body } = require('express-validator') +const { body, param, query } = require('express-validator') const { toUpperCaseArray, isFlatStringArray } = require('../../middleware/middleware') const CONSTANTS = getConstants() const errorMsgs = require('../../middleware/errorMessages') const utils = require('../../utils/utils') +const mw = require('../../middleware/middleware') const _ = require('lodash') function isOrgRole (val) { @@ -132,6 +133,76 @@ function validateCreateOrgParameters () { } } +function validateUpdateOrgParameters () { + return async (req, res, next) => { + const useRegistry = req.query.registry === 'true' + + const legacyParametersOnly = ['id_quota', 'name'] + const registryParametersOnly = ['hard_quota', 'long_name', 'cve_program_org_function', 'oversees', 'root_or_tlr', 'charter_or_scope', 'disclosure_policy', 'product_list'] + const sharedParameters = ['new_short_name', 'active_roles.add', 'active_roles.remove', 'registry'] + + const allParameters = [ + ...legacyParametersOnly, ...registryParametersOnly, ...sharedParameters + ] + + const validations = [query().custom((query) => { return mw.validateQueryParameterNames(query, allParameters) }), + query(allParameters).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['new_short_name']).optional().isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query(['active_roles.add']).optional().toArray() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), + query(['active_roles.remove']).optional().toArray() + .custom(isFlatStringArray) + .customSanitizer(toUpperCaseArray) + .custom(isOrgRole).withMessage(errorMsgs.ORG_ROLES), + param(['shortname']).isString().trim().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH })] + + if (useRegistry) { + validations.push( + + query(['hard_quota']).optional().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), + query(['long_name']).optional().isString().trim().notEmpty(), + query(['oversees']).optional().isArray(), + query(['root_or_tlr']).optional().isBoolean(), + query( + [ + 'cve_program_org_function', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'contact_info.website' + ]) + .optional() + .isString(), + ...isNotAllowedQuery(...legacyParametersOnly) + // if we decide that we want to allow more, we can add them here. + + ) + } else { + validations.push( + + query(['id_quota']).optional().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), + query(['name']).optional().isString().trim().notEmpty(), + ...isNotAllowedQuery(...registryParametersOnly) + + ) + } + + for (const validation of validations) { + const result = await validation.run(req) + if (!result.isEmpty()) { + return res.status(400).json({ errors: result.array() }) + } + } + next() + } +} + function isNotAllowed (...fields) { return fields.map(field => body(field) @@ -142,6 +213,16 @@ function isNotAllowed (...fields) { ) } +function isNotAllowedQuery (...fields) { + return fields.map(field => + query(field) + .if((value, { req }) => _.has(req.query, field)) + .custom(() => { + throw new Error(`${field} must not be present`) + }) + ) +} + function isUserRole (val) { const CONSTANTS = getConstants() @@ -160,15 +241,24 @@ function parsePostParams (req, res, next) { 'new_short_name', 'name', 'id_quota', 'active', 'active_roles.add', 'active_roles.remove', 'new_username', 'org_short_name', - 'name.first', 'name.last', 'name.middle', 'name.suffix' + 'name.first', 'name.last', 'name.middle', 'name.suffix', 'long_name', 'cve_program_org_function', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'contact_info.poc', + 'contact_info.poc_email', + 'contact_info.poc_phone', + 'contact_info.org_email', + 'hard_quota', + 'contact_info.website', 'root_or_tlr', 'oversees' ]) utils.reqCtxMapping(req, 'params', ['shortname', 'username']) next() } function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier']) - utils.reqCtxMapping(req, 'query', ['page']) + utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier', 'registry']) + utils.reqCtxMapping(req, 'query', ['page', 'registry']) next() } @@ -197,5 +287,6 @@ module.exports = { isOrgRole, isUserRole, isValidUsername, - validateCreateOrgParameters + validateCreateOrgParameters, + validateUpdateOrgParameters } From 784cfb1b7ddbf7f6bcf972e6ee63481cd4cd84fb Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 14:53:04 -0400 Subject: [PATCH 32/35] Get quota is now backwards compatible --- src/controller/org.controller/index.js | 1 + src/controller/org.controller/org.controller.js | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 906331353..bc58c8777 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -380,6 +380,7 @@ router.get('/org/:shortname/id_quota', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), parseError, parseGetParams, diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 5a8b81a25..b553fbd4c 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -208,9 +208,11 @@ async function getUser(req, res, next) { **/ async function getOrgIdQuota(req, res, next) { try { + const isRegistry = req.query.registry === 'true' const orgShortName = req.ctx.org const shortName = req.ctx.params.shortname - const repo = req.ctx.repositories.getOrgRepository() + + const repo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() const isSecretariat = await repo.isSecretariat(orgShortName) if (orgShortName !== shortName && !isSecretariat) { @@ -225,7 +227,7 @@ async function getOrgIdQuota(req, res, next) { } const returnPayload = { - id_quota: result.policies.id_quota, + ...(isRegistry ? { hard_quota: result.hard_quota } : { id_quota: result.policies.id_quota }), total_reserved: null, available: null } @@ -237,7 +239,11 @@ async function getOrgIdQuota(req, res, next) { const cveIdRepo = req.ctx.repositories.getCveIdRepository() result = await cveIdRepo.countDocuments(query) returnPayload.total_reserved = result - returnPayload.available = returnPayload.id_quota - returnPayload.total_reserved + if (isRegistry) { + returnPayload.available = returnPayload.hard_quota - returnPayload.total_reserved + } else { + returnPayload.available = returnPayload.id_quota - returnPayload.total_reserved + } 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) From fb4f5948ca145dd66fdaa5cb2f9576186a504eff Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 15:34:26 -0400 Subject: [PATCH 33/35] get users are backwards compatible --- src/controller/user.controller/index.js | 5 ++-- .../user.controller/user.controller.js | 29 +++++++++++++++++-- .../user.controller/user.middleware.js | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/controller/user.controller/index.js b/src/controller/user.controller/index.js index 5d153266b..9a768ef72 100644 --- a/src/controller/user.controller/index.js +++ b/src/controller/user.controller/index.js @@ -1,7 +1,7 @@ const express = require('express') const router = express.Router() const mw = require('../../middleware/middleware') -const { query } = require('express-validator') +const { query, param } = require('express-validator') const controller = require('./user.controller') const { parseGetParams, parseError } = require('./user.middleware') const getConstants = require('../../constants').getConstants @@ -74,8 +74,9 @@ router.get('/users', */ mw.validateUser, mw.onlySecretariat, + param(['registry']).optional().isBoolean(), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + query(['page', 'registry']).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 fb4429ef6..44c435827 100644 --- a/src/controller/user.controller/user.controller.js +++ b/src/controller/user.controller/user.controller.js @@ -1,3 +1,4 @@ + require('dotenv').config() const logger = require('../../middleware/logger') const getConstants = require('../../constants').getConstants @@ -9,6 +10,7 @@ const getConstants = require('../../constants').getConstants async function getAllUsers (req, res, next) { try { const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -19,9 +21,9 @@ async function getAllUsers (req, res, next) { const options = CONSTANTS.PAGINATOR_OPTIONS 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 repo = req.ctx.repositories.getUserRepository() + const repo = isRegistry ? req.ctx.repositories.getRegistryUserRepository() : req.ctx.repositories.getUserRepository() - const agt = setAggregateUserObj({}) + const agt = isRegistry ? setAggregateRegistryUserObj({}) : setAggregateUserObj({}) const pg = await repo.aggregatePaginate(agt, options) const payload = { users: pg.itemsList } @@ -41,6 +43,29 @@ async function getAllUsers (req, res, next) { } } +function setAggregateRegistryUserObj (query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} + function setAggregateUserObj (query) { return [ { diff --git a/src/controller/user.controller/user.middleware.js b/src/controller/user.controller/user.middleware.js index 95a900313..e9477fb70 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']) + utils.reqCtxMapping(req, 'query', ['page', 'registry']) next() } From bea2a23f470bfb7e9baf37c68a9f472c2d11191b Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 15:50:53 -0400 Subject: [PATCH 34/35] get users by org is now backwards compatible --- src/controller/org.controller/index.js | 1 + .../org.controller/org.controller.js | 31 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index bc58c8777..8381fb109 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -453,6 +453,7 @@ router.get('/org/:shortname/users', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), 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, diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index b553fbd4c..aba6e85d0 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -115,6 +115,7 @@ async function getOrg(req, res, next) { async function getUsers(req, res, next) { try { const CONSTANTS = getConstants() + const isRegistry = req.query.registry === 'true' // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -127,8 +128,9 @@ async function getUsers(req, res, next) { options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value const shortName = req.ctx.org const orgShortName = req.ctx.params.shortname - const orgRepo = req.ctx.repositories.getOrgRepository() - const userRepo = req.ctx.repositories.getUserRepository() + const orgRepo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() + const userRepo = isRegistry ? req.ctx.repositories.getRegistryUserRepository() : req.ctx.repositories.getUserRepository() + const orgUUID = await orgRepo.getOrgUUID(orgShortName) const isSecretariat = await orgRepo.isSecretariat(shortName) @@ -142,7 +144,7 @@ async function getUsers(req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - const agt = setAggregateUserObj({ org_UUID: orgUUID }) + const agt = isRegistry ? setAggregateRegistryUserObj({ 'cve_program_org_membership.program_org': orgUUID }) : setAggregateUserObj({ org_UUID: orgUUID }) const pg = await userRepo.aggregatePaginate(agt, options) const payload = { users: pg.itemsList } @@ -1151,7 +1153,28 @@ function setAggregateUserObj(query) { } ] } - +function setAggregateRegistryUserObj(query) { + return [ + { + $match: query + }, + { + $project: { + _id: false, + UUID: true, + user_id: true, + name: true, + org_affiliations: true, + cve_program_org_membership: true, + created: true, + created_by: true, + last_updated: true, + deactivation_date: true, + last_active: true + } + } + ] +} function parseUserName(newUser) { if (newUser.name) { if (!newUser.name.first) { From 559329d0e52952906ea127965764b8f1ccdf063a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 17:43:34 -0400 Subject: [PATCH 35/35] Get secret is now backwards compatible. --- src/controller/org.controller/index.js | 2 + .../org.controller/org.controller.js | 103 ++++++++++++------ src/middleware/middleware.js | 2 +- src/repositories/registryOrgRepository.js | 4 +- src/repositories/registryUserRepository.js | 19 +++- src/repositories/userRepository.js | 11 +- src/utils/utils.js | 12 +- 7 files changed, 104 insertions(+), 49 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 8381fb109..fd839e984 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -620,6 +620,7 @@ router.get('/org/:shortname/user/:username', } */ mw.validateUser, + param(['registry']).optional().isBoolean(), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), parseError, @@ -801,6 +802,7 @@ router.put('/org/:shortname/user/:username/reset_secret', */ mw.validateUser, mw.onlyOrgWithPartnerRole, + param(['registry']).optional().isBoolean(), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), parseError, diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index aba6e85d0..66cf66aae 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -170,10 +170,12 @@ async function getUsers(req, res, next) { **/ async function getUser(req, res, next) { try { + const isRegistry = req.query.registry === 'true' const shortName = req.ctx.org const username = req.ctx.params.username const orgShortName = req.ctx.params.shortname - const orgRepo = req.ctx.repositories.getOrgRepository() + + const orgRepo = isRegistry ? req.ctx.repositories.getRegistryOrgRepository() : req.ctx.repositories.getOrgRepository() const isSecretariat = await orgRepo.isSecretariat(shortName) if (orgShortName !== shortName && !isSecretariat) { @@ -187,8 +189,8 @@ async function getUser(req, res, next) { return res.status(404).json(error.orgDnePathParam(orgShortName)) } - const userRepo = req.ctx.repositories.getUserRepository() - const agt = setAggregateUserObj({ username: username, org_UUID: orgUUID }) + const userRepo = isRegistry ? req.ctx.repositories.getRegistryUserRepository() : req.ctx.repositories.getUserRepository() + const agt = isRegistry ? setAggregateRegistryUserObj({ user_id: username, 'cve_program_org_membership.program_org': orgUUID }) : setAggregateUserObj({ username: username, org_UUID: orgUUID }) let result = await userRepo.aggregate(agt) result = result.length > 0 ? result[0] : null @@ -1024,47 +1026,84 @@ async function updateUser(req, res, next) { // Called by PUT /org/{shortname}/user/{username}/reset_secret async function resetSecret(req, res, next) { + const session = await mongoose.startSession() + session.startTransaction() try { + let randomKey + const requesterShortName = req.ctx.org const requesterUsername = req.ctx.user const username = req.ctx.params.username const orgShortName = req.ctx.params.shortname + const userRepo = req.ctx.repositories.getUserRepository() const orgRepo = req.ctx.repositories.getOrgRepository() - const isSecretariat = await orgRepo.isSecretariat(requesterShortName) - const orgUUID = await orgRepo.getOrgUUID(orgShortName) // userUUID may be null if user does not exist - if (!orgUUID) { - logger.info({ uuid: req.ctx.uuid, messsage: orgShortName + ' organization does not exist.' }) - return res.status(404).json(error.orgDnePathParam(orgShortName)) - } - if (orgShortName !== requesterShortName && !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 userRegistryRepo = req.ctx.repositories.getRegistryUserRepository() + const orgRegistryRepo = req.ctx.repositories.getRegistryOrgRepository() - const oldUser = await userRepo.findOneByUserNameAndOrgUUID(username, orgUUID) - if (!oldUser) { - logger.info({ uuid: req.ctx.uuid, messsage: username + ' user does not exist.' }) - return res.status(404).json(error.userDne(username)) - } + try { + const isSecretariatLeg = await orgRepo.isSecretariat(requesterShortName, { session }) + const isSecretariatReg = await orgRegistryRepo.isSecretariat(requesterShortName, { session }) + const isSecretariat = isSecretariatLeg && isSecretariatReg - const isAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName) - // check if the user is not the requester or if the requester is not a secretariat - if ((orgShortName !== requesterShortName || username !== requesterUsername) && !isSecretariat) { - // check if the requester is not and admin; if admin, the requester must be from the same org as the user - if (!isAdmin || (isAdmin && orgShortName !== requesterShortName)) { - 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.' }) - return res.status(403).json(error.notSameUserOrSecretariat()) + const orgUUID = await orgRepo.getOrgUUID(orgShortName, { session }) // userUUID may be null if user does not exist + const orgRegUUID = await orgRegistryRepo.getOrgUUID(orgShortName, { session }) + + // check if orgUUID and orgRegUUID are the same + if (orgUUID.toString() !== orgRegUUID.toString()) { + logger.info({ uuid: req.ctx.uuid, message: 'The organization UUID and the organization registry UUID are not the same.' }) + return res.status(500).json(error.internalServerError()) } - } - const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) - oldUser.secret = await argon2.hash(randomKey) // store in db - const user = await userRepo.updateByUserNameAndOrgUUID(oldUser.username, orgUUID, oldUser) - if (user.matchedCount === 0) { - logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + orgShortName + ' organization.' }) - return res.status(404).json(error.userDne(username)) + if (!orgUUID && !orgRegUUID) { + logger.info({ uuid: req.ctx.uuid, messsage: orgShortName + ' organization does not exist.' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } + + if (orgShortName !== requesterShortName && !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 oldUser = await userRepo.findOneByUserNameAndOrgUUID(username, orgUUID, null, { session }) + const oldUserRegistry = await userRegistryRepo.findOneByUserNameAndOrgUUID(username, orgRegUUID) + + if (!oldUser && !oldUserRegistry) { + logger.info({ uuid: req.ctx.uuid, messsage: username + ' user does not exist.' }) + return res.status(404).json(error.userDne(username)) + } + + const isLegAdmin = await userRepo.isAdmin(requesterUsername, requesterShortName, { session }) + const isRegAdmin = await userRegistryRepo.isAdmin(requesterUsername, orgRegUUID, { session }) + const isAdmin = isLegAdmin && isRegAdmin + + // check if the user is not the requester or if the requester is not a secretariat + if ((orgShortName !== requesterShortName || username !== requesterUsername) && !isSecretariat) { + // check if the requester is not and admin; if admin, the requester must be from the same org as the user + if (!isAdmin || (isAdmin && orgShortName !== requesterShortName)) { + 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.' }) + return res.status(403).json(error.notSameUserOrSecretariat()) + } + } + + randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) + oldUser.secret = await argon2.hash(randomKey) // store in db + oldUserRegistry.secret = await argon2.hash(randomKey) // store in db + + const user = await userRepo.updateByUserNameAndOrgUUID(oldUser.username, orgUUID, oldUser, { session }) + const userReg = await userRegistryRepo.updateByUserNameAndOrgUUID(oldUserRegistry.user_id, orgRegUUID, oldUserRegistry, { session }) + + if (user.matchedCount === 0 || userReg.matchedCount === 0) { + logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + orgShortName + ' organization.' }) + return res.status(404).json(error.userDne(username)) + } + await session.commitTransaction() + } catch (error) { + await session.abortTransaction() + throw error + } finally { + session.endSession() } logger.info({ uuid: req.ctx.uuid, message: `The API secret was successfully reset and sent to ${username}` }) diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 9077654d0..c929ea3d2 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -131,7 +131,7 @@ async function validateUser (req, res, next) { // Check if user has active status organization's registry org membership list for (var organization of result.cve_program_org_membership) { if (organization.program_org === orgUUID) { - if (organization.status === 'active') { + if (organization.status === 'active' || organization.status === 'true') { activeInOrg = true } break diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js index 5c7266d32..e2bc12fbc 100644 --- a/src/repositories/registryOrgRepository.js +++ b/src/repositories/registryOrgRepository.js @@ -17,8 +17,8 @@ class RegistryOrgRepository extends BaseRepository { return this.collection.findOne().byUUID(UUID) } - async getOrgUUID (shortName) { - return utils.getOrgUUID(shortName, true) // use registryOrgRepository to find org UUID + async getOrgUUID (shortName, options = {}) { + return utils.getOrgUUID(shortName, true, options) // use registryOrgRepository to find org UUID } async getAllOrgs () { diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js index 352ec99db..6985b326f 100644 --- a/src/repositories/registryUserRepository.js +++ b/src/repositories/registryUserRepository.js @@ -19,16 +19,27 @@ class RegistryUserRepository extends BaseRepository { return this.collection.find() } - async isSecretariat (org) { - return utils.isSecretariat(org, true) + async isSecretariat (org, options = {}) { + return utils.isSecretariat(org, true, options) + } + + async isAdmin (username, orgShortname, options = {}) { + return utils.isAdmin(username, orgShortname, true, options) } async updateByUUID (uuid, user, options = {}) { return this.collection.findOneAndUpdate().byUUID(uuid).updateOne(user).setOptions(options) } - async findOneByUserNameAndOrgUUID (userName, orgUUID) { - return this.collection.findOne().byUserIdAndOrgUUID(userName, orgUUID) + async findOneByUserNameAndOrgUUID (userName, orgUUID, projection = null, options = {}) { + const query = { user_id: userName, 'org_affiliations.org_id': orgUUID } + return this.collection.findOne(query, projection, options) + } + + async updateByUserNameAndOrgUUID (userName, orgUUID, user, options = {}) { + const filter = { user_id: userName, 'org_affiliations.org_id': orgUUID } + const updatePayload = { $set: user } + return this.collection.findOneAndUpdate(filter, updatePayload, options) } async deleteByUUID (uuid) { diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js index 0d1913658..1be45a610 100644 --- a/src/repositories/userRepository.js +++ b/src/repositories/userRepository.js @@ -11,8 +11,8 @@ class UserRepository extends BaseRepository { return utils.getUserUUID(userName, orgUUID, options) } - async isAdmin (username, shortname) { - return utils.isAdmin(username, shortname) + async isAdmin (username, shortname, options = {}) { + return utils.isAdmin(username, shortname, false, options) } async isAdminUUID (username, orgUUID) { @@ -28,11 +28,14 @@ class UserRepository extends BaseRepository { } async findOneByUserNameAndOrgUUID (userName, orgUUID, projection = null, options = {}) { - return this.collection.findOne().byUserNameAndOrgUUID(userName, orgUUID) + const query = { username: userName, org_UUID: orgUUID } + return this.collection.findOne(query, projection, options) } async updateByUserNameAndOrgUUID (username, orgUUID, user, options = {}) { - return this.collection.findOneAndUpdate().byUserNameAndOrgUUID(username, orgUUID).updateOne(user).setOptions(options) + const filter = { username: username, org_UUID: orgUUID } + const updatePayload = { $set: user } + return this.collection.findOneAndUpdate(filter, updatePayload, options) } async getAllUsers () { diff --git a/src/utils/utils.js b/src/utils/utils.js index 8aecdfe77..47dfbabd7 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -51,17 +51,17 @@ async function getUserUUID (userIdentifier, orgUUID, useRegistry = false, option return userDocument ? userDocument.UUID : null } -async function isSecretariat (shortName, useRegistry = false) { +async function isSecretariat (shortName, useRegistry = false, options = {}) { let result = false let orgUUID = null let secretariats = [] const CONSTANTS = getConstants() if (useRegistry) { - orgUUID = await getOrgUUID(shortName, useRegistry) // may be null if org does not exists + orgUUID = await getOrgUUID(shortName, useRegistry, options) // may be null if org does not exists secretariats = await RegistryOrg.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) } else { - orgUUID = await getOrgUUID(shortName) // may be null if org does not exists + orgUUID = await getOrgUUID(shortName, false, options) // may be null if org does not exists secretariats = await Org.find({ 'authority.active_roles': { $in: [CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT] } }) } @@ -109,13 +109,13 @@ async function isBulkDownload (shortName) { return result // org does not have bulk download as a role } -async function isAdmin (requesterUsername, requesterShortName) { +async function isAdmin (requesterUsername, requesterShortName, isRegistry = false, options = {}) { let result = false const CONSTANTS = getConstants() - const requesterOrgUUID = await getOrgUUID(requesterShortName) // may be null if org does not exists + const requesterOrgUUID = await getOrgUUID(requesterShortName, isRegistry, options) // may be null if org does not exists if (requesterOrgUUID) { - const user = await User.findOne().byUserNameAndOrgUUID(requesterUsername, requesterOrgUUID) + const user = isRegistry ? await RegistryUser.findOne().byUserNameAndOrgUUID(requesterUsername, requesterShortName) : await User.findOne().byUserNameAndOrgUUID(requesterUsername, requesterShortName) if (user) { result = user.authority.active_roles.includes(CONSTANTS.USER_ROLE_ENUM.ADMIN)