diff --git a/api-docs/openapi.json b/api-docs/openapi.json
index 5fd0bee55..6a5872063 100644
--- a/api-docs/openapi.json
+++ b/api-docs/openapi.json
@@ -1,7 +1,7 @@
{
"openapi": "3.0.2",
"info": {
- "version": "2.8.0",
+ "version": "2.7.5",
"title": "CVE Services API",
"description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:
If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials Contact your Root (Google , INCIBE , JPCERT/CC , or Red Hat ) or Top-Level Root (CISA ICS or MITRE ) to request credentials CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located here .
Contact the CVE Services team ",
"contact": {
@@ -5398,110 +5398,6 @@
}
}
},
- "/review/{uuid}/approve": {
- "put": {
- "tags": [
- "Review Object"
- ],
- "summary": "Approves a review object and applies changes to the organization (accessible to Secretariat only)",
- "description": " Access Control User must belong to an organization with the Secretariat role
",
- "operationId": "approveReviewObject",
- "parameters": [
- {
- "name": "uuid",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- },
- "description": "The UUID of the review object"
- },
- {
- "$ref": "#/components/parameters/apiEntityHeader"
- },
- {
- "$ref": "#/components/parameters/apiUserHeader"
- },
- {
- "$ref": "#/components/parameters/apiSecretHeader"
- }
- ],
- "responses": {
- "200": {
- "description": "Returns the updated organization",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "description": "The updated organization object"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/bad-request.json"
- }
- }
- }
- },
- "401": {
- "description": "Not Authenticated",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- },
- "500": {
- "description": "Internal Server Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- }
- },
- "requestBody": {
- "required": false,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "description": "Optional override data to apply instead of the review object data"
- }
- }
- }
- }
- }
- },
"/review/{uuid}/reject": {
"put": {
"tags": [
@@ -5593,90 +5489,6 @@
}
}
}
- },
- "/review/org/": {
- "post": {
- "tags": [
- "Review Object"
- ],
- "summary": "Creates a new review object (accessible to Secretariat only)",
- "description": " Access Control User must belong to an organization with the Secretariat role
",
- "operationId": "createReviewObject",
- "parameters": [
- {
- "$ref": "#/components/parameters/apiEntityHeader"
- },
- {
- "$ref": "#/components/parameters/apiUserHeader"
- },
- {
- "$ref": "#/components/parameters/apiSecretHeader"
- }
- ],
- "responses": {
- "200": {
- "description": "Returns the created review object",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/review/review.json"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/bad-request.json"
- }
- }
- }
- },
- "401": {
- "description": "Not Authenticated",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- },
- "500": {
- "description": "Internal Server Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../schemas/errors/generic.json"
- }
- }
- }
- }
- },
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "description": "The review object data"
- }
- }
- }
- }
- }
}
},
"components": {
diff --git a/src/controller/conversation.controller/conversation.controller.js b/src/controller/conversation.controller/conversation.controller.js
index 1b4dbb3ad..0813c0955 100644
--- a/src/controller/conversation.controller/conversation.controller.js
+++ b/src/controller/conversation.controller/conversation.controller.js
@@ -37,6 +37,7 @@ async function createConversationForTargetUUID (req, res, next) {
const repo = req.ctx.repositories.getConversationRepository()
const userRepo = req.ctx.repositories.getBaseUserRepository()
+ const orgRepo = req.ctx.repositories.getBaseOrgRepository()
const requesterOrg = req.ctx.org
const requesterUsername = req.ctx.user
const targetUUID = req.params.uuid
@@ -48,7 +49,8 @@ async function createConversationForTargetUUID (req, res, next) {
return res.status(400).json(error.invalidConversationObject())
}
- const result = await repo.createConversation(targetUUID, body, user, true, { session })
+ const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org)
+ const result = await repo.createConversation(targetUUID, body, user, isSecretariat, { session })
await session.commitTransaction()
if (!result) {
return res.status(500).json({ message: 'Failed to create conversation' })
diff --git a/src/controller/conversation.controller/index.js b/src/controller/conversation.controller/index.js
index 45eeeee53..ea2bf62ed 100644
--- a/src/controller/conversation.controller/index.js
+++ b/src/controller/conversation.controller/index.js
@@ -252,7 +252,7 @@ router.post('/conversation/target/:uuid',
}
*/
mw.validateUser,
- mw.onlySecretariat,
+ mw.onlySecretariatOrAdmin,
param(['uuid']).isUUID(4),
controller.createConversationForTargetUUID
)
diff --git a/src/controller/org.controller/error.js b/src/controller/org.controller/error.js
index cde82381f..67c305c31 100644
--- a/src/controller/org.controller/error.js
+++ b/src/controller/org.controller/error.js
@@ -36,6 +36,13 @@ class OrgControllerError extends idrErr.IDRError {
return err
}
+ aliasCollision (conflictingString) {
+ const err = {}
+ err.error = 'ALIAS_COLLISION'
+ err.message = `The organization could not be created or updated because the string '${conflictingString}' is already in use as a short_name, name, or alias by another organization.`
+ return err
+ }
+
userExists (username) { // org
const err = {}
err.error = 'USER_EXISTS'
diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js
index 4af27cbd3..a1bc0cf58 100644
--- a/src/controller/org.controller/org.controller.js
+++ b/src/controller/org.controller/org.controller.js
@@ -281,6 +281,16 @@ async function createOrg (req, res, next) {
return res.status(400).json(error.orgExists(body?.short_name))
}
+ // Check for alias collisions
+ const collisionString = await repo.checkAliasCollisions(body?.short_name, body?.name, body?.aliases, null, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${body?.short_name} organization was not created because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
const userRepo = req.ctx.repositories.getBaseUserRepository()
const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session })
const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
@@ -363,6 +373,18 @@ async function updateOrg (req, res, next) {
return res.status(403).json(error.duplicateShortname(queryParametersJson.new_short_name))
}
+ // Check for alias collisions
+ const shortNameToExclude = shortNameUrlParameter
+ const collisionString = await orgRepository.checkAliasCollisions(queryParametersJson.new_short_name || shortNameUrlParameter, queryParametersJson.name, queryParametersJson.aliases, shortNameToExclude, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${shortNameUrlParameter} organization could not be updated because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
+
const userRepo = req.ctx.repositories.getBaseUserRepository()
const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
const isSecretariat = await orgRepository.isSecretariatByShortName(req.ctx.org, { session })
@@ -472,7 +494,8 @@ async function createUser (req, res, next) {
return res.status(400).json(error.userLimitReached())
}
- returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, !!req.useRegistry)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, !!req.useRegistry, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -647,7 +670,8 @@ async function updateUser (req, res, next) {
}
}
- const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, !!req.useRegistry)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ const payload = await userRepo.updateUser(usernameParams, shortNameParams, queryParametersJson, { session }, !!req.useRegistry, requestingUserUUID)
await session.commitTransaction()
return res.status(200).json({ message: `${usernameParams} was successfully updated.`, updated: payload })
} catch (err) {
diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js
index aa54d2fc1..b105e373e 100644
--- a/src/controller/registry-org.controller/error.js
+++ b/src/controller/registry-org.controller/error.js
@@ -36,6 +36,13 @@ class RegistryOrgControllerError extends idrErr.IDRError {
return err
}
+ aliasCollision (conflictingString) {
+ const err = {}
+ err.error = 'ALIAS_COLLISION'
+ err.message = `The organization could not be created or updated because the string '${conflictingString}' is already in use as a short_name, name, or alias by another organization.`
+ return err
+ }
+
userExists (username) { // org
const err = {}
err.error = 'USER_EXISTS'
diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js
index 0b247fbe6..e66646d5a 100644
--- a/src/controller/registry-org.controller/registry-org.controller.js
+++ b/src/controller/registry-org.controller/registry-org.controller.js
@@ -97,7 +97,7 @@ async function getOrg (req, res, next) {
// fetch conversation
const conversation = await conversationRepo.getAllByTargetUUID(returnValue.UUID, isSecretariat)
if (isSecretariat) {
- returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid'])) : undefined
+ returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid'])) : undefined
} else {
returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid', 'visibility'])) : undefined
}
@@ -177,6 +177,17 @@ async function createOrg (req, res, next) {
return res.status(400).json(error.orgExists(body?.short_name))
}
+ // Check for alias collisions
+ const collisionString = await repo.checkAliasCollisions(body?.short_name, body?.name, body?.aliases, null, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${body?.short_name} organization was not created because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
+
const userRepo = req.ctx.repositories.getBaseUserRepository()
const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
// Create the org – repo.createOrg will handle field mapping
@@ -327,6 +338,18 @@ async function updateOrg (req, res, next) {
return res.status(400).json(error.duplicateShortname(body?.short_name))
}
+ // Check for alias collisions
+ const shortNameToExclude = shortName
+ const collisionString = await repo.checkAliasCollisions(body?.short_name || shortName, body?.name, body?.aliases, shortNameToExclude, { session })
+ if (collisionString) {
+ logger.info({
+ uuid: req.ctx.uuid,
+ message: `${shortName} organization could not be updated because the string '${collisionString}' collides with another organization's short_name, name, or alias.`
+ })
+ await session.abortTransaction()
+ return res.status(400).json(error.aliasCollision(collisionString))
+ }
+
// Handle secretariat "stomping" of pending review objects
if (isSecretariat) {
const reviewRepo = req.ctx.repositories.getReviewObjectRepository()
@@ -591,7 +614,8 @@ async function createUserByOrg (req, res, next) {
return res.status(400).json(error.userLimitReached())
}
- returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
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 3133c197e..4fabccc37 100644
--- a/src/controller/registry-user.controller/registry-user.controller.js
+++ b/src/controller/registry-user.controller/registry-user.controller.js
@@ -186,7 +186,8 @@ async function createUser (req, res, next) {
return res.status(400).json(error.userLimitReached())
}
- returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true })
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ returnValue = await userRepo.createUser(orgShortName, body, { session, upsert: true }, true, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -351,8 +352,9 @@ async function updateUser (req, res, next) {
}
// UUID of the user will not change, lets get it before we write to avoid read after write issues.
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
updatedUserUUID = await userRepo.getUserUUID(req.ctx.user, org.UUID)
- updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session })
+ updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session }, true, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -395,7 +397,8 @@ async function deleteUser (req, res, next) {
return res.status(404).json(error.userDne(userUUID))
}
- await userRepo.deleteUserByUUID(userUUID)
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org)
+ await userRepo.deleteUserByUUID(userUUID, {}, requestingUserUUID)
const payload = {
action: 'delete_registry_user',
@@ -462,7 +465,8 @@ async function grantRole (req, res, next) {
try {
session.startTransaction()
- await orgRepo.addAdmin(orgShortName, targetUser.UUID, { session })
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ await orgRepo.addAdmin(orgShortName, targetUser.UUID, { session }, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
@@ -530,7 +534,8 @@ async function revokeRole (req, res, next) {
try {
session.startTransaction()
- await orgRepo.removeAdmin(orgShortName, targetUser.UUID, { session })
+ const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session })
+ await orgRepo.removeAdmin(orgShortName, targetUser.UUID, { session }, requestingUserUUID)
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
diff --git a/src/controller/review-object.controller/index.js b/src/controller/review-object.controller/index.js
index 702da7529..daf8a7a74 100644
--- a/src/controller/review-object.controller/index.js
+++ b/src/controller/review-object.controller/index.js
@@ -411,6 +411,7 @@ router.put('/review/:uuid/approve',
/*
#swagger.tags = ['Review Object']
#swagger.operationId = 'approveReviewObject'
+ #swagger.ignore = true
#swagger.summary = "Approves a review object and applies changes to the organization (accessible to Secretariat only)"
#swagger.description = "
Access Control
@@ -567,6 +568,7 @@ router.post('/review/org/',
/*
#swagger.tags = ['Review Object']
#swagger.operationId = 'createReviewObject'
+ #swagger.ignore = true
#swagger.summary = "Creates a new review object (accessible to Secretariat only)"
#swagger.description = "
Access Control
diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js
index 7b4556d29..7de67c58d 100644
--- a/src/controller/review-object.controller/review-object.controller.js
+++ b/src/controller/review-object.controller/review-object.controller.js
@@ -38,6 +38,18 @@ async function getReviewObjectByUUID (req, res, next) {
const isSecretariat = await orgRepo.isSecretariatByShortName(req.ctx.org)
const UUID = req.params.uuid
const value = await repo.findOneByUUIDWithConversation(UUID, isSecretariat)
+
+ if (!value) {
+ return res.status(404).json({ message: 'Review object not found' })
+ }
+
+ if (!isSecretariat) {
+ const orgUUID = await orgRepo.getOrgUUID(req.ctx.org)
+ if (value.target_object_uuid !== orgUUID) {
+ return res.status(403).json({ error: 'NOT_SAME_ORG_OR_SECRETARIAT', message: 'This information can only be viewed by the users of the same organization or the Secretariat.' })
+ }
+ }
+
return res.status(200).json(value)
}
@@ -197,6 +209,10 @@ async function getReviewHistoryByOrgShortNamePaginated (req, res, next) {
return res.status(404).json(error.orgDnePathParam(orgShortName))
}
+ if (!isSecretariat && req.ctx.org !== orgShortName) {
+ return res.status(403).json({ error: 'NOT_SAME_ORG_OR_SECRETARIAT', message: 'This information can only be viewed by the users of the same organization or the Secretariat.' })
+ }
+
if (req.TEST_PAGINATOR_LIMIT) {
CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
}
diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js
index a4edec5af..3b1afc328 100644
--- a/src/repositories/baseOrgRepository.js
+++ b/src/repositories/baseOrgRepository.js
@@ -152,7 +152,7 @@ class BaseOrgRepository extends BaseRepository {
async findOneByShortName (shortName, options = {}, returnLegacyFormat = false, projection = {}) {
const OrgRepository = require('./orgRepository')
const legacyOrgRepo = new OrgRepository()
- if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options)
+ if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options, projection)
const data = await BaseOrgModel.findOne({ short_name: shortName }, projection, options)
return data
}
@@ -198,12 +198,68 @@ class BaseOrgRepository extends BaseRepository {
* @returns {Promise} True if the organization exists, false otherwise.
*/
async orgExists (shortName, options = {}, returnLegacyFormat = false) {
- if (await this.findOneByShortName(shortName, options, returnLegacyFormat)) {
+ if (!shortName) return false
+ const query = { $expr: { $eq: [{ $toLower: '$short_name' }, shortName.toLowerCase()] } }
+ const exists = await BaseOrgModel.findOne(query, null, options)
+ if (exists) {
return true
}
return false
}
+ /**
+ * @async
+ * @function checkAliasCollisions
+ * @description Checks if the provided short_name, name, or aliases collide with any other organization's short_name, long_name, or aliases.
+ * @param {string} shortName - The proposed short name of the organization.
+ * @param {string} name - The proposed name of the organization.
+ * @param {string[]} aliases - The proposed aliases of the organization.
+ * @param {string} [shortNameToExclude=null] - The short name of the organization to exclude from the check (e.g., during an update).
+ * @param {object} [options={}] - Optional settings for the repository query.
+ * @returns {Promise} The conflicting string if a collision is found, otherwise null.
+ */
+ async checkAliasCollisions (shortName, name, aliases, shortNameToExclude = null, options = {}) {
+ const searchStrings = [shortName, name, ...(aliases || [])].filter(str => str != null && str !== '')
+ if (searchStrings.length === 0) {
+ return null
+ }
+
+ const searchStringsLower = searchStrings.map(s => s.toLowerCase())
+
+ const query = {
+ $or: [
+ {
+ $expr: {
+ $or: searchStringsLower.map(s => ({
+ $eq: [{ $toLower: '$short_name' }, s]
+ }))
+ }
+ },
+ { long_name: { $in: searchStrings } },
+ { aliases: { $in: searchStrings } }
+ ]
+ }
+
+ if (shortNameToExclude) {
+ query.$and = [
+ { $expr: { $ne: [{ $toLower: '$short_name' }, shortNameToExclude.toLowerCase()] } }
+ ]
+ }
+
+ const collisionOrg = await BaseOrgModel.findOne(query, 'short_name long_name aliases', options)
+ if (collisionOrg) {
+ // Determine which string collided for better error reporting
+ for (const str of searchStrings) {
+ if (collisionOrg.short_name?.toLowerCase() === str.toLowerCase() || collisionOrg.long_name === str || (collisionOrg.aliases && collisionOrg.aliases.includes(str))) {
+ return str
+ }
+ }
+ return searchStrings[0] // Fallback
+ }
+
+ return null
+ }
+
/**
* @async
* @function addUserToOrg
@@ -215,7 +271,7 @@ class BaseOrgRepository extends BaseRepository {
* @param {boolean} [isLegacyObject=false] - Unused parameter.
* @returns {Promise}
*/
- async addUserToOrg (orgShortName, userUUID, isAdmin = false, options = {}, isLegacyObject = false) {
+ async addUserToOrg (orgShortName, userUUID, isAdmin = false, options = {}, isLegacyObject = false, requestingUserUUID = null) {
const update = {
$addToSet: { users: userUUID }
}
@@ -224,7 +280,11 @@ class BaseOrgRepository extends BaseRepository {
update.$addToSet.admins = userUUID
}
- await BaseOrgModel.updateOne({ short_name: orgShortName }, update, options)
+ const originalOrg = await BaseOrgModel.findOneAndUpdate({ short_name: orgShortName }, update, options)
+ if (requestingUserUUID && originalOrg) {
+ const updatedOrg = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options)
+ await createAuditLogEntry(updatedOrg, originalOrg.toObject(), requestingUserUUID, options)
+ }
}
/**
@@ -236,18 +296,24 @@ class BaseOrgRepository extends BaseRepository {
* @param {object} [options={}] - Optional settings for the repository query.
* @returns {Promise}
*/
- async addAdmin (orgShortName, userUUID, options = {}) {
+ async addAdmin (orgShortName, userUUID, options = {}, requestingUserUUID = null) {
const UserRepository = require('./userRepository')
const legacyUserRepo = new UserRepository()
- const executeOptions = { ...options, new: true }
+ const executeOptions = { ...options, new: false }
- const updatedOrg = await BaseOrgModel.findOneAndUpdate(
+ const originalOrg = await BaseOrgModel.findOneAndUpdate(
{ short_name: orgShortName },
{ $addToSet: { admins: userUUID } },
executeOptions
)
+ const updatedOrg = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options)
+
+ if (requestingUserUUID && originalOrg) {
+ await createAuditLogEntry(updatedOrg, originalOrg.toObject(), requestingUserUUID, options)
+ }
+
await legacyUserRepo.collection.findOneAndUpdate(
{ UUID: userUUID },
{ $addToSet: { 'authority.active_roles': 'ADMIN' } },
@@ -266,18 +332,24 @@ class BaseOrgRepository extends BaseRepository {
* @param {object} [options={}] - Optional settings for the repository query.
* @returns {Promise}
*/
- async removeAdmin (orgShortName, userUUID, options = {}) {
+ async removeAdmin (orgShortName, userUUID, options = {}, requestingUserUUID = null) {
const UserRepository = require('./userRepository')
const legacyUserRepo = new UserRepository()
- const executeOptions = { ...options, new: true }
+ const executeOptions = { ...options, new: false }
- const updatedOrg = await BaseOrgModel.findOneAndUpdate(
+ const originalOrg = await BaseOrgModel.findOneAndUpdate(
{ short_name: orgShortName },
{ $pull: { admins: userUUID } },
executeOptions
)
+ const updatedOrg = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options)
+
+ if (requestingUserUUID && originalOrg) {
+ await createAuditLogEntry(updatedOrg, originalOrg.toObject(), requestingUserUUID, options)
+ }
+
await legacyUserRepo.collection.findOneAndUpdate(
{ UUID: userUUID },
{ $pull: { 'authority.active_roles': 'ADMIN' } },
@@ -370,6 +442,25 @@ class BaseOrgRepository extends BaseRepository {
result.reports_to = parentOrg.UUID
}
+ const relatedOrgUUIDs = new Set()
+ if (Array.isArray(result.oversees)) {
+ result.oversees.forEach(uuid => relatedOrgUUIDs.add(uuid))
+ }
+ if (result.reports_to) {
+ relatedOrgUUIDs.add(result.reports_to)
+ }
+
+ if (relatedOrgUUIDs.size > 0) {
+ const relatedOrgs = await BaseOrgModel.find({ UUID: { $in: Array.from(relatedOrgUUIDs) } }).select('short_name long_name UUID').lean()
+ if (relatedOrgs.length > 0) {
+ result._relatedOrganizations = relatedOrgs.map(org => ({
+ short_name: org.short_name,
+ long_name: org.long_name,
+ UUID: org.UUID
+ }))
+ }
+ }
+
return deepRemoveEmpty(result)
}
diff --git a/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js
index 0bac9a580..869d619c7 100644
--- a/src/repositories/baseOrgRepositoryHelpers.js
+++ b/src/repositories/baseOrgRepositoryHelpers.js
@@ -200,22 +200,24 @@ async function createAuditLogEntry (registryOrg, originalRegistryOrgObject, requ
try {
const AuditRepository = require('./auditRepository')
const auditRepo = new AuditRepository()
- await auditRepo.seedAuditHistoryForOrg(
- registryOrg.UUID,
- originalRegistryOrgObject,
- requestingUserUUID,
- { ...options, upsert: true }
- )
+
const beforeUpdateObject = originalRegistryOrgObject
const afterUpdateObject = registryOrg.toObject()
const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt'])
const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt'])
+ await auditRepo.seedAuditHistoryForOrg(
+ registryOrg.UUID,
+ cleanBefore,
+ requestingUserUUID,
+ { ...options, upsert: true }
+ )
+
if (!_.isEqual(cleanBefore, cleanAfter)) {
await auditRepo.appendToAuditHistoryForOrg(
registryOrg.UUID,
- registryOrg.toObject(),
+ cleanAfter,
requestingUserUUID,
{ ...options, upsert: true }
)
diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js
index be6164758..720fae515 100644
--- a/src/repositories/baseUserRepository.js
+++ b/src/repositories/baseUserRepository.js
@@ -187,7 +187,7 @@ class BaseUserRepository extends BaseRepository {
* @param {object} options - Mongoose options for the delete operations.
* @returns {Promise} Number of deleted documents (should be 1 if successful).
*/
- async deleteUserByUUID (uuid, options = {}) {
+ async deleteUserByUUID (uuid, options = {}, requestingUserUUID = null) {
// Delete from BaseUser collection
const deleteResult = await BaseUser.deleteOne({ UUID: uuid }, options)
@@ -195,13 +195,20 @@ class BaseUserRepository extends BaseRepository {
await RegistryUser.deleteOne({ UUID: uuid }, options)
// Remove user from any organization’s users and admins arrays
+ const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
const orgs = await BaseOrgModel.find({ $or: [{ users: uuid }, { admins: uuid }] })
for (const org of orgs) {
- org.users = org.users.filter(u => u !== uuid)
+ const originalOrg = org.toObject()
+ if (Array.isArray(org.users)) {
+ org.users.pull(uuid)
+ }
if (Array.isArray(org.admins)) {
- org.admins = org.admins.filter(a => a !== uuid)
+ org.admins.pull(uuid)
}
await org.save(options)
+ if (requestingUserUUID) {
+ await createAuditLogEntry(org, originalOrg, requestingUserUUID, options)
+ }
}
return deleteResult.deletedCount
@@ -332,7 +339,7 @@ class BaseUserRepository extends BaseRepository {
* @param {boolean} [isRegistryObject=true] - If false, accepts legacy user object.
* @returns {Promise} The created user object (registry or legacy format).
*/
- async createUser (orgShortName, incomingUser, options = {}, isRegistryObject = true) {
+ async createUser (orgShortName, incomingUser, options = {}, isRegistryObject = true, requestingUserUUID = null) {
const { deepRemoveEmpty } = require('../utils/utils')
// TO-DO: org_UUID is not necessarily the shortname. Is this info lost during conversion?
let legacyObjectRaw = null
@@ -373,7 +380,7 @@ class BaseUserRepository extends BaseRepository {
const registryUserToSave = new RegistryUser(registryObjectRaw)
registryObject = await registryUserToSave.save(options)
- baseOrgRepository.addUserToOrg(orgShortName, incomingUser.UUID, (incomingUser.role === 'ADMIN' || incomingUser.authority?.active_roles?.includes('ADMIN')))
+ await baseOrgRepository.addUserToOrg(orgShortName, incomingUser.UUID, (incomingUser.role === 'ADMIN' || incomingUser.authority?.active_roles?.includes('ADMIN')), options, false, requestingUserUUID)
// We now have to make sure the user is added to the ORG's user array
await legacyUserRepo.updateByUserNameAndOrgUUID(incomingUser.username, existingOrg.UUID, legacyObjectRaw, { ...options, upsert: true })
@@ -405,11 +412,13 @@ class BaseUserRepository extends BaseRepository {
* @param {boolean} [isRegistryObject=true] - If false, returns a legacy user object.
* @returns {Promise} The updated user object.
*/
- async updateUser (username, orgShortname, incomingParameters, options = {}, isRegistryObject = true) {
+ async updateUser (username, orgShortname, incomingParameters, options = {}, isRegistryObject = true, requestingUserUUID = null) {
const { deepRemoveEmpty } = require('../utils/utils')
const baseOrgRepository = new BaseOrgRepository()
const legacyUserRepo = new UserRepository()
+ const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
const registryOrg = await baseOrgRepository.getOrgObject(orgShortname, false, options)
+ const originalRegistryOrg = registryOrg.toObject()
const legacyUser = await legacyUserRepo.findOneByUserNameAndOrgUUID(username, registryOrg.UUID, null, options)
const registryUser = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, true) // WE always want the registry user
@@ -430,13 +439,17 @@ class BaseUserRepository extends BaseRepository {
const rolesToAdd = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.add')))
const rolesToRemove = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.remove')))
if (rolesToRemove.includes('ADMIN')) {
- const filteredUuids = registryOrg.admins.filter(uuid => uuid !== registryUser.UUID)
- registryOrg.admins = filteredUuids
+ if (Array.isArray(registryOrg.admins)) {
+ registryOrg.admins.pull(registryUser.UUID)
+ }
}
if (rolesToAdd.includes('ADMIN') && !incomingParameters?.org_short_name) {
// Use the already fetched registryOrg instead of querying again
- registryOrg.admins = [...new Set([...(registryOrg.admins || []), registryUser.UUID])]
+ if (!Array.isArray(registryOrg.admins)) {
+ registryOrg.admins = []
+ }
+ registryOrg.admins.addToSet(registryUser.UUID)
}
const initialRoles = legacyUser.authority?.active_roles ?? []
@@ -446,23 +459,40 @@ class BaseUserRepository extends BaseRepository {
if (incomingParameters?.org_short_name) {
// Remove us from the old users Array
- const filteredUuids = registryOrg.users.filter(uuid => uuid !== registryUser.UUID)
- registryOrg.users = filteredUuids
+ if (Array.isArray(registryOrg.users)) {
+ registryOrg.users.pull(registryUser.UUID)
+ }
+ if (registryOrg.admins && registryOrg.admins.includes(registryUser.UUID)) {
+ registryOrg.admins.pull(registryUser.UUID)
+ }
// Add us to the new org (this is a genuine cross-org migration, so we must fetch the new org)
const newOrg = await baseOrgRepository.getOrgObject(incomingParameters.org_short_name)
- newOrg.users = [...new Set([...newOrg.users, registryUser.UUID])]
+ const originalNewOrg = newOrg.toObject()
+ if (!Array.isArray(newOrg.users)) {
+ newOrg.users = []
+ }
+ newOrg.users.addToSet(registryUser.UUID)
if (registryUser.role.includes('ADMIN')) {
- newOrg.admins = [...new Set([...(newOrg.admins || []), registryUser.UUID])]
+ if (!Array.isArray(newOrg.admins)) {
+ newOrg.admins = []
+ }
+ newOrg.admins.addToSet(registryUser.UUID)
}
legacyUser.org_UUID = newOrg.UUID
await newOrg.save(options)
+ if (requestingUserUUID) {
+ await createAuditLogEntry(newOrg, originalNewOrg, requestingUserUUID, options)
+ }
}
delete registryUser.role
// Single unified save for the primary org at the end
await registryOrg.save(options)
+ if (requestingUserUUID) {
+ await createAuditLogEntry(registryOrg, originalRegistryOrg, requestingUserUUID, options)
+ }
await legacyUser.save(options)
await registryUser.save(options)
@@ -496,7 +526,7 @@ class BaseUserRepository extends BaseRepository {
* @param {boolean} [isRegistryObject=true] - If false, accepts/returns legacy format.
* @returns {Promise} The updated user object.
*/
- async updateUserFull (identifier, incomingUser, options = {}, isRegistryObject = true) {
+ async updateUserFull (identifier, incomingUser, options = {}, isRegistryObject = true, requestingUserUUID = null) {
const legacyUserRepo = new UserRepository()
// Find registry user by UUID
@@ -539,30 +569,41 @@ class BaseUserRepository extends BaseRepository {
try {
if (incomingUser.org_short_name) {
const baseOrgRepository = new BaseOrgRepository()
+ const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
const currentOrgUUID = legacyUser.org_UUID
const currentOrg = await baseOrgRepository.findOneByUUID(currentOrgUUID)
+ const originalCurrentOrg = currentOrg.toObject()
const newOrg = await baseOrgRepository.findOneByShortName(incomingUser.org_short_name)
+ const originalNewOrg = newOrg.toObject()
if (!newOrg) {
throw new Error(`Organization ${incomingUser.org_short_name} not found`)
}
// 1. Remove user from old org's users list
- currentOrg.users = currentOrg.users.filter(u => u !== identifier)
+ if (Array.isArray(currentOrg.users)) {
+ currentOrg.users.pull(identifier)
+ }
// 2. Remove user from old org's admins list (if present)
if (currentOrg.admins && currentOrg.admins.includes(identifier)) {
- currentOrg.admins = currentOrg.admins.filter(a => a !== identifier)
+ currentOrg.admins.pull(identifier)
}
// 3. Add user to new org's users list
- newOrg.users = [...new Set([...newOrg.users, identifier])]
+ if (!Array.isArray(newOrg.users)) {
+ newOrg.users = []
+ }
+ newOrg.users.addToSet(identifier)
// 4. Add user to new org's admins list (if they are an admin)
const isAdmin = updatedRegistryUser.role === 'ADMIN' || (updatedLegacyUser.authority && updatedLegacyUser.authority.active_roles && updatedLegacyUser.authority.active_roles.includes('ADMIN'))
if (isAdmin) {
- newOrg.admins = [...new Set([...(newOrg.admins || []), identifier])]
+ if (!Array.isArray(newOrg.admins)) {
+ newOrg.admins = []
+ }
+ newOrg.admins.addToSet(identifier)
}
// 5. Update user's org_UUID
@@ -571,6 +612,11 @@ class BaseUserRepository extends BaseRepository {
// Save org changes
await currentOrg.save(options)
await newOrg.save(options)
+
+ if (requestingUserUUID) {
+ await createAuditLogEntry(currentOrg, originalCurrentOrg, requestingUserUUID, options)
+ await createAuditLogEntry(newOrg, originalNewOrg, requestingUserUUID, options)
+ }
}
await updatedLegacyUser.save(options)
diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js
index c8a8d48cc..b4ef2e437 100644
--- a/src/repositories/conversationRepository.js
+++ b/src/repositories/conversationRepository.js
@@ -92,7 +92,15 @@ class ConversationRepository extends BaseRepository {
}
const newConversation = new ConversationModel(conversationObj)
const result = await newConversation.save(options)
- return result.toObject()
+
+ const rawObject = result.toObject()
+
+ delete rawObject._id
+ delete rawObject.__v
+ delete rawObject.previous_conversation_uuid
+ delete rawObject.next_conversation_uuid
+
+ return rawObject
}
async editConversation (UUID, incomingParameters, options = {}) {
diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js
index 099cc2fa8..d514434dd 100644
--- a/src/repositories/orgRepository.js
+++ b/src/repositories/orgRepository.js
@@ -7,9 +7,9 @@ class OrgRepository extends BaseRepository {
super(Org)
}
- async findOneByShortName (shortName, options = {}) {
+ async findOneByShortName (shortName, options = {}, projection = {}) {
const query = { short_name: shortName }
- return this.collection.findOne(query, null, options)
+ return this.collection.findOne(query, projection, options)
}
async findOneByUUID (UUID) {
diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js
index 61400d343..19730b6ae 100644
--- a/src/repositories/reviewObjectRepository.js
+++ b/src/repositories/reviewObjectRepository.js
@@ -2,6 +2,18 @@ const ReviewObjectModel = require('../model/reviewobject')
const BaseRepository = require('./baseRepository')
const BaseOrgRepository = require('./baseOrgRepository')
const uuid = require('uuid')
+const _ = require('lodash')
+const getConstants = require('../constants').getConstants
+
+function filterReviewOrgData (orgData, isSecretariat = false) {
+ if (!orgData) return orgData
+ const CONSTANTS = getConstants()
+ let fieldsToOmit = [...CONSTANTS.ORG_EXCLUDED_FIELDS]
+ if (!isSecretariat) {
+ fieldsToOmit = [...fieldsToOmit, ...CONSTANTS.ORG_RESTRICTED_FIELDS]
+ }
+ return _.omit(orgData, fieldsToOmit)
+}
class ReviewObjectRepository extends BaseRepository {
constructor () {
@@ -14,13 +26,13 @@ class ReviewObjectRepository extends BaseRepository {
if (!org) {
return null
}
- const reviewObject = await ReviewObjectModel.find({ target_object_uuid: org.UUID }, null, options)
+ const reviewObject = await ReviewObjectModel.find({ target_object_uuid: org.UUID }, { _id: 0, __v: 0 }, options)
return reviewObject || null
}
async findOneByUUID (UUID, options = {}) {
- const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, { _id: 0, __v: 0 }, options)
return reviewObject || null
}
@@ -29,21 +41,19 @@ class ReviewObjectRepository extends BaseRepository {
const conversationRepository = new ConversationRepository()
let reviewObject
const query = pending ? { uuid: UUID, status: 'pending' } : { uuid: UUID }
- const reviewObjectRaw = await ReviewObjectModel.findOne(query, null, options)
+ const reviewObjectRaw = await ReviewObjectModel.findOne(query, { _id: 0, __v: 0 }, options)
if (reviewObjectRaw) {
reviewObject = reviewObjectRaw.toObject()
const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.target_object_uuid, isSecretariat, options)
reviewObject.conversation = conversations?.length ? conversations : undefined
- if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) {
- delete reviewObject.new_review_data.program_data
- }
+ reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat)
}
return reviewObject || null
}
async getAllReviewObjects (options = {}) {
- const reviewObjects = await ReviewObjectModel.find({}, null, {
+ const reviewObjects = await ReviewObjectModel.find({}, { _id: 0, __v: 0 }, {
...options,
sort: { created: -1 }
})
@@ -60,7 +70,8 @@ class ReviewObjectRepository extends BaseRepository {
const agt = [
{ $match: query },
- { $sort: { created: -1 } }
+ { $sort: { created: -1 } },
+ { $project: { _id: 0, __v: 0 } }
]
const pg = await this.aggregatePaginate(agt, options)
@@ -96,7 +107,7 @@ class ReviewObjectRepository extends BaseRepository {
target_object_uuid: org.UUID,
status: 'pending'
},
- null,
+ { _id: 0, __v: 0 },
{
...options,
sort: { created: -1 }
@@ -106,9 +117,7 @@ class ReviewObjectRepository extends BaseRepository {
reviewObject = reviewObjectRaw.toObject()
const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options)
reviewObject.conversation = conversations?.length ? conversations : undefined
- if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) {
- delete reviewObject.new_review_data.program_data
- }
+ reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat)
}
return reviewObject || null
@@ -128,7 +137,7 @@ class ReviewObjectRepository extends BaseRepository {
target_object_uuid: org.UUID,
status: 'pending'
},
- null,
+ { _id: 0, __v: 0 },
{
...options,
sort: { created: -1 }
@@ -138,9 +147,7 @@ class ReviewObjectRepository extends BaseRepository {
reviewObject = reviewObjectRaw.toObject()
const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options)
reviewObject.conversation = conversations?.length ? conversations : undefined
- if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) {
- delete reviewObject.new_review_data.program_data
- }
+ reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat)
}
return reviewObject || null
@@ -160,12 +167,12 @@ class ReviewObjectRepository extends BaseRepository {
const reviewObject = new ReviewObjectModel(reviewObjectRaw)
await reviewObject.save(options)
- return reviewObject.toObject()
+ return _.omit(reviewObject.toObject(), ['_id', '__v'])
}
async updateReviewOrgObject (body, UUID, options = {}) {
console.log('Updating review object with UUID:', UUID)
- const reviewObject = await this.findOneByUUID(UUID, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
if (!reviewObject) {
return null
}
@@ -173,12 +180,12 @@ class ReviewObjectRepository extends BaseRepository {
reviewObject.new_review_data = body
const result = await reviewObject.save(options)
- return result.toObject()
+ return _.omit(result.toObject(), ['_id', '__v'])
}
async approveReviewOrgObject (UUID, approverUsername, options = {}) {
console.log('Approving review object with UUID:', UUID)
- const reviewObject = await this.findOneByUUID(UUID, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
if (!reviewObject) {
return null
}
@@ -191,7 +198,7 @@ class ReviewObjectRepository extends BaseRepository {
}
await reviewObject.save(options)
- return reviewObject.toObject()
+ return _.omit(reviewObject.toObject(), ['_id', '__v'])
}
/**
@@ -207,7 +214,8 @@ class ReviewObjectRepository extends BaseRepository {
const agt = [
{ $match: { target_object_uuid: org.UUID } },
- { $sort: { created: -1 } }
+ { $sort: { created: -1 } },
+ { $project: { _id: 0, __v: 0 } }
]
const pg = await this.aggregatePaginate(agt, options)
@@ -221,11 +229,9 @@ class ReviewObjectRepository extends BaseRepository {
data.nextPage = pg.nextPage
}
- if (!isSecretariat && data.reviewObjects) {
+ if (data.reviewObjects) {
for (const review of data.reviewObjects) {
- if (review.new_review_data && review.new_review_data.program_data) {
- delete review.new_review_data.program_data
- }
+ review.new_review_data = filterReviewOrgData(review.new_review_data, isSecretariat)
}
}
@@ -260,7 +266,7 @@ class ReviewObjectRepository extends BaseRepository {
async rejectReviewOrgObject (UUID, rejectorUsername, options = {}) {
console.log('Rejecting review object with UUID:', UUID)
- const reviewObject = await this.findOneByUUID(UUID, options)
+ const reviewObject = await ReviewObjectModel.findOne({ uuid: UUID }, null, options)
if (!reviewObject) {
return null
}
@@ -273,7 +279,7 @@ class ReviewObjectRepository extends BaseRepository {
}
await reviewObject.save(options)
- return reviewObject.toObject()
+ return _.omit(reviewObject.toObject(), ['_id', '__v'])
}
}
module.exports = ReviewObjectRepository
diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js
index 92adb7849..d9777c1d8 100644
--- a/test/integration-tests/audit/registryOrgCreatesAuditTest.js
+++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js
@@ -249,4 +249,76 @@ describe('Create and Update Audit Collection with Org Endpoints', () => {
// Should have 2 entries: initial creation of current org object + new update
expect(auditResCreation.body.history).to.have.lengthOf(2)
})
+
+ it('Should add audit entry when users or admins are modified via user endpoints', async () => {
+ const testOrg = await createTestOrg({
+ hard_quota: 1500,
+ authority: ['CNA']
+ })
+
+ // 1. Create User
+ const username = `user_${uuidv4().slice(0, 8)}`
+ const createUserRes = await chai.request(app)
+ .post(`/api/registry/org/${testOrg.shortName}/user`)
+ .set(secretariatHeaders)
+ .send({
+ username: username,
+ name: {
+ first: 'Test',
+ last: 'User'
+ }
+ })
+
+ expect(createUserRes).to.have.status(200)
+
+ // Check audit history
+ const auditRes1 = await chai.request(app)
+ .get(`/api/audit/org/${testOrg.uuid}`)
+ .set(constants.headers)
+
+ expect(auditRes1.body.history).to.have.lengthOf(2) // 1. Org creation, 2. User creation
+
+ const historyAfterCreate = auditRes1.body.history[1].audit_object
+ expect(historyAfterCreate.users).to.be.an('array').that.includes(createUserRes.body.created.UUID)
+ expect(historyAfterCreate.admins).to.be.an('array').that.does.not.include(createUserRes.body.created.UUID)
+
+ // 2. Grant Admin Role
+ const grantRoleRes = await chai.request(app)
+ .post(`/api/registry/org/${testOrg.shortName}/user/${username}/grant-role`)
+ .set(secretariatHeaders)
+ .send({
+ role: 'ADMIN'
+ })
+
+ expect(grantRoleRes).to.have.status(200)
+
+ // Check audit history again
+ const auditRes2 = await chai.request(app)
+ .get(`/api/audit/org/${testOrg.uuid}`)
+ .set(constants.headers)
+
+ expect(auditRes2.body.history).to.have.lengthOf(3) // 3. Admin role granted
+
+ const historyAfterGrant = auditRes2.body.history[2].audit_object
+ expect(historyAfterGrant.admins).to.be.an('array').that.includes(createUserRes.body.created.UUID)
+
+ // 3. Revoke Admin Role
+ const revokeRoleRes = await chai.request(app)
+ .post(`/api/registry/org/${testOrg.shortName}/user/${username}/revoke-role`)
+ .set(secretariatHeaders)
+ .send({
+ role: 'ADMIN'
+ })
+ expect(revokeRoleRes).to.have.status(200)
+
+ // Check audit history again
+ const auditRes3 = await chai.request(app)
+ .get(`/api/audit/org/${testOrg.uuid}`)
+ .set(constants.headers)
+
+ expect(auditRes3.body.history).to.have.lengthOf(4) // 4. Admin role revoked
+
+ const historyAfterRevoke = auditRes3.body.history[3].audit_object
+ expect(historyAfterRevoke.admins).to.be.an('array').that.does.not.include(createUserRes.body.created.UUID)
+ })
})
diff --git a/test/integration-tests/conversation/conversationTest.js b/test/integration-tests/conversation/conversationTest.js
index d2c5db976..8e14ea32e 100644
--- a/test/integration-tests/conversation/conversationTest.js
+++ b/test/integration-tests/conversation/conversationTest.js
@@ -54,11 +54,6 @@ describe('Testing Conversation endpoints', () => {
expect(res.body).to.haveOwnProperty('target_uuid')
expect(res.body.target_uuid).to.equal(orgUUID)
- expect(res.body).to.haveOwnProperty('previous_conversation_uuid')
- expect(res.body.previous_conversation_uuid).to.be.null
- expect(res.body).to.haveOwnProperty('next_conversation_uuid')
- expect(res.body.next_conversation_uuid).to.be.null
-
expect(res.body).to.haveOwnProperty('author_id')
expect(res.body.author_id).to.equal(secUserUUID)
@@ -110,11 +105,6 @@ describe('Testing Conversation endpoints', () => {
expect(rootMessage).to.exist
expect(rootMessage.previous_conversation_uuid).to.be.null
expect(rootMessage.next_conversation_uuid).to.be.equal(secondUUID)
-
- expect(res.body).to.haveOwnProperty('previous_conversation_uuid')
- expect(res.body.previous_conversation_uuid).to.be.equal(rootConvoUUID)
- expect(res.body).to.haveOwnProperty('next_conversation_uuid')
- expect(res.body.next_conversation_uuid).to.be.null
})
it('Should get all conversations', async () => {
await chai.request(app)
diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js
index 60886b86f..6f6ec4823 100644
--- a/test/integration-tests/registry-org/registryOrgCRUDTest.js
+++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js
@@ -87,6 +87,21 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.message).to.equal(`The '${testRegistryOrg.short_name}' organization already exists.`)
})
})
+ it('Fails to create a new registry organization with an existing short name (case-insensitive)', async () => {
+ const bodyWithDuplicateCase = { ...createdOrg }
+ delete bodyWithDuplicateCase.UUID
+ delete bodyWithDuplicateCase.uuid
+ bodyWithDuplicateCase.short_name = bodyWithDuplicateCase.short_name.toUpperCase()
+
+ const res = await chai.request(app)
+ .post('/api/registry/org')
+ .set(secretariatHeaders)
+ .send(bodyWithDuplicateCase)
+
+ expect(res).to.have.status(400)
+ expect(res.body.message).to.equal(`The '${bodyWithDuplicateCase.short_name}' organization already exists.`)
+ })
+
it('Fails to create a new registry organization with invalid data', async () => {
await chai.request(app)
.post('/api/registry/org')
@@ -146,6 +161,21 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.errors[0].instancePath).to.equal('/partner_role_type')
})
})
+ it('Fails to create a new registry organization with an alias that collides with an existing short_name', async () => {
+ await chai.request(app)
+ .post('/api/registry/org')
+ .set(secretariatHeaders)
+ .send({
+ ...testRegistryOrg,
+ short_name: 'test_create_alias_collision',
+ aliases: ['mitre']
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.equal('ALIAS_COLLISION')
+ expect(res.body.message).to.equal("The organization could not be created or updated because the string 'mitre' is already in use as a short_name, name, or alias by another organization.")
+ })
+ })
})
})
context('Testing GET /registryOrg endpoints', () => {
@@ -393,6 +423,22 @@ describe('Testing /registryOrg endpoints', () => {
.then(res => {
expect(res).to.have.status(200)
expect(res.body).to.have.property('reports_to', createdOrg.UUID)
+ expect(res.body).to.have.property('_relatedOrganizations')
+ expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1)
+ expect(res.body._relatedOrganizations[0].UUID).to.equal(createdOrg.UUID)
+ expect(res.body._relatedOrganizations[0].short_name).to.equal(createdOrg.short_name)
+ })
+
+ // Assert that the main org also has _relatedOrganizations for the sub org it oversees
+ await chai.request(app)
+ .get(`/api/registry/org/${createdOrg.short_name}`)
+ .set(secretariatHeaders)
+ .then(res => {
+ expect(res).to.have.status(200)
+ expect(res.body).to.have.property('_relatedOrganizations')
+ expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1)
+ expect(res.body._relatedOrganizations[0].UUID).to.equal(createdSubOrgUUID)
+ expect(res.body._relatedOrganizations[0].short_name).to.equal(subOrg.short_name)
})
// Cleanup sub org
@@ -470,6 +516,20 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.message).to.equal("The organization cannot be renamed as 'mitre' because this shortname is used by another organization.")
})
})
+ it('Fails to update a registry organization with an alias that collides with an existing short_name', async () => {
+ await chai.request(app)
+ .put('/api/registry/org/registry_org_test')
+ .set(secretariatHeaders)
+ .send({
+ ...createdOrg,
+ aliases: ['mitre']
+ })
+ .then((res) => {
+ expect(res).to.have.status(400)
+ expect(res.body.error).to.equal('ALIAS_COLLISION')
+ expect(res.body.message).to.equal("The organization could not be created or updated because the string 'mitre' is already in use as a short_name, name, or alias by another organization.")
+ })
+ })
it('Fails to update a registry organization providing invalid data', async () => {
await chai.request(app)
.put('/api/registry/org/registry_org_test')
diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js
index 6fa5fa212..3d9c10e46 100644
--- a/test/integration-tests/review-object/reviewObjectTest.js
+++ b/test/integration-tests/review-object/reviewObjectTest.js
@@ -40,6 +40,8 @@ describe('Review Object Controller Integration Tests', () => {
expect(res.body).to.have.property('target_object_uuid', orgUUID)
expect(res.body).to.have.property('new_review_data')
expect(res.body.status).to.equal('pending')
+ expect(res.body).to.not.have.property('_id')
+ expect(res.body).to.not.have.property('__v')
reviewUUID = res.body.uuid
})
@@ -50,6 +52,8 @@ describe('Review Object Controller Integration Tests', () => {
.set({ ...constants.headers })
expect(res).to.have.status(200)
expect(res.body).to.have.property('uuid', reviewUUID)
+ expect(res.body).to.not.have.property('_id')
+ expect(res.body).to.not.have.property('__v')
})
it('Retrieves the review object by org UUID', async () => {
@@ -59,6 +63,8 @@ describe('Review Object Controller Integration Tests', () => {
.set({ ...constants.headers })
expect(res).to.have.status(200)
expect(res.body).to.have.property('uuid', reviewUUID)
+ expect(res.body).to.not.have.property('_id')
+ expect(res.body).to.not.have.property('__v')
})
it('Retrieves the review object by review UUID', async () => {
@@ -81,6 +87,10 @@ describe('Review Object Controller Integration Tests', () => {
expect(res.body.reviewObjects).to.be.an('array')
const found = res.body.reviewObjects.find(obj => obj.uuid === reviewUUID)
expect(found).to.exist
+ if (res.body.reviewObjects.length > 0) {
+ expect(res.body.reviewObjects[0]).to.not.have.property('_id')
+ expect(res.body.reviewObjects[0]).to.not.have.property('__v')
+ }
})
it('Updates the review object with new short_name', async () => {
@@ -283,6 +293,9 @@ describe('Review Object Controller Integration Tests', () => {
expect(res.body.reviewObjects).to.be.an('array')
if (res.body.reviewObjects.length > 0) {
expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('program_data')
+ expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('inUse')
+ expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('_id')
+ expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('__v')
}
})
@@ -296,6 +309,9 @@ describe('Review Object Controller Integration Tests', () => {
expect(res.body.reviewObjects).to.be.an('array')
if (res.body.reviewObjects.length > 0) {
expect(res.body.reviewObjects[0].new_review_data).to.have.property('program_data')
+ expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('inUse')
+ expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('_id')
+ expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('__v')
}
})
@@ -306,6 +322,9 @@ describe('Review Object Controller Integration Tests', () => {
.set({ ...constants.nonSecretariatUserHeaders2 })
expect(res).to.have.status(200)
expect(res.body.new_review_data).to.not.have.property('program_data')
+ expect(res.body.new_review_data).to.not.have.property('inUse')
+ expect(res.body.new_review_data).to.not.have.property('_id')
+ expect(res.body.new_review_data).to.not.have.property('__v')
})
// ------------------------------------------------------------------------------------------------
@@ -460,8 +479,8 @@ describe('Review Object Controller Integration Tests', () => {
.request(app)
.get(`/api/review/byUUID/${fakeUUID}`)
.set({ ...constants.headers })
- expect(res).to.have.status(200)
- expect(res.body).to.be.null
+ expect(res).to.have.status(404)
+ expect(res.body.message).to.equal('Review object not found')
})
it('Returns 404 for review history of non-existent organization', async () => {
@@ -523,5 +542,23 @@ describe('Review Object Controller Integration Tests', () => {
.set({ ...constants.nonSecretariatUserHeaders })
expect(res).to.have.status(403)
})
+
+ it('Non-secretariat user of another org cannot access review object by UUID', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/byUUID/${reviewUUID}`)
+ .set({ ...constants.nonSecretariatUserHeaders2 })
+ expect(res).to.have.status(403)
+ expect(res.body.error).to.equal('NOT_SAME_ORG_OR_SECRETARIAT')
+ })
+
+ it('Non-secretariat user of another org cannot access review history', async () => {
+ const res = await chai
+ .request(app)
+ .get(`/api/review/org/${constants.testRegistryOrg2.short_name}/reviews`)
+ .set({ ...constants.nonSecretariatUserHeaders2 })
+ expect(res).to.have.status(403)
+ expect(res.body.error).to.equal('NOT_SAME_ORG_OR_SECRETARIAT')
+ })
})
})
diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js
index 3871a4b2a..ef8d00eed 100644
--- a/test/integration-tests/user/updateUserTest.js
+++ b/test/integration-tests/user/updateUserTest.js
@@ -10,6 +10,88 @@ const app = require('../../../src/index.js')
describe('Testing Edit user endpoint', () => {
context('Positive Tests', () => {
+ it('Should correctly remove an admin from the original organization admins array when migrated to a new organization', async () => {
+ // Create org A
+ const orgA = 'mig_org_a'
+ await chai.request(app)
+ .post('/api/registry/org')
+ .set(constants.headers)
+ .send({
+ short_name: orgA,
+ long_name: 'Migration Org A',
+ authority: ['CNA'],
+ hard_quota: 1000
+ })
+ .then(res => {
+ expect(res.status).to.equal(200, JSON.stringify(res.body))
+ })
+ // Create org B
+ const orgB = 'mig_org_b'
+ await chai.request(app)
+ .post('/api/registry/org')
+ .set(constants.headers)
+ .send({
+ short_name: orgB,
+ long_name: 'Migration Org B',
+ authority: ['CNA'],
+ hard_quota: 1000
+ })
+ .then(res => {
+ expect(res.status).to.equal(200, JSON.stringify(res.body))
+ })
+
+ // Create Admin user in org A
+ const username = 'mig_admin@mig_org_a.com'
+ await chai.request(app)
+ .post(`/api/registry/org/${orgA}/user`)
+ .set(constants.headers)
+ .send({
+ username,
+ role: 'ADMIN',
+ name: {
+ first: 'Admin',
+ last: 'User'
+ },
+ status: 'active'
+ })
+ .then(res => {
+ expect(res.status).to.equal(200, JSON.stringify(res.body))
+ })
+ // Verify user is in orgA's admins array
+ const BaseOrgModel = require('../../../src/model/baseorg')
+ const orgARecordBefore = await BaseOrgModel.findOne({ short_name: orgA })
+ expect(orgARecordBefore.admins).to.be.an('array')
+ // We need the user's UUID
+ let userResponse
+ await chai.request(app).get(`/api/registry/org/${orgA}/user/${username}`).set(constants.headers).then((res) => { userResponse = res.body })
+ delete userResponse.created_by
+ delete userResponse.created
+ delete userResponse.last_updated
+
+ const userUUID = userResponse.UUID
+ expect(orgARecordBefore.admins).to.include(userUUID)
+
+ // Migrate user to org B
+ await chai.request(app)
+ .put(`/api/registry/org/${orgA}/user/${username}`)
+ .set(constants.headers)
+ .send({
+ ...userResponse,
+ org_short_name: orgB
+ })
+ .then((res) => {
+ expect(res).to.have.status(200)
+ })
+
+ // Verify user is removed from orgA
+ const orgARecordAfter = await BaseOrgModel.findOne({ short_name: orgA })
+ expect(orgARecordAfter.admins).to.not.include(userUUID)
+
+ // Verify user is in orgB's admins array
+ const orgBRecordAfter = await BaseOrgModel.findOne({ short_name: orgB })
+ expect(orgBRecordAfter.admins).to.include(userUUID)
+ })
+
it('Should return 200 when only name changes are done', async () => {
await chai.request(app)
.put('/api/org/win_5/user/jasminesmith@win_5.com?name.first=NewName')
diff --git a/test/unit-tests/org/orgCreateADPTest.js b/test/unit-tests/org/orgCreateADPTest.js
index 51c352890..e982cd20d 100644
--- a/test/unit-tests/org/orgCreateADPTest.js
+++ b/test/unit-tests/org/orgCreateADPTest.js
@@ -68,6 +68,7 @@ describe('Testing creating orgs with the ADP role', () => {
// --- Method Stubbing --
sinon.stub(regOrgRepo, 'findOneByShortName').resolves(null)
sinon.stub(regOrgRepo, 'isSecretariatByShortName').resolves(true)
+ sinon.stub(regOrgRepo, 'orgExists').resolves(false)
// 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' }
@@ -76,6 +77,7 @@ describe('Testing creating orgs with the ADP role', () => {
// Stub UUID getters to resolve with fake UUIDs
sinon.stub(regOrgRepo, 'getOrgUUID').resolves('org-uuid-123')
sinon.stub(userRegistryRepo, 'getUserUUID').resolves('user-uuid-123')
+ sinon.stub(regOrgRepo, 'checkAliasCollisions').resolves(null)
})
afterEach(() => {
diff --git a/test/unit-tests/org/orgCreateTest.js b/test/unit-tests/org/orgCreateTest.js
index 70a83f0bf..c4662b082 100644
--- a/test/unit-tests/org/orgCreateTest.js
+++ b/test/unit-tests/org/orgCreateTest.js
@@ -137,6 +137,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => {
baseUserRepo = new BaseUserRepository()
getBaseUserRepository = sinon.stub().returns(baseUserRepo)
sinon.stub(BaseUserRepository.prototype, 'findUserByUUID').resolves({ username: 'test_user' })
+ sinon.stub(baseOrgRepo, 'checkAliasCollisions').resolves(null)
})
// Restore all stubs after each test
@@ -167,6 +168,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => {
it('Should fail if the organization already exists', async () => {
sinon.stub(orgRepo, 'findOneByShortName').resolves(orgFixtures.existentOrg)
sinon.stub(baseOrgRepo, 'findOneByShortName').resolves(orgFixtures.existentOrg)
+ sinon.stub(baseOrgRepo, 'orgExists').resolves(true)
const testOrgPayload = { ...orgFixtures.existentOrg }
delete testOrgPayload.UUID
@@ -196,6 +198,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => {
beforeEach(() => {
sinon.stub(baseOrgRepo, 'findOneByShortName').resolves(null)
+ sinon.stub(baseOrgRepo, 'orgExists').resolves(false)
aggregateOrgStub = sinon.stub(orgRepo, 'aggregate')
aggregateRegOrgStub = sinon.stub(baseOrgRepo, 'aggregate')
diff --git a/test/unit-tests/org/orgUpdateTest.js b/test/unit-tests/org/orgUpdateTest.js
index e53f8cbd3..f34bf7c85 100644
--- a/test/unit-tests/org/orgUpdateTest.js
+++ b/test/unit-tests/org/orgUpdateTest.js
@@ -64,6 +64,10 @@ class OrgUpdatedAddingRole {
async orgExists () {
return true
}
+
+ async checkAliasCollisions () {
+ return null
+ }
}
class OrgUpdatedRemovingRole {
@@ -97,6 +101,10 @@ class OrgUpdatedRemovingRole {
async getOrgUUID () {
return null
}
+
+ async checkAliasCollisions () {
+ return null
+ }
}
// eslint-disable-next-line mocha/no-skipped-tests
@@ -352,6 +360,10 @@ describe('Testing the PUT /org/:shortname endpoint in Org Controller', () => {
async aggregate () {
return [orgFixtures.existentOrg]
}
+
+ async checkAliasCollisions () {
+ return null
+ }
}
app.route('/org-not-updated-no-query-parameters/:shortname')
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 45f4f779c..ece2d91ca 100644
--- a/test/unit-tests/review-object/review-object.controller.test.js
+++ b/test/unit-tests/review-object/review-object.controller.test.js
@@ -117,13 +117,13 @@ describe('Review Object Controller', function () {
expect(repoStub.findOneByUUIDWithConversation.calledWith(uuid, false)).to.be.true
})
- it('should return null when review object not found', async () => {
+ it('should return 404 when review object not found', async () => {
const uuid = 'nonexistent-uuid'
req.params.uuid = uuid
repoStub.findOneByUUIDWithConversation = sinon.stub().resolves(null)
await controller.getReviewObjectByUUID(req, res, next)
- expect(res.status.calledWith(200)).to.be.true
- expect(res.json.calledWith(null)).to.be.true
+ expect(res.status.calledWith(404)).to.be.true
+ expect(res.json.calledWith({ message: 'Review object not found' })).to.be.true
})
})
@@ -348,6 +348,7 @@ describe('Review Object Controller', function () {
it('should pass isSecretariat flag to repository', async () => {
orgRepoStub.orgExists = sinon.stub().resolves(true)
orgRepoStub.isSecretariatByShortName = sinon.stub().resolves(false)
+ req.ctx.org = orgShortName
repoStub.getReviewHistoryByOrgShortNamePaginated = sinon.stub().resolves({ reviewObjects: [], totalDocs: 0 })
await controller.getReviewHistoryByOrgShortNamePaginated(req, res, next)
const callArgs = repoStub.getReviewHistoryByOrgShortNamePaginated.getCall(0).args
diff --git a/test/unit-tests/user/userCreateTest.js b/test/unit-tests/user/userCreateTest.js
index 35e16b247..281539f94 100644
--- a/test/unit-tests/user/userCreateTest.js
+++ b/test/unit-tests/user/userCreateTest.js
@@ -107,10 +107,12 @@ describe('Testing the POST /org/:shortname/user endpoint in Org Controller', ()
sinon.stub(baseOrgRepo, 'getOrgUUID').resolves(true)
sinon.stub(argon2, 'hash').resolves('hashedPassword')
sinon.stub(BaseOrgRepository.prototype, 'findOneByShortName').resolves(fakeOrgMongooseDocument)
+ sinon.stub(BaseOrgRepository.prototype, 'addUserToOrg').resolves()
sinon.stub(baseUserRepo, 'findUsersByOrgShortname').resolves([fakeUserMongooseDocument])
sinon.stub(RegistryUserModel.prototype, 'save').resolves(fakeBaseUserSavedObject)
// stub the prototype since createUser in baseUserRepository creates a new internal instance of the legacy UserRepository
sinon.stub(UserRepository.prototype, 'updateByUserNameAndOrgUUID').resolves(fakeUserMongooseDocument)
+ sinon.stub(baseUserRepo, 'getUserUUID').resolves('mock-uuid')
await USER_CREATE_SINGLE(req, res, next)
expect(status.args[0][0]).to.equal(200)