From a7305f9662e99610ddf5a84a3991dee73a700565 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 17 Mar 2025 15:32:02 -0400 Subject: [PATCH 01/60] 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 6c99c53560ed4cbc12d6c705bc1b58ecc082b9d1 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 7 Apr 2025 12:17:13 -0400 Subject: [PATCH 02/60] [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 40e7960116b62cd0375d6b1ce4581e976df265c0 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 21 Apr 2025 09:58:52 -0400 Subject: [PATCH 03/60] [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 aa22b17d14b75d50f85598aae2bdbdcb75ecf8e8 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:20:03 -0400 Subject: [PATCH 04/60] 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 7eddb02c1172f94f6aa8a327ad2e79ccfa770f01 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:21:03 -0400 Subject: [PATCH 05/60] 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 1a1495d97b04d388699685fab86da40033b13c45 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:21:30 -0400 Subject: [PATCH 06/60] 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 60d87bcfdebebe1d06fdfb5770a58de506f0e7fe Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 16:15:32 -0400 Subject: [PATCH 07/60] 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 43e1887e7..8f99be68f 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 af5d04700..58bd81ef8 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 fc00d4b695d8a3bdb3e55c259d86745cfdfd448a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 10:17:48 -0400 Subject: [PATCH 08/60] 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 8f99be68f..cadc418b1 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 e68e460e6f1144f56033a04488ea02c8cb9b3cbe Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 5 May 2025 16:40:43 -0400 Subject: [PATCH 09/60] 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 da339b4f5f3d57ef28e50301f8f8694186f7ed19 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 5 May 2025 16:43:11 -0400 Subject: [PATCH 10/60] 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 278956be405dcfce52dcac179b71683c268e67c2 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 14:26:37 -0400 Subject: [PATCH 11/60] 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 2d514909e319a0a4892c97639a012eb8d15be49c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 15:13:44 -0400 Subject: [PATCH 12/60] 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 dec23301a675f4e58bd99b1c4ed671be7e883996 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 12:30:53 -0400 Subject: [PATCH 13/60] 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 6deaa157784ed98452ae4580c883c141e07d7255 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 14:02:16 -0400 Subject: [PATCH 14/60] 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 8b827a3f16da13d3cbcb6c1f694b77c13ba610d9 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 15:13:10 -0400 Subject: [PATCH 15/60] 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 9e8b0e812e72f3d464d1ac1e04b8a2de83196791 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 15:50:36 -0400 Subject: [PATCH 16/60] 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 cadc418b1..68b928a49 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 df0e75b3cb571bff67cbf9a16fbee0bcf1cabbb1 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 13 May 2025 14:11:08 -0400 Subject: [PATCH 17/60] 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 1925ee6f767f79035493afc8ad4330315aa56ca6 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 13 May 2025 15:21:50 -0400 Subject: [PATCH 18/60] 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 68b928a49..e820c2a1d 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 f6b4b6042bf2976e4977c197f7489b6669092506 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 14 May 2025 10:55:15 -0400 Subject: [PATCH 19/60] 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 e820c2a1d..8cdf9c4e7 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 396374b4337a6af2cc09664c3233fd744ae0802d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 15 May 2025 16:56:39 -0400 Subject: [PATCH 20/60] 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 8cdf9c4e7..212afd295 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 d1a094b57..be17abc6e 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) { @@ -825,6 +905,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 671b55498dffafed2b8b52acb36aa802716e8214 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 19 May 2025 15:11:41 -0400 Subject: [PATCH 21/60] 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 be17abc6e..55024ede0 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 (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 (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.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 - } + legOrg.inUse = false + regOrg.inUse = false + const sharedUuid = uuid.v4() + legOrg.UUID = sharedUuid + regOrg.UUID = sharedUuid - 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 1c298185cb6fd27a8d59a6d7ed643139cfaeabf5 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 10:12:31 -0400 Subject: [PATCH 22/60] working state --- src/controller/org.controller/index.js | 4 +- .../org.controller/org.controller.js | 266 +++++++++++++----- .../org.controller/org.middleware.js | 4 +- src/repositories/baseRepository.js | 19 +- src/repositories/orgRepository.js | 19 +- 5 files changed, 236 insertions(+), 76 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 55024ede0..e65a9513e 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 @@ -430,105 +431,233 @@ async function createOrg (req, res, next) { * Called by PUT /api/org/{shortname} **/ 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 } - }) + } - // updating the org's roles - if (org) { - const roles = org.authority.active_roles + handlers['active_roles.add'] = () => { + const rolesFromQuery = queryParams['active_roles.add'] + if (rolesFromQuery) (Array.isArray(rolesFromQuery) ? rolesFromQuery : [rolesFromQuery]).forEach(r => addRolesCollector.push(r)) + } + + 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.n === 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() } } @@ -915,6 +1044,7 @@ async function resetSecret (req, res, next) { } function setAggregateOrgObj (query) { + console.log('CRITICAL DEBUG: Query object received by setAggregateOrgObj:', JSON.stringify(query)) return [ { $match: query 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 2d25cf5f8ffe54f5b3eb8c1b0897a9ef4c3af2a4 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:22:01 -0400 Subject: [PATCH 23/60] 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 e65a9513e..ed3efb52a 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 198c3277c9b2ceacc2c202ba76fe6228176cf1df Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:26:34 -0400 Subject: [PATCH 24/60] 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 9e187515bdbc8c1089b838fa8b9d5d4584f7f9d8 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:49:59 -0400 Subject: [PATCH 25/60] 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 ed3efb52a..bb165d7b3 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 bee70b5aafed9f4ba82050136efb8e99a447b91c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 13:52:50 -0400 Subject: [PATCH 26/60] 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 bb165d7b3..8bef235df 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 aeea28c8e6927b35f36381c218bf85960b02c186 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 14:53:04 -0400 Subject: [PATCH 27/60] 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 8bef235df..a31b6e3f9 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 f15f8bd54044c4522b91399f08754836fe7db722 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 15:34:26 -0400 Subject: [PATCH 28/60] 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 b9454c0dde4dc7bb3032f650d753f83231c353bc Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 15:50:53 -0400 Subject: [PATCH 29/60] 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 a31b6e3f9..729489848 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 } @@ -1143,7 +1145,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 69bc7a4edc9ad3e3828022e5f8a9a83fe2ed3a84 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 17:43:34 -0400 Subject: [PATCH 30/60] Get secret is now backwards compatible. --- src/controller/org.controller/index.js | 2 + .../org.controller/org.controller.js | 101 ++++++++++++------ 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, 103 insertions(+), 48 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 729489848..f1b96c93e 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 @@ -1016,47 +1018,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) { + 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()) + } + + 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()) + 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 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) { - 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)) + 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) From 4592aedb4972236701d38304fceca77807ce17ef Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 17 Mar 2025 15:32:02 -0400 Subject: [PATCH 31/60] 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 1c3fe65e9e8ae946ace5edbb5ceb5aac14584431 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 7 Apr 2025 12:17:13 -0400 Subject: [PATCH 32/60] [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 addafc24f33a4b9ff2010d719d8b93b1be46d3df Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 21 Apr 2025 09:58:52 -0400 Subject: [PATCH 33/60] [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 e691ab5d5e9f8c8d5edf2abe56909ab919af525c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:20:03 -0400 Subject: [PATCH 34/60] 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 295c5a6bd8eb4a8b913bad839d67a62a8fbe389c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:21:03 -0400 Subject: [PATCH 35/60] 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 72f9bbfcb4b9c3ae346034aa7e5161cb7d4bbbed Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 15:21:30 -0400 Subject: [PATCH 36/60] 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 cab5e20087dfb3cdad0ee58771ae52c5c49c7da8 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 21 Apr 2025 16:15:32 -0400 Subject: [PATCH 37/60] 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 b09e9d602f8e2da13b817546c2eaaf64baa723ee Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 10:17:48 -0400 Subject: [PATCH 38/60] 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 8aa543c3c824c00e200fd5522f60960c3bf86628 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 5 May 2025 16:40:43 -0400 Subject: [PATCH 39/60] 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 5721c9f12a7a0b95e03a8a0de64afcd44ce0119f Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 5 May 2025 16:43:11 -0400 Subject: [PATCH 40/60] 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 97b8a464cd62ee6f115ad780744d5fae785e9b5f Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 14:26:37 -0400 Subject: [PATCH 41/60] 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 06a73dff81084f30c90e8104cb236f045ce57004 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 22 Apr 2025 15:13:44 -0400 Subject: [PATCH 42/60] 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 f4bdc151c04832ce2a8eb1cebe5e3083866e12b3 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 12:30:53 -0400 Subject: [PATCH 43/60] 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 0cf59a5f5e54ba06eebd5e91b227d5804322e705 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 14:02:16 -0400 Subject: [PATCH 44/60] 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 db60bfac663a39d2fbc4dc0eff35e3e08cad88af Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 15:13:10 -0400 Subject: [PATCH 45/60] 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 87352f116b347cb1f1207a06d467fa4082eac501 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 6 May 2025 15:50:36 -0400 Subject: [PATCH 46/60] 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 abdc658a41aca1b34f54518a8c18e81f79538ed4 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 13 May 2025 14:11:08 -0400 Subject: [PATCH 47/60] 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 3d12cf28d926e35a10d44615844906ba7ba7c268 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 13 May 2025 15:21:50 -0400 Subject: [PATCH 48/60] 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 a1254e955d7ab7b17a610f960e2d5d59e35a7921 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 14 May 2025 10:55:15 -0400 Subject: [PATCH 49/60] 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 f103d0172cf6de4412cf48f7df9da9f06a6d43e1 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 15 May 2025 16:56:39 -0400 Subject: [PATCH 50/60] 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 e22ee0c90069a56aa2b8833548e7eb9d1d550e8a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 19 May 2025 15:11:41 -0400 Subject: [PATCH 51/60] 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 dfb4d984aa2805ff049d50477c3dc8e6b23ea1a5 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 10:12:31 -0400 Subject: [PATCH 52/60] 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 2862c7113f650326a778176416ed5e1f66f51c04 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:22:01 -0400 Subject: [PATCH 53/60] 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 a6fd4d5460cc9200ea0af9ad5bb06ed46e066d42 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:26:34 -0400 Subject: [PATCH 54/60] 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 4db3eeab9d5fcfc13ff8e83d7e84844e3652d742 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 28 May 2025 16:49:59 -0400 Subject: [PATCH 55/60] 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 e1bd6e87fc34bb493783e68e97814cd1fb7c6a20 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 13:52:50 -0400 Subject: [PATCH 56/60] 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 16a234ba51e2c19b5ab33b25c7b6b4e24df55944 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 14:53:04 -0400 Subject: [PATCH 57/60] 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 aa24567f08395e9ef077fffce572222c0e27b953 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 15:34:26 -0400 Subject: [PATCH 58/60] 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 828fbd11425cdabbd530813ed9c9f4b35cdab22d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 15:50:53 -0400 Subject: [PATCH 59/60] 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 5e21d7832d4a95ef369545951510cd7148341614 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 29 May 2025 17:43:34 -0400 Subject: [PATCH 60/60] Get secret is now backwards compatible. --- src/controller/org.controller/index.js | 2 + .../org.controller/org.controller.js | 133 +++++++++++------- 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, 119 insertions(+), 64 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..8a61db65a 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -19,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() const isRegistry = req.query.registry === 'true' @@ -67,7 +67,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 isRegistry = req.query.registry === 'true' @@ -112,7 +112,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() const isRegistry = req.query.registry === 'true' @@ -168,12 +168,14 @@ 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 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 @@ -208,7 +210,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 isRegistry = req.query.registry === 'true' const orgShortName = req.ctx.org @@ -259,7 +261,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 @@ -456,7 +458,7 @@ 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 @@ -697,7 +699,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 @@ -812,7 +814,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 @@ -1023,48 +1025,85 @@ 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) { + 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}` }) @@ -1082,7 +1121,7 @@ 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 [ { @@ -1102,7 +1141,7 @@ function setAggregateOrgObj(query) { ] } -function setAggregateRegistryOrgObj(query) { +function setAggregateRegistryOrgObj (query) { return [ { $match: query @@ -1134,7 +1173,7 @@ function setAggregateRegistryOrgObj(query) { ] } -function setAggregateUserObj(query) { +function setAggregateUserObj (query) { return [ { $match: query @@ -1153,7 +1192,7 @@ function setAggregateUserObj(query) { } ] } -function setAggregateRegistryUserObj(query) { +function setAggregateRegistryUserObj (query) { return [ { $match: query @@ -1175,7 +1214,7 @@ function setAggregateRegistryUserObj(query) { } ] } -function parseUserName(newUser) { +function parseUserName (newUser) { if (newUser.name) { if (!newUser.name.first) { newUser.name.first = '' 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)