From 5ce50712d925ed26da052269d0696d9505b4a691 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 10 Nov 2025 14:42:49 -0500 Subject: [PATCH 01/39] Handle conversations in registryOrg create/update --- .../registry-org.controller.js | 15 ++++++++++++--- src/repositories/conversationRepository.js | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 638650b5..2a38de08 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -115,7 +115,8 @@ async function createOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getBaseOrgRepository() - const body = req.ctx.body + const conversationRepo = req.ctx.repositories.getConversationRepository() + const { conversation, ...body } = req.ctx.body let createdOrg // Do not allow the user to pass in a UUID @@ -125,7 +126,7 @@ async function createOrg (req, res, next) { try { session.startTransaction() - const result = repo.validateOrg(req.ctx.body, { session }) + const result = repo.validateOrg(body, { session }) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) await session.abortTransaction() @@ -153,6 +154,9 @@ async function createOrg (req, res, next) { // Create the org – repo.createOrg will handle field mapping createdOrg = await repo.createOrg(body, { session, upsert: true }) + // Handle conversation + await conversationRepo.processConversationHistory(conversation, createdOrg.UUID, { session }) + await session.commitTransaction() } catch (createErr) { await session.abortTransaction() @@ -198,7 +202,8 @@ async function updateOrg (req, res, next) { const session = await mongoose.startSession() const shortName = req.ctx.params.shortname const repo = req.ctx.repositories.getBaseOrgRepository() - const body = req.ctx.body + const conversationRepo = req.ctx.repositories.getConversationRepository() + const { conversation, ...body } = req.ctx.body let updatedOrg try { @@ -228,6 +233,10 @@ async function updateOrg (req, res, next) { } updatedOrg = await repo.updateOrgFull(shortName, body, { session }) + + // Handle conversation + await conversationRepo.processConversationHistory(conversation, updatedOrg.UUID, { session }) + await session.commitTransaction() } catch (updateErr) { await session.abortTransaction() diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 1844718c..63c206dc 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -72,6 +72,22 @@ class ConversationRepository extends BaseRepository { const result = await conversation.save(options) return result.toObject() } + + // Takes in a list of conversations representing the conversation history for + // an org and creates/updates the objects as necessary + async processConversationHistory (conversationList, targetUUID, options = {}) { + const promises = conversationList.map(convo => { + return (async () => { + if (convo.UUID) return await this.updateConversation(convo, convo.UUID, options) + const newConvo = { + ...convo, + target_uuid: targetUUID + } + return await this.createConversation(newConvo, options) + })() + }) + return await Promise.all(promises) + } } module.exports = ConversationRepository From 8329b767e2c9fe5cfa3984c1fbe7b9683da56638 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 10 Nov 2025 15:21:00 -0500 Subject: [PATCH 02/39] Joint approval pass --- src/constants/index.js | 2 + .../registry-org.controller.js | 6 +- .../registry-user.controller.js | 2 - .../review-object.controller/index.js | 1 + .../review-object.controller.js | 49 +++++--- src/model/registry-org.js | 119 ------------------ src/model/registry-user.js | 111 ---------------- src/repositories/baseOrgRepository.js | 111 ++++++++++++---- src/repositories/registryOrgRepository.js | 104 --------------- src/repositories/registryUserRepository.js | 83 ------------ src/repositories/repositoryFactory.js | 12 -- src/repositories/reviewObjectRepository.js | 46 +++++-- src/scripts/populate.js | 4 +- .../review-object/reviewObjectTest.js | 22 ++-- 14 files changed, 172 insertions(+), 500 deletions(-) delete mode 100644 src/model/registry-org.js delete mode 100644 src/model/registry-user.js delete mode 100644 src/repositories/registryOrgRepository.js delete mode 100644 src/repositories/registryUserRepository.js diff --git a/src/constants/index.js b/src/constants/index.js index d06ad3e0..4b519dc0 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,6 +44,8 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name'], + JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name'], USER_ROLE_ENUM: { ADMIN: 'ADMIN' }, diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 638650b5..5a75e25a 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -198,11 +198,15 @@ async function updateOrg (req, res, next) { const session = await mongoose.startSession() const shortName = req.ctx.params.shortname const repo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() const body = req.ctx.body let updatedOrg try { session.startTransaction() + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) const org = await repo.findOneByShortName(shortName) if (!org) { logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated because it does not exist.' }) @@ -227,7 +231,7 @@ async function updateOrg (req, res, next) { return res.status(400).json(error.duplicateShortname(body?.short_name)) } - updatedOrg = await repo.updateOrgFull(shortName, body, { session }) + updatedOrg = await repo.updateOrgFull(shortName, body, { session }, false, requestingUserUUID, isAdmin, false) await session.commitTransaction() } catch (updateErr) { await session.abortTransaction() diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 7261bda2..5a39f6f5 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -3,8 +3,6 @@ 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') -const RegistryOrg = require('../../model/registry-org') const errors = require('../user.controller/error') const error = new errors.UserControllerError() diff --git a/src/controller/review-object.controller/index.js b/src/controller/review-object.controller/index.js index e13cb5b3..d99c843b 100644 --- a/src/controller/review-object.controller/index.js +++ b/src/controller/review-object.controller/index.js @@ -5,6 +5,7 @@ const mw = require('../../middleware/middleware') router.get('/review/org/:identifier', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getReviewObjectByOrgIdentifier) router.get('/review/orgs', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getAllReviewObjects) router.put('/review/org/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.updateReviewObjectByReviewUUID) +router.put('/review/org/:uuid/approve', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.approveReviewObject) router.post('/review/org/', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.createReviewObject) module.exports = router diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index 3ef73148..1b515690 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -1,5 +1,6 @@ const validateUUID = require('uuid').validate +const mongoose = require('mongoose') async function getReviewObjectByOrgIdentifier (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() const identifier = req.params.identifier @@ -26,6 +27,32 @@ async function getAllReviewObjects (req, res, next) { return res.status(200).json(value) } +async function approveReviewObject (req, res, next) { + const repo = req.ctx.repositories.getReviewObjectRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() + const UUID = req.params.uuid + const session = await mongoose.startSession() + let value + + try { + session.startTransaction() + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + + value = await repo.approveReviewOrgObject(UUID, requestingUserUUID, { session }) + await session.commitTransaction() + } catch (updateErr) { + await session.abortTransaction() + throw updateErr + } finally { + await session.endSession() + } + + if (!value) { + return res.status(404).json({ message: `No review object found with UUID ${UUID}` }) + } + return res.status(200).json(value) +} + async function updateReviewObjectByReviewUUID (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() const UUID = req.params.uuid @@ -47,27 +74,8 @@ async function updateReviewObjectByReviewUUID (req, res, next) { async function createReviewObject (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() const body = req.body - if (body.uuid) { - return res.status(400).json({ message: 'Do not pass in a uuid key when creating a review object' }) - } - - if (!body.target_object_uuid) { - return res.status(400).json({ message: 'Missing required field target_object_uuid' }) - } - - if (!body.new_review_data) { - return res.status(400).json({ message: 'Missing required field new_review_data' }) - } - - // Validate the data going into "new_review_data" - const result = orgRepo.validateOrg(body.new_review_data) - if (!result.isValid) { - return res.status(400).json({ message: 'Invalid new_review_data', errors: result.errors }) - } - const value = await repo.createReviewOrgObject(body) if (!value) { @@ -79,5 +87,6 @@ module.exports = { getReviewObjectByOrgIdentifier, getAllReviewObjects, updateReviewObjectByReviewUUID, - createReviewObject + createReviewObject, + approveReviewObject } diff --git a/src/model/registry-org.js b/src/model/registry-org.js deleted file mode 100644 index 4e44f491..00000000 --- a/src/model/registry-org.js +++ /dev/null @@ -1,119 +0,0 @@ -const mongoose = require('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', 'ADP'] - }, - authority: { - active_roles: [String] - }, - 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 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) { - return this.where({ short_name: shortName }) -} - -RegistryOrgSchema.query.byUUID = function (uuid) { - return this.where({ UUID: uuid }) -} - -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 }).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) - item.reports_to = org ? org.toObject() : item.reports_to // Return the org object if found, otherwise return the UUID - } - } - - 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 }) - -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 deleted file mode 100644 index 8029a377..00000000 --- a/src/model/registry-user.js +++ /dev/null @@ -1,111 +0,0 @@ -const mongoose = require('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: String, - email: String, - phone: String - }], - cve_program_org_membership: [{ - program_org: String, - roles: { - 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 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 }) -} - -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) { - 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.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 }) - -RegistryUserSchema.plugin(aggregatePaginate) - -// Cursor pagination -RegistryUserSchema.plugin(MongoPaging.mongoosePlugin) -const RegistryUser = mongoose.model('RegistryUser-Old', RegistryUserSchema) -module.exports = RegistryUser diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 04c470e9..c817a6f3 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -323,18 +323,19 @@ class BaseOrgRepository extends BaseRepository { * * @returns {Promise} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values. */ - async updateOrg (shortName, incomingParameters, options = {}, isLegacyObject = false, requestingUserUUID = null) { + async updateOrg (shortName, incomingParameters, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) { const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') // If we get here, we know the org exists const legacyOrgRepo = new OrgRepository() + const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) const registryOrg = await this.findOneByShortName(shortName, options) // Both legacy and registry - if (shortName && typeof shortName === 'string' && shortName.trim() !== '') { - registryOrg.short_name = incomingParameters?.new_short_name ?? registryOrg.short_name - legacyOrg.short_name = incomingParameters?.new_short_name ?? legacyOrg.short_name + if (incomingParameters?.new_short_name) { + registryOrg.short_name = incomingParameters.new_short_name + legacyOrg.short_name = incomingParameters.new_short_name } registryOrg.long_name = incomingParameters?.name ?? registryOrg.long_name @@ -351,26 +352,40 @@ class BaseOrgRepository extends BaseRepository { registryOrg.authority = finalRoles _.set(legacyOrg, 'authority.active_roles', finalRoles) + const directRegistryKeys = [ + 'root_or_tlr', + 'charter_or_scope', + 'disclosure_policy', + 'product_list', + 'oversees', + 'reports_to', + 'contact_info' // Handles all nested contact_info fields automatically + ] + + // Create a patch object by picking only the defined, relevant keys + // We filter out undefined values so _.merge doesn't overwrite existing fields with undefined + const registryUpdates = _.omitBy( + _.pick(incomingParameters, directRegistryKeys), + _.isUndefined + ) + + // Apply the patch object. + _.merge(registryOrg, registryUpdates) + // Registry Only Stuff // Only a CNA object can have quota - if (registryOrg.__t === 'CNAOrg') { - registryOrg.hard_quota = incomingParameters?.id_quota ?? registryOrg.hard_quota + if (registryOrg.__t === 'CNAOrg' && incomingParameters?.id_quota !== undefined) { + registryOrg.hard_quota = incomingParameters.id_quota } - registryOrg.root_or_tlr = incomingParameters?.root_or_tlr ?? registryOrg.root_or_tlr - registryOrg.charter_or_scope = incomingParameters?.charter_or_scope ?? registryOrg.charter_or_scope - registryOrg.disclosure_policy = incomingParameters?.disclosure_policy ?? registryOrg.disclosure_policy - registryOrg.product_list = incomingParameters?.product_list ?? registryOrg.product_list - - registryOrg.oversees = incomingParameters?.oversees ?? registryOrg.oversees - registryOrg.reports_to = incomingParameters?.reports_to ?? registryOrg.reports_to; - - ['contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website'].forEach(field => { - _.set(registryOrg, field, _.get(incomingParameters, field, _.get(registryOrg, field, ''))) - }) + const legacyUpdates = {} // legacy Only Stuff - _.set(legacyOrg, 'policies.id_quota', (incomingParameters?.id_quota ?? legacyOrg.policies.id_quota)) + if (incomingParameters.id_quota !== undefined) { + _.set(legacyUpdates, 'policies.id_quota', incomingParameters.id_quota) + } + + _.merge(legacyOrg, legacyUpdates) // Save changes await registryOrg.save({ options }) @@ -405,6 +420,25 @@ class BaseOrgRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptRegistryOrg) } + getJointApprovalFields (orgObjectOriginal, orgObjectUpdated, isLegacyObject = false) { + // Get the list of fields that require joint approval + let jointApprovalFields + if (isLegacyObject) { + jointApprovalFields = getConstants().JOINT_APPROVAL_FIELDS_LEGACY + } else { + jointApprovalFields = getConstants().JOINT_APPROVAL_FIELDS + } + + // Filter the list to find only fields that have changed + const changedFields = _.filter(jointApprovalFields, field => { + // Check if the value in the original object is different from the updated object + return _.get(orgObjectOriginal, field) !== _.get(orgObjectUpdated, field) + }) + + // Return the array of fields that had changes (will be empty if none changed) + return changedFields + } + /** * @async * @function updateOrgFull @@ -418,12 +452,18 @@ class BaseOrgRepository extends BaseRepository { * * @returns {Promise} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values. */ - async updateOrgFull (shortName, incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null) { + async updateOrgFull (shortName, incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) { + // TODO: Fix these imports, remove the circular imports const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') + const ReviewObjectRepository = require('./reviewObjectRepository') + const legacyOrgRepo = new OrgRepository() + const reviewObjectRepo = new ReviewObjectRepository() const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) const registryOrg = await this.findOneByShortName(shortName, options) + // check to see if there is a review object: + const reviewObject = await reviewObjectRepo.getOrgReviewObjectByOrgUUID(registryOrg.UUID) let legacyObjectRaw let registryObjectRaw @@ -435,12 +475,35 @@ class BaseOrgRepository extends BaseRepository { legacyObjectRaw = this.convertRegistryToLegacy(incomingOrg) } - const updatedLegacyOrg = _.merge(legacyOrg, legacyObjectRaw) - const updatedRegistryOrg = _.merge(registryOrg, registryObjectRaw) + // Checking for joint approval fields + const jointApprovalFieldsRegistry = this.getJointApprovalFields({}, registryOrg, registryObjectRaw) + const jointApprovalFieldsLegacy = this.getJointApprovalFields({}, legacyOrg, legacyObjectRaw, true) + let updatedRegistryOrg = null + let updatedLegacyOrg = null + let jointApprovalRegistry = null + // If there are no joint approval fields, merge the original and updated objects. Otherwise, update the registry object and legacy object separately considering joint approval. + if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { + updatedLegacyOrg = _.merge(legacyOrg, legacyObjectRaw) + updatedRegistryOrg = _.merge(registryOrg, registryObjectRaw) + } else { + // write the joint approval to the database + jointApprovalRegistry = _.merge({}, registryOrg.toObject(), registryObjectRaw) + if (reviewObject) { + await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, { options }) + } else { + await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, { options }) + } + updatedRegistryOrg = _.merge(registryOrg, _.omit(registryObjectRaw, jointApprovalFieldsRegistry)) + updatedLegacyOrg = _.merge(legacyOrg, _.omit(legacyObjectRaw, jointApprovalFieldsLegacy)) + } + + try { + await updatedLegacyOrg.save({ options }) + await updatedRegistryOrg.save({ options }) + } catch (error) { + throw new Error(`Failed to update organization ${shortName}. Error: ${error.message}`) + } - // Save changes - await updatedLegacyOrg.save({ options }) - await updatedRegistryOrg.save({ options }) if (isLegacyObject) { const plainJavascriptLegacyOrg = updatedLegacyOrg.toObject() delete plainJavascriptLegacyOrg.__v diff --git a/src/repositories/registryOrgRepository.js b/src/repositories/registryOrgRepository.js deleted file mode 100644 index 3badf2d1..00000000 --- a/src/repositories/registryOrgRepository.js +++ /dev/null @@ -1,104 +0,0 @@ -const BaseRepository = require('./baseRepository') -const RegistryOrg = require('../model/registry-org') -const utils = require('../utils/utils') - -class RegistryOrgRepository extends BaseRepository { - constructor () { - super(RegistryOrg) - } - - 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) { - return this.collection.findOne().byUUID(UUID) - } - - async getOrgUUID (shortName, options = {}) { - return utils.getOrgUUID(shortName, true, options) // use registryOrgRepository to find org UUID - } - - async getAllOrgs () { - return this.collection.find() - } - - async isSecretariat (shortName, options = {}) { - return utils.isSecretariat(shortName, true, options) - } - - async updateByUUID (uuid, org, 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) { - return this.collection.deleteOne({ UUID: uuid }) - } - - async removeUserFromOrgList (registryOrgUUID, userUUIDToRemove, isAdmin = false, options = {}) { - if (!registryOrgUUID || !userUUIDToRemove) { - throw new Error('RegistryOrg UUID and User UUID to remove are required for removeUserFromOrgList.') - } - - const filter = { UUID: registryOrgUUID } - const updateOperation = { - $pull: { - users: userUUIDToRemove - } - } - - if (isAdmin) { - updateOperation.$pull['contact_info.admins'] = userUUIDToRemove - } - - try { - const result = await this.collection.updateOne(filter, updateOperation, options) - if (result.matchedCount === 0) { - console.warn(`removeUserFromOrgList: No RegistryOrg found with UUID '${registryOrgUUID}'. User UUID not removed.`) - } else if (result.modifiedCount === 0) { - console.info(`removeUserFromOrgList: User UUID '${userUUIDToRemove}' was not found in relevant lists for RegistryOrg '${registryOrgUUID}', or no change was needed.`) - } - return result - } catch (error) { - console.error(`Error in removeUserFromOrgList for RegistryOrg ${registryOrgUUID}, User ${userUUIDToRemove}:`, error) - throw error - } - } - - async addUserToOrgList (registryOrgUUID, userUUIDToAdd, isAdmin = false, options = {}) { - if (!registryOrgUUID || !userUUIDToAdd) { - throw new Error('RegistryOrg UUID and User UUID to add are required for addUserToOrgList.') - } - - const filter = { UUID: registryOrgUUID } - const updateOperation = { - $addToSet: { - users: userUUIDToAdd - } - } - - if (isAdmin) { - updateOperation.$addToSet['contact_info.admins'] = userUUIDToAdd - } - - try { - const result = await this.collection.updateOne(filter, updateOperation, options) - if (result.matchedCount === 0) { - console.warn(`addUserToOrgList: No RegistryOrg found with UUID '${registryOrgUUID}'. User UUID not added.`) - } else if (result.modifiedCount === 0 && result.matchedCount === 1) { - console.info(`addUserToOrgList: User UUID '${userUUIDToAdd}' was already present in relevant lists for RegistryOrg '${registryOrgUUID}', or no change was needed.`) - } - return result - } catch (error) { - console.error(`Error in addUserToOrgList for RegistryOrg ${registryOrgUUID}, User ${userUUIDToAdd}:`, error) - throw error - } - } -} - -module.exports = RegistryOrgRepository diff --git a/src/repositories/registryUserRepository.js b/src/repositories/registryUserRepository.js deleted file mode 100644 index da5f4b4b..00000000 --- a/src/repositories/registryUserRepository.js +++ /dev/null @@ -1,83 +0,0 @@ -const BaseRepository = require('./baseRepository') -const RegistryUser = require('../model/registry-user') -const utils = require('../utils/utils') - -class RegistryUserRepository extends BaseRepository { - constructor () { - super(RegistryUser) - } - - async getUserUUID (username, orgUUID, options = {}) { - return utils.getUserUUID(username, orgUUID, true, options) - } - - async findOneByUUID (UUID) { - return this.collection.findOne().byUUID(UUID) - } - - async findUsersByOrgUUID (orgUUID, options = {}) { - const filter = { 'org_affiliations.org_id': orgUUID } - return this.collection.countDocuments(filter, options) - } - - async isSecretariat (org, options = {}) { - return utils.isSecretariat(org, true, options) - } - - async isAdmin (username, orgShortname, options = {}) { - return utils.isAdmin(username, orgShortname, true, options) - } - - async isAdminUUID (username, OrgUUID, options = {}) { - return utils.isAdminUUID(username, OrgUUID, true, 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 updateByUUID (uuid, updatePayload, options = {}) { - const filter = { UUID: uuid } - - const updateOperation = { $set: updatePayload } - - return this.collection.findOneAndUpdate(filter, updateOperation, options) - } - - 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 deleteByUUID (uuid) { - return this.collection.deleteOne({ UUID: uuid }) - } - - async addOrgToUserAffiliation (userUUID, orgUUID, options = {}) { - const filter = { UUID: userUUID } - const updateOperation = { - $addToSet: { - org_affiliations: [{ - org_id: orgUUID - }] - } - } - - try { - const result = await this.collection.updateOne(filter, updateOperation, options) - if (result.matchedCount === 0) { - console.warn(`addOrgToUserAffiliation: No ORG found with UUID '${orgUUID}'. User UUID not added.`) - } else if (result.modifiedCount === 0 && result.matchedCount === 1) { - console.info(`addOrgToUserAffiliation: ORG UUID '${orgUUID}' was already present in relevant lists for RegistryUser '${userUUID}', or no change was needed.`) - } - return result - } catch (error) { - console.error(`Error in addOrgToUserAffiliation for RegistryOrg ${orgUUID}, User ${userUUID}:`, error) - throw error - } - } -} - -module.exports = RegistryUserRepository diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 41b16d1b..4750fffe 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -3,8 +3,6 @@ const CveRepository = require('./cveRepository') const CveIdRepository = require('./cveIdRepository') const CveIdRangeRepository = require('./cveIdRangeRepository') const UserRepository = require('./userRepository') -const RegistryUserRepository = require('./registryUserRepository') -const RegistryOrgRepository = require('./registryOrgRepository') const BaseOrgRepository = require('./baseOrgRepository') const BaseUserRepository = require('./baseUserRepository') const ConversationRepository = require('./conversationRepository') @@ -36,16 +34,6 @@ class RepositoryFactory { return repo } - getRegistryUserRepository () { - const repo = new RegistryUserRepository() - return repo - } - - getRegistryOrgRepository () { - const repo = new RegistryOrgRepository() - return repo - } - getBaseOrgRepository () { const repo = new BaseOrgRepository() return repo diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index b96fff4b..95a2ebbe 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -52,12 +52,18 @@ class ReviewObjectRepository extends BaseRepository { return reviewObject || null } - async createReviewOrgObject (body, options = {}) { - console.log('Creating review object for organization:', body.target_object_uuid) - body.uuid = uuid.v4() - const reviewObject = new ReviewObjectModel(body) - const result = await reviewObject.save(options) - return result.toObject() + async createReviewOrgObject (orgBody, options = {}) { + console.log('Creating review object for organization:', orgBody.UUID) + const reviewObjectRaw = { + uuid: uuid.v4(), + target_object_uuid: orgBody.UUID, + status: 'pending', + new_review_data: orgBody || {} + } + + const reviewObject = new ReviewObjectModel(reviewObjectRaw) + await reviewObject.save({ options }) + return reviewObject.toObject() } async updateReviewOrgObject (body, UUID, options = {}) { @@ -67,12 +73,34 @@ class ReviewObjectRepository extends BaseRepository { return null } - // For each item waiting for approval, for testing we are going to just do shortname - reviewObject.new_review_data.short_name = body.new_review_data.short_name || reviewObject.new_review_data.short_name + reviewObject.new_review_data = body - const result = await reviewObject.save(options) + const result = await reviewObject.save({ options }) return result.toObject() } + + async approveReviewOrgObject (UUID, requestingUserUUID, options = {}) { + console.log('Approving review object with UUID:', UUID) + const reviewObject = await this.findOneByUUID(UUID, options) + if (!reviewObject) { + return null + } + + const baseOrgRepository = new BaseOrgRepository() + const org = await baseOrgRepository.findOneByUUID(reviewObject.target_object_uuid) + if (!org) { + return null + } + + // We need to trigger the org to update + await baseOrgRepository.updateOrgFull(org.short_name, reviewObject.new_review_data, { options }, false, requestingUserUUID, false, true) + + reviewObject.status = 'approved' + + await reviewObject.save({ options }) + const result = reviewObject.toObject() + return result + } } module.exports = ReviewObjectRepository diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 69bbb4bf..de72c4e3 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -18,6 +18,7 @@ const Org = require('../model/org') const User = require('../model/user') const BaseOrg = require('../model/baseorg') const BaseUser = require('../model/baseuser') +const ReviewObject = require('../model/reviewobject') const error = new errors.IDRError() @@ -28,7 +29,8 @@ const populateTheseCollections = { User: User, Org: Org, BaseOrg: BaseOrg, - BaseUser: BaseUser + BaseUser: BaseUser, + ReviewObject: ReviewObject } const indexesToCreate = { diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js index 44f0aec5..b1fb3984 100644 --- a/test/integration-tests/review-object/reviewObjectTest.js +++ b/test/integration-tests/review-object/reviewObjectTest.js @@ -6,13 +6,9 @@ chai.use(require('chai-http')) const constants = require('../constants.js') const app = require('../../../src/index.js') -describe('Review Object Controller Integration Tests', () => { +describe.only('Review Object Controller Integration Tests', () => { let orgUUID let reviewUUID - const reviewPayload = { - target_object_uuid: '', - new_review_data: {} - } context('Positive Tests', () => { it('Creates an organization to use for review object tests', async () => { @@ -28,13 +24,13 @@ describe('Review Object Controller Integration Tests', () => { }) it('Creates a review object for the organization', async () => { - reviewPayload.target_object_uuid = orgUUID - reviewPayload.new_review_data = constants.testRegistryOrg2 + const reviewObject = constants.testRegistryOrg2 + reviewObject.UUID = orgUUID const res = await chai .request(app) .post('/api/review/org/') .set({ ...constants.headers }) - .send(reviewPayload) + .send(constants.testRegistryOrg2) expect(res).to.have.status(200) expect(res.body).to.have.property('uuid') expect(res.body).to.have.property('target_object_uuid', orgUUID) @@ -72,16 +68,14 @@ describe('Review Object Controller Integration Tests', () => { }) it('Updates the review object with new short_name', async () => { - const updatePayload = { - new_review_data: constants.testRegistryOrg2 - } - - updatePayload.new_review_data.short_name = 'updated_org' + const reviewObject = constants.testRegistryOrg2 + reviewObject.UUID = orgUUID + reviewObject.short_name = 'updated_org' const res = await chai .request(app) .put(`/api/review/org/${reviewUUID}`) .set({ ...constants.headers }) - .send(updatePayload) + .send(reviewObject) expect(res).to.have.status(200) expect(res.body).to.have.property('uuid', reviewUUID) expect(res.body.new_review_data).to.have.property('short_name', 'updated_org') From 0100b88258f4f2198a9f5caf7fad89b605d06b1f Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 10 Nov 2025 15:52:15 -0500 Subject: [PATCH 03/39] Small fixes for integration --- .../registry-org.controller.js | 14 ++++++++++---- src/repositories/conversationRepository.js | 8 ++++++-- src/scripts/populate.js | 4 +++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index a1b0081d..37583458 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -116,7 +116,7 @@ async function createOrg (req, res, next) { const session = await mongoose.startSession() const repo = req.ctx.repositories.getBaseOrgRepository() const conversationRepo = req.ctx.repositories.getConversationRepository() - const { conversation, ...body } = req.ctx.body + let { conversation, ...body } = req.ctx.body let createdOrg // Do not allow the user to pass in a UUID @@ -155,7 +155,10 @@ async function createOrg (req, res, next) { createdOrg = await repo.createOrg(body, { session, upsert: true }) // Handle conversation - await conversationRepo.processConversationHistory(conversation, createdOrg.UUID, { session }) + if (!conversation || !conversation.length) { + conversation = [] + } + // await conversationRepo.processConversationHistory(conversation, createdOrg.UUID, { session }) await session.commitTransaction() } catch (createErr) { @@ -204,7 +207,7 @@ async function updateOrg (req, res, next) { const repo = req.ctx.repositories.getBaseOrgRepository() const userRepo = req.ctx.repositories.getBaseUserRepository() const conversationRepo = req.ctx.repositories.getConversationRepository() - const { conversation, ...body } = req.ctx.body + let { conversation, ...body } = req.ctx.body let updatedOrg try { @@ -238,7 +241,10 @@ async function updateOrg (req, res, next) { updatedOrg = await repo.updateOrgFull(shortName, body, { session }, false, requestingUserUUID, isAdmin, false) // Handle conversation - await conversationRepo.processConversationHistory(conversation, updatedOrg.UUID, { session }) + if (!conversation || !conversation.length) { + conversation = [] + } + await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, { session }) await session.commitTransaction() } catch (updateErr) { diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 63c206dc..444041ce 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -1,6 +1,7 @@ const uuid = require('uuid') const ConversationModel = require('../model/conversation') const BaseRepository = require('./baseRepository') +const ReviewObjectRepository = require('./reviewObjectRepository') class ConversationRepository extends BaseRepository { constructor () { @@ -75,13 +76,16 @@ class ConversationRepository extends BaseRepository { // Takes in a list of conversations representing the conversation history for // an org and creates/updates the objects as necessary - async processConversationHistory (conversationList, targetUUID, options = {}) { + async processConversationHistory (conversationList, targetShortname, options = {}) { + const reviewObjectRepository = new ReviewObjectRepository() + const reviewObject = await reviewObjectRepository.findOneByOrgShortName(targetShortname) + const promises = conversationList.map(convo => { return (async () => { if (convo.UUID) return await this.updateConversation(convo, convo.UUID, options) const newConvo = { ...convo, - target_uuid: targetUUID + target_uuid: reviewObject.UUID } return await this.createConversation(newConvo, options) })() diff --git a/src/scripts/populate.js b/src/scripts/populate.js index de72c4e3..efd98aa9 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -19,6 +19,7 @@ const User = require('../model/user') const BaseOrg = require('../model/baseorg') const BaseUser = require('../model/baseuser') const ReviewObject = require('../model/reviewobject') +const Conversation = require('../model/conversation') const error = new errors.IDRError() @@ -30,7 +31,8 @@ const populateTheseCollections = { Org: Org, BaseOrg: BaseOrg, BaseUser: BaseUser, - ReviewObject: ReviewObject + ReviewObject: ReviewObject, + Conversation: Conversation } const indexesToCreate = { From 032c4a975857d6ded9141e51637d69a5b6c3408f Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Mon, 10 Nov 2025 17:09:32 -0500 Subject: [PATCH 04/39] Default values for conversation object --- .../registry-org.controller.js | 14 +++----------- src/repositories/conversationRepository.js | 14 +++++++++++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 37583458..608903e2 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -115,8 +115,7 @@ async function createOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getBaseOrgRepository() - const conversationRepo = req.ctx.repositories.getConversationRepository() - let { conversation, ...body } = req.ctx.body + const body = req.ctx.body let createdOrg // Do not allow the user to pass in a UUID @@ -154,12 +153,6 @@ async function createOrg (req, res, next) { // Create the org – repo.createOrg will handle field mapping createdOrg = await repo.createOrg(body, { session, upsert: true }) - // Handle conversation - if (!conversation || !conversation.length) { - conversation = [] - } - // await conversationRepo.processConversationHistory(conversation, createdOrg.UUID, { session }) - await session.commitTransaction() } catch (createErr) { await session.abortTransaction() @@ -207,7 +200,7 @@ async function updateOrg (req, res, next) { const repo = req.ctx.repositories.getBaseOrgRepository() const userRepo = req.ctx.repositories.getBaseUserRepository() const conversationRepo = req.ctx.repositories.getConversationRepository() - let { conversation, ...body } = req.ctx.body + const { conversation, ...body } = req.ctx.body let updatedOrg try { @@ -242,9 +235,8 @@ async function updateOrg (req, res, next) { updatedOrg = await repo.updateOrgFull(shortName, body, { session }, false, requestingUserUUID, isAdmin, false) // Handle conversation if (!conversation || !conversation.length) { - conversation = [] + await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, req.ctx.user, isSecretariat, { session }) } - await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, { session }) await session.commitTransaction() } catch (updateErr) { diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 444041ce..c13cdfc1 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -76,15 +76,23 @@ class ConversationRepository extends BaseRepository { // Takes in a list of conversations representing the conversation history for // an org and creates/updates the objects as necessary - async processConversationHistory (conversationList, targetShortname, options = {}) { + async processConversationHistory (conversationList, targetShortname, user, isSecretariat, options = {}) { const reviewObjectRepository = new ReviewObjectRepository() const reviewObject = await reviewObjectRepository.findOneByOrgShortName(targetShortname) const promises = conversationList.map(convo => { return (async () => { - if (convo.UUID) return await this.updateConversation(convo, convo.UUID, options) + const populatedConvo = { + UUID: convo.UUID || undefined, + author_id: convo.author_id || user.UUID, + author_name: convo.author_name || (isSecretariat ? 'Secretariat' : [user.name.first, user.name.last].join(' ')), + author_role: convo.author_role || (isSecretariat ? 'Secretariat' : 'Partner'), + visibility: !isSecretariat ? 'public' : (convo.visibility || 'private'), + body: convo.body + } + if (populatedConvo.UUID) return await this.updateConversation(populatedConvo, populatedConvo.UUID, options) const newConvo = { - ...convo, + ...populatedConvo, target_uuid: reviewObject.UUID } return await this.createConversation(newConvo, options) From 208893bee6ea1bf8bb07d991a79044ff52e29b00 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 11 Nov 2025 12:16:33 -0500 Subject: [PATCH 05/39] Non sec users can request orgs --- .../org.controller/org.controller.js | 20 +++- .../registry-org.controller/index.js | 2 - .../registry-org.controller.js | 113 ++++++++++++++---- src/repositories/baseOrgRepository.js | 40 ++++++- src/repositories/reviewObjectRepository.js | 6 + 5 files changed, 142 insertions(+), 39 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index ba90751b..850260ce 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -249,7 +249,8 @@ async function registryCreateOrg (req, res, next) { // If we get here, we know we are good to create const userRepo = req.ctx.repositories.getBaseUserRepository() const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, false, requestingUserUUID) + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, false, requestingUserUUID, isSecretariat) await session.commitTransaction() logger.info({ @@ -293,7 +294,10 @@ async function createOrg (req, res, next) { await session.abortTransaction() return res.status(400).json(error.orgExists(body?.short_name)) } - returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, true) + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + const userRepo = req.ctx.repositories.getBaseUserRepository() + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + returnValue = await repo.createOrg(req.ctx.body, { session, upsert: true }, true, requestingUserUUID, isSecretariat) await session.commitTransaction() } catch (error) { @@ -366,7 +370,9 @@ async function registryUpdateOrg (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, false, requestingUserUUID) + const isSecretariat = await orgRepository.isSecretariatByShortName(req.ctx.org, { session }) + const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, false, requestingUserUUID, isAdmin, isSecretariat) responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message const payload = { action: 'update_org', change: `${updatedOrg.short_name} organization was successfully updated.`, org: updatedOrg } @@ -413,9 +419,13 @@ async function updateOrg (req, res, next) { return res.status(403).json(error.duplicateShortname(queryParametersJson.new_short_name)) } - const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, true) - const userRepo = req.ctx.repositories.getBaseUserRepository() + const isSecretariat = await orgRepository.isSecretariatByShortName(req.ctx.org, { session }) + const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + + const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, true, requestingUserUUID, isAdmin, isSecretariat) + responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message const payload = { action: 'update_org', change: `${updatedOrg.short_name} organization was successfully updated.`, org: updatedOrg } payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, updatedOrg.UUID) diff --git a/src/controller/registry-org.controller/index.js b/src/controller/registry-org.controller/index.js index 25bc038a..36dad9aa 100644 --- a/src/controller/registry-org.controller/index.js +++ b/src/controller/registry-org.controller/index.js @@ -213,7 +213,6 @@ router.post('/registryOrg', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, parseError, parsePostParams, controller.CREATE_ORG @@ -300,7 +299,6 @@ router.put('/registryOrg/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, param(['shortname']).isString().trim(), parseError, parsePostParams, diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 608903e2..25ff29c7 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') +const _ = require('lodash') const errors = require('./error') const error = new errors.RegistryOrgControllerError() const validateUUID = require('uuid').validate @@ -115,7 +116,11 @@ async function createOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() const body = req.ctx.body + const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + let createdOrg // Do not allow the user to pass in a UUID @@ -151,7 +156,7 @@ async function createOrg (req, res, next) { } // Create the org – repo.createOrg will handle field mapping - createdOrg = await repo.createOrg(body, { session, upsert: true }) + createdOrg = await repo.createOrg(body, { session, upsert: true }, false, requestingUserUUID, isSecretariat) await session.commitTransaction() } catch (createErr) { @@ -161,17 +166,32 @@ async function createOrg (req, res, next) { await session.endSession() } - const responseMessage = { - message: `${body?.short_name} organization was successfully created.`, - created: createdOrg - } + let responseMessage + let payload + if (isSecretariat) { + responseMessage = { + message: `${body?.short_name} organization was successfully created.`, + created: createdOrg + } - const payload = { - action: 'create_org', - change: `${body?.short_name} organization was successfully created.`, - req_UUID: req.ctx.uuid, - org_UUID: createdOrg.UUID, - org: createdOrg + payload = { + action: 'create_org', + change: `${body?.short_name} organization was successfully created.`, + req_UUID: req.ctx.uuid, + org_UUID: createdOrg.UUID, + org: createdOrg + } + } else { + payload = { + action: 'create_review_org', + change: body?.short_name + ' was successfully requested to be Reviewed.', + req_UUID: req.ctx.uuid + } + + responseMessage = { + message: body?.short_name + ' was successfully received to be reviewed. By using Load ReviewObject data, you can check for a reply from the Secretariat about Joint Approval items.', + created: body?.shortName + } } logger.info(JSON.stringify(payload)) @@ -202,6 +222,7 @@ async function updateOrg (req, res, next) { const conversationRepo = req.ctx.repositories.getConversationRepository() const { conversation, ...body } = req.ctx.body let updatedOrg + let jointApprovalRequired try { session.startTransaction() @@ -209,10 +230,30 @@ async function updateOrg (req, res, next) { const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) const org = await repo.findOneByShortName(shortName) + + // Edge Case: if a user has requested an org, but it is not approved yet, then we need to check to see if if there is a review org for the shortname request. + if (!org) { - logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated because it does not exist.' }) - await session.abortTransaction() - return res.status(404).json(error.orgDnePathParam(shortName)) + // resolve edge case + const reviewRepo = req.ctx.repositories.getReviewObjectRepository() + const reviewOrg = await reviewRepo.getOrgReviewObjectStandaloneByRequestedOrgShortname(shortName, { session }) + + // Eventually we should validate this, but this is a bit tricky. + if (reviewOrg) { + const updateResult = await reviewRepo.updateReviewOrgObject(body, reviewOrg.uuid, { session }) + if (updateResult) { + updatedOrg = reviewOrg + if (conversation && conversation.length) { + // await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, req.ctx.user, isSecretariat, { session }) + } + await session.commitTransaction() + return res.status(200).json({ message: 'Review object updated successfully' }) + } + } else { + logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated because it does not exist.' }) + await session.abortTransaction() + return res.status(404).json(error.orgDnePathParam(shortName)) + } } const result = repo.validateOrg(body, { session }) @@ -233,6 +274,8 @@ async function updateOrg (req, res, next) { } updatedOrg = await repo.updateOrgFull(shortName, body, { session }, false, requestingUserUUID, isAdmin, false) + jointApprovalRequired = _.get(updatedOrg, 'joint_approval_required', false) + _.unset(updatedOrg, 'joint_approval_required') // Handle conversation if (!conversation || !conversation.length) { await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, req.ctx.user, isSecretariat, { session }) @@ -246,20 +289,38 @@ async function updateOrg (req, res, next) { await session.endSession() } - const responseMessage = { - message: `${body?.short_name} organization was successfully updated.`, - updated: updatedOrg - } + if (jointApprovalRequired) { + const responseMessage = { + message: `${body?.short_name} organization was successfully updated, but joint approval is required for some fields. Check the ReviewObject for your org to check for a reply from the Secretariat about Joint Approval items.`, + updated: updatedOrg + } - const payload = { - action: 'update_registry_org', - change: body?.short_name + ' was successfully updated.', - req_UUID: req.ctx.uuid, - org_UUID: await repo.getOrgUUID(req.ctx.org), - org: updatedOrg + const payload = { + action: 'update_registry_org', + change: body?.short_name + 'organization was successfully updated, but joint approval is required for some fields. Check the ReviewObject for your org to check for a reply from the Secretariat about Joint Approval items.', + req_UUID: req.ctx.uuid, + org_UUID: await repo.getOrgUUID(req.ctx.org), + org: updatedOrg + } + + logger.info(JSON.stringify(payload)) + return res.status(200).json(responseMessage) + } else { + const responseMessage = { + message: `${body?.short_name} organization was successfully updated.`, + updated: updatedOrg + } + + const payload = { + action: 'update_registry_org', + change: body?.short_name + ' was successfully updated.', + req_UUID: req.ctx.uuid, + org_UUID: await repo.getOrgUUID(req.ctx.org), + org: updatedOrg + } + logger.info(JSON.stringify(payload)) + return res.status(200).json(responseMessage) } - logger.info(JSON.stringify(payload)) - return res.status(200).json(responseMessage) } catch (err) { next(err) } diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index c817a6f3..4c95a672 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -167,7 +167,7 @@ class BaseOrgRepository extends BaseRepository { * @returns {Promise} A promise that resolves to a plain JavaScript object representing the newly created organization. The format of the returned object (legacy or registry) is determined by the `isLegacyObject` parameter. The object is stripped of internal properties and empty values. * @throws {string} Throws an error if the organization's authority role is not 'SECRETARIAT' or 'CNA'. */ - async createOrg (incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null) { + async createOrg (incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isSecretariat = false) { const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') const CONSTANTS = getConstants() @@ -177,6 +177,8 @@ class BaseOrgRepository extends BaseRepository { let legacyObject = null let registryObject = null const legacyOrgRepo = new OrgRepository() + const ReviewObjectRepository = require('./reviewObjectRepository') + const reviewObjectRepo = new ReviewObjectRepository() // generate a shared uuid const sharedUUID = uuid.v4() @@ -205,13 +207,18 @@ class BaseOrgRepository extends BaseRepository { // Figure out why this is not working.... // registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value)) + // For all of these writes, if we are a secretariat, then we can write directly to the database, otherwise, we write to the review objects // Write - use org type specific model if (registryObjectRaw.authority.includes('SECRETARIAT')) { // Write // testing: registryObjectRaw.authority = 'SECRETARIAT' const SecretariatObjectToSave = new SecretariatOrgModel(registryObjectRaw) - registryObject = await SecretariatObjectToSave.save(options) + if (isSecretariat) { + registryObject = await SecretariatObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else if (registryObjectRaw.authority.includes('CNA')) { // A special case, we should make sure we have the default quota if it is not set if (!registryObjectRaw.hard_quota) { @@ -220,15 +227,27 @@ class BaseOrgRepository extends BaseRepository { } // Write const CNAObjectToSave = new CNAOrgModel(registryObjectRaw) - registryObject = await CNAObjectToSave.save(options) + if (isSecretariat) { + registryObject = await CNAObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else if (registryObjectRaw.authority.includes('ADP')) { registryObjectRaw.hard_quota = 0 const adpObjectToSave = new ADPOrgModel(registryObjectRaw) - registryObject = await adpObjectToSave.save(options) + if (isSecretariat) { + registryObject = await adpObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else if (registryObjectRaw.authority.includes('BULK_DOWNLOAD')) { registryObjectRaw.hard_quota = 0 const bulkDownloadObjectToSave = new BulkDownloadModel(registryObjectRaw) - registryObject = await bulkDownloadObjectToSave.save(options) + if (isSecretariat) { + registryObject = await bulkDownloadObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) + } } else { // eslint-disable-next-line no-throw-literal throw 'dave you screwed up' @@ -269,7 +288,14 @@ class BaseOrgRepository extends BaseRepository { // The legacy way of doing this, the way this is written under the hood there is no other way // This await does not return a value, even though there is a return in it. :shrugg: - await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + if (isSecretariat) { + await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + } + + // If we are not a secretariat, then we need to return the uuid of the review object. + if (!isSecretariat) { + return {} + } if (isLegacyObject) { // This gets us the mongoose object that has all the right data in it, the "legacyObjectRaw" is the custom JSON we are sending. NOT the post written object. @@ -508,6 +534,7 @@ class BaseOrgRepository extends BaseRepository { const plainJavascriptLegacyOrg = updatedLegacyOrg.toObject() delete plainJavascriptLegacyOrg.__v delete plainJavascriptLegacyOrg._id + plainJavascriptLegacyOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) return deepRemoveEmpty(plainJavascriptLegacyOrg) } @@ -531,6 +558,7 @@ class BaseOrgRepository extends BaseRepository { delete plainJavascriptRegistryOrg.__v delete plainJavascriptRegistryOrg._id delete plainJavascriptRegistryOrg.__t + plainJavascriptRegistryOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) return deepRemoveEmpty(plainJavascriptRegistryOrg) } diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 95a2ebbe..dac111e5 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -30,6 +30,12 @@ class ReviewObjectRepository extends BaseRepository { return result.deletedCount } + async getOrgReviewObjectStandaloneByRequestedOrgShortname (requestedOrgShortName, options = {}) { + const reviewObject = await ReviewObjectModel.findOne({ 'new_review_data.short_name': requestedOrgShortName }, null, options) + + return reviewObject || null + } + async getOrgReviewObjectByOrgShortname (orgShortName, options = {}) { const baseOrgRepository = new BaseOrgRepository() const org = await baseOrgRepository.findOneByShortName(orgShortName) From 327994058592343db6d8fad3a3b73166147b8eb0 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 11 Nov 2025 14:31:51 -0500 Subject: [PATCH 06/39] fixing tests --- .../registry-org.controller.js | 4 +- .../review-object.controller.js | 2 +- src/repositories/baseOrgRepository.js | 4 +- .../review-object/reviewObjectTest.js | 51 +------------------ 4 files changed, 6 insertions(+), 55 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 25ff29c7..ee7a75e8 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -273,11 +273,11 @@ async function updateOrg (req, res, next) { return res.status(400).json(error.duplicateShortname(body?.short_name)) } - updatedOrg = await repo.updateOrgFull(shortName, body, { session }, false, requestingUserUUID, isAdmin, false) + updatedOrg = await repo.updateOrgFull(shortName, body, { session }, false, requestingUserUUID, isAdmin, isSecretariat) jointApprovalRequired = _.get(updatedOrg, 'joint_approval_required', false) _.unset(updatedOrg, 'joint_approval_required') // Handle conversation - if (!conversation || !conversation.length) { + if (conversation && conversation.length) { await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, req.ctx.user, isSecretariat, { session }) } diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index 1b515690..a6d16419 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -59,7 +59,7 @@ async function updateReviewObjectByReviewUUID (req, res, next) { const orgRepo = req.ctx.repositories.getBaseOrgRepository() const body = req.body - const result = orgRepo.validateOrg(body.new_review_data) + const result = orgRepo.validateOrg(body) if (!result.isValid) { return res.status(400).json({ message: 'Invalid new_review_data', errors: result.errors }) } diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 4c95a672..f5053109 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -502,8 +502,8 @@ class BaseOrgRepository extends BaseRepository { } // Checking for joint approval fields - const jointApprovalFieldsRegistry = this.getJointApprovalFields({}, registryOrg, registryObjectRaw) - const jointApprovalFieldsLegacy = this.getJointApprovalFields({}, legacyOrg, legacyObjectRaw, true) + const jointApprovalFieldsRegistry = this.getJointApprovalFields(registryOrg, registryObjectRaw) + const jointApprovalFieldsLegacy = this.getJointApprovalFields(legacyOrg, legacyObjectRaw, true) let updatedRegistryOrg = null let updatedLegacyOrg = null let jointApprovalRegistry = null diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js index b1fb3984..e2fd6c44 100644 --- a/test/integration-tests/review-object/reviewObjectTest.js +++ b/test/integration-tests/review-object/reviewObjectTest.js @@ -6,7 +6,7 @@ chai.use(require('chai-http')) const constants = require('../constants.js') const app = require('../../../src/index.js') -describe.only('Review Object Controller Integration Tests', () => { +describe('Review Object Controller Integration Tests', () => { let orgUUID let reviewUUID @@ -83,40 +83,6 @@ describe.only('Review Object Controller Integration Tests', () => { }) context('Negative Tests', () => { - it('Fails when target_object_uuid is missing', async () => { - const res = await chai - .request(app) - .post('/api/review/org/') - .set({ ...constants.headers }) - .send({ new_review_data: constants.testOrg }) - expect(res).to.have.status(400) - expect(res.body).to.have.property('message', 'Missing required field target_object_uuid') - }) - - it('Fails when new_review_data is missing', async () => { - const res = await chai - .request(app) - .post('/api/review/org/') - .set({ ...constants.headers }) - .send({ target_object_uuid: orgUUID }) - expect(res).to.have.status(400) - expect(res.body).to.have.property('message', 'Missing required field new_review_data') - }) - - it('Fails when uuid is provided in creation payload', async () => { - const res = await chai - .request(app) - .post('/api/review/org/') - .set({ ...constants.headers }) - .send({ - uuid: 'should-not-be-here', - target_object_uuid: orgUUID, - new_review_data: constants.testOrg - }) - expect(res).to.have.status(400) - expect(res.body).to.have.property('message', 'Do not pass in a uuid key when creating a review object') - }) - it('Returns 404 for non-existent review object GET', async () => { const res = await chai .request(app) @@ -124,20 +90,5 @@ describe.only('Review Object Controller Integration Tests', () => { .set({ ...constants.headers }) expect(res).to.have.status(404) }) - - it('Returns 404 for non-existent review object UPDATE', async () => { - const updatePayload = { - new_review_data: constants.testRegistryOrg2 - } - - updatePayload.new_review_data.short_name = 'updated_org' - const res = await chai - .request(app) - .put('/api/review/org/nonexistent-uuid') - .set({ ...constants.headers }) - .send(updatePayload) - expect(res).to.have.status(404) - expect(res.body).to.have.property('message') - }) }) }) From 0c213bb494a1bd943389345c7d42a6f7ba0e512a Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Tue, 11 Nov 2025 14:43:33 -0500 Subject: [PATCH 07/39] Conversations now properly tied to review objects --- .../registry-org.controller.js | 10 +++----- .../review-object.controller.js | 6 +++-- src/repositories/baseOrgRepository.js | 23 ++++++++++++++----- src/repositories/conversationRepository.js | 10 +++----- src/repositories/reviewObjectRepository.js | 16 +++++++++++-- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index ee7a75e8..d9bf95e8 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -228,7 +228,7 @@ async function updateOrg (req, res, next) { session.startTransaction() const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) - const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + const requestingUser = await userRepo.findOneByUsernameAndOrgShortname(req.ctx.user, req.ctx.org, { session }) const org = await repo.findOneByShortName(shortName) // Edge Case: if a user has requested an org, but it is not approved yet, then we need to check to see if if there is a review org for the shortname request. @@ -244,7 +244,7 @@ async function updateOrg (req, res, next) { if (updateResult) { updatedOrg = reviewOrg if (conversation && conversation.length) { - // await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, req.ctx.user, isSecretariat, { session }) + await conversationRepo.processConversationHistory(conversation, updateResult.uuid, requestingUser, isSecretariat, { session }) } await session.commitTransaction() return res.status(200).json({ message: 'Review object updated successfully' }) @@ -273,13 +273,9 @@ async function updateOrg (req, res, next) { return res.status(400).json(error.duplicateShortname(body?.short_name)) } - updatedOrg = await repo.updateOrgFull(shortName, body, { session }, false, requestingUserUUID, isAdmin, isSecretariat) + updatedOrg = await repo.updateOrgFull(shortName, req.ctx.body, { session }, false, requestingUser.UUID, isAdmin, isSecretariat) jointApprovalRequired = _.get(updatedOrg, 'joint_approval_required', false) _.unset(updatedOrg, 'joint_approval_required') - // Handle conversation - if (conversation && conversation.length) { - await conversationRepo.processConversationHistory(conversation, updatedOrg.short_name, req.ctx.user, isSecretariat, { session }) - } await session.commitTransaction() } catch (updateErr) { diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index a6d16419..3719e1d2 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -3,6 +3,8 @@ const validateUUID = require('uuid').validate const mongoose = require('mongoose') async function getReviewObjectByOrgIdentifier (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const isSecretariat = await orgRepo.isSecretariatByShortname(req.ctx.org) const identifier = req.params.identifier const identifierIsUUID = validateUUID(identifier) if (!identifier) { @@ -11,9 +13,9 @@ async function getReviewObjectByOrgIdentifier (req, res, next) { let value // We may want this to be something different, but for now we are just testing if (identifierIsUUID) { - value = await repo.getOrgReviewObjectByOrgUUID(identifier) + value = await repo.getOrgReviewObjectByOrgUUID(identifier, isSecretariat) } else { - value = await repo.getOrgReviewObjectByOrgShortname(identifier) + value = await repo.getOrgReviewObjectByOrgShortname(identifier, isSecretariat) } if (!value) { return res.status(404).json({ message: 'Review Object does not exist' }) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index f5053109..608712f8 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -9,6 +9,7 @@ const uuid = require('uuid') const _ = require('lodash') const BaseOrg = require('../model/baseorg') const AuditRepository = require('./auditRepository') +const ConversationRepository = require('./conversationRepository') const getConstants = require('../constants').getConstants function setAggregateOrgObj (query) { @@ -483,22 +484,26 @@ class BaseOrgRepository extends BaseRepository { const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') const ReviewObjectRepository = require('./reviewObjectRepository') + const BaseUserRepository = require('./baseUserRepository') const legacyOrgRepo = new OrgRepository() const reviewObjectRepo = new ReviewObjectRepository() + const userRepo = new BaseUserRepository() + const conversationRepo = new ConversationRepository() const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) const registryOrg = await this.findOneByShortName(shortName, options) // check to see if there is a review object: const reviewObject = await reviewObjectRepo.getOrgReviewObjectByOrgUUID(registryOrg.UUID) + const { conversation, ...incomingOrgBody } = incomingOrg let legacyObjectRaw let registryObjectRaw if (isLegacyObject) { - legacyObjectRaw = incomingOrg - registryObjectRaw = this.convertLegacyToRegistry(incomingOrg) + legacyObjectRaw = incomingOrgBody + registryObjectRaw = this.convertLegacyToRegistry(incomingOrgBody) } else { - registryObjectRaw = incomingOrg - legacyObjectRaw = this.convertRegistryToLegacy(incomingOrg) + registryObjectRaw = incomingOrgBody + legacyObjectRaw = this.convertRegistryToLegacy(incomingOrgBody) } // Checking for joint approval fields @@ -514,10 +519,16 @@ class BaseOrgRepository extends BaseRepository { } else { // write the joint approval to the database jointApprovalRegistry = _.merge({}, registryOrg.toObject(), registryObjectRaw) + let updatedReviewObj if (reviewObject) { - await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, { options }) + updatedReviewObj = await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, { options }) } else { - await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, { options }) + updatedReviewObj = await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, { options }) + } + // handle conversation + const requestingUser = await userRepo.findUserByUUID(requestingUserUUID) + if (conversation && conversation.length) { + await conversationRepo.processConversationHistory(conversation, updatedReviewObj.uuid, requestingUser, isSecretariat, { options }) } updatedRegistryOrg = _.merge(registryOrg, _.omit(registryObjectRaw, jointApprovalFieldsRegistry)) updatedLegacyOrg = _.merge(legacyOrg, _.omit(legacyObjectRaw, jointApprovalFieldsLegacy)) diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index c13cdfc1..18586c0f 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -1,7 +1,6 @@ const uuid = require('uuid') const ConversationModel = require('../model/conversation') const BaseRepository = require('./baseRepository') -const ReviewObjectRepository = require('./reviewObjectRepository') class ConversationRepository extends BaseRepository { constructor () { @@ -76,16 +75,13 @@ class ConversationRepository extends BaseRepository { // Takes in a list of conversations representing the conversation history for // an org and creates/updates the objects as necessary - async processConversationHistory (conversationList, targetShortname, user, isSecretariat, options = {}) { - const reviewObjectRepository = new ReviewObjectRepository() - const reviewObject = await reviewObjectRepository.findOneByOrgShortName(targetShortname) - + async processConversationHistory (conversationList, targetUUID, user, isSecretariat, options = {}) { const promises = conversationList.map(convo => { return (async () => { const populatedConvo = { UUID: convo.UUID || undefined, author_id: convo.author_id || user.UUID, - author_name: convo.author_name || (isSecretariat ? 'Secretariat' : [user.name.first, user.name.last].join(' ')), + author_name: convo.author_name || (isSecretariat ? 'Secretariat' : [user.name?.first, user.name?.last].join(' ')), author_role: convo.author_role || (isSecretariat ? 'Secretariat' : 'Partner'), visibility: !isSecretariat ? 'public' : (convo.visibility || 'private'), body: convo.body @@ -93,7 +89,7 @@ class ConversationRepository extends BaseRepository { if (populatedConvo.UUID) return await this.updateConversation(populatedConvo, populatedConvo.UUID, options) const newConvo = { ...populatedConvo, - target_uuid: reviewObject.UUID + target_uuid: targetUUID } return await this.createConversation(newConvo, options) })() diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index dac111e5..194b6098 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -36,24 +36,36 @@ class ReviewObjectRepository extends BaseRepository { return reviewObject || null } - async getOrgReviewObjectByOrgShortname (orgShortName, options = {}) { + async getOrgReviewObjectByOrgShortname (orgShortName, isSecretariat, options = {}) { const baseOrgRepository = new BaseOrgRepository() + const ConversationRepository = require('./conversationRepository') + const conversationRepository = new ConversationRepository() const org = await baseOrgRepository.findOneByShortName(orgShortName) if (!org) { return null } const reviewObject = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + if (reviewObject) { + const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) + if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') + } return reviewObject || null } - async getOrgReviewObjectByOrgUUID (orgUUID, options = {}) { + async getOrgReviewObjectByOrgUUID (orgUUID, isSecretariat, options = {}) { const baseOrgRepository = new BaseOrgRepository() + const ConversationRepository = require('./conversationRepository') + const conversationRepository = new ConversationRepository() const org = await baseOrgRepository.findOneByUUID(orgUUID) if (!org) { return null } const reviewObject = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + if (reviewObject) { + const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) + if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') + } return reviewObject || null } From e46eb1ae73c4d75f791be6a827d96c604edf83e0 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 11 Nov 2025 14:55:22 -0500 Subject: [PATCH 08/39] resolving regregression --- .../review-object.controller/review-object.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index 3719e1d2..fc5bb6a7 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -4,7 +4,7 @@ const mongoose = require('mongoose') async function getReviewObjectByOrgIdentifier (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const isSecretariat = await orgRepo.isSecretariatByShortname(req.ctx.org) + const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org) const identifier = req.params.identifier const identifierIsUUID = validateUUID(identifier) if (!identifier) { From c408dbbecb61d984914e89a2350139951832b1f2 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 11 Nov 2025 17:09:27 -0500 Subject: [PATCH 09/39] integration tests for new stuff --- .../registryOrgWithJointReviewTest.js | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 test/integration-tests/registry-org/registryOrgWithJointReviewTest.js diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js new file mode 100644 index 00000000..4ba3b37d --- /dev/null +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -0,0 +1,301 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +const expect = chai.expect + +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +const nonAdminHeaders = { + 'CVE-API-ORG': 'non_secretariat_org', + 'content-type': 'application/json', + 'CVE-API-USER': 'drocca_admin_user' +} + +const nonAdminHeaders2 = { + 'CVE-API-ORG': 'non_with_comments', + 'content-type': 'application/json', + 'CVE-API-USER': 'drocca_admin_user_comments' +} + +const testRegistryOrgForReview = { + short_name: 'non_secretariat_org', + long_name: 'Non Secretariat Org', + authority: 'CNA', + hard_quota: 1000 +} + +const testRegistryOrgForReviewWithComments = { + short_name: 'non_with_comments', + long_name: 'Non Secretariat Org', + authority: 'CNA', + hard_quota: 1000 +} + +const testRegistryOrgAdminUser = { + username: 'drocca_admin_user', + active: 'true', + name: { + first: 'David', + last: 'Rocca', + middle: 'N', + suffix: 'I' + }, + authority: { + active_roles: ['Admin'] + } +} + +const testRegistryOrgAdminUserWithComments = { + username: 'drocca_admin_user_comments', + active: 'true', + name: { + first: 'David', + last: 'Rocca', + middle: 'N', + suffix: 'I' + }, + authority: { + active_roles: ['Admin'] + } +} + +describe('Testing Joint approval', () => { + describe('Admin user attempts to edit a joint approval field', () => { + let secret + let orgUUID + let reviewUUID + let createdOrg + it('Create an org to use for testing', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send(testRegistryOrgForReview) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal(testRegistryOrgForReview.short_name + ' organization was successfully created.') + + expect(res.body).to.haveOwnProperty('created') + + expect(res.body.created).to.haveOwnProperty('UUID') + + expect(res.body.created).to.haveOwnProperty('short_name') + expect(res.body.created.short_name).to.equal(testRegistryOrgForReview.short_name) + + expect(res.body.created).to.haveOwnProperty('long_name') + expect(res.body.created.long_name).to.equal(testRegistryOrgForReview.long_name) + + expect(res.body.created).to.haveOwnProperty('authority') + expect(res.body.created.authority).to.deep.equal(['CNA']) + + expect(res.body.created).to.haveOwnProperty('hard_quota') + expect(res.body.created.hard_quota).to.equal(testRegistryOrgForReview.hard_quota) + + createdOrg = res.body.created + }) + }) + it('Create an User', async () => { + await chai.request(app) + .post('/api/registry/org/non_secretariat_org/user') + .set(constants.headers) + .send(testRegistryOrgAdminUser) + .then((res, err) => { + expect(err).to.be.undefined + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(testRegistryOrgAdminUser.username) + expect(res).to.have.status(200) + secret = res.body.created.secret + nonAdminHeaders['CVE-API-KEY'] = secret + }) + }) + it('Attempt to change the short name of the org', async () => { + await chai.request(app) + .put('/api/registryOrg/non_secretariat_org') + .set(nonAdminHeaders) + .send({ ...testRegistryOrgForReview, short_name: 'new_non_secretariat_org', hard_quota: 10000 }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.') + orgUUID = res.body.updated.UUID + }) + }) + it('Check to see if an ORG review was created', async () => { + await chai.request(app) + .get(`/api/review/org/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body).to.have.property('status', 'pending') + expect(res.body.target_object_uuid).to.equal(orgUUID) + expect(res.body.new_review_data.short_name).to.equal('new_non_secretariat_org') + reviewUUID = res.body.uuid + }) + }) + it('Check to see if the org was partially updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('non_secretariat_org') + expect(res.body.hard_quota).to.equal(10000) + }) + }) + it('Secretariat can approve the ORG review', async () => { + await chai.request(app) + .put(`/api/review/org/${reviewUUID}/approve`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.status).to.equal('approved') + }) + }) + it('Check to see if the org was fully updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('new_non_secretariat_org') + expect(res.body.hard_quota).to.equal(10000) + }) + }) + }) + describe('Admin user attempts to edit a joint approval field, Secretariat leaves comment, admin fixes with a comment, secretariat approves', () => { + let secret + let orgUUID + let reviewUUID + let createdOrg + it('Create an org to use for testing', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send(testRegistryOrgForReviewWithComments) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal(testRegistryOrgForReviewWithComments.short_name + ' organization was successfully created.') + + expect(res.body).to.haveOwnProperty('created') + + expect(res.body.created).to.haveOwnProperty('UUID') + + expect(res.body.created).to.haveOwnProperty('short_name') + expect(res.body.created.short_name).to.equal(testRegistryOrgForReviewWithComments.short_name) + + expect(res.body.created).to.haveOwnProperty('long_name') + expect(res.body.created.long_name).to.equal(testRegistryOrgForReviewWithComments.long_name) + + expect(res.body.created).to.haveOwnProperty('authority') + expect(res.body.created.authority).to.deep.equal(['CNA']) + + expect(res.body.created).to.haveOwnProperty('hard_quota') + expect(res.body.created.hard_quota).to.equal(testRegistryOrgForReviewWithComments.hard_quota) + + createdOrg = res.body.created + }) + }) + it('Create an User', async () => { + await chai.request(app) + .post('/api/registry/org/non_with_comments/user') + .set(constants.headers) + .send(testRegistryOrgAdminUserWithComments) + .then((res, err) => { + expect(err).to.be.undefined + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(testRegistryOrgAdminUserWithComments.username) + expect(res).to.have.status(200) + secret = res.body.created.secret + nonAdminHeaders2['CVE-API-KEY'] = secret + }) + }) + it('Attempt to change the short name of the org', async () => { + await chai.request(app) + .put('/api/registryOrg/non_with_comments') + .set(nonAdminHeaders2) + .send({ ...testRegistryOrgForReviewWithComments, short_name: 'new_non_with_comments', hard_quota: 10000 }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.') + orgUUID = res.body.updated.UUID + }) + }) + it('Check to see if an ORG review was created', async () => { + await chai.request(app) + .get(`/api/review/org/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body).to.have.property('status', 'pending') + expect(res.body.target_object_uuid).to.equal(orgUUID) + expect(res.body.new_review_data.short_name).to.equal('new_non_with_comments') + reviewUUID = res.body.uuid + }) + }) + it('Check to see if the org was partially updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('non_with_comments') + expect(res.body.hard_quota).to.equal(10000) + }) + }) + it('Secretariat leaves a public comment on the org review', async () => { + await chai.request(app) + .post(`/api/conversation/org/${reviewUUID}`) + .set(secretariatHeaders) + .send({ + visibility: 'public', + body: 'This is a comment left by the secretariat.' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.author_role).to.equal('Secretariat') + expect(res.body.visibility).to.equal('public') + expect(res.body.body).to.equal('This is a comment left by the secretariat.') + }) + }) + it('Secretariat leaves a private on the org review', async () => { + await chai.request(app) + .post(`/api/conversation/org/${reviewUUID}`) + .set(secretariatHeaders) + .send({ + visibility: 'private', + body: 'This is a private comment left by the secretariat.' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.author_role).to.equal('Secretariat') + expect(res.body.visibility).to.equal('private') + expect(res.body.body).to.equal('This is a private comment left by the secretariat.') + }) + }) + it('Admin checks org review', async () => { + await chai.request(app) + .get(`/api/conversation/org/${orgUUID}`) + .set(nonAdminHeaders2) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + }) + }) +}) From 94ad3a0e0541d3424ddea31532bc83e1522d9f88 Mon Sep 17 00:00:00 2001 From: emathew Date: Wed, 12 Nov 2025 14:14:05 -0500 Subject: [PATCH 10/39] auditChanges --- .../audit.controller/audit.controller.js | 118 +++++---- src/controller/audit.controller/index.js | 6 +- src/repositories/auditRepository.js | 61 ++--- src/repositories/baseOrgRepository.js | 47 +++- .../audit/registryOrgCreatesAuditTest.js | 224 ++++++++++++++++++ 5 files changed, 372 insertions(+), 84 deletions(-) create mode 100644 test/integration-tests/audit/registryOrgCreatesAuditTest.js diff --git a/src/controller/audit.controller/audit.controller.js b/src/controller/audit.controller/audit.controller.js index 1540f41b..771b9a66 100644 --- a/src/controller/audit.controller/audit.controller.js +++ b/src/controller/audit.controller/audit.controller.js @@ -8,7 +8,7 @@ const validateUUID = require('uuid').validate * Create a new audit document * Called by POST /api/audit/org/ */ -async function createAuditDocument (req, res, next) { +async function createAuditDocumentForOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -72,9 +72,25 @@ async function createAuditDocument (req, res, next) { await session.abortTransaction() return res.status(400).json(error.missingRequiredField('change_author')) } + + // Process entry immediately after validation + returnValue = await repo.appendToAuditHistoryForOrg( + body.target_uuid, + entry.audit_object, + entry.change_author, + { session } + ) } + } else { + // Create audit document with initial empty entry or default entry + returnValue = await repo.appendToAuditHistoryForOrg( + body.target_uuid, + body.audit_object || {}, + body.change_author || req.ctx.org, + { session } + ) } - returnValue = await repo.createAuditDocument(body, { session }) + await session.commitTransaction() logger.info({ @@ -100,7 +116,7 @@ async function createAuditDocument (req, res, next) { * Called by PUT /api/audit/org/ * Allows for multiple appends in a single request */ -async function appendToAuditHistory (req, res, next) { +async function appendToAuditHistoryForOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -149,7 +165,7 @@ async function appendToAuditHistory (req, res, next) { } // Append this history entry - returnValue = await repo.appendToAuditHistory( + returnValue = await repo.appendToAuditHistoryForOrg( body.target_uuid, entry.audit_object, entry.change_author, @@ -190,7 +206,7 @@ async function appendToAuditHistory (req, res, next) { * Get all audit documents * Called by GET /api/audit/org/ */ -async function getAllAuditDocuments (req, res, next) { +async function getAllOrgAuditDocuments (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -213,7 +229,7 @@ async function getAllAuditDocuments (req, res, next) { * Get audit document by its document UUID * Called by GET /api/audit/org/document/:document_uuid */ -async function getAuditByDocumentUUID (req, res, next) { +async function getOrgAuditByDocumentUUID (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -247,48 +263,53 @@ async function getAuditByDocumentUUID (req, res, next) { next(err) } } + /** - * Get audit history by target UUID - * Called by GET /api/audit/org/:target_uuid - * TODO: remove comment-> I changed parameter name from org_identifier to target_uuid to be more generic. + * Get audit history by target identifier (shortname or UUID) + * Called by GET /api/audit/org/:identifier */ -async function getAuditByTargetUUID (req, res, next) { +async function getOrgAuditByOrgIdentifier (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const targetUUID = req.ctx.params.target_uuid + const identifier = req.ctx.params.org_identifier + const identifierIsUUID = validateUUID(identifier) let returnValue - if (!targetUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'Missing target_uuid parameter' }) - return res.status(400).json(error.missingRequiredField('target_uuid')) - } - - if (!validateUUID(targetUUID)) { - logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' }) - return res.status(400).json(error.invalidUUID('target_uuid')) + if (!identifier) { + return res.status(400).json(error.missingRequiredField('identifier')) } try { session.startTransaction() - // Find the target organization - const targetOrg = await orgRepo.findOneByUUID(targetUUID, { session }) + // Find the target organization by either UUID or shortname + const targetOrg = identifierIsUUID + ? await orgRepo.findOneByUUID(identifier, { session }) + : await orgRepo.findOneByShortName(identifier, { session }) + if (!targetOrg) { - logger.info({ uuid: req.ctx.uuid, message: `No organization found with UUID ${targetUUID}` }) + logger.info({ + uuid: req.ctx.uuid, + message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}` + }) await session.abortTransaction() - return res.status(404).json(error.orgDne(targetUUID)) + return res.status(404).json(error.orgDne(identifier)) } - // TODO: confirm middleware is checking admin and secretariat permissions properly + // Get the org's UUID for audit lookup + const targetUUID = targetOrg.UUID returnValue = await repo.findOneByTargetUUID(targetUUID, { session }) if (!returnValue) { - logger.info({ uuid: req.ctx.uuid, message: `No audit history found for target UUID ${targetUUID}` }) + logger.info({ + uuid: req.ctx.uuid, + message: `No audit history found for organization ${identifier} (UUID: ${targetUUID})` + }) await session.abortTransaction() - return res.status(404).json(error.auditDneByTarget(targetUUID)) + return res.status(404).json(error.auditDneByTarget(identifier)) } await session.commitTransaction() @@ -301,7 +322,7 @@ async function getAuditByTargetUUID (req, res, next) { logger.info({ uuid: req.ctx.uuid, - message: `Audit history for target UUID ${targetUUID} sent to user ${req.ctx.user}` + message: `Audit history for ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier} sent to user ${req.ctx.user}` }) return res.status(200).json(returnValue) } catch (err) { @@ -317,18 +338,14 @@ async function getLastXChanges (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() - const targetUUID = req.ctx.params.target_uuid + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const identifier = req.ctx.params.org_identifier + const identifierIsUUID = validateUUID(identifier) const numberOfChanges = parseInt(req.ctx.params.number_of_changes) let returnValue - if (!targetUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'Missing org_identifier parameter' }) - return res.status(400).json(error.missingRequiredField('org_identifier')) - } - - if (!validateUUID(targetUUID)) { - logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' }) - return res.status(400).json(error.invalidUUID('target_uuid')) + if (!identifier) { + return res.status(400).json(error.missingRequiredField('identifier')) } if (isNaN(numberOfChanges) || numberOfChanges < 1) { @@ -339,6 +356,23 @@ async function getLastXChanges (req, res, next) { try { session.startTransaction() + // Find the target organization by either UUID or shortname + const targetOrg = identifierIsUUID + ? await orgRepo.findOneByUUID(identifier, { session }) + : await orgRepo.findOneByShortName(identifier, { session }) + + if (!targetOrg) { + logger.info({ + uuid: req.ctx.uuid, + message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}` + }) + await session.abortTransaction() + return res.status(404).json(error.orgDne(identifier)) + } + + // Get the org's UUID for audit lookup + const targetUUID = targetOrg.UUID + const lastChanges = await repo.getLastXChanges(targetUUID, numberOfChanges, { session }) if (!lastChanges || lastChanges.length === 0) { @@ -362,7 +396,7 @@ async function getLastXChanges (req, res, next) { logger.info({ uuid: req.ctx.uuid, - message: `Last ${numberOfChanges} changes for ${targetUUID} sent to user ${req.ctx.user}` + message: `Last ${numberOfChanges} changes for ${identifier} sent to user ${req.ctx.user}` }) return res.status(200).json(returnValue) } catch (err) { @@ -371,10 +405,10 @@ async function getLastXChanges (req, res, next) { } module.exports = { - AUDIT_CREATE_SINGLE: createAuditDocument, - AUDIT_UPDATE: appendToAuditHistory, - AUDIT_GET_ALL: getAllAuditDocuments, - AUDIT_GET_BY_UUID: getAuditByDocumentUUID, - AUDIT_GET_BY_TARGET_UUID: getAuditByTargetUUID, + AUDIT_CREATE_SINGLE: createAuditDocumentForOrg, + AUDIT_UPDATE: appendToAuditHistoryForOrg, + AUDIT_GET_ALL: getAllOrgAuditDocuments, + AUDIT_GET_BY_UUID: getOrgAuditByDocumentUUID, + AUDIT_GET_BY_ORG_IDENTIFIER: getOrgAuditByOrgIdentifier, AUDIT_GET_LAST: getLastXChanges } diff --git a/src/controller/audit.controller/index.js b/src/controller/audit.controller/index.js index ba5b876a..10da8f70 100644 --- a/src/controller/audit.controller/index.js +++ b/src/controller/audit.controller/index.js @@ -27,15 +27,15 @@ router.get('/audit/org/document/:document_uuid', ) // Get audit by org identifier (Secretariat or Admin) -router.get('/audit/org/:target_uuid', +router.get('/audit/org/:org_identifier', mw.validateUser, mw.onlySecretariatOrAdmin, auditMw.parseGetParams, - controller.AUDIT_GET_BY_TARGET_UUID + controller.AUDIT_GET_BY_ORG_IDENTIFIER ) // Get last X changes (Secretariat or Org Admin) -router.get('/audit/org/:target_uuid/:number_of_changes', +router.get('/audit/org/:org_identifier/:number_of_changes', mw.onlySecretariatOrAdmin, mw.validateUser, auditMw.parseGetParams, diff --git a/src/repositories/auditRepository.js b/src/repositories/auditRepository.js index d79578d4..1beabdbc 100644 --- a/src/repositories/auditRepository.js +++ b/src/repositories/auditRepository.js @@ -1,5 +1,6 @@ const Audit = require('../model/audit') const BaseRepository = require('./baseRepository') +const BaseOrgRepository = require('./baseOrgRepository') const uuid = require('uuid') class AuditRepository extends BaseRepository { @@ -13,49 +14,52 @@ class AuditRepository extends BaseRepository { return validateObject } - /** - * Create a new audit document - */ - async createAuditDocument (data, options = {}) { - const auditData = { - uuid: uuid.v4(), - target_uuid: data.target_uuid, - history: data.history || [] - } - - const audit = new Audit(auditData) - const result = await audit.save(options) - return result.toObject() - } - /** * Append a new entry to the audit history * Creates document if it doesn't exist */ - async appendToAuditHistory (targetUUID, auditObject, changeAuthor, options = {}) { + async appendToAuditHistoryForOrg (targetUUID, auditObject, changeAuthor, options = {}) { const historyEntry = { timestamp: new Date(), audit_object: auditObject, change_author: changeAuthor } + try { // Try to find existing document - let audit = await Audit.findOne({ target_uuid: targetUUID }) + let audit = await this.findOneByTargetUUID(targetUUID, options) - if (!audit) { + if (!audit) { // Create new document if doesn't exist - audit = new Audit({ - uuid: uuid.v4(), - target_uuid: targetUUID, - history: [historyEntry] - }) - } else { + // Assuming 'uuid' is available for generating a new UUID + audit = new Audit({ + uuid: uuid.v4(), + target_uuid: targetUUID, + history: [historyEntry] + }) + } else { // Append to existing history - audit.history.push(historyEntry) + audit.history.push(historyEntry) + } + + const result = await audit.save(options) + return result.toObject() + } catch (error) { + throw new Error('Failed to save audit history entry.') } + } - const result = await audit.save(options) - return result.toObject() + /** + * Find audit document by target UUID + */ + async findOneByOrgShortname (orgShortName, options = {}) { + const baseOrgRepository = new BaseOrgRepository() + const org = await baseOrgRepository.findOneByShortName(orgShortName) + if (!org) { + return null + } + const query = { target_uuid: org.UUID } + return this.collection.findOne(query, null, options) } /** @@ -63,7 +67,8 @@ class AuditRepository extends BaseRepository { */ async findOneByTargetUUID (targetUUID, options = {}) { const query = { target_uuid: targetUUID } - return this.collection.findOne(query, null, options) + const auditObject = await Audit.findOne(query, null, options) + return auditObject } /** diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 56d89378..0917a57c 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -238,14 +238,13 @@ class BaseOrgRepository extends BaseRepository { if (requestingUserUUID) { try { const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( + await auditRepo.appendToAuditHistoryForOrg( registryObjectRaw.UUID, registryObjectRaw, requestingUserUUID, options ) } catch (auditError) { - // Don't fail the transaction if audit fails - just log it } } @@ -261,7 +260,7 @@ class BaseOrgRepository extends BaseRepository { if ( legacyObjectRaw.authority.active_roles.length === 1 && ( legacyObjectRaw.authority.active_roles[0] === 'ADP' || - legacyObjectRaw.authority.active_roles[0] === 'BULK_DOWNLOAD') + legacyObjectRaw.authority.active_roles[0] === 'BULK_DOWNLOAD') ) { // ADPs have quota of 0 _.set(legacyObjectRaw, 'policies.id_quota', 0) @@ -269,7 +268,11 @@ class BaseOrgRepository extends BaseRepository { // The legacy way of doing this, the way this is written under the hood there is no other way // This await does not return a value, even though there is a return in it. :shrugg: - await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + try { + await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + } catch (error) { + + } if (isLegacyObject) { // This gets us the mongoose object that has all the right data in it, the "legacyObjectRaw" is the custom JSON we are sending. NOT the post written object. @@ -386,14 +389,36 @@ class BaseOrgRepository extends BaseRepository { if (requestingUserUUID) { try { const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( - registryOrg.UUID, - registryOrg.toObject(), - requestingUserUUID, - options - ) + // Check if an audit document exists, if not we need to create one first and seed it with the existing org data + if (!(await auditRepo.findOneByTargetUUID(registryOrg.UUID, options))) { + const currentRegistryOrg = await this.findOneByShortName(shortName, options) + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + currentRegistryOrg.toObject(), + requestingUserUUID, + options + ) + } + // Get the org state before save for comparison + const beforeUpdateOrg = await this.findOneByShortName(shortName, options) + const beforeUpdateObject = beforeUpdateOrg.toObject() + const afterUpdateObject = registryOrg.toObject() + + // Clean objects for comparison (remove Mongoose metadata) + const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + + // Only add audit entry if there are changes + if (!_.isEqual(cleanBefore, cleanAfter)) { + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + registryOrg.toObject(), + requestingUserUUID, + options + ) + } } catch (auditError) { - // Don't fail the transaction if audit fails - just log it + // Don't fail the transaction if audit fails - just log it } } diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js new file mode 100644 index 00000000..74d9534b --- /dev/null +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -0,0 +1,224 @@ +const chai = require('chai') +chai.use(require('chai-http')) +const { expect } = chai +const { v4: uuidv4 } = require('uuid') +const AuditRepo = require('../../../src/repositories/auditRepository') + +const app = require('../../../src/index.js') +const constants = require('../constants.js') + +const secretariatHeaders = { ...constants.headers } +const MAX_SHORTNAME_LENGTH = 32 + +async function createTestOrg (customProps = {}) { + const shortName = uuidv4().slice(0, MAX_SHORTNAME_LENGTH) + const defaultProps = { + short_name: shortName, + long_name: `Test Org ${shortName}`, + hard_quota: 1000, + authority: ['CNA'] + } + + const orgData = { ...defaultProps, ...customProps } + + const res = await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(orgData) + + expect(res).to.have.status(200) + + return { + shortName: orgData.short_name, + longName: orgData.long_name, + uuid: res.body.created.UUID, + fullResponse: res.body + } +} + +describe.only('Create and Update Audit Collection with Org Endpoints', () => { + it('Should automatically create audit document when org is created', async () => { + // Create org + const org = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + + // Verify audit was created + const auditRes = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + + expect(auditRes).to.have.status(200) + + // Verify audit structure + const audit = auditRes.body + expect(audit).to.have.property('uuid') + expect(audit).to.have.property('target_uuid') + expect(audit).to.have.property('history') + expect(audit.target_uuid).to.equal(org.uuid) + expect(audit.history).to.be.an('array').with.lengthOf(1) + + // Verify initial history entry + const initialEntry = audit.history[0] + expect(initialEntry).to.have.property('audit_object') + expect(initialEntry.timestamp).to.be.a('string') + expect(initialEntry.change_author).to.be.a('string') + + // Verify audit object matches created org + const auditObject = initialEntry.audit_object + expect(auditObject.short_name).to.equal(org.shortName) + expect(auditObject.long_name).to.equal(org.longName) + expect(auditObject.hard_quota).to.equal(1500) + expect(auditObject.UUID).to.equal(org.uuid) + }) + + it('Should create separate audit documents for multiple orgs', async () => { + // Create multiple orgs + const [org1, org2, org3] = await Promise.all([ + createTestOrg({ long_name: 'First Org' }), + createTestOrg({ long_name: 'Second Org' }), + createTestOrg({ long_name: 'Third Org' }) + ]) + + // Verify each has its own audit + const audits = await Promise.all([ + chai.request(app).get(`/api/audit/org/${org1.uuid}`).set(constants.headers), + chai.request(app).get(`/api/audit/org/${org2.uuid}`).set(constants.headers), + chai.request(app).get(`/api/audit/org/${org3.uuid}`).set(constants.headers) + ]) + + // Each should have its own audit document + audits.forEach((auditRes, index) => { + expect(auditRes).to.have.status(200) + const org = [org1, org2, org3][index] + expect(auditRes.body.target_uuid).to.equal(org.uuid) + expect(auditRes.body.history[0].audit_object.long_name).to.equal(org.longName) + }) + + // Audit UUIDs should all be different + const auditUUIDs = audits.map(res => res.body.uuid) + expect(new Set(auditUUIDs).size).to.equal(3) + }) + + it('Should NOT add audit entry when updating with no actual changes', async () => { + const org = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + const updateRes = await chai.request(app) + .put(`/api/registry/org/${org.shortName}?long_name=${org.longName}`) + .set(secretariatHeaders) + expect(updateRes).to.have.status(200) + const auditResUpdate = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + expect(auditResUpdate.body.history).to.have.lengthOf(1) + + // Now update with same values + const updateResAgain = await chai.request(app) + .put(`/api/registry/org/${org.shortName}?long_name=${org.longName}`) + .set(secretariatHeaders) + expect(updateResAgain).to.have.status(200) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(1) + }) + + it.only('Should add audit entry when single field is changed', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + + // Update org name + const updateRes = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=100`) + .set(secretariatHeaders) + + expect(updateRes).to.have.status(200) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.shortName}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(2) + + // Original entry + expect(auditRes.body.history[0].audit_object.hard_quota).to.equal(1500) + + // New entry + expect(auditRes.body.history[1].audit_object.hard_quota).to.equal(100) + }) + + it('Should maintain chronological order in audit history', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}`) + .set(secretariatHeaders) + // Make sequential updates + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=2000`) + .set(secretariatHeaders) + + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=3000`) + .set(secretariatHeaders) + + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=4000`) + .set(secretariatHeaders) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(4) + + // Verify chronological order + const quotas = auditRes.body.history.map(h => h.audit_object.hard_quota) + expect(quotas).to.deep.equal([1000, 2000, 3000, 4000]) + + // Verify timestamps are in order + for (let i = 1; i < auditRes.body.history.length; i++) { + const prev = new Date(auditRes.body.history[i - 1].timestamp) + const curr = new Date(auditRes.body.history[i].timestamp) + expect(curr.getTime()).to.be.greaterThan(prev.getTime()) + } + }) + + it('Should create an audit when updating an Org if it does not exist', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + // Manually delete audit document + const repo = new AuditRepo() + repo.deleteByTargetUUID(testOrg.uuid) + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + expect(auditRes).to.have.status(404) + // Now update org to trigger audit creation + const updateRes = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=2500`) + .set(secretariatHeaders) + expect(updateRes).to.have.status(200) + // Check audit history + const auditResCreation = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + + expect(auditResCreation.body.history).to.have.lengthOf(1) + }) +}) From d8983df2649421ea8b2d352a8cacd269c70092d4 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Tue, 18 Nov 2025 15:32:58 -0500 Subject: [PATCH 11/39] Added endpoint to get review object by UUID with conversation --- src/controller/review-object.controller/index.js | 1 + .../review-object.controller.js | 11 +++++++++++ src/repositories/reviewObjectRepository.js | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/src/controller/review-object.controller/index.js b/src/controller/review-object.controller/index.js index d99c843b..fe37cfe8 100644 --- a/src/controller/review-object.controller/index.js +++ b/src/controller/review-object.controller/index.js @@ -3,6 +3,7 @@ const controller = require('./review-object.controller') const mw = require('../../middleware/middleware') router.get('/review/org/:identifier', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getReviewObjectByOrgIdentifier) +router.get('/review/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getReviewObjectByUUID) router.get('/review/orgs', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getAllReviewObjects) router.put('/review/org/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.updateReviewObjectByReviewUUID) router.put('/review/org/:uuid/approve', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.approveReviewObject) diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index fc5bb6a7..44717797 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -1,6 +1,7 @@ const validateUUID = require('uuid').validate const mongoose = require('mongoose') + async function getReviewObjectByOrgIdentifier (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() @@ -23,6 +24,15 @@ async function getReviewObjectByOrgIdentifier (req, res, next) { return res.status(200).json(value) } +async function getReviewObjectByUUID (req, res, next) { + const repo = req.ctx.repositories.getReviewObjectRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org) + const UUID = req.params.uuid + const value = await repo.findOneByUUIDWithConversation(UUID, isSecretariat) + return res.status(200).json(value) +} + async function getAllReviewObjects (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() const value = await repo.getAllReviewObjects() @@ -87,6 +97,7 @@ async function createReviewObject (req, res, next) { } module.exports = { getReviewObjectByOrgIdentifier, + getReviewObjectByUUID, getAllReviewObjects, updateReviewObjectByReviewUUID, createReviewObject, diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 194b6098..821837a8 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -20,6 +20,18 @@ class ReviewObjectRepository extends BaseRepository { return reviewObject || null } + async findOneByUUIDWithConversation (UUID, isSecretariat, options = {}) { + const ConversationRepository = require('./conversationRepository') + const conversationRepository = new ConversationRepository() + const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options) + if (reviewObject) { + const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) + if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') + } + + return reviewObject || null + } + async getAllReviewObjects (options = {}) { const reviewObjects = await ReviewObjectModel.find({}, null, options) return reviewObjects || [] From da63ecd2204d78e289c1b1c0c145d30bfa2fad67 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Wed, 19 Nov 2025 12:48:24 -0500 Subject: [PATCH 12/39] Fixed review object endpoints not returning conversation --- .../review-object.controller/index.js | 2 +- src/repositories/conversationRepository.js | 20 ++----------------- src/repositories/reviewObjectRepository.js | 20 ++++++++++++------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/controller/review-object.controller/index.js b/src/controller/review-object.controller/index.js index fe37cfe8..8c2b65a0 100644 --- a/src/controller/review-object.controller/index.js +++ b/src/controller/review-object.controller/index.js @@ -2,8 +2,8 @@ const router = require('express').Router() const controller = require('./review-object.controller') const mw = require('../../middleware/middleware') +router.get('/review/byUUID/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariatOrAdmin, controller.getReviewObjectByUUID) router.get('/review/org/:identifier', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getReviewObjectByOrgIdentifier) -router.get('/review/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getReviewObjectByUUID) router.get('/review/orgs', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.getAllReviewObjects) router.put('/review/org/:uuid', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.updateReviewObjectByReviewUUID) router.put('/review/org/:uuid/approve', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, controller.approveReviewObject) diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 18586c0f..d7e405ff 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -36,24 +36,8 @@ class ConversationRepository extends BaseRepository { } async getAllByTargetUUID (targetUUID, options = {}) { - const agt = [ - { - $match: { - target_uuid: targetUUID - } - } - ] - const pg = await this.aggregatePaginate(agt, options) - const data = { conversations: pg.itemsList } - if (pg.itemCount >= options.limit) { - data.totalCount = pg.itemCount - data.itemsPerPage = pg.itemsPerPage - data.pageCount = pg.pageCount - data.currentPage = pg.currentPage - data.prevPage = pg.prevPage - data.nextPage = pg.nextPage - } - return data + const conversations = await ConversationModel.find({ target_uuid: targetUUID }, null, options) + return conversations.map(convo => convo.toObject()) } async createConversation (body, options = {}) { diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 821837a8..a7980bc2 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -23,10 +23,12 @@ class ReviewObjectRepository extends BaseRepository { async findOneByUUIDWithConversation (UUID, isSecretariat, options = {}) { const ConversationRepository = require('./conversationRepository') const conversationRepository = new ConversationRepository() - const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options) - if (reviewObject) { + let reviewObject + const reviewObjectRaw = await ReviewObjectModel.findOne({ uuid: UUID }, null, options) + if (reviewObjectRaw) { + reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) - if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') + if (conversations && conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') } return reviewObject || null @@ -56,8 +58,10 @@ class ReviewObjectRepository extends BaseRepository { if (!org) { return null } - const reviewObject = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) - if (reviewObject) { + let reviewObject + const reviewObjectRaw = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + if (reviewObjectRaw) { + reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') } @@ -73,8 +77,10 @@ class ReviewObjectRepository extends BaseRepository { if (!org) { return null } - const reviewObject = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) - if (reviewObject) { + let reviewObject + const reviewObjectRaw = await ReviewObjectModel.findOne({ target_object_uuid: org.UUID }, null, options) + if (reviewObjectRaw) { + reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.uuid) if (conversations.length) reviewObject.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public') } From 48a303f8577f3e93563757a7277a23d0f4c5acca Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 19 Nov 2025 14:13:18 -0500 Subject: [PATCH 13/39] fixing tests --- .../registryOrgWithJointReviewTest.js | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index 4ba3b37d..b42fa1cd 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -175,7 +175,6 @@ describe('Testing Joint approval', () => { let secret let orgUUID let reviewUUID - let createdOrg it('Create an org to use for testing', async () => { await chai.request(app) .post('/api/registryOrg') @@ -203,8 +202,6 @@ describe('Testing Joint approval', () => { expect(res.body.created).to.haveOwnProperty('hard_quota') expect(res.body.created.hard_quota).to.equal(testRegistryOrgForReviewWithComments.hard_quota) - - createdOrg = res.body.created }) }) it('Create an User', async () => { @@ -290,12 +287,45 @@ describe('Testing Joint approval', () => { }) it('Admin checks org review', async () => { await chai.request(app) - .get(`/api/conversation/org/${orgUUID}`) + .get(`/api/review/byUUID/${reviewUUID}`) .set(nonAdminHeaders2) .then((res, err) => { expect(err).to.be.undefined + expect(res.body).to.have.property('conversation') + expect(res.body.conversation).to.have.length(1) expect(res).to.have.status(200) }) }) + it('Secretariat checks org review', async () => { + await chai.request(app) + .get(`/api/review/byUUID/${reviewUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res.body).to.have.property('conversation') + expect(res.body.conversation).to.have.length(2) + expect(res).to.have.status(200) + }) + }) + it('Secretariat can approve the ORG review', async () => { + await chai.request(app) + .put(`/api/review/org/${reviewUUID}/approve`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.status).to.equal('approved') + }) + }) + it('Check to see if the org was fully updated', async () => { + await chai.request(app) + .get(`/api/registryOrg/${orgUUID}`) + .set(secretariatHeaders) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.short_name).to.equal('new_non_with_comments') + expect(res.body.hard_quota).to.equal(10000) + }) + }) }) }) From 239e475a672b264eb0a8fe711be5340db4ff65c8 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Thu, 20 Nov 2025 13:18:44 -0500 Subject: [PATCH 14/39] Integration tests for conversation endpoints --- .../conversation.controller.js | 72 ++-------- .../conversation.controller/index.js | 25 +--- .../conversation/conversationTest.js | 134 ++++++++++++++++++ .../registryOrgWithJointReviewTest.js | 4 +- 4 files changed, 156 insertions(+), 79 deletions(-) create mode 100644 test/integration-tests/conversation/conversationTest.js diff --git a/src/controller/conversation.controller/conversation.controller.js b/src/controller/conversation.controller/conversation.controller.js index db9aab09..0ed9a2c9 100644 --- a/src/controller/conversation.controller/conversation.controller.js +++ b/src/controller/conversation.controller/conversation.controller.js @@ -19,73 +19,27 @@ async function getAllConversations (req, res, next) { return res.status(200).json(response) } -async function getConversationsForOrg (req, res, next) { - const session = await mongoose.startSession() - - try { - session.startTransaction() - - const repo = req.ctx.repositories.getConversationRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const requesterOrg = req.ctx.org - const targetOrgUUID = req.params.uuid - - // Make sure target org matches user org if not secretariat - const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session }) - const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session }) - if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) { - return res.status(400).json({ message: 'User is not secretariat or admin for target org' }) - } - - // 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 = { posted_at: 'desc' } +async function getConversationsForTargetUUID (req, res, next) { + const repo = req.ctx.repositories.getConversationRepository() + const targetUUID = req.params.uuid - const response = await repo.getAllByTargetUUID(targetOrgUUID, options) - await session.commitTransaction() - return res.status(200).json(response) - } catch (err) { - if (session && session.inTransaction()) { - await session.abortTransaction() - } - next(err) - } finally { - if (session && session.id) { // Check if session is still valid before trying to end - try { - await session.endSession() - } catch (sessionEndError) { - logger.error({ uuid: req.ctx.uuid, message: 'Error ending session in finally block', error: sessionEndError }) - } - } - } + const response = await repo.getAllByTargetUUID(targetUUID) + return res.status(200).json(response) } -async function createConversationForOrg (req, res, next) { +async function createConversationForTargetUUID (req, res, next) { const session = await mongoose.startSession() try { session.startTransaction() const repo = req.ctx.repositories.getConversationRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() const userRepo = req.ctx.repositories.getBaseUserRepository() const requesterOrg = req.ctx.org const requesterUsername = req.ctx.user - const targetOrgUUID = req.params.uuid + const targetUUID = req.params.uuid const body = req.body - // Make sure target org matches user org if not secretariat - const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session }) - const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session }) - if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) { - return res.status(400).json({ message: 'User is not secretariat or admin for target org' }) - } - const user = await userRepo.findOneByUsernameAndOrgShortname(requesterUsername, requesterOrg, { session }) if (!body.body) { @@ -93,10 +47,10 @@ async function createConversationForOrg (req, res, next) { } const conversationBody = { - target_uuid: targetOrgUUID, + target_uuid: targetUUID, author_id: user.UUID, author_name: [user.name.first, user.name.last].join(' '), - author_role: isSecretariat ? 'Secretariat' : 'Partner', + author_role: 'Secretariat', visibility: body.visibility ? body.visibility.toLowerCase() : 'private', body: body.body } @@ -129,20 +83,20 @@ async function createConversationForOrg (req, res, next) { async function updateMessage (req, res, next) { const repo = req.ctx.repositories.getConversationRepository() - const targetOrgUUID = req.params.uuid + const targetUUID = req.params.uuid const body = req.body if (!body.body) { return res.status(400).json({ message: 'Missing required field body' }) } - const result = await repo.updateConversation(body, targetOrgUUID) + const result = await repo.updateConversation(body, targetUUID) return res.status(200).json(result) } module.exports = { getAllConversations, - getConversationsForOrg, - createConversationForOrg, + getConversationsForTargetUUID, + createConversationForTargetUUID, updateMessage } diff --git a/src/controller/conversation.controller/index.js b/src/controller/conversation.controller/index.js index dfcb2b79..d2a8c44e 100644 --- a/src/controller/conversation.controller/index.js +++ b/src/controller/conversation.controller/index.js @@ -15,33 +15,22 @@ router.get('/conversation', controller.getAllConversations ) -// Get conversations for all orgs - SEC only -router.get('/conversation/org', +// Get all conversations for target UUID - SEC only +router.get('/conversation/target/:uuid', 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 }), - controller.getAllConversations // TODO: for now, all conversations are targeted to orgs. Update this when conversations added for other objects + controller.getConversationsForTargetUUID ) -// Get conversations for org - SEC/ADMIN -router.get('/conversation/org/:uuid', +// Post conversation for target UUID - SEC only +router.post('/conversation/target/:uuid', mw.validateUser, - mw.onlySecretariatOrAdmin, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), - query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), - param(['uuid']).isUUID(4), - controller.getConversationsForOrg -) - -// Post conversation for org - SEC/ADMIN -router.post('/conversation/org/:uuid', - mw.validateUser, - mw.onlySecretariatOrAdmin, + mw.onlySecretariat, param(['uuid']).isUUID(4), - controller.createConversationForOrg + controller.createConversationForTargetUUID ) // Update conversation message - SEC only diff --git a/test/integration-tests/conversation/conversationTest.js b/test/integration-tests/conversation/conversationTest.js new file mode 100644 index 00000000..108c8896 --- /dev/null +++ b/test/integration-tests/conversation/conversationTest.js @@ -0,0 +1,134 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +describe('Testing Conversation endpoints', () => { + let orgUUID + let conversationUUID + + before(async () => { + await chai + .request(app) + .get('/api/org/win_5') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + orgUUID = res.body.UUID + }) + }) + + context('Positive Tests', () => { + it('Should create a conversation', async () => { + const conversation = { + visibility: 'public', + body: 'test' + } + await chai.request(app) + .post(`/api/conversation/target/${orgUUID}`) + .set(constants.headers) + .send(conversation) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('UUID') + conversationUUID = res.body.UUID + + expect(res.body).to.haveOwnProperty('target_uuid') + expect(res.body.target_uuid).to.equal(orgUUID) + + expect(res.body).to.haveOwnProperty('author_id') + expect(res.body).to.haveOwnProperty('author_name') + + expect(res.body).to.haveOwnProperty('author_role') + expect(res.body.author_role).to.equal('Secretariat') + + expect(res.body).to.haveOwnProperty('visibility') + expect(res.body.visibility).to.equal('public') + + expect(res.body).to.haveOwnProperty('body') + expect(res.body.body).to.equal('test') + }) + }) + it('Should get all conversations', async () => { + await chai.request(app) + .get('/api/conversation') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('conversations') + expect(res.body.conversations).to.be.an('array') + expect(res.body.conversations).to.have.lengthOf(1) + }) + }) + it('Should get all conversations for target UUID', async () => { + await chai.request(app) + .get(`/api/conversation/target/${orgUUID}`) + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.be.an('array') + expect(res.body).to.have.lengthOf(1) + expect(res.body[0]).to.haveOwnProperty('target_uuid') + expect(res.body[0].target_uuid).to.equal(orgUUID) + }) + }) + it('Should update the message for a conversation', async () => { + const updateBody = { + body: 'test update' + } + await chai.request(app) + .put(`/api/conversation/${conversationUUID}/message`) + .set(constants.headers) + .send(updateBody) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('UUID') + expect(res.body.UUID).to.equal(conversationUUID) + expect(res.body).to.haveOwnProperty('body') + expect(res.body.body).to.equal('test update') + }) + }) + }) + + context('Negative Tests', () => { + it('Should fail to post a conversation with no body', async () => { + await chai.request(app) + .post(`/api/conversation/target/${orgUUID}`) + .set(constants.headers) + .send({}) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('Missing required field body') + }) + }) + it('Should fail to update a conversation message with no body', async () => { + await chai.request(app) + .put(`/api/conversation/${conversationUUID}/message`) + .set(constants.headers) + .send({}) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('Missing required field body') + }) + }) + }) +}) diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index b42fa1cd..902652f9 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -255,7 +255,7 @@ describe('Testing Joint approval', () => { }) it('Secretariat leaves a public comment on the org review', async () => { await chai.request(app) - .post(`/api/conversation/org/${reviewUUID}`) + .post(`/api/conversation/target/${reviewUUID}`) .set(secretariatHeaders) .send({ visibility: 'public', @@ -271,7 +271,7 @@ describe('Testing Joint approval', () => { }) it('Secretariat leaves a private on the org review', async () => { await chai.request(app) - .post(`/api/conversation/org/${reviewUUID}`) + .post(`/api/conversation/target/${reviewUUID}`) .set(secretariatHeaders) .send({ visibility: 'private', From f3a82417b53cc38feba5b1b296142e3816c66a06 Mon Sep 17 00:00:00 2001 From: emathew Date: Sun, 23 Nov 2025 21:56:17 -0500 Subject: [PATCH 15/39] fix tests --- src/repositories/baseOrgRepository.js | 54 +++++++++++++------ .../audit/registryOrgCreatesAuditTest.js | 29 +++++----- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 71fd2733..59a77b10 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -375,16 +375,6 @@ class BaseOrgRepository extends BaseRepository { // legacy Only Stuff _.set(legacyOrg, 'policies.id_quota', (incomingParameters?.id_quota ?? legacyOrg.policies.id_quota)) - // Save changes - await registryOrg.save({ options }) - await legacyOrg.save({ options }) - if (isLegacyObject) { - const plainJavascriptLegacyOrg = legacyOrg.toObject() - delete plainJavascriptLegacyOrg.__v - delete plainJavascriptLegacyOrg._id - return deepRemoveEmpty(plainJavascriptLegacyOrg) - } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object. At this point permissions and object validation have been done. if (requestingUserUUID) { try { @@ -421,6 +411,15 @@ class BaseOrgRepository extends BaseRepository { // Don't fail the transaction if audit fails - just log it } } + // Save changes + await registryOrg.save({ options }) + await legacyOrg.save({ options }) + if (isLegacyObject) { + const plainJavascriptLegacyOrg = legacyOrg.toObject() + delete plainJavascriptLegacyOrg.__v + delete plainJavascriptLegacyOrg._id + return deepRemoveEmpty(plainJavascriptLegacyOrg) + } const plainJavascriptRegistryOrg = registryOrg.toObject() // Remove private things @@ -473,16 +472,37 @@ class BaseOrgRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptLegacyOrg) } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object. At this point permissions and object validation have been done. if (requestingUserUUID) { try { const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( - updatedRegistryOrg.UUID, - updatedRegistryOrg.toObject(), - requestingUserUUID, - options - ) + // Check if an audit document exists, if not we need to create one first and seed it with the existing org data + if (!(await auditRepo.findOneByTargetUUID(registryOrg.UUID, options))) { + const currentRegistryOrg = await this.findOneByShortName(shortName, options) + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + currentRegistryOrg.toObject(), + requestingUserUUID, + options + ) + } + // Get the org state before save for comparison + const beforeUpdateOrg = await this.findOneByShortName(shortName, options) + const beforeUpdateObject = beforeUpdateOrg.toObject() + const afterUpdateObject = registryOrg.toObject() + + // Clean objects for comparison (remove Mongoose metadata) + const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + + // Only add audit entry if there are changes + if (!_.isEqual(cleanBefore, cleanAfter)) { + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + registryOrg.toObject(), + requestingUserUUID, + options + ) + } } catch (auditError) { // Don't fail the transaction if audit fails - just log it } diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js index 74d9534b..524d84be 100644 --- a/test/integration-tests/audit/registryOrgCreatesAuditTest.js +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -36,7 +36,7 @@ async function createTestOrg (customProps = {}) { } } -describe.only('Create and Update Audit Collection with Org Endpoints', () => { +describe('Create and Update Audit Collection with Org Endpoints', () => { it('Should automatically create audit document when org is created', async () => { // Create org const org = await createTestOrg({ @@ -113,7 +113,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { const auditResUpdate = await chai.request(app) .get(`/api/audit/org/${org.uuid}`) .set(constants.headers) - expect(auditResUpdate.body.history).to.have.lengthOf(1) + expect(auditResUpdate.body.history).to.have.lengthOf(2) // Now update with same values const updateResAgain = await chai.request(app) @@ -126,10 +126,10 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { .get(`/api/audit/org/${org.uuid}`) .set(constants.headers) - expect(auditRes.body.history).to.have.lengthOf(1) + expect(auditRes.body.history).to.have.lengthOf(2) }) - it.only('Should add audit entry when single field is changed', async () => { + it('Should add audit entry when single field is changed', async () => { const testOrg = await createTestOrg({ hard_quota: 1500, authority: ['CNA'] @@ -161,22 +161,21 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { hard_quota: 1500, authority: ['CNA'] }) - await chai.request(app) - .put(`/api/registry/org/${testOrg.shortName}`) - .set(secretariatHeaders) // Make sequential updates - await chai.request(app) + const updatedRes1 = await chai.request(app) .put(`/api/registry/org/${testOrg.shortName}?id_quota=2000`) .set(secretariatHeaders) + expect(updatedRes1).to.have.status(200) - await chai.request(app) + const updatedRes2 = await chai.request(app) .put(`/api/registry/org/${testOrg.shortName}?id_quota=3000`) .set(secretariatHeaders) + expect(updatedRes2).to.have.status(200) - await chai.request(app) + const updatedRes3 = await chai.request(app) .put(`/api/registry/org/${testOrg.shortName}?id_quota=4000`) .set(secretariatHeaders) - + expect(updatedRes3).to.have.status(200) // Check audit history const auditRes = await chai.request(app) .get(`/api/audit/org/${testOrg.uuid}`) @@ -186,7 +185,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { // Verify chronological order const quotas = auditRes.body.history.map(h => h.audit_object.hard_quota) - expect(quotas).to.deep.equal([1000, 2000, 3000, 4000]) + expect(quotas).to.deep.equal([1500, 2000, 3000, 4000]) // Verify timestamps are in order for (let i = 1; i < auditRes.body.history.length; i++) { @@ -203,7 +202,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { }) // Manually delete audit document const repo = new AuditRepo() - repo.deleteByTargetUUID(testOrg.uuid) + await repo.deleteByTargetUUID(testOrg.uuid) // Check audit history const auditRes = await chai.request(app) .get(`/api/audit/org/${testOrg.uuid}`) @@ -218,7 +217,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { const auditResCreation = await chai.request(app) .get(`/api/audit/org/${testOrg.uuid}`) .set(constants.headers) - - expect(auditResCreation.body.history).to.have.lengthOf(1) + // Should have 2 entries: initial creation of current org object + new update + expect(auditResCreation.body.history).to.have.lengthOf(2) }) }) From 9439f328339e48950b2d677142a40fdb2281e72a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 24 Nov 2025 13:32:39 -0500 Subject: [PATCH 16/39] remove unused import --- test/unit-tests/org/orgCreateADPTest.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/unit-tests/org/orgCreateADPTest.js b/test/unit-tests/org/orgCreateADPTest.js index b5dac740..47cd9f12 100644 --- a/test/unit-tests/org/orgCreateADPTest.js +++ b/test/unit-tests/org/orgCreateADPTest.js @@ -6,12 +6,6 @@ const { faker } = require('@faker-js/faker') const expect = chai.expect const mongoose = require('mongoose') -const OrgRepository = require('../../../src/repositories/orgRepository.js') -const UserRepository = require('../../../src/repositories/userRepository.js') - -const RegistryOrgRepository = require('../../../src/repositories/registryOrgRepository.js') -const RegistryUserRepository = require('../../../src/repositories/registryUserRepository.js') - const { ORG_CREATE_SINGLE } = require('../../../src/controller/org.controller/org.controller.js') const CONSTANTS = require('../../../src/constants/index.js') const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository.js') From 1fa78d65c8f1a073776de4e042b422a6ac59c210 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 24 Nov 2025 13:42:35 -0500 Subject: [PATCH 17/39] linting issues --- .../registry-user.controller/registry-user.controller.js | 3 --- .../registry-org/registryOrgWithJointReviewTest.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 5a39f6f5..5815add6 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -24,9 +24,6 @@ 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/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index 902652f9..f863250f 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -69,7 +69,6 @@ describe('Testing Joint approval', () => { let secret let orgUUID let reviewUUID - let createdOrg it('Create an org to use for testing', async () => { await chai.request(app) .post('/api/registryOrg') @@ -97,8 +96,6 @@ describe('Testing Joint approval', () => { expect(res.body.created).to.haveOwnProperty('hard_quota') expect(res.body.created.hard_quota).to.equal(testRegistryOrgForReview.hard_quota) - - createdOrg = res.body.created }) }) it('Create an User', async () => { From 51c810768e4f2f4d23e65b3280b10ea1286073f0 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 19 Nov 2025 10:14:13 -0500 Subject: [PATCH 18/39] Pass at removing --- .../registry-user.controller/index.js | 5 +- .../registry-user.controller.js | 177 +++++++----------- .../user.controller/user.controller.js | 13 -- src/repositories/baseUserRepository.js | 5 + 4 files changed, 71 insertions(+), 129 deletions(-) diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 55d650ae..db97b684 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -145,7 +145,7 @@ router.get('/registryUser/:identifier', controller.SINGLE_USER ) -router.post('/registryUser', +router.post('/registryUser/:shortname', /* #swagger.tags = ['Registry User'] #swagger.operationId = 'createRegistryUser' @@ -212,9 +212,6 @@ router.post('/registryUser', */ mw.validateUser, mw.onlySecretariat, - // mw.onlySecretariat, // TODO: permissions - // TODO: validation - // parseError, parsePostParams, controller.CREATE_USER ) diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 5815add6..73570335 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -1,6 +1,4 @@ -const argon2 = require('argon2') -const cryptoRandomString = require('crypto-random-string') -const uuid = require('uuid') +const mongoose = require('mongoose') const logger = require('../../middleware/logger') const { getConstants } = require('../../constants') const errors = require('../user.controller/error') @@ -9,6 +7,9 @@ const error = new errors.UserControllerError() async function getAllUsers (req, res, next) { try { const CONSTANTS = getConstants() + const session = await mongoose.startSession() + const repo = req.ctx.repositories.getBaseUserRepository() + let returnValue // temporary measure to allow tests to work after fixing #920 // tests required changing the global limit to force pagination @@ -19,24 +20,15 @@ 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.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 + try { + returnValue = await repo.getAllUsers(options) + } finally { + await session.endSession() } logger.info({ uuid: req.ctx.uuid, message: 'The user information was sent to the secretariat user.' }) - return res.status(200).json(payload) + return res.status(200).json(returnValue) } catch (err) { next(err) } @@ -44,13 +36,14 @@ async function getAllUsers (req, res, next) { async function getUser (req, res, next) { try { - const repo = req.ctx.repositories.getRegistryUserRepository() + const repo = req.ctx.repositories.getBaseUserRepository() 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 }) + const result = await repo.findUserByUUID(identifier) + if (!result) { + logger.info({ uuid: req.ctx.uuid, message: identifier + 'user could not be found.' }) + return res.status(404).json(error.userDne(identifier)) + } return res.status(200).json(result) } catch (err) { next(err) @@ -58,68 +51,67 @@ async function getUser (req, res, next) { } async function createUser (req, res, next) { + const session = await mongoose.startSession() 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 orgRepo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() const body = req.ctx.body + const orgShortName = req.ctx.params.shortname + let returnValue + + const orgUUID = await orgRepo.getOrgUUID(orgShortName) + if (!orgUUID) { + logger.info({ uuid: req.ctx.uuid, message: 'The user could not be created because ' + orgShortName + ' organization does not exist.' }) + return res.status(404).json(error.orgDnePathParam(orgShortName)) + } - // Short circuit if UUID provided - const bodyKeys = Object.keys(body).map((k) => k.toLowerCase()) - if (bodyKeys.includes('uuid')) { + // Do not allow the user to pass in a UUID + if ((body?.UUID ?? null) || (body?.uuid ?? null)) { return res.status(400).json(error.uuidProvided('user')) } - // 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 - } - }) + if ((body?.org_UUID ?? null) || (body?.org_uuid ?? null)) { + return res.status(400).json(error.uuidProvided('org')) + } - // TODO: check that requesting user is admin of org for new user + try { + session.startTransaction() - 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 + const result = await userRepo.validateUser(body) + if (body?.role && typeof body?.role !== 'string') { + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) + } + if (!result.isValid) { + logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) + await session.abortTransaction() + return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) + } - 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 + // Ask repo if user already exists + if (await userRepo.orgHasUser(orgShortName, body?.username, { session })) { + logger.info({ uuid: req.ctx.uuid, message: `${body?.username} user was not created because it already exists.` }) + await session.abortTransaction() + return res.status(400).json(error.userExists(body?.username)) + } - const 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 + const users = await userRepo.findUsersByOrgShortname(orgShortName, { session }) + if (users.length >= 100) { + await session.abortTransaction() + return res.status(400).json(error.userLimitReached()) + } + + returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }) + await session.commitTransaction() + } catch (error) { + await session.abortTransaction() + throw error + } finally { + await session.endSession() } - 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 + message: `${body?.user_id} + ' was successfully created.`, + created: returnValue } return res.status(200).json(responseMessage) @@ -130,48 +122,9 @@ async function createUser (req, res, next) { async function updateUser (req, res, next) { try { - // 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) - // 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 - - // 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 - } + const userRepo = req.ctx.repositories.getBaseUserRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() await registryUserRepo.updateByUUID(userUUID, newUser) const agt = setAggregateUserObj({ UUID: userUUID }) diff --git a/src/controller/user.controller/user.controller.js b/src/controller/user.controller/user.controller.js index 2bad2914..e66edf0c 100644 --- a/src/controller/user.controller/user.controller.js +++ b/src/controller/user.controller/user.controller.js @@ -31,19 +31,6 @@ async function getAllUsers (req, res, next) { await session.endSession() } - /* const agt = isRegistry ? setAggregateRegistryUserObj({}) : 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(returnValue) } catch (err) { diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 85b036fc..c5a1605a 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -316,6 +316,11 @@ class BaseUserRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptRegistryUser) } + async updateUserFull () { + const baseOrgRepository = new BaseOrgRepository() + const legacyUserRepo = new UserRepository() + } + async resetSecret (username, orgShortName, options = {}, isLegacyObject = false) { const legacyUserRepo = new UserRepository() const baseOrgRepository = new BaseOrgRepository() From 3234bdf4e5b5004eedd89a660fddb5e492324994 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 25 Nov 2025 13:54:38 -0500 Subject: [PATCH 19/39] Another pass --- .../registry-user.controller.js | 57 +++++++++---------- src/repositories/baseUserRepository.js | 53 ++++++++++++++++- 2 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 73570335..2ce44cd5 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -121,15 +121,33 @@ async function createUser (req, res, next) { } async function updateUser (req, res, next) { - try { - const userUUID = req.ctx.params.identifier - const userRepo = req.ctx.repositories.getBaseUserRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const session = await mongoose.startSession() + const userUUID = req.ctx.params.identifier + const userRepo = req.ctx.repositories.getBaseUserRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const body = req.ctx.body + let result - await registryUserRepo.updateByUUID(userUUID, newUser) - const agt = setAggregateUserObj({ UUID: userUUID }) - let result = await registryUserRepo.aggregate(agt) - result = result.length > 0 ? result[0] : null + try { + session.startTransaction() + try { + result = await userRepo.validateUser(body) + if (body?.role && typeof body?.role !== 'string') { + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) + } + if (!result.isValid) { + logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) + await session.abortTransaction() + return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) + } + await userRepo.updateUserFull(userUUID, body, { session }) + await session.commitTransaction() + } catch (error) { + await session.abortTransaction() + throw error + } finally { + await session.endSession() + } const payload = { action: 'update_registry_user', @@ -190,29 +208,6 @@ async function deleteUser (req, res, next) { } } -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, diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index c5a1605a..d61f8f8c 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -316,9 +316,58 @@ class BaseUserRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptRegistryUser) } - async updateUserFull () { - const baseOrgRepository = new BaseOrgRepository() + async updateUserFull (identifier, incomingUser, options = {}, isLegacyObject = false) { const legacyUserRepo = new UserRepository() + + // Find registry user by UUID + const registryUser = await this.findUserByUUID(identifier, options) + if (!registryUser) { + throw new Error('Registry user not found') + } + + // Find legacy user + const legacyUser = await legacyUserRepo.findOneByUUID(identifier) + if (!legacyUser) { + throw new Error('Legacy user not found') + } + + let legacyObjectRaw + let registryObjectRaw + + if (isLegacyObject) { + legacyObjectRaw = incomingUser + registryObjectRaw = this.convertRegistryToLegacy(incomingUser) + } else { + registryObjectRaw = incomingUser + legacyObjectRaw = this.convertRegistryToLegacy(incomingUser) + } + + const updatedLegacyUser = _.merge(legacyUser, legacyObjectRaw) + const updatedRegistryUser = _.merge(registryUser, registryObjectRaw) + + try { + await updatedLegacyUser.save({ options }) + await updatedRegistryUser.save({ options }) + } catch (error) { + throw new Error('Failed to update user') + } + + if (isLegacyObject) { + const plain = updatedLegacyUser.toObject() + delete plain._id + delete plain.__v + delete plain.secret + return plain + } + + // Retrieve updated registry user + const plainJsRegistryUser = updatedRegistryUser.toObject() + delete plainJsRegistryUser._id + delete plainJsRegistryUser.__v + delete plainJsRegistryUser.secret + delete plainJsRegistryUser.authority + + return plainJsRegistryUser } async resetSecret (username, orgShortName, options = {}, isLegacyObject = false) { From 15a62572f21a8cd6457264949d766dd1e1031e6d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 25 Nov 2025 15:47:29 -0500 Subject: [PATCH 20/39] Fixed some unit tests --- .../review-object.controller.js | 18 ++++++++++++++++++ test/unit-tests/org/orgCreateADPTest.js | 1 + test/unit-tests/org/orgCreateTest.js | 6 +++++- test/unit-tests/org/orgUpdateTest.js | 12 ++++++++++++ .../review-object.controller.test.js | 6 ++++-- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index 44717797..1314e9a9 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -86,8 +86,26 @@ async function updateReviewObjectByReviewUUID (req, res, next) { async function createReviewObject (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() + const orgRepo = req.ctx.repositories.getBaseOrgRepository() const body = req.body + if (body.uuid) { + return res.status(400).json({ message: 'Do not pass in a uuid key when creating a review object' }) + } + + if (body.target_object_uuid === undefined) { + return res.status(400).json({ message: 'Missing required field target_object_uuid' }) + } + + if (!body.new_review_data) { + return res.status(400).json({ message: 'Missing required field new_review_data' }) + } + + const result = orgRepo.validateOrg(body.new_review_data) + if (!result.isValid) { + return res.status(400).json({ message: 'Invalid new_review_data', errors: result.errors }) + } + const value = await repo.createReviewOrgObject(body) if (!value) { diff --git a/test/unit-tests/org/orgCreateADPTest.js b/test/unit-tests/org/orgCreateADPTest.js index 47cd9f12..03188547 100644 --- a/test/unit-tests/org/orgCreateADPTest.js +++ b/test/unit-tests/org/orgCreateADPTest.js @@ -67,6 +67,7 @@ describe('Testing creating orgs with the ADP role', () => { // --- Method Stubbing -- sinon.stub(regOrgRepo, 'findOneByShortName').resolves(null) + sinon.stub(regOrgRepo, 'isSecretariatByShortName').resolves(true) // Stub aggregate to return an array with a fake object, so result[0] works const fakeAggregatedOrg = { UUID: 'org-uuid-123', short_name: 'fakeOrg', name: 'Fake Org Name' } diff --git a/test/unit-tests/org/orgCreateTest.js b/test/unit-tests/org/orgCreateTest.js index a99ec839..cf098cdc 100644 --- a/test/unit-tests/org/orgCreateTest.js +++ b/test/unit-tests/org/orgCreateTest.js @@ -16,6 +16,7 @@ const SecretariatOrgModel = require('../../../src/model/secretariatorg.js') const CNAOrgModel = require('../../../src/model/cnaorg.js') const ADPOrgModel = require('../../../src/model/adporg.js') const Org = require('../../../src/model/org.js') +const AuditRepository = require('../../../src/repositories/auditRepository.js') // Mocks for error messages and constants const { OrgControllerError } = require('../../../src/controller/org.controller/error.js') @@ -53,7 +54,7 @@ const orgFixtures = { describe('Testing the ORG_CREATE_SINGLE controller', () => { let status, json, res, next, getOrgRepository, orgRepo, getUserRepository, getBaseOrgRepository, getBaseUserRepository, - userRepo, mockSession, baseOrgRepo, baseUserRepo, fakeBaseSavedObject, saveStub, fakeLegacySavedObject, fakeMongooseDocument, fakeBaseSavedObjectCisco, fakeLegacySavedObjectCisco + userRepo, mockSession, baseOrgRepo, baseUserRepo, fakeBaseSavedObject, saveStub, fakeLegacySavedObject, fakeMongooseDocument, fakeBaseSavedObjectCisco, fakeLegacySavedObjectCisco, auditRepo // Runs before each test case beforeEach(() => { @@ -123,6 +124,8 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { saveStub = sinon.stub(SecretariatOrgModel.prototype, 'save').resolves(fakeBaseSavedObject) fakeMongooseDocument = new Org(fakeLegacySavedObject) + sinon.stub(AuditRepository.prototype, 'appendToAuditHistoryForOrg').resolves(true) + // Stub repository getters orgRepo = new OrgRepository() getOrgRepository = sinon.stub().returns(orgRepo) @@ -201,6 +204,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { sinon.stub(orgRepo, 'getOrgUUID').resolves('org-uuid-123') sinon.stub(userRepo, 'getUserUUID').resolves('user-uuid-123') sinon.stub(baseOrgRepo, 'getOrgUUID').resolves('org-uuid-123') + sinon.stub(baseOrgRepo, 'isSecretariatByShortName').resolves(true) sinon.stub(baseUserRepo, 'getUserUUID').resolves('user-uuid-123') }) diff --git a/test/unit-tests/org/orgUpdateTest.js b/test/unit-tests/org/orgUpdateTest.js index 9542b59f..6d9f624c 100644 --- a/test/unit-tests/org/orgUpdateTest.js +++ b/test/unit-tests/org/orgUpdateTest.js @@ -39,6 +39,10 @@ class OrgUpdatedAddingRole { return orgFixtures.owningOrg } + async isSecretariatByShortName () { + return true + } + async aggregate () { return [orgFixtures.owningOrg] } @@ -78,6 +82,10 @@ class OrgUpdatedRemovingRole { return temp } + async isSecretariatByShortName () { + return true + } + async orgExists () { return true } @@ -333,6 +341,10 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => { return true } + async isSecretariatByShortName () { + return true + } + async updateOrg () { return orgFixtures.existentOrg } diff --git a/test/unit-tests/review-object/review-object.controller.test.js b/test/unit-tests/review-object/review-object.controller.test.js index 06d0416b..9515f6c3 100644 --- a/test/unit-tests/review-object/review-object.controller.test.js +++ b/test/unit-tests/review-object/review-object.controller.test.js @@ -23,6 +23,7 @@ describe('Review Object Controller', function () { } next = sinon.stub() + orgRepoStub.isSecretariatByShortName = sinon.stub().resolves(true) }) describe('getReviewObjectByOrgIdentifier', function () { @@ -70,7 +71,7 @@ describe('Review Object Controller', function () { req.body.new_review_data = { invalid: true } orgRepoStub.validateOrg = sinon.stub().returns({ isValid: false, errors: ['bad data'] }) await controller.updateReviewObjectByReviewUUID(req, res, next) - expect(orgRepoStub.validateOrg.calledWith(req.body.new_review_data)).to.be.true + expect(orgRepoStub.validateOrg.calledOnce).to.be.true expect(res.status.calledWith(400)).to.be.true expect(res.json.calledWith({ message: 'Invalid new_review_data', errors: ['bad data'] })).to.be.true }) @@ -128,7 +129,7 @@ describe('Review Object Controller', function () { req.body.new_review_data = { bad: true } orgRepoStub.validateOrg = sinon.stub().returns({ isValid: false, errors: ['err'] }) await controller.createReviewObject(req, res, next) - expect(orgRepoStub.validateOrg.calledWith(req.body.new_review_data)).to.be.true + expect(orgRepoStub.validateOrg.calledOnce).to.be.true expect(res.status.calledWith(400)).to.be.true expect(res.json.calledWith({ message: 'Invalid new_review_data', errors: ['err'] })).to.be.true }) @@ -137,6 +138,7 @@ describe('Review Object Controller', function () { req.body.target_object_uuid = 'obj-uuid' req.body.new_review_data = { foo: 'bar' } orgRepoStub.validateOrg = sinon.stub().returns({ isValid: true }) + repoStub.validateOrg = repoStub.createReviewOrgObject = sinon.stub().resolves(undefined) await controller.createReviewObject(req, res, next) expect(repoStub.createReviewOrgObject.calledWith(req.body)).to.be.true From 23c07d459e3fa01d5c9807ff5c9336bb21a924e9 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 25 Nov 2025 15:55:41 -0500 Subject: [PATCH 21/39] Old tests are old --- .../review-object.controller.js | 18 -------- .../review-object.controller.test.js | 41 ------------------- 2 files changed, 59 deletions(-) diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index 1314e9a9..44717797 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -86,26 +86,8 @@ async function updateReviewObjectByReviewUUID (req, res, next) { async function createReviewObject (req, res, next) { const repo = req.ctx.repositories.getReviewObjectRepository() - const orgRepo = req.ctx.repositories.getBaseOrgRepository() const body = req.body - if (body.uuid) { - return res.status(400).json({ message: 'Do not pass in a uuid key when creating a review object' }) - } - - if (body.target_object_uuid === undefined) { - return res.status(400).json({ message: 'Missing required field target_object_uuid' }) - } - - if (!body.new_review_data) { - return res.status(400).json({ message: 'Missing required field new_review_data' }) - } - - const result = orgRepo.validateOrg(body.new_review_data) - if (!result.isValid) { - return res.status(400).json({ message: 'Invalid new_review_data', errors: result.errors }) - } - const value = await repo.createReviewOrgObject(body) if (!value) { diff --git a/test/unit-tests/review-object/review-object.controller.test.js b/test/unit-tests/review-object/review-object.controller.test.js index 9515f6c3..fb720280 100644 --- a/test/unit-tests/review-object/review-object.controller.test.js +++ b/test/unit-tests/review-object/review-object.controller.test.js @@ -66,16 +66,6 @@ describe('Review Object Controller', function () { }) describe('updateReviewObjectByReviewUUID', function () { - it('should return 400 if new_review_data is invalid', async () => { - req.params.uuid = 'some-uuid' - req.body.new_review_data = { invalid: true } - orgRepoStub.validateOrg = sinon.stub().returns({ isValid: false, errors: ['bad data'] }) - await controller.updateReviewObjectByReviewUUID(req, res, next) - expect(orgRepoStub.validateOrg.calledOnce).to.be.true - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Invalid new_review_data', errors: ['bad data'] })).to.be.true - }) - it('should return 404 if review object not found', async () => { const uuid = 'rev-uuid' req.params.uuid = uuid @@ -103,37 +93,6 @@ describe('Review Object Controller', function () { }) describe('createReviewObject', function () { - it('should return 400 if body contains uuid', async () => { - req.body.uuid = 'should-not-be-here' - await controller.createReviewObject(req, res, next) - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Do not pass in a uuid key when creating a review object' })).to.be.true - }) - - it('should return 400 if target_object_uuid missing', async () => { - req.body.new_review_data = { foo: 'bar' } - await controller.createReviewObject(req, res, next) - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Missing required field target_object_uuid' })).to.be.true - }) - - it('should return 400 if new_review_data missing', async () => { - req.body.target_object_uuid = 'obj-uuid' - await controller.createReviewObject(req, res, next) - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Missing required field new_review_data' })).to.be.true - }) - - it('should return 400 if new_review_data is invalid', async () => { - req.body.target_object_uuid = 'obj-uuid' - req.body.new_review_data = { bad: true } - orgRepoStub.validateOrg = sinon.stub().returns({ isValid: false, errors: ['err'] }) - await controller.createReviewObject(req, res, next) - expect(orgRepoStub.validateOrg.calledOnce).to.be.true - expect(res.status.calledWith(400)).to.be.true - expect(res.json.calledWith({ message: 'Invalid new_review_data', errors: ['err'] })).to.be.true - }) - it('should return 500 if repo create fails', async () => { req.body.target_object_uuid = 'obj-uuid' req.body.new_review_data = { foo: 'bar' } From 4cc8e6593c75c316b5e34d23cb80df9f6ab4f66d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 1 Dec 2025 13:06:55 -0500 Subject: [PATCH 22/39] removed incorrect throw documentation --- src/controller/org.controller/org.controller.js | 4 ++++ .../registry-org.controller/registry-org.controller.js | 3 +++ src/repositories/baseOrgRepository.js | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 850260ce..e77cfe86 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -67,6 +67,10 @@ async function getOrg (req, res, next) { returnValue = await repo.getOrg(identifier, identifierIsUUID, { session }, !req.useRegistry) } catch (error) { await session.abortTransaction() + // Handle the specific error thrown by BaseOrgRepository.createOrg + if (error.message && error.message.includes('Unknown Org type requested')) { + return res.status(400).json({ message: error.message }) + } throw error } finally { await session.endSession() diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index d9bf95e8..a0f98e61 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -161,6 +161,9 @@ async function createOrg (req, res, next) { await session.commitTransaction() } catch (createErr) { await session.abortTransaction() + if (createErr.message && createErr.message.includes('Unknown Org type requested')) { + return res.status(400).json({ message: createErr.message }) + } throw createErr } finally { await session.endSession() diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index af4f59fa..18a053c8 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -250,8 +250,8 @@ class BaseOrgRepository extends BaseRepository { await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, { options }) } } else { - // eslint-disable-next-line no-throw-literal - throw 'dave you screwed up' + // Throw an Error instance so callers can catch and handle it properly + throw new Error("Unknown Org type requested. Please use either 'SECRETARIAT', 'CNA', 'ADP', or 'BULK_DOWNLOAD' as the authority role.") } // ADD AUDIT ENTRY AUTOMATICALLY for the registry object From 3f90ca68f06d7306efc5926ccb0f58543926aa0c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 3 Dec 2025 12:56:32 -0500 Subject: [PATCH 23/39] added more values to the joint approval fields --- src/constants/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants/index.js b/src/constants/index.js index 4b519dc0..4f1ae54c 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,8 +44,8 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name'], - JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email'], + JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles'], USER_ROLE_ENUM: { ADMIN: 'ADMIN' }, From 120e6d7cff178a19539816263df699e8f3a961c2 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 3 Dec 2025 15:49:38 -0500 Subject: [PATCH 24/39] Various small fixes and clean up --- src/controller/org.controller/index.js | 6 +++++- src/middleware/errorMessages.js | 3 +++ src/repositories/baseOrgRepository.js | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 00ab0aba..0cdefffe 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -1004,7 +1004,10 @@ router.post( */ mw.validateUser, mw.onlySecretariat, - body(['short_name']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + body(['short_name']) + .isString().withMessage(errorMsgs.MUST_BE_STRING).trim() + .notEmpty().withMessage(errorMsgs.NOT_EMPTY) + .isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }).withMessage(errorMsgs.SHORTNAME_LENGTH), body(['name']).isString().trim().notEmpty(), body(['authority.active_roles']).optional() .custom(isFlatStringArray) @@ -1178,6 +1181,7 @@ router.put('/org/:shortname', parseError, parsePostParams, controller.ORG_UPDATE_SINGLE) + router.get('/org/:shortname/id_quota', /* #swagger.tags = ['Organization'] diff --git a/src/middleware/errorMessages.js b/src/middleware/errorMessages.js index c7aa7db1..7ed28d79 100644 --- a/src/middleware/errorMessages.js +++ b/src/middleware/errorMessages.js @@ -3,6 +3,9 @@ module.exports = { ORG_ROLES: 'Invalid role. Valid roles are CNA, SECRETARIAT', USER_ROLES: 'Invalid role. Valid role is ADMIN', + NOT_EMPTY: 'Value must not be empty', + SHORTNAME_LENGTH: 'Value must be between 2 and 20 characters in length.', + MUST_BE_STRING: 'Value must be a string', ID_QUOTA: 'The id_quota does not comply with CVE id quota limitations', ID_STATES: 'Invalid CVE ID state. Valid states are: RESERVED, PUBLISHED, REJECTED', ID_MODIFY_STATES: 'Invalid CVE ID state. Valid states are: RESERVED, REJECTED', diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 18a053c8..4ed6c009 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -35,6 +35,12 @@ function setAggregateRegistryOrgObj (query) { return [ { $match: query + }, + { + $project: { + _id: false, + __t: false + } } ] } From faf5e6d51275bcf800e673fc0b8564cfcc1ef71f Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 3 Dec 2025 16:27:18 -0500 Subject: [PATCH 25/39] Fixed a typing issue for authority --- src/controller/org.controller/index.js | 4 +++- src/controller/org.controller/org.controller.js | 5 +++-- src/middleware/schemas/ADPOrg.json | 2 +- src/middleware/schemas/BaseOrg.json | 6 +++++- src/middleware/schemas/BulkDownloadOrg.json | 2 +- src/middleware/schemas/CNAOrg.json | 2 +- src/middleware/schemas/SecretariatOrg.json | 2 +- src/repositories/baseOrgRepository.js | 12 ++++++++++-- test/integration-tests/constants.js | 6 +++--- .../registry-org/registryOrgCRUDTest.js | 2 +- .../registry-org/registryOrgWithJointReviewTest.js | 4 ++-- 11 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 0cdefffe..0345c1b4 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -1008,7 +1008,9 @@ router.post( .isString().withMessage(errorMsgs.MUST_BE_STRING).trim() .notEmpty().withMessage(errorMsgs.NOT_EMPTY) .isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }).withMessage(errorMsgs.SHORTNAME_LENGTH), - body(['name']).isString().trim().notEmpty(), + body(['name']) + .isString().withMessage(errorMsgs.MUST_BE_STRING).trim() + .notEmpty().withMessage(errorMsgs.NOT_EMPTY), body(['authority.active_roles']).optional() .custom(isFlatStringArray) .customSanitizer(toUpperCaseArray) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index e77cfe86..5067a7c7 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -1,5 +1,6 @@ require('dotenv').config() const mongoose = require('mongoose') +const _ = require('lodash') const logger = require('../../middleware/logger') const getConstants = require('../../constants').getConstants const errors = require('./error') @@ -238,9 +239,9 @@ async function registryCreateOrg (req, res, next) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) await session.abortTransaction() if (!Array.isArray(body?.authority) || body?.authority.some(item => typeof item !== 'string')) { - return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) + return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', details: [{ param: 'authority', msg: 'Parameter must be a one-dimensional array of strings' }] }) } - return res.status(400).json({ message: 'Parameters were invalid', errors: result.errors }) + return res.status(400).json({ error: 'BAD_INPUT', message: 'Parameters were invalid', errors: result.errors }) } // Check to see if the org already exists diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json index fd0998ff..7979d1f5 100644 --- a/src/middleware/schemas/ADPOrg.json +++ b/src/middleware/schemas/ADPOrg.json @@ -9,7 +9,7 @@ { "properties": { "authority": { - "const": "ADP" + "const": ["ADP"] } } } diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json index fdabde49..8b8f76d1 100644 --- a/src/middleware/schemas/BaseOrg.json +++ b/src/middleware/schemas/BaseOrg.json @@ -62,7 +62,11 @@ } }, "authority": { - "$ref": "#/definitions/authority" + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/authority" + } }, "root_or_tlr": { "type": "boolean" diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json index f29c6336..09853637 100644 --- a/src/middleware/schemas/BulkDownloadOrg.json +++ b/src/middleware/schemas/BulkDownloadOrg.json @@ -9,7 +9,7 @@ { "properties": { "authority": { - "const": "BULK_DOWNLOAD" + "const": ["BULK_DOWNLOAD"] } } } diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json index 274e6182..c1188c8c 100644 --- a/src/middleware/schemas/CNAOrg.json +++ b/src/middleware/schemas/CNAOrg.json @@ -9,7 +9,7 @@ { "properties": { "authority": { - "const": "CNA" + "const": ["CNA"] }, "oversees": { "type": "array", diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json index 7dcb7797..4e658b57 100644 --- a/src/middleware/schemas/SecretariatOrg.json +++ b/src/middleware/schemas/SecretariatOrg.json @@ -9,7 +9,7 @@ { "properties": { "authority": { - "const": "SECRETARIAT" + "const": ["SECRETARIAT"] }, "oversees": { "type": "array", diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 4ed6c009..239e256f 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -642,14 +642,22 @@ class BaseOrgRepository extends BaseRepository { if (Array.isArray(org.authority)) { // User passed in an array, we need to decide how we handle this. if (org.authority.includes('SECRETARIAT')) { - org.authority = 'SECRETARIAT' + org.authority = ['SECRETARIAT'] validateObject = SecretariatOrgModel.validateOrg(org) } else { // We are not a secretariat, so we need to take most priv if (org.authority.includes('CNA') || org.authority.length === 0) { - org.authority = 'CNA' + org.authority = ['CNA'] validateObject = CNAOrgModel.validateOrg(org) } + if (org.authority.includes('ADP')) { + org.authority = ['ADP'] + validateObject = ADPOrgModel.validateOrg(org) + } + if (org.authority.includes('BULK_DOWNLOAD')) { + org.authority = ['BULK_DOWNLOAD'] + validateObject = BulkDownloadModel.validateOrg(org) + } } } else { if (org.authority === 'ADP') { diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index c27d7d97..de494682 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -373,7 +373,7 @@ const testRegistryOrg = { org_email: 'contact@test.org', website: 'https://test.org' }, - authority: 'CNA', + authority: ['CNA'], hard_quota: 100000 } @@ -387,7 +387,7 @@ const testRegistryOrg2 = { org_email: 'contact@test.org', website: 'https://test.org' }, - authority: 'CNA', + authority: ['CNA'], hard_quota: 100000 } @@ -415,7 +415,7 @@ const existingRegistryOrg = { org_email: 'contact@test.org', website: 'https://test.org' }, - authority: 'CNA', + authority: ['CNA'], hard_quota: 100000 } diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 4238bc23..35e897f5 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -11,7 +11,7 @@ const secretariatHeaders = { ...constants.headers, 'content-type': 'application/ const testRegistryOrg = { short_name: 'registry_org_test', long_name: 'Registry Org Test', - authority: 'CNA', + authority: ['CNA'], hard_quota: 1000 } let createdOrg diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index f863250f..b9092720 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -25,14 +25,14 @@ const nonAdminHeaders2 = { const testRegistryOrgForReview = { short_name: 'non_secretariat_org', long_name: 'Non Secretariat Org', - authority: 'CNA', + authority: ['CNA'], hard_quota: 1000 } const testRegistryOrgForReviewWithComments = { short_name: 'non_with_comments', long_name: 'Non Secretariat Org', - authority: 'CNA', + authority: ['CNA'], hard_quota: 1000 } From e9407084f5b8220a91da282e96bb4ef49c243a04 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 3 Dec 2025 16:40:21 -0500 Subject: [PATCH 26/39] now will return all errors at once when making registry orgs --- src/middleware/schemas/BaseOrg.json | 3 ++- src/model/adporg.js | 2 +- src/model/bulkdownloadorg.js | 2 +- src/model/cnaorg.js | 2 +- src/model/secretariatorg.js | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json index 8b8f76d1..d5d9203b 100644 --- a/src/middleware/schemas/BaseOrg.json +++ b/src/middleware/schemas/BaseOrg.json @@ -121,6 +121,7 @@ } }, "required": [ - "short_name" + "short_name", + "long_name" ] } \ No newline at end of file diff --git a/src/model/adporg.js b/src/model/adporg.js index 7932626c..f9d345fc 100644 --- a/src/model/adporg.js +++ b/src/model/adporg.js @@ -5,7 +5,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const AdpOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/ADPOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/bulkdownloadorg.js b/src/model/bulkdownloadorg.js index 0acba5ee..e196b5ff 100644 --- a/src/model/bulkdownloadorg.js +++ b/src/model/bulkdownloadorg.js @@ -5,7 +5,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BulkDownloadOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js index df1f3ca3..ab17599c 100644 --- a/src/model/cnaorg.js +++ b/src/model/cnaorg.js @@ -5,7 +5,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const CnaOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/CNAOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/secretariatorg.js b/src/model/secretariatorg.js index 446fb0ca..127d236a 100644 --- a/src/model/secretariatorg.js +++ b/src/model/secretariatorg.js @@ -5,7 +5,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) const SecretariatOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/SecretariatOrg.json')) -const ajv = new Ajv({ allErrors: false }) +const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) From 0dfb080cd949c508ba63afd218ec5b75b06b1afa Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 4 Dec 2025 16:16:19 -0500 Subject: [PATCH 27/39] we should now be changing types --- src/model/adporg.js | 7 +- src/repositories/baseOrgRepository.js | 79 +++++++++++++++++-- src/repositories/baseUserRepository.js | 2 - src/repositories/reviewObjectRepository.js | 2 +- .../org/regularUsersTestRegistryFlag.js | 4 +- .../registryOrgWithJointReviewTest.js | 4 +- 6 files changed, 83 insertions(+), 15 deletions(-) diff --git a/src/model/adporg.js b/src/model/adporg.js index f9d345fc..f5efa867 100644 --- a/src/model/adporg.js +++ b/src/model/adporg.js @@ -11,7 +11,12 @@ ajv.addSchema(BaseOrgSchema) const validate = ajv.compile(AdpOrgSchema) -const schema = {} +// Hard and soft quotas should be retained if something was a cna, then became an adp, then back to cna +// In general, this should never happen, but we have a test case for it, so I want to make sure it works as expected. +const schema = { + hard_quota: Number, + soft_quota: Number +} const options = { discriminatorKey: 'kind' } const ADPSchema = new mongoose.Schema(schema, options) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 239e256f..f1f3328b 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -362,7 +362,7 @@ class BaseOrgRepository extends BaseRepository { const legacyOrgRepo = new OrgRepository() const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) - const registryOrg = await this.findOneByShortName(shortName, options) + let registryOrg = await this.findOneByShortName(shortName, options) // Both legacy and registry if (incomingParameters?.new_short_name) { @@ -374,14 +374,46 @@ class BaseOrgRepository extends BaseRepository { legacyOrg.name = incomingParameters?.name ?? legacyOrg.name // TODO: We should probably limit this so it only puts in things that we allow - // Deal with the special way roles are added / removed - // TODO: We are going to need to really check this, this works for single adds / removes. But Matt has some good tests that we should run. - // TODO: What should we do if something is a CNA type, and then gets removed. Does its descriminator need to change? const rolesToAdd = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.add'))) const rolesToRemove = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.remove'))) const initialRoles = legacyOrg.authority?.active_roles ?? [] const finalRoles = [...new Set([...initialRoles, ...rolesToAdd])].filter(role => !rolesToRemove.includes(role)) + + let roleChange = false + // Check if final roles match the original roles in the registry org + if (!_.isEqual(finalRoles.sort(), registryOrg.authority.sort())) { + roleChange = true + } + + // Update authority and discriminator based on role changes registryOrg.authority = finalRoles + // Determine the target model based on the new authority + let TargetModel = null + if (finalRoles.includes('SECRETARIAT')) { + TargetModel = SecretariatOrgModel + } else if (finalRoles.includes('CNA')) { + TargetModel = CNAOrgModel + } else if (finalRoles.includes('ADP')) { + TargetModel = ADPOrgModel + } else if (finalRoles.includes('BULK_DOWNLOAD')) { + TargetModel = BulkDownloadModel + } + + // Save changes - handle possible model type change + if (TargetModel && roleChange) { + const oldId = registryOrg._id + // Remove the old document + await BaseOrgModel.deleteOne({ _id: oldId }, options) + // Create a new document of the correct type, preserving the UUID + const newDocData = registryOrg.toObject() + delete newDocData.__t + newDocData._id = oldId + const newDoc = new TargetModel(newDocData) + // Save the new document (validation will now use the correct schema) + await newDoc.save(options) + // Replace the reference so later code works with the newly saved document + registryOrg = newDoc + } _.set(legacyOrg, 'authority.active_roles', finalRoles) const directRegistryKeys = [ @@ -456,8 +488,8 @@ class BaseOrgRepository extends BaseRepository { } // Save changes - await registryOrg.save({ options }) await legacyOrg.save({ options }) + await registryOrg.save({ options }) if (isLegacyObject) { const plainJavascriptLegacyOrg = legacyOrg.toObject() delete plainJavascriptLegacyOrg.__v @@ -596,9 +628,42 @@ class BaseOrgRepository extends BaseRepository { } } + // Handle possible authority (discriminator) changes that require a different Mongoose model + let roleChange = false + if (!_.isEqual([...registryOrg?.authority].sort(), [...updatedRegistryOrg?.authority].sort())) { + roleChange = true + } + + // Determine the correct model based on the updated authority + let TargetModel = null + if (updatedRegistryOrg.authority?.includes('SECRETARIAT')) { + TargetModel = SecretariatOrgModel + } else if (updatedRegistryOrg.authority?.includes('CNA')) { + TargetModel = CNAOrgModel + } else if (updatedRegistryOrg.authority?.includes('ADP')) { + TargetModel = ADPOrgModel + } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { + TargetModel = BulkDownloadModel + } + + // If the model type has changed, replace the document with a new one of the correct type + if (TargetModel && roleChange) { + const oldId = updatedRegistryOrg._id + // Remove the old document + await BaseOrgModel.deleteOne({ _id: oldId }, options) + // Prepare data for the new document, preserving the UUID and _id + const newDocData = updatedRegistryOrg.toObject() + delete newDocData.__t + newDocData._id = oldId + const newDoc = new TargetModel(newDocData) + await newDoc.save(options) + // Update reference so subsequent code works with the newly saved document + updatedRegistryOrg = newDoc + } + try { - await updatedLegacyOrg.save({ options }) - await updatedRegistryOrg.save({ options }) + await updatedLegacyOrg.save(options) + await updatedRegistryOrg.save(options) } catch (error) { throw new Error(`Failed to update organization ${shortName}. Error: ${error.message}`) } diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index d61f8f8c..d72d6b18 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -443,8 +443,6 @@ class BaseUserRepository extends BaseRepository { async getAllUsersByOrgShortname (orgShortname, options = {}, returnLegacyFormat = false) { const CONSTANTS = getConstants() const baseOrgRepository = new BaseOrgRepository() - console.log('Repository is using model:', BaseOrgModel.modelName) - console.log('Model is targeting collection:', BaseOrgModel.collection.name) const userRepository = new UserRepository() const org = await baseOrgRepository.findOneByShortName(orgShortname) const usersInOrg = org.toObject().users diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index a7980bc2..1a3c24ef 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -129,7 +129,7 @@ class ReviewObjectRepository extends BaseRepository { } // We need to trigger the org to update - await baseOrgRepository.updateOrgFull(org.short_name, reviewObject.new_review_data, { options }, false, requestingUserUUID, false, true) + await baseOrgRepository.updateOrgFull(org.short_name, reviewObject.new_review_data, options, false, requestingUserUUID, false, true) reviewObject.status = 'approved' diff --git a/test/integration-tests/org/regularUsersTestRegistryFlag.js b/test/integration-tests/org/regularUsersTestRegistryFlag.js index 9cef52e4..38424290 100644 --- a/test/integration-tests/org/regularUsersTestRegistryFlag.js +++ b/test/integration-tests/org/regularUsersTestRegistryFlag.js @@ -370,8 +370,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing ORG GET Endpoints for regular users with `registry=true` flag - describe('Testing ORG GET endpoint with `registry=true`', () => { + // Testing ORG GET Endpoints for regular users + describe('Testing Registry ORG GET', () => { /* Positive Tests */ context('Positive Test', () => { it('regular users can view the organization they belong to', async () => { diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index b9092720..9e6e66ad 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -147,7 +147,7 @@ describe('Testing Joint approval', () => { expect(res.body.hard_quota).to.equal(10000) }) }) - it('Secretariat can approve the ORG review', async () => { + it('Secretariat can approve the ORG review', async function () { await chai.request(app) .put(`/api/review/org/${reviewUUID}/approve`) .set(secretariatHeaders) @@ -304,7 +304,7 @@ describe('Testing Joint approval', () => { expect(res).to.have.status(200) }) }) - it('Secretariat can approve the ORG review', async () => { + it('Secretariat can approve the ORG review', async function () { await chai.request(app) .put(`/api/review/org/${reviewUUID}/approve`) .set(secretariatHeaders) From ed03e2931b9c1ec08a3e36f17079789778255a3e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 5 Dec 2025 09:52:43 -0500 Subject: [PATCH 28/39] Update --- test-http/src/test/org_user_tests/org.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-http/src/test/org_user_tests/org.py b/test-http/src/test/org_user_tests/org.py index f2ca448b..72083809 100644 --- a/test-http/src/test/org_user_tests/org.py +++ b/test-http/src/test/org_user_tests/org.py @@ -54,7 +54,7 @@ def test_get_all_orgs(): """secretariat users can request a list of all organizations""" res = requests.get(f"{env.AWG_BASE_URL}{ORG_URL}", headers=utils.BASE_HEADERS) - ok_response_contains(res, '"active_roles":["SECRETARIAT","CNA"]') + ok_response_contains(res, '"active_roles":["SECRETARIAT"]') assert len(json.loads(res.content.decode())["organizations"]) >= 1 From 2533bf4665fe43cb2751eb9b3ebd397644600b76 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 5 Dec 2025 09:54:22 -0500 Subject: [PATCH 29/39] linting issues --- src/controller/org.controller/org.controller.js | 1 - test-http/src/test/org_user_tests/org.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 5067a7c7..e9788804 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -1,6 +1,5 @@ require('dotenv').config() const mongoose = require('mongoose') -const _ = require('lodash') const logger = require('../../middleware/logger') const getConstants = require('../../constants').getConstants const errors = require('./error') diff --git a/test-http/src/test/org_user_tests/org.py b/test-http/src/test/org_user_tests/org.py index 72083809..a9d324b4 100644 --- a/test-http/src/test/org_user_tests/org.py +++ b/test-http/src/test/org_user_tests/org.py @@ -54,7 +54,7 @@ def test_get_all_orgs(): """secretariat users can request a list of all organizations""" res = requests.get(f"{env.AWG_BASE_URL}{ORG_URL}", headers=utils.BASE_HEADERS) - ok_response_contains(res, '"active_roles":["SECRETARIAT"]') + ok_response_contains(res, '"active_roles":["CNA"]') assert len(json.loads(res.content.decode())["organizations"]) >= 1 From 5c8e0989611744bd7d8f293d232b65f5aa49f13e Mon Sep 17 00:00:00 2001 From: emathew Date: Fri, 5 Dec 2025 10:25:49 -0500 Subject: [PATCH 30/39] remove registry query parameters and update swagger --- api-docs/openapi.json | 126 +++++++----------- schemas/registry-org/ADPOrg.json | 17 +++ schemas/registry-org/BaseOrg.json | 121 +++++++++++++++++ schemas/registry-org/BulkDownloadOrg.json | 17 +++ schemas/registry-org/CNAOrg.json | 42 ++++++ schemas/registry-org/SecretariatOrg.json | 33 +++++ .../create-registry-org-request.json | 20 +-- src/controller/org.controller/index.js | 98 +++++++------- .../org.controller/org.middleware.js | 2 +- ...tryFlag.js => regularUsersTestRegistry.js} | 24 ++-- .../user/getUserTestRegistryFlag.js | 118 ---------------- 11 files changed, 342 insertions(+), 276 deletions(-) create mode 100644 schemas/registry-org/ADPOrg.json create mode 100644 schemas/registry-org/BaseOrg.json create mode 100644 schemas/registry-org/BulkDownloadOrg.json create mode 100644 schemas/registry-org/CNAOrg.json create mode 100644 schemas/registry-org/SecretariatOrg.json rename test/integration-tests/org/{regularUsersTestRegistryFlag.js => regularUsersTestRegistry.js} (95%) delete mode 100644 test/integration-tests/user/getUserTestRegistryFlag.js diff --git a/api-docs/openapi.json b/api-docs/openapi.json index e344f94b..1a122a3f 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1974,18 +1974,12 @@ }, "post": { "tags": [ - "Organization" + "Registry Organization" ], - "summary": "Retrieves all organizations (accessible to Secretariat)", - "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves information about all organizations

", - "operationId": "orgAll", + "summary": "Creates an organization (accessible to Secretariat)", + "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates a new organization

", + "operationId": "orgCreateSingle", "parameters": [ - { - "$ref": "#/components/parameters/pageQuery" - }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2057,6 +2051,29 @@ } } } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "../schemas/registry-org/SecretariatOrg.json" + }, + { + "$ref": "../schemas/registry-org/CNAOrg.json" + }, + { + "$ref": "../schemas/registry-org/ADPOrg.json" + }, + { + "$ref": "../schemas/registry-org/BulkDownloadOrg.json" + } + ] + } + } + } } } }, @@ -2597,9 +2614,6 @@ { "$ref": "#/components/parameters/active_roles_remove" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -2887,10 +2901,14 @@ "operationId": "orgAll", "parameters": [ { - "$ref": "#/components/parameters/pageQuery" + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } }, { - "$ref": "#/components/parameters/registry" + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -2980,9 +2998,6 @@ "description": "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Creates an organization

", "operationId": "orgCreateSingle", "parameters": [ - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3067,14 +3082,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/create-org-request.json" - }, - { - "$ref": "../schemas/registry-org/create-registry-org-request.json" - } - ] + "$ref": "../schemas/org/create-org-request.json" } } } @@ -3099,9 +3107,6 @@ }, "description": "The shortname or UUID of the organization" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3118,14 +3123,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/get-org-response.json" - }, - { - "$ref": "../schemas/registry-org/get-registry-org-response.json" - } - ] + "$ref": "../schemas/org/get-org-response.json" } } } @@ -3201,6 +3199,13 @@ }, "description": "The shortname of the organization" }, + { + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } + }, { "$ref": "#/components/parameters/id_quota" }, @@ -3216,9 +3221,6 @@ { "$ref": "#/components/parameters/active_roles_remove" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3235,14 +3237,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/update-org-response.json" - }, - { - "$ref": "../schemas/registry-org/update-registry-org-response.json" - } - ] + "$ref": "../schemas/org/update-org-response.json" } } } @@ -3318,9 +3313,6 @@ }, "description": "The shortname of the organization" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3337,14 +3329,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/get-org-quota-response.json" - }, - { - "$ref": "../schemas/registry-org/get-registry-org-quota-response.json" - } - ] + "$ref": "../schemas/org/get-org-quota-response.json" } } } @@ -3421,10 +3406,14 @@ "description": "The shortname of the organization" }, { - "$ref": "#/components/parameters/pageQuery" + "name": "registry", + "in": "query", + "schema": { + "type": "string" + } }, { - "$ref": "#/components/parameters/registry" + "$ref": "#/components/parameters/pageQuery" }, { "$ref": "#/components/parameters/apiEntityHeader" @@ -3442,14 +3431,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/user/list-users-response.json" - }, - { - "$ref": "../schemas/registry-user/list-registry-users-response.json" - } - ] + "$ref": "../schemas/user/list-users-response.json" } } } @@ -3525,9 +3507,6 @@ }, "description": "The shortname of the organization" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, @@ -3765,9 +3744,6 @@ { "$ref": "#/components/parameters/orgShortname" }, - { - "$ref": "#/components/parameters/registry" - }, { "$ref": "#/components/parameters/apiEntityHeader" }, diff --git a/schemas/registry-org/ADPOrg.json b/schemas/registry-org/ADPOrg.json new file mode 100644 index 00000000..be982900 --- /dev/null +++ b/schemas/registry-org/ADPOrg.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "ADPOrg", + "type": "object", + "title": "CVE ADP Organization", + "description": "Schema for a CVE ADP Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["ADP"] + } + } + } + ] +} diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json new file mode 100644 index 00000000..87f1b1e5 --- /dev/null +++ b/schemas/registry-org/BaseOrg.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "./BaseOrg.json", + "type": "object", + "title": "CVE Base Organization", + "description": "Base schema for a CVE Organization", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "uriType": { + "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", + "type": "string", + "format": "uri", + "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", + "minLength": 1, + "maxLength": 2048 + }, + "shortName": { + "description": "A 2-32 character name that can be used to complement an organization's UUID.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "longName": { + "description": "A 1-256 character name that can be used to complement an organization's short_name.", + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authority": { + "description": "The authority (role) of this organization within the CVE program", + "type": "string", + "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + } + }, + "properties": { + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "short_name": { + "$ref": "#/definitions/shortName" + }, + "long_name": { + "$ref": "#/definitions/longName" + }, + "aliases": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/authority" + } + }, + "root_or_tlr": { + "type": "boolean" + }, + "reports_to": { + "$ref": "#/definitions/uuidType" + }, + "users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "admins": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "contact_info": { + "type": "object", + "properties": { + "additional_contact_users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + }, + "poc_phone": { + "type": "string" + }, + "org_email": { + "type": "string", + "format": "email" + }, + "website": { + "type": "string", + "format": "uri", + "description": "Organization's website URL" + } + }, + "additionalProperties": false + } + }, + "required": [ + "short_name", + "long_name" + ] +} \ No newline at end of file diff --git a/schemas/registry-org/BulkDownloadOrg.json b/schemas/registry-org/BulkDownloadOrg.json new file mode 100644 index 00000000..cabc0777 --- /dev/null +++ b/schemas/registry-org/BulkDownloadOrg.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "BaseOrg", + "type": "object", + "title": "CVE Bulk Download Organization", + "description": "Schema for a CVE Bulk Download Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["BULK_DOWNLOAD"] + } + } + } + ] +} diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json new file mode 100644 index 00000000..0402e833 --- /dev/null +++ b/schemas/registry-org/CNAOrg.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "$id": "CNAOrg", + "title": "CVE CNA Organization", + "description": "Schema for a CVE CNA Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["CNA"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "./BaseOrg.json#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0 + }, + "soft_quota": { + "type": "integer", + "minimum": 0 + }, + "charter_or_scope": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "disclosure_policy": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "product_list": { + "$ref": "/BaseOrg#/definitions/uriType" + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/schemas/registry-org/SecretariatOrg.json b/schemas/registry-org/SecretariatOrg.json new file mode 100644 index 00000000..469bd7df --- /dev/null +++ b/schemas/registry-org/SecretariatOrg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "SecretariatOrg", + "type": "object", + "title": "CVE Secretariat Organization", + "description": "Schema for a CVE Secretariat Organization", + "allOf": [ + { "$ref": "./BaseOrg.json" }, + { + "properties": { + "authority": { + "const": ["SECRETARIAT"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "./BaseOrg.json#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0 + }, + "soft_quota": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 481a8085..b7fa78bf 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -20,23 +20,12 @@ }, "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", + "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Root", "Secretariat"] + "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT"] } - } - }, - "required": ["active_roles"] }, "reports_to": { "type": ["string", "null"], @@ -117,10 +106,7 @@ }, "required": [ "short_name", - "cve_program_org_function", "authority", - "root_or_tlr", - "users", - "contact_info" + "long_name" ] } diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 0345c1b4..9d8ce5d5 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -161,6 +161,8 @@ router.get('/registry/org/:shortname/users', mw.useRegistry(), mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, parseGetParams, @@ -237,6 +239,7 @@ router.get('/registry/org/:shortname/id_quota', mw.useRegistry(), mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_ID_QUOTA) @@ -311,6 +314,7 @@ router.get('/registry/org/:identifier', */ mw.useRegistry(), mw.validateUser, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_SINGLE @@ -394,27 +398,41 @@ router.get('/registry/org/:shortname/user/:username', mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.USER_SINGLE ) router.post('/registry/org', /* - #swagger.tags = ['Organization'] - #swagger.operationId = 'orgAll' - #swagger.summary = "Retrieves all organizations (accessible to Secretariat)" + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'orgCreateSingle' + #swagger.summary = "Creates an organization (accessible to Secretariat)" #swagger.description = "

Access Control

User must belong to an organization with the Secretariat role

Expected Behavior

-

Secretariat: Retrieves information about all organizations

" +

Secretariat: Creates a new organization

" #swagger.parameters['$ref'] = [ - '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + anyOf: [ + { $ref: '../schemas/registry-org/SecretariatOrg.json' }, + { $ref: '../schemas/registry-org/CNAOrg.json' }, + { $ref: '../schemas/registry-org/ADPOrg.json' }, + { $ref: '../schemas/registry-org/BulkDownloadOrg.json' } + ] + } + } + } + } #swagger.responses[200] = { description: 'Returns information about all organizations, along with pagination fields if results span multiple pages of data', content: { @@ -469,6 +487,7 @@ router.post('/registry/org', mw.useRegistry(), mw.validateUser, mw.onlySecretariat, + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parsePostParams, parseError, controller.REGISTRY_CREATE_ORG @@ -491,7 +510,6 @@ router.put('/registry/org/:shortname', '#/components/parameters/newShortname', '#/components/parameters/active_roles_add', '#/components/parameters/active_roles_remove', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -733,10 +751,10 @@ router.put('/registry/org/:shortname/user/:username', mw.onlyOrgWithPartnerRole, query().custom((query) => { return mw.validateQueryParameterNames(query, ['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']) + 'name.suffix', 'active_roles.add', 'active_roles.remove']) }), query(['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + 'name.suffix', 'active_roles.add', 'active_roles.remove']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), query(['active']).optional().isBoolean({ loose: true }), @@ -847,7 +865,6 @@ router.get('/org',

Secretariat: Retrieves information about all organizations

" #swagger.parameters['$ref'] = [ '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -906,11 +923,10 @@ router.get('/org', } } */ - param(['registry']).optional().isBoolean(), mw.handleRegistryParameter, mw.validateUser, mw.onlySecretariat, - query().custom((query) => { return mw.validateQueryParameterNames(query, ['page', 'registry']) }), + 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, @@ -930,7 +946,6 @@ router.post(

Secretariat: Creates an organization

" #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -939,12 +954,7 @@ router.post( required: true, content: { 'application/json': { - schema: { - oneOf: [ - { $ref: '../schemas/org/create-org-request.json' }, - { $ref: '../schemas/registry-org/create-registry-org-request.json' } - ] - } + schema: { $ref: '../schemas/org/create-org-request.json' } } } } @@ -1034,7 +1044,6 @@ router.get(

Secretariat: Retrieves information about any organization

" #swagger.parameters['identifier'] = { description: 'The shortname or UUID of the organization' } #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1043,11 +1052,7 @@ router.get( description: 'Returns the organization information', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/get-org-response.json' }, - { $ref: '../schemas/registry-org/get-registry-org-response.json' } - ] + schema: { $ref: '../schemas/org/get-org-response.json' } } } } @@ -1093,10 +1098,9 @@ router.get( } } */ - param(['registry']).optional().isBoolean(), - mw.handleRegistryParameter, mw.validateUser, param(['identifier']).isString().trim(), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_SINGLE @@ -1118,7 +1122,6 @@ router.put('/org/:shortname', '#/components/parameters/newShortname', '#/components/parameters/active_roles_add', '#/components/parameters/active_roles_remove', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1127,12 +1130,7 @@ router.put('/org/:shortname', description: 'Returns information about the organization updated', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/update-org-response.json' }, - { $ref: '../schemas/registry-org/update-registry-org-response.json' } - ] - } + schema: { $ref: '../schemas/org/update-org-response.json' } } } } @@ -1180,6 +1178,7 @@ router.put('/org/:shortname', mw.validateUser, mw.onlySecretariat, validateUpdateOrgParameters(), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parsePostParams, controller.ORG_UPDATE_SINGLE) @@ -1197,7 +1196,6 @@ router.get('/org/:shortname/id_quota',

Secretariat: Retrieves the CVE ID quota for any organization

" #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1206,11 +1204,7 @@ router.get('/org/:shortname/id_quota', description: 'Returns the CVE ID quota for an organization', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/get-org-quota-response.json' }, - { $ref: '../schemas/registry-org/get-registry-org-quota-response.json' } - ] + schema: { $ref: '../schemas/org/get-org-quota-response.json' } } } } @@ -1256,9 +1250,9 @@ router.get('/org/:shortname/id_quota', } } */ - mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.ORG_ID_QUOTA) @@ -1276,7 +1270,6 @@ router.get('/org/:shortname/users', #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } #swagger.parameters['$ref'] = [ '#/components/parameters/pageQuery', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1285,12 +1278,7 @@ router.get('/org/:shortname/users', description: 'Returns all users for the organization, along with pagination fields if results span multiple pages of data', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/user/list-users-response.json' }, - { $ref: '../schemas/registry-user/list-registry-users-response.json' } - ] - } + schema: { $ref: '../schemas/user/list-users-response.json' } } } } @@ -1335,10 +1323,11 @@ router.get('/org/:shortname/users', } } */ - param(['registry']).optional().isBoolean(), mw.handleRegistryParameter, mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }), + query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), parseError, parseGetParams, @@ -1357,7 +1346,6 @@ router.post('/org/:shortname/user',

Secretariat: Creates a user for any organization

" #swagger.parameters['shortname'] = { description: 'The shortname of the organization' } #swagger.parameters['$ref'] = [ - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1424,6 +1412,7 @@ router.post('/org/:shortname/user', mw.onlySecretariatOrAdmin, mw.onlyOrgWithPartnerRole, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), body(['org_uuid']).optional().isString().trim(), body(['uuid']).optional().isString().trim(), body(['name.first']).optional().isString().trim().isLength({ max: CONSTANTS.MAX_FIRSTNAME_LENGTH }).withMessage(errorMsgs.FIRSTNAME_LENGTH), @@ -1508,9 +1497,11 @@ router.get('/org/:shortname/user/:username', mw.validateUser, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parseGetParams, controller.USER_SINGLE) + router.put('/org/:shortname/user/:username', /* #swagger.tags = ['Users'] @@ -1535,7 +1526,6 @@ router.put('/org/:shortname/user/:username', '#/components/parameters/nameSuffix', '#/components/parameters/newUsername', '#/components/parameters/orgShortname', - '#/components/parameters/registry', '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', '#/components/parameters/apiSecretHeader' @@ -1594,10 +1584,10 @@ router.put('/org/:shortname/user/:username', mw.onlyOrgWithPartnerRole, query().custom((query) => { return mw.validateQueryParameterNames(query, ['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']) + 'name.suffix', 'active_roles.add', 'active_roles.remove']) }), query(['active', 'new_username', 'org_short_name', 'name.first', 'name.last', 'name.middle', - 'name.suffix', 'active_roles.add', 'active_roles.remove', 'registry']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), + 'name.suffix', 'active_roles.add', 'active_roles.remove']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), query(['active']).optional().isBoolean({ loose: true }), @@ -1619,6 +1609,7 @@ router.put('/org/:shortname/user/:username', parseError, parsePostParams, controller.USER_UPDATE_SINGLE) + router.put('/org/:shortname/user/:username/reset_secret', /* #swagger.tags = ['Users'] @@ -1691,6 +1682,7 @@ router.put('/org/:shortname/user/:username/reset_secret', mw.onlyOrgWithPartnerRole, param(['shortname']).isString().trim().notEmpty().isLength({ min: CONSTANTS.MIN_SHORTNAME_LENGTH, max: CONSTANTS.MAX_SHORTNAME_LENGTH }), param(['username']).isString().trim().notEmpty().custom(isValidUsername), + query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parsePostParams, controller.USER_RESET_SECRET) diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 6e8cbcd0..d8287381 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -170,7 +170,7 @@ function validateUpdateOrgParameters () { 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 sharedParameters = ['new_short_name', 'active_roles.add', 'active_roles.remove'] const allParameters = [ ...legacyParametersOnly, ...registryParametersOnly, ...sharedParameters diff --git a/test/integration-tests/org/regularUsersTestRegistryFlag.js b/test/integration-tests/org/regularUsersTestRegistry.js similarity index 95% rename from test/integration-tests/org/regularUsersTestRegistryFlag.js rename to test/integration-tests/org/regularUsersTestRegistry.js index 38424290..6ffe1986 100644 --- a/test/integration-tests/org/regularUsersTestRegistryFlag.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -7,12 +7,12 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') const MAX_SHORTNAME_LENGTH = 32 /** - * Unit Tests for testing regular user permissions for Org and User /api/org endpoints with the `registry=true` flag + * Unit Tests for testing regular user permissions for Org and User /api/registry/org */ -describe('Testing regular user permissions for /api/org/ endpoints with `registry=true`', () => { - // Testing USER PUT Endpoints for regular users with `registry=true` flag - describe('Testing USER PUT endpoint with `registry=true`', () => { +describe('Testing regular user permissions for /api/registry/org/ endpoints with ', () => { + // Testing USER PUT Endpoints for regular users with /api/registry/org + describe('Testing USER PUT endpoint ', () => { /* Positive Tests */ context('Positive Test', () => { it('regular user can update their name', async () => { @@ -232,8 +232,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing USER POST Endpoints for regular users with `registry=true` flag - describe('Testing USER POST endpoint with `registry=true`', () => { + // Testing USER POST Endpoints for regular users with /api/registry/org + describe('Testing USER POST endpoint', () => { /* Negative Tests */ context('Negative Test', () => { it('regular user cannot create another user', async () => { @@ -252,8 +252,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing USER GET Endpoints for regular users with `registry=true` flag - describe('Testing USER GET endpoint with `registry=true`', () => { + // Testing USER GET Endpoints for regular users with /api/registry/org + describe('Testing USER GET endpoint with /api/registry/org', () => { /* Positive Tests */ context('Positive Test', () => { it('regular users can view users of the same organization', async () => { @@ -336,8 +336,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing ORG PUT Endpoints for regular users with `registry=true` flag - describe('Testing ORG PUT endpoint with `registry=true`', () => { + // Testing ORG PUT Endpoints for regular users with /api/registry/org + describe('Testing ORG PUT endpoint with /api/registry/org', () => { /* Negative Tests */ context('Negative Test', () => { it('regular user cannot update an organization', async () => { @@ -354,8 +354,8 @@ describe('Testing regular user permissions for /api/org/ endpoints with `registr }) }) }) - // Testing ORG POST Endpoints for regular users with `registry=true` flag - describe('Testing ORG POST endpoint with `registry=true`', () => { + // Testing ORG POST Endpoints for regular users with /api/registry/org + describe('Testing ORG POST endpoint with /api/registry/org', () => { context('Negative Test', () => { it('regular users cannot create new org', async () => { await chai.request(app) diff --git a/test/integration-tests/user/getUserTestRegistryFlag.js b/test/integration-tests/user/getUserTestRegistryFlag.js deleted file mode 100644 index 0a8f3bea..00000000 --- a/test/integration-tests/user/getUserTestRegistryFlag.js +++ /dev/null @@ -1,118 +0,0 @@ -const chai = require('chai') -chai.use(require('chai-http')) -const expect = chai.expect - -const constants = require('../constants.js') -const app = require('../../../src/index.js') -const BASE_URL = '/api' -/** - * Unit Tests for testing User Get Request for /api/org with the `registry=true` flag - */ - -describe('Testing /api/org/ user endpoints with `registry=true`', () => { - // Testing USER GET Endpoints with `registry=true` flag - describe('Testing USER GET endpoint with `registry=true`', () => { - /* Positive Tests */ - it('secretariat users can request a list of all users', async () => { - await chai.request(app) - .get(`${BASE_URL}/users?registry=true`) - .set(constants.headers) - .send({ - }) - .then((res) => { - expect(res).to.have.status(200) - // check the fields returned - }) - }) - it('page must be a positive int', async () => { - await chai.request(app) - .get(`${BASE_URL}/registry/users?page=1`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - }) - }) - it('can retrieve user after an update', async () => { - const user = constants.nonSecretariatUserHeaders3['CVE-API-USER'] - const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG'] - const newFirstName = 'testFirstName' - var oldFirstName = '' - await chai.request(app) - .get(`${BASE_URL}/registry/org/${org}/user/${user}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - oldFirstName = res.body.name.first - }) - await chai.request(app) - .put(`${BASE_URL}/registry/org/${org}/user/${user}?name.first=${newFirstName}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - }) - await chai.request(app) - .get(`${BASE_URL}/registry/org/${org}/user/${user}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - expect(res.body.name.first).to.contain(newFirstName) - }) - await chai.request(app) - .put(`${BASE_URL}/registry/org/${org}/user/${user}?name.first=${oldFirstName}`) - .set(constants.headers) - .send() - .then((res) => { - expect(res).to.have.status(200) - }) - }) - }) - /* Negative Tests */ - context('Negative Test', () => { - it('regular users cannot request a list of all users', async () => { - await chai.request(app) - .get(`${BASE_URL}/registry/users`) - .set(constants.nonSecretariatUserHeaders) - .send({ - }) - .then((res) => { - expect(res).to.have.status(403) - expect(res.body.error).to.contain('SECRETARIAT_ONLY') - }) - }) - it('org admins cannot request a list of all users', async () => { - await chai.request(app) - .get(`${BASE_URL}/registry/users`) - .set(constants.nonSecretariatUserHeaders2) - .send({ - }) - .then((res) => { - expect(res).to.have.status(403) - expect(res.body.error).to.contain('SECRETARIAT_ONLY') - }) - }) - it('page must be a positive int', async () => { - // test negative int - await chai.request(app) - .get(`${BASE_URL}/registry/users?page=-1`) - .set(constants.headers) - .send({}) - .then((res) => { - expect(res).to.have.status(400) - expect(res.body.error).to.contain('BAD_INPUT') - }) - // test string - await chai.request(app) - .get(`${BASE_URL}/registry/users?page=abc`) - .set(constants.headers) - .send({}) - .then((res) => { - expect(res).to.have.status(400) - expect(res.body.error).to.contain('BAD_INPUT') - }) - }) - }) -}) From 1af6b9eaaa737b9e39d69a982c77bf143b957798 Mon Sep 17 00:00:00 2001 From: emathew Date: Fri, 5 Dec 2025 10:37:44 -0500 Subject: [PATCH 31/39] fix bulk download schema reference --- src/middleware/schemas/BulkDownloadOrg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json index 09853637..ada14085 100644 --- a/src/middleware/schemas/BulkDownloadOrg.json +++ b/src/middleware/schemas/BulkDownloadOrg.json @@ -5,7 +5,7 @@ "title": "CVE Bulk Download Organization", "description": "Schema for a CVE Bulk Download Organization", "allOf": [ - { "$ref": "BaseOrg" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { From a05e94059ac6d2869026fce5c131ddb12498cda6 Mon Sep 17 00:00:00 2001 From: emathew Date: Fri, 5 Dec 2025 11:10:54 -0500 Subject: [PATCH 32/39] remove query check for updateOrg --- src/controller/org.controller/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 9d8ce5d5..bd32de1a 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -1178,7 +1178,6 @@ router.put('/org/:shortname', mw.validateUser, mw.onlySecretariat, validateUpdateOrgParameters(), - query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }), parseError, parsePostParams, controller.ORG_UPDATE_SINGLE) From fb8a43c41cb223d997c9c5ac2f17a41cda10674d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 5 Dec 2025 14:08:40 -0500 Subject: [PATCH 33/39] Removed hard coded true --- src/controller/org.controller/org.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index e9788804..2b373b94 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -122,7 +122,7 @@ async function getUsers (req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } - const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, true) + const payload = await userRepo.getAllUsersByOrgShortname(orgShortName, options, !req.useRegistry) logger.info({ uuid: req.ctx.uuid, message: `The users of ${orgShortName} organization were sent to the user.` }) return res.status(200).json(payload) From 7f63def274fd4aa492ebb7a240897bf80296b9ae Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 5 Dec 2025 14:16:21 -0500 Subject: [PATCH 34/39] Removed _id and secret --- src/repositories/baseUserRepository.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index d72d6b18..a3ebef5d 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -33,6 +33,12 @@ function setAggregateRegistryUserObj (query) { return [ { $match: query + }, + { + $project: { + _id: false, + secret: false + } } ] } From 05845b4f5ca3bb1afdf9d06c23c78650b7f50da3 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Fri, 5 Dec 2025 14:29:58 -0500 Subject: [PATCH 35/39] Removed role field from BaseUser schema --- src/controller/org.controller/org.controller.js | 4 ++-- src/middleware/schemas/BaseUser.json | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 2b373b94..52da5fd3 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -89,7 +89,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 + * Called by GET /api/registry/org/{shortname}/users, GET /api/org/{shortname}/users **/ async function getUsers (req, res, next) { try { @@ -133,7 +133,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} + * Called by GET /api/registry/org/{shortname}/user/{username}, GET /api/org/{shortname}/user/{username} **/ async function getUser (req, res, next) { try { diff --git a/src/middleware/schemas/BaseUser.json b/src/middleware/schemas/BaseUser.json index 70c898ad..2c1bf93d 100644 --- a/src/middleware/schemas/BaseUser.json +++ b/src/middleware/schemas/BaseUser.json @@ -57,11 +57,6 @@ "UUID": { "$ref": "#/definitions/uuidType" }, - "role": { - "description": "Users permissions, Like ADMIN", - "type": "string", - "enum": ["ADMIN"] - }, "status": { "description": "User status: 'active' or 'inactive'", "type": "string", From 588cc8d36eb7b184bdebbb24a7990ed47ff4a8e9 Mon Sep 17 00:00:00 2001 From: Chris Berger Date: Fri, 5 Dec 2025 15:03:11 -0500 Subject: [PATCH 36/39] Validate role field on user create --- src/controller/org.controller/org.controller.js | 8 ++++++-- src/repositories/baseUserRepository.js | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 52da5fd3..32ff68af 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -452,7 +452,7 @@ async function updateOrg (req, res, next) { /** * Creates a user only if the org exists and * the user does not exist for the specified shortname and username - * Called by POST /api/org/{shortname}/user + * Called by POST /api/registry/org/{shortname}/user, POST /api/org/{shortname}/user **/ async function createUser (req, res, next) { const session = await mongoose.startSession() @@ -461,6 +461,7 @@ async function createUser (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() const orgShortName = req.ctx.params.shortname + const constants = getConstants() let returnValue // Check to make sure Org Exists first @@ -486,6 +487,9 @@ async function createUser (req, res, next) { if (body?.role && typeof body?.role !== 'string') { return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: 'Parameter must be a string' }] }) } + if (body?.role && !constants.USER_ROLES.includes(body?.role)) { + return res.status(400).json({ message: 'Parameters were invalid', details: [{ param: 'role', msg: `Role must be one of the following: ${constants.USER_ROLES}` }] }) + } if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'User JSON schema validation FAILED.' })) await session.abortTransaction() @@ -548,7 +552,7 @@ async function createUser (req, res, next) { /** * Updates a user only if the user exist for the specified username. * If no user exists, it does not create the user. - * Called by PUT /org/{shortname}/user/{username} + * Called by PUT /org/{shortname}/user/{username}, PUT /org/{shortname}/user/{username} **/ async function updateUser (req, res, next) { const session = await mongoose.startSession() diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index a3ebef5d..46ee0cd2 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -241,6 +241,7 @@ class BaseUserRepository extends BaseRepository { delete rawRegistryUserJson._id delete rawRegistryUserJson.__v delete rawRegistryUserJson.authority + delete rawRegistryUserJson.role return deepRemoveEmpty(rawRegistryUserJson) } From c5c4bf5d6dfe84ff1bad2ed1ac664e135353b99b Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 5 Dec 2025 15:16:05 -0500 Subject: [PATCH 37/39] added some middleware to reject bad things in the body --- src/controller/org.controller/index.js | 2 ++ src/middleware/middleware.js | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index bd32de1a..595df78c 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -1423,9 +1423,11 @@ router.post('/org/:shortname/user', .bail() .customSanitizer(toUpperCaseArray) .custom(isUserRole), + mw.rejectUnexpectedKeys(['username', 'org_uuid', 'uuid', 'name', 'authority']), parseError, parsePostParams, controller.USER_CREATE_SINGLE) + router.get('/org/:shortname/user/:username', /* #swagger.tags = ['Users'] diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 83d8c8a6..c07ebab7 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -547,6 +547,27 @@ function containsNoInvalidCharacters (val) { return true } +/** + * Middleware factory that rejects any keys in the request body + * that are not listed in the allowedKeys array. + * + * @param {Array} allowedKeys - List of permitted keys in req.body + * @returns {function} Express middleware + */ +function rejectUnexpectedKeys (allowedKeys) { + return (req, res, next) => { + const bodyKeys = Object.keys(req.body || {}) + const unexpected = bodyKeys.filter(k => !allowedKeys.includes(k)) + if (unexpected.length > 0) { + return res.status(400).json({ + error: 'Unexpected keys in request body', + unexpected + }) + } + next() + } +} + module.exports = { setCacheControl, optionallyValidateUser, @@ -572,5 +593,6 @@ module.exports = { toUpperCaseArray, toLowerCaseArray, containsNoInvalidCharacters, - trimJSONWhitespace + trimJSONWhitespace, + rejectUnexpectedKeys } From 678c3502e3e9a13fcc8692fc656060e2cb398315 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 5 Dec 2025 15:26:52 -0500 Subject: [PATCH 38/39] Fixing issues --- src/controller/org.controller/index.js | 2 +- .../integration-tests/org/postOrgUsersTest.js | 39 +++++++------------ 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 595df78c..ec3ea8c7 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -1423,7 +1423,7 @@ router.post('/org/:shortname/user', .bail() .customSanitizer(toUpperCaseArray) .custom(isUserRole), - mw.rejectUnexpectedKeys(['username', 'org_uuid', 'uuid', 'name', 'authority']), + mw.rejectUnexpectedKeys(['username', 'active', 'org_uuid', 'uuid', 'name', 'authority']), parseError, parsePostParams, controller.USER_CREATE_SINGLE) diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js index aa7e0e8c..098763b1 100644 --- a/test/integration-tests/org/postOrgUsersTest.js +++ b/test/integration-tests/org/postOrgUsersTest.js @@ -11,7 +11,6 @@ const User = require('../../../src/model/user') // const RegistryUser = require('../../../src/model/registry-user.js') const shortName = { shortname: 'win_5' } -const registryFlag = { registry: true } describe('Testing user post endpoint', () => { let orgUuid @@ -88,11 +87,10 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long first name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser9999', + username: 'fakeregistryuser9999', name: { first: 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1', @@ -100,9 +98,7 @@ describe('Testing user post endpoint', () => { middle: 'Cool', suffix: 'Mr.' }, - authority: { - active_roles: ['ADMIN'] - } + role: ['ADMIN'] }) .then((res, err) => { expect(res).to.have.status(400) @@ -141,20 +137,18 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long last name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser1000', + username: 'fakeregistryuser1000', name: { first: 'FirstName', last: 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1', middle: 'Cool', suffix: 'Mr.' }, - authority: { - active_roles: ['ADMIN'] - } + role: ['ADMIN'] + }) .then((res, err) => { expect(res).to.have.status(400) @@ -193,11 +187,10 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long middle name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser1001', + username: 'fakeregistryuser1001', name: { first: 'FirstName', last: 'LastName', @@ -205,9 +198,8 @@ describe('Testing user post endpoint', () => { 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1', suffix: 'Mr.' }, - authority: { - active_roles: ['ADMIN'] - } + role: ['ADMIN'] + }) .then((res, err) => { expect(res).to.have.status(400) @@ -246,11 +238,10 @@ describe('Testing user post endpoint', () => { it('Fails creation of user for bad long suffix name with registry enabled', async () => { await chai .request(app) - .post('/api/org/win_5/user') + .post('/api/registry/org/win_5/user') .set({ ...constants.headers, ...shortName }) - .query(registryFlag) .send({ - user_id: 'fakeregistryuser1002', + username: 'fakeregistryuser1002', name: { first: 'FirstName', last: 'LastName', @@ -258,9 +249,7 @@ describe('Testing user post endpoint', () => { suffix: 'VerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnmVerylongnm1' }, - authority: { - active_roles: ['ADMIN'] - } + role: 'ADMIN' }) .then((res, err) => { expect(res).to.have.status(400) From 4e4b3e6e3b16d93727b5f7a198b3dc58b64ae435 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 5 Dec 2025 15:32:59 -0500 Subject: [PATCH 39/39] Apparently, there is an ancient test that says we should allow this. Keeping the middleware, but removing its invocation --- src/controller/org.controller/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index ec3ea8c7..10d0ef5b 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -1423,7 +1423,6 @@ router.post('/org/:shortname/user', .bail() .customSanitizer(toUpperCaseArray) .custom(isUserRole), - mw.rejectUnexpectedKeys(['username', 'active', 'org_uuid', 'uuid', 'name', 'authority']), parseError, parsePostParams, controller.USER_CREATE_SINGLE)