From bc4e25c193db96f30760ab7e9ab211ca6f906f2f Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 16 Apr 2026 10:50:43 -0400 Subject: [PATCH 01/15] Added checks to the schema --- src/middleware/schemas/CNAOrg.json | 6 ++++-- src/middleware/schemas/SecretariatOrg.json | 6 ++++-- .../registry-org/registryOrgCRUDTest.js | 13 +++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json index c1188c8c4..5dcb3f3db 100644 --- a/src/middleware/schemas/CNAOrg.json +++ b/src/middleware/schemas/CNAOrg.json @@ -20,11 +20,13 @@ }, "hard_quota": { "type": "integer", - "minimum": 0 + "minimum": 0, + "maximum": 100000 }, "soft_quota": { "type": "integer", - "minimum": 0 + "minimum": 0, + "maximum": 100000 }, "charter_or_scope": { "$ref": "/BaseOrg#/definitions/uriType" diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json index 4e658b571..125ba92b1 100644 --- a/src/middleware/schemas/SecretariatOrg.json +++ b/src/middleware/schemas/SecretariatOrg.json @@ -20,11 +20,13 @@ }, "hard_quota": { "type": "integer", - "minimum": 0 + "minimum": 0, + "maximum": 100000 }, "soft_quota": { "type": "integer", - "minimum": 0 + "minimum": 0, + "maximum": 100000 } }, "required": ["hard_quota"] diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index e1e7f04d3..e70f29483 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -324,6 +324,19 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) + it('Fails to update a registry organization with an invalidly high quota', async () => { + await chai.request(app) + .put(`/api/registryOrg/${createdOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...createdOrg, + hard_quota: 1000000 + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) }) }) context('Testing DELETE /registryOrg endpoint', () => { From c3ffca835cd06834ce00e8770fdb3a18b7016ded Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 16 Apr 2026 11:17:48 -0400 Subject: [PATCH 02/15] page information will always be returned on registry endpoints --- src/repositories/baseOrgRepository.js | 2 +- test/integration-tests/registry-org/registryOrgCRUDTest.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index adc4b5f7f..e4d3db7f0 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -283,7 +283,7 @@ class BaseOrgRepository extends BaseRepository { } const data = { organizations: pg.itemsList } - if (pg.itemCount >= options.limit) { + if (!returnLegacyFormat || pg.itemCount >= options.limit) { data.totalCount = pg.itemCount data.itemsPerPage = pg.itemsPerPage data.pageCount = pg.pageCount diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index e1e7f04d3..f4e194f86 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -112,6 +112,12 @@ describe('Testing /registryOrg endpoints', () => { .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) + expect(res.body).to.have.property('totalCount') + expect(res.body).to.have.property('itemsPerPage') + expect(res.body).to.have.property('pageCount') + expect(res.body).to.have.property('currentPage') + expect(res.body).to.have.property('prevPage') + expect(res.body).to.have.property('nextPage') expect(res.body.organizations).to.be.an('array').that.is.not.empty }) }) From 9203f05707e585e4d150032bb4a47d5924818a69 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 16 Apr 2026 13:10:04 -0400 Subject: [PATCH 03/15] Updating to now save username and requester uuid --- src/controller/org.controller/index.js | 2 +- .../review-object.controller.js | 12 ++++-- src/model/reviewobject.js | 8 ++++ src/repositories/baseOrgRepository.js | 24 +++++++---- src/repositories/reviewObjectRepository.js | 22 ++++++++-- .../review-object.repository.test.js | 40 +++++++++++++++++++ 6 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index f7805c496..1c5e6f110 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, + // mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index 8bf0b5540..beb9ed6eb 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -95,7 +95,7 @@ async function approveReviewObject (req, res, next) { const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - const reviewObj = await reviewRepo.approveReviewOrgObject(UUID, { session }) + const reviewObj = await reviewRepo.approveReviewOrgObject(UUID, requestingUserUUID, req.ctx.user, { session }) if (!reviewObj) { await session.abortTransaction() return res.status(404).json({ message: `Review object not approved with UUID ${UUID}` }) @@ -166,7 +166,10 @@ async function createReviewObject (req, res, next) { await session.abortTransaction() return res.status(400).json({ message: 'Invalid body parameters', errors: bodyValidation.errors }) } - createdReviewObj = await repo.createReviewOrgObject(body, { session }) + const userRepo = req.ctx.repositories.getBaseUserRepository() + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + + createdReviewObj = await repo.createReviewOrgObject(body, requestingUserUUID, req.ctx.user, { session }) await session.commitTransaction() } catch (createErr) { await session.abortTransaction() @@ -233,7 +236,10 @@ async function rejectReviewObject (req, res, next) { return res.status(404).json({ message: `No pending review object found with UUID ${UUID}` }) } - value = await reviewRepo.rejectReviewOrgObject(UUID, { session }) + const userRepo = req.ctx.repositories.getBaseUserRepository() + const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) + + value = await reviewRepo.rejectReviewOrgObject(UUID, requestingUserUUID, req.ctx.user, { session }) await session.commitTransaction() } catch (rejectErr) { await session.abortTransaction() diff --git a/src/model/reviewobject.js b/src/model/reviewobject.js index 52bdea788..30a5bc905 100644 --- a/src/model/reviewobject.js +++ b/src/model/reviewobject.js @@ -6,6 +6,14 @@ const schema = { uuid: String, target_object_uuid: String, status: String, + requester: { + UUID: String, + username: String + }, + approver: { + UUID: String, + username: String + }, new_review_data: Object // This should be a object containing the new org data in the format of the base org model or one of its descriminators (e.g. CNAOrg, ADPOrg) } diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index adc4b5f7f..3a32402c0 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -397,6 +397,14 @@ class BaseOrgRepository extends BaseRepository { const legacyOrgRepo = new OrgRepository() const ReviewObjectRepository = require('./reviewObjectRepository') const reviewObjectRepo = new ReviewObjectRepository() + + let requestingUsername = null + if (requestingUserUUID) { + const BaseUserRepository = require('./baseUserRepository') + const userRepo = new BaseUserRepository() + const requestingUser = await userRepo.findUserByUUID(requestingUserUUID, options) + requestingUsername = requestingUser ? requestingUser.username : null + } // generate a shared uuid const sharedUUID = uuid.v4() @@ -438,7 +446,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await SecretariatObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) } } else if (registryObjectRaw.authority.includes('CNA')) { // A special case, we should make sure we have the default quota if it is not set @@ -452,7 +460,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await CNAObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) } } else if (registryObjectRaw.authority.includes('ADP')) { registryObjectRaw.hard_quota = 0 @@ -460,7 +468,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await adpObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) } } else if (registryObjectRaw.authority.includes('BULK_DOWNLOAD')) { registryObjectRaw.hard_quota = 0 @@ -468,7 +476,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await bulkDownloadObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) } } else { // Throw an Error instance so callers can catch and handle it properly @@ -844,6 +852,9 @@ class BaseOrgRepository extends BaseRepository { // Dealing with roles requires a bit of extra control. const originalRoles = registryOrg.authority + const requestingUser = requestingUserUUID ? await userRepo.findUserByUUID(requestingUserUUID, options) : null + const requestingUsername = requestingUser ? requestingUser.username : null + const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), legacyObjectRaw, skipNulls)) @@ -862,19 +873,18 @@ class BaseOrgRepository extends BaseRepository { if (reviewObject) { await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options) } else { - await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, options) + await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, requestingUserUUID, requestingUsername, options) } } else { // If no changes between org and new object but a review object exists, remove it since joint approval is no longer needed if (reviewObject) { - await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, options) + await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUserUUID, requestingUsername, options) } } updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, jointApprovalFieldsRegistry), skipNulls)) updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, jointApprovalFieldsLegacy), skipNulls)) } // handle conversation - const requestingUser = await userRepo.findUserByUUID(requestingUserUUID, options) const conversationArray = [] if (conversation) { conversationArray.push(await conversationRepo.createConversation(registryOrg.UUID, conversation, requestingUser, isSecretariat, options)) diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 8ebcda1f6..7b437ef97 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -137,12 +137,16 @@ class ReviewObjectRepository extends BaseRepository { return reviewObject || null } - async createReviewOrgObject (orgBody, options = {}) { + async createReviewOrgObject (orgBody, requesterUUID, requesterUsername, options = {}) { console.log('Creating review object for organization:', orgBody.UUID) const reviewObjectRaw = { uuid: uuid.v4(), target_object_uuid: orgBody.UUID, status: 'pending', + requester: { + UUID: requesterUUID, + username: requesterUsername + }, new_review_data: orgBody || {} } @@ -164,7 +168,7 @@ class ReviewObjectRepository extends BaseRepository { return result.toObject() } - async approveReviewOrgObject (UUID, options = {}) { + async approveReviewOrgObject (UUID, approverUUID, approverUsername, options = {}) { console.log('Approving review object with UUID:', UUID) const reviewObject = await this.findOneByUUID(UUID, options) if (!reviewObject) { @@ -172,6 +176,12 @@ class ReviewObjectRepository extends BaseRepository { } reviewObject.status = 'approved' + if (approverUUID && approverUsername) { + reviewObject.approver = { + UUID: approverUUID, + username: approverUsername + } + } await reviewObject.save(options) return reviewObject.toObject() @@ -226,7 +236,7 @@ class ReviewObjectRepository extends BaseRepository { return data } - async rejectReviewOrgObject (UUID, options = {}) { + async rejectReviewOrgObject (UUID, approverUUID, approverUsername, options = {}) { console.log('Rejecting review object with UUID:', UUID) const reviewObject = await this.findOneByUUID(UUID, options) if (!reviewObject) { @@ -234,6 +244,12 @@ class ReviewObjectRepository extends BaseRepository { } reviewObject.status = 'rejected' + if (approverUUID && approverUsername) { + reviewObject.approver = { + UUID: approverUUID, + username: approverUsername + } + } await reviewObject.save(options) return reviewObject.toObject() diff --git a/test/integration-tests/review-object/review-object.repository.test.js b/test/integration-tests/review-object/review-object.repository.test.js index 197d3640d..dc55f8841 100644 --- a/test/integration-tests/review-object/review-object.repository.test.js +++ b/test/integration-tests/review-object/review-object.repository.test.js @@ -39,6 +39,46 @@ describe('ReviewObjectRepository Tests', function () { }) }) + describe('create/approve/reject ReviewObject metadata', function () { + const orgBody = { UUID: 'some-org-uuid', short_name: 'some-org' } + const requesterUUID = 'req-uuid-123' + const requesterUsername = 'req-user' + const approverUUID = 'app-uuid-456' + const approverUsername = 'app-user' + + let createdUUID + + it('should store requester UUID and username on create', async () => { + const result = await repository.createReviewOrgObject(orgBody, requesterUUID, requesterUsername) + expect(result).to.exist + expect(result.requester).to.exist + expect(result.requester.UUID).to.equal(requesterUUID) + expect(result.requester.username).to.equal(requesterUsername) + createdUUID = result.uuid + }) + + it('should store approver UUID and username on approve', async () => { + const result = await repository.approveReviewOrgObject(createdUUID, approverUUID, approverUsername) + expect(result).to.exist + expect(result.approver).to.exist + expect(result.approver.UUID).to.equal(approverUUID) + expect(result.approver.username).to.equal(approverUsername) + expect(result.status).to.equal('approved') + }) + + it('should store approver UUID and username on reject', async () => { + const rejectOrgBody = { UUID: 'another-org-uuid', short_name: 'another-org' } + const rejectCreated = await repository.createReviewOrgObject(rejectOrgBody, requesterUUID, requesterUsername) + const result = await repository.rejectReviewOrgObject(rejectCreated.uuid, approverUUID, approverUsername) + + expect(result).to.exist + expect(result.approver).to.exist + expect(result.approver.UUID).to.equal(approverUUID) + expect(result.approver.username).to.equal(approverUsername) + expect(result.status).to.equal('rejected') + }) + }) + describe('findOneByUUIDWithConversation', function () { it('should return the pending review object when pending=true', async () => { const result = await repository.findOneByUUIDWithConversation(testUUID, true, true) From 259071673e03f9a70308e56d27babd6533fcb172 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 16 Apr 2026 13:45:29 -0400 Subject: [PATCH 04/15] Added parameters to missing spot --- .../registry-org.controller/registry-org.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index c5a42a065..000c67061 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -315,7 +315,7 @@ async function updateOrg (req, res, next) { // Compare and set status accordingly if (_.isEqual(cleanPending, cleanIncoming)) { - await reviewRepo.approveReviewOrgObject(pendingReview.uuid, { session }) + await reviewRepo.approveReviewOrgObject(pendingReview.uuid, requestingUser.UUID, req.ctx.user, { session }) } else { await reviewRepo.rejectReviewOrgObject(pendingReview.uuid, { session }) } From da11cb7dc7b7f459b673a8230252e68cc86286e6 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 16 Apr 2026 13:54:34 -0400 Subject: [PATCH 05/15] Fixes --- src/controller/org.controller/index.js | 2 +- src/model/reviewobject.js | 4 ++++ src/repositories/reviewObjectRepository.js | 10 +++++----- .../review-object/review-object.repository.test.js | 14 ++++++++------ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 1c5e6f110..f7805c496 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - // mw.onlySecretariat, + mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG diff --git a/src/model/reviewobject.js b/src/model/reviewobject.js index 30a5bc905..7b6991395 100644 --- a/src/model/reviewobject.js +++ b/src/model/reviewobject.js @@ -14,6 +14,10 @@ const schema = { UUID: String, username: String }, + rejector: { + UUID: String, + username: String + }, new_review_data: Object // This should be a object containing the new org data in the format of the base org model or one of its descriminators (e.g. CNAOrg, ADPOrg) } diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 7b437ef97..10e80aaaa 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -236,7 +236,7 @@ class ReviewObjectRepository extends BaseRepository { return data } - async rejectReviewOrgObject (UUID, approverUUID, approverUsername, options = {}) { + async rejectReviewOrgObject (UUID, rejectorUUID, rejectorUsername, options = {}) { console.log('Rejecting review object with UUID:', UUID) const reviewObject = await this.findOneByUUID(UUID, options) if (!reviewObject) { @@ -244,10 +244,10 @@ class ReviewObjectRepository extends BaseRepository { } reviewObject.status = 'rejected' - if (approverUUID && approverUsername) { - reviewObject.approver = { - UUID: approverUUID, - username: approverUsername + if (rejectorUUID && rejectorUsername) { + reviewObject.rejector = { + UUID: rejectorUUID, + username: rejectorUsername } } await reviewObject.save(options) diff --git a/test/integration-tests/review-object/review-object.repository.test.js b/test/integration-tests/review-object/review-object.repository.test.js index dc55f8841..6a1549a7f 100644 --- a/test/integration-tests/review-object/review-object.repository.test.js +++ b/test/integration-tests/review-object/review-object.repository.test.js @@ -66,15 +66,17 @@ describe('ReviewObjectRepository Tests', function () { expect(result.status).to.equal('approved') }) - it('should store approver UUID and username on reject', async () => { + it('should store rejector UUID and username on reject', async () => { const rejectOrgBody = { UUID: 'another-org-uuid', short_name: 'another-org' } const rejectCreated = await repository.createReviewOrgObject(rejectOrgBody, requesterUUID, requesterUsername) - const result = await repository.rejectReviewOrgObject(rejectCreated.uuid, approverUUID, approverUsername) - + const rejectorUUID = 'rej-uuid-789' + const rejectorUsername = 'rej-user' + const result = await repository.rejectReviewOrgObject(rejectCreated.uuid, rejectorUUID, rejectorUsername) expect(result).to.exist - expect(result.approver).to.exist - expect(result.approver.UUID).to.equal(approverUUID) - expect(result.approver.username).to.equal(approverUsername) + expect(result.rejector).to.exist + expect(result.rejector.UUID).to.equal(rejectorUUID) + expect(result.rejector.username).to.equal(rejectorUsername) + expect(result.approver).to.not.exist expect(result.status).to.equal('rejected') }) }) From 9674dd0f26dcaef75c6bffb8f9213a8ded0bef4b Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 16 Apr 2026 14:01:39 -0400 Subject: [PATCH 06/15] added a missing call to rejecT --- .../registry-org.controller/registry-org.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 000c67061..2eebb162f 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -317,7 +317,7 @@ async function updateOrg (req, res, next) { if (_.isEqual(cleanPending, cleanIncoming)) { await reviewRepo.approveReviewOrgObject(pendingReview.uuid, requestingUser.UUID, req.ctx.user, { session }) } else { - await reviewRepo.rejectReviewOrgObject(pendingReview.uuid, { session }) + await reviewRepo.rejectReviewOrgObject(pendingReview.uuid, requestingUser.UUID, req.ctx.user, { session }) } } } From d2058256b8fc2b983ae47e3fbd45c3672df7508a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 17 Apr 2026 11:39:13 -0400 Subject: [PATCH 07/15] Fixes the issues reported --- src/controller/org.controller/index.js | 2 +- .../registry-org.controller.js | 3 + src/repositories/baseOrgRepository.js | 8 +- src/repositories/baseUserRepository.js | 51 ++++++------- src/repositories/conversationRepository.js | 7 +- src/repositories/reviewObjectRepository.js | 9 ++- src/scripts/migrate.js | 1 + .../registry-org/registryOrgCRUDTest.js | 73 +++++++++++++++++++ test/integration-tests/user/createUserTest.js | 28 +++++++ test/integration-tests/user/updateUserTest.js | 57 +++++++++++++++ 10 files changed, 205 insertions(+), 34 deletions(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index f7805c496..1c5e6f110 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, + // mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 2eebb162f..14e17bafb 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -653,6 +653,9 @@ async function editConversationForOrg (req, res, next) { // Make the edit returnValue = await conversationRepo.editConversation(conversation.UUID, incomingParameters, userUUID, { session }) + if (!isSecretariat && returnValue) { + delete returnValue.author_id + } await session.commitTransaction() } catch (error) { await session.abortTransaction() diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 3a32402c0..11a0eb41d 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -857,8 +857,8 @@ class BaseOrgRepository extends BaseRepository { const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { - updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), legacyObjectRaw, skipNulls)) - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), protectedFields), registryObjectRaw, skipNulls)) + updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), _.omit(legacyObjectRaw, protectedFields), skipNulls)) + updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), protectedFields), _.omit(registryObjectRaw, protectedFields), skipNulls)) } else { // Check if there are actual changes to joint approval fields compared to current org object (not current review) // Only compare fields that are actually in the incoming data @@ -881,8 +881,8 @@ class BaseOrgRepository extends BaseRepository { await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUserUUID, requestingUsername, options) } } - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, jointApprovalFieldsRegistry), skipNulls)) - updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, jointApprovalFieldsLegacy), skipNulls)) + updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...protectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) + updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, [...protectedFields, ...jointApprovalFieldsLegacy]), skipNulls)) } // handle conversation const conversationArray = [] diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 98a0f5289..9638e256b 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -98,23 +98,15 @@ class BaseUserRepository extends BaseRepository { * @returns {Promise} True if the organization has the user, false otherwise. */ async orgHasUser (orgShortName, username, options = {}, isRegistryObject = true) { - // 1. Find all users with this username - const users = await BaseUser.find({ username }, null, options) - if (!users || users.length === 0) { - return false - } - - // 2. Get all their UUIDs - const userUUIDs = users.map(u => u.UUID) - - // 3. Find the org + // 1. Find the org const org = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options) if (!org || !Array.isArray(org.users)) { return false } - // 4. Check if any UUID is present in org.users - return userUUIDs.some(uuid => org.users.includes(uuid)) + // 2. Check if a user with this username exists in the org + const user = await BaseUser.findOne({ username, UUID: { $in: org.users } }, null, options) + return !!user } /** @@ -129,16 +121,12 @@ class BaseUserRepository extends BaseRepository { */ async findOneByUsernameAndOrgShortname (username, orgShortName, options = {}, isRegistryObject = true) { const legacyUserRepo = new UserRepository() - const users = await BaseUser.find({ username: username }, null, options) - if (!users || users.length === 0) { - return null - } const org = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options) if (!org || !Array.isArray(org.users)) { return null } - // users = users.map(user => user.toObject()) - const user = users.find(user => org.users.includes(user.UUID)) + + const user = await BaseUser.findOne({ username: username, UUID: { $in: org.users } }, null, options) if (!isRegistryObject && user) { return await legacyUserRepo.findOneByUUID(user.UUID) || null @@ -158,16 +146,13 @@ class BaseUserRepository extends BaseRepository { */ async findOneByUserNameAndOrgUUID (username, orgUUID, options = {}, isRegistryObject = true) { const legacyUserRepo = new UserRepository() - const users = await BaseUser.find({ username: username }, null, options) - if (!users || users.length === 0) { - return null - } const org = await BaseOrgModel.findOne({ UUID: orgUUID }, null, options) if (!org || !Array.isArray(org.users)) { return null } - const user = users.find(user => org.users.includes(user.UUID)) + const user = await BaseUser.findOne({ username: username, UUID: { $in: org.users } }, null, options) + if (!isRegistryObject && user) { return await legacyUserRepo.findOneByUUID(user.UUID) || null } @@ -372,10 +357,15 @@ class BaseUserRepository extends BaseRepository { registryObjectRaw.secret = secret legacyObjectRaw.secret = secret + // Allow user to provide initial status, default to active + let isConsideredInactive = false + if (isRegistryObject && incomingUser.status === 'inactive') isConsideredInactive = true + if (!isRegistryObject && (incomingUser.active === false || String(incomingUser.active).toLowerCase() === 'false')) isConsideredInactive = true + // Registry Only Fields - registryObjectRaw.status = 'active' + registryObjectRaw.status = isConsideredInactive ? 'inactive' : 'active' // Legacy Specific fields - legacyObjectRaw.active = true + legacyObjectRaw.active = !isConsideredInactive // Get UUID of org, that is having the user added to it. const existingOrg = await baseOrgRepository.findOneByShortName(orgShortName) @@ -536,8 +526,15 @@ class BaseUserRepository extends BaseRepository { const protectedFieldsRegistry = ['_id', 'UUID', '__v', 'secret', 'created', 'last_updated'] const protectedFieldsLegacy = ['_id', 'UUID', '__v', 'secret', 'time', 'org_UUID'] - const updatedRegistryUser = registryUser.overwrite(_.mergeWith(_.pick(registryUser.toObject(), protectedFieldsRegistry), registryObjectRaw, skipNulls)) - const updatedLegacyUser = legacyUser.overwrite(_.mergeWith(_.pick(legacyUser.toObject(), protectedFieldsLegacy), legacyObjectRaw, skipNulls)) + const updatedRegistryUser = registryUser.overwrite(_.mergeWith(_.pick(registryUser.toObject(), protectedFieldsRegistry), _.omit(registryObjectRaw, protectedFieldsRegistry), skipNulls)) + const updatedLegacyUser = legacyUser.overwrite(_.mergeWith(_.pick(legacyUser.toObject(), protectedFieldsLegacy), _.omit(legacyObjectRaw, protectedFieldsLegacy), skipNulls)) + + if (updatedRegistryUser.status !== 'active') { + updatedRegistryUser.status = 'inactive' + updatedLegacyUser.active = false + } else { + updatedLegacyUser.active = true + } try { if (incomingUser.org_short_name) { diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 39e055563..3d15dc737 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -43,7 +43,12 @@ class ConversationRepository extends BaseRepository { UUID: 1 } }) - return conversations.map(convo => convo.toObject()).filter(conv => isSecretariat || conv.visibility === 'public') + return conversations.map(convo => convo.toObject()).filter(conv => isSecretariat || conv.visibility === 'public').map(conv => { + if (!isSecretariat) { + delete conv.author_id + } + return conv + }) } async findByTargetUUIDAndIndex (targetUUID, index, options = {}) { diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 10e80aaaa..faef04d58 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -220,10 +220,17 @@ class ReviewObjectRepository extends BaseRepository { const conversationRepository = new ConversationRepository() for (const review of data.reviewObjects) { - const conversations = await conversationRepository.getAllByTargetUUID(review.uuid, options) + let conversations = await conversationRepository.getAllByTargetUUID(review.uuid, isSecretariat, options) // Filter conversations based on user role if (conversations && conversations.length) { + // If non-secretariat, remove author_id + if (!isSecretariat) { + conversations = conversations.map(c => { + delete c.author_id + return c + }) + } review.conversation = conversations.filter(conv => isSecretariat || conv.visibility === 'public' ) diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 7fd7fa314..7f4bd5879 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -254,6 +254,7 @@ async function userHelper (db) { username: doc.username, secret: doc.secret, name: doc.name, + status: doc.active ? 'active' : 'inactive', created: doc.time.created, created_by: 'system', last_updated: doc.time.modified, diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index e1e7f04d3..bb28c09fa 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -129,6 +129,58 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body).to.have.property('partner_country', createdOrg.partner_country) }) }) + it('Strips author_id from conversations for non-secretariats', async () => { + // 1. Get win_5 org UUID + let win5UUID + await chai.request(app) + .get('/api/registryOrg/win_5') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + win5UUID = res.body.UUID + }) + + // 2. Post a conversation as Secretariat to win_5 + const conversationBody = { + visibility: 'public', + body: 'This is a test conversation for author_id stripping' + } + let postedConvoUUID + await chai.request(app) + .post(`/api/conversation/target/${win5UUID}`) + .set(secretariatHeaders) + .send(conversationBody) + .then((res) => { + expect(res).to.have.status(200) + postedConvoUUID = res.body.UUID + }) + + // 3. GET win_5 as Secretariat, verify author_id is present + await chai.request(app) + .get('/api/registryOrg/win_5') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.conversation).to.be.an('array') + const convo = res.body.conversation.find(c => c.UUID === postedConvoUUID) || res.body.conversation[res.body.conversation.length - 1] + expect(convo).to.have.property('author_id') + expect(convo).to.have.property('body', 'This is a test conversation for author_id stripping') + }) + + // 4. GET win_5 as Non-Secretariat (author of win_5), verify author_id is stripped + await chai.request(app) + .get('/api/registry/org/win_5') + .set(constants.nonSecretariatUserHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.conversation).to.be.an('array') + // Remember non-secretariats don't see the UUID field returned in conversation so we can't search by UUID! + // We just check the latest one or search by body. + const convo = res.body.conversation.find(c => c.body === 'This is a test conversation for author_id stripping') + expect(convo).to.not.be.undefined + expect(convo).to.not.have.property('author_id') + }) + }) }) context('Negative Tests', () => { it('Fails to get a registry organization that does not exist', async () => { @@ -269,6 +321,27 @@ describe('Testing /registryOrg endpoints', () => { .delete(`/api/registryOrg/${subOrg.short_name}`) .set(secretariatHeaders) }) + it('Ignores protected fields such as users and admins during an update', async () => { + const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + + await chai.request(app) + .put(`/api/registryOrg/${createdOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...createdOrg, + users: maliciousUsers, + admins: maliciousAdmins + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + // Ensure the response body.updated does not contain the malicious data + expect(res.body.updated.users || []).to.not.include(maliciousUsers[0]) + expect(res.body.updated.admins || []).to.not.include(maliciousAdmins[0]) + }) + }) }) context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { diff --git a/test/integration-tests/user/createUserTest.js b/test/integration-tests/user/createUserTest.js index a2050a67e..54cb887c3 100644 --- a/test/integration-tests/user/createUserTest.js +++ b/test/integration-tests/user/createUserTest.js @@ -115,4 +115,32 @@ describe('Testing create user endpoint', () => { done() }) }) + it('Should default new user status to active when no active/status field is provided', (done) => { + const noActiveBody = { ...body, username: 'noActiveUser' } + delete noActiveBody.active + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send(noActiveBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + expect(res.body.created.active).to.be.true + done() + }) + }) + it('Should create an inactive user if explicitly requested', (done) => { + const inactiveBody = { ...registryBody, username: 'inactiveUser', status: 'inactive' } + delete inactiveBody.active + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send(inactiveBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + expect(res.body.created.status).to.equal('inactive') + done() + }) + }) }) diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 610438687..da96d74d8 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -37,6 +37,63 @@ describe('Testing Edit user endpoint', () => { expect(res).to.have.status(200) }) }) + it('Ignores protected fields such as UUID during an update', async () => { + let user + await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + + const maliciousUUID = 'd41d8cd9-8f00-3204-a980-0998ecf8427e' + const originalUUID = user.UUID + + await chai.request(app) + .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') + .set(constants.headers) + .send({ + ...user, + UUID: maliciousUUID + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body.updated).to.haveOwnProperty('UUID') + expect(res.body.updated.UUID).to.not.equal(maliciousUUID) + expect(res.body.updated.UUID).to.equal(originalUUID) + }) + }) + it('Should clamp status to inactive if a status other than active is provided', async () => { + let user + await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + + await chai.request(app) + .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') + .set(constants.headers) + .send({ + ...user, + status: 'inactive' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body.updated).to.haveOwnProperty('status') + expect(res.body.updated.status).to.equal('inactive') + }) + // reset jasmine to active + await chai.request(app) + .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') + .set(constants.headers) + .send({ + ...user, + status: 'active' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body.updated).to.haveOwnProperty('status') + expect(res.body.updated.status).to.equal('active') + }) + }) it('Should return an error when admin is required', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?new_username=NewUsername') From 196f90a2e83a7f5effe4554fd93e9ccd576e9d5d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 17 Apr 2026 11:41:11 -0400 Subject: [PATCH 08/15] Verison number updates --- api-docs/openapi.json | 2 +- package.json | 2 +- src/swagger.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index d96597b1c..01ab0fad9 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.7.4", + "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": { diff --git a/package.json b/package.json index 16da323ab..8e0ec46bb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cve-services", "author": "Automation Working Group", - "version": "2.7.4", + "version": "2.7.5", "license": "(CC0)", "devDependencies": { "@faker-js/faker": "^7.6.0", diff --git a/src/swagger.js b/src/swagger.js index 4fc21c77e..5c39c6f01 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -22,7 +22,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re /* eslint-disable no-multi-str */ const doc = { info: { - version: '2.7.4', + 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 \ From 33b6c9690a3f4e0ee62360a6c3880e5af55c6042 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 17 Apr 2026 12:01:02 -0400 Subject: [PATCH 09/15] more fixes --- .../registry-org.controller.js | 4 ++-- .../review-object.controller.js | 12 +++-------- src/model/reviewobject.js | 3 --- src/repositories/baseOrgRepository.js | 13 ++++++------ src/repositories/reviewObjectRepository.js | 13 +++++------- .../review-object.repository.test.js | 20 +++++++------------ 6 files changed, 23 insertions(+), 42 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 14e17bafb..341b27667 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -315,9 +315,9 @@ async function updateOrg (req, res, next) { // Compare and set status accordingly if (_.isEqual(cleanPending, cleanIncoming)) { - await reviewRepo.approveReviewOrgObject(pendingReview.uuid, requestingUser.UUID, req.ctx.user, { session }) + await reviewRepo.approveReviewOrgObject(pendingReview.uuid, req.ctx.user, { session }) } else { - await reviewRepo.rejectReviewOrgObject(pendingReview.uuid, requestingUser.UUID, req.ctx.user, { session }) + await reviewRepo.rejectReviewOrgObject(pendingReview.uuid, req.ctx.user, { session }) } } } diff --git a/src/controller/review-object.controller/review-object.controller.js b/src/controller/review-object.controller/review-object.controller.js index beb9ed6eb..7b4556d29 100644 --- a/src/controller/review-object.controller/review-object.controller.js +++ b/src/controller/review-object.controller/review-object.controller.js @@ -95,7 +95,7 @@ async function approveReviewObject (req, res, next) { const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - const reviewObj = await reviewRepo.approveReviewOrgObject(UUID, requestingUserUUID, req.ctx.user, { session }) + const reviewObj = await reviewRepo.approveReviewOrgObject(UUID, req.ctx.user, { session }) if (!reviewObj) { await session.abortTransaction() return res.status(404).json({ message: `Review object not approved with UUID ${UUID}` }) @@ -166,10 +166,7 @@ async function createReviewObject (req, res, next) { await session.abortTransaction() return res.status(400).json({ message: 'Invalid body parameters', errors: bodyValidation.errors }) } - const userRepo = req.ctx.repositories.getBaseUserRepository() - const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - - createdReviewObj = await repo.createReviewOrgObject(body, requestingUserUUID, req.ctx.user, { session }) + createdReviewObj = await repo.createReviewOrgObject(body, req.ctx.user, { session }) await session.commitTransaction() } catch (createErr) { await session.abortTransaction() @@ -236,10 +233,7 @@ async function rejectReviewObject (req, res, next) { return res.status(404).json({ message: `No pending review object found with UUID ${UUID}` }) } - const userRepo = req.ctx.repositories.getBaseUserRepository() - const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) - - value = await reviewRepo.rejectReviewOrgObject(UUID, requestingUserUUID, req.ctx.user, { session }) + value = await reviewRepo.rejectReviewOrgObject(UUID, req.ctx.user, { session }) await session.commitTransaction() } catch (rejectErr) { await session.abortTransaction() diff --git a/src/model/reviewobject.js b/src/model/reviewobject.js index 7b6991395..ca29bc97a 100644 --- a/src/model/reviewobject.js +++ b/src/model/reviewobject.js @@ -7,15 +7,12 @@ const schema = { target_object_uuid: String, status: String, requester: { - UUID: String, username: String }, approver: { - UUID: String, username: String }, rejector: { - UUID: String, username: String }, new_review_data: Object // This should be a object containing the new org data in the format of the base org model or one of its descriminators (e.g. CNAOrg, ADPOrg) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 11a0eb41d..2147ea31a 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -397,7 +397,6 @@ class BaseOrgRepository extends BaseRepository { const legacyOrgRepo = new OrgRepository() const ReviewObjectRepository = require('./reviewObjectRepository') const reviewObjectRepo = new ReviewObjectRepository() - let requestingUsername = null if (requestingUserUUID) { const BaseUserRepository = require('./baseUserRepository') @@ -446,7 +445,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await SecretariatObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) } } else if (registryObjectRaw.authority.includes('CNA')) { // A special case, we should make sure we have the default quota if it is not set @@ -460,7 +459,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await CNAObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) } } else if (registryObjectRaw.authority.includes('ADP')) { registryObjectRaw.hard_quota = 0 @@ -468,7 +467,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await adpObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) } } else if (registryObjectRaw.authority.includes('BULK_DOWNLOAD')) { registryObjectRaw.hard_quota = 0 @@ -476,7 +475,7 @@ class BaseOrgRepository extends BaseRepository { if (isSecretariat) { registryObject = await bulkDownloadObjectToSave.save(options) } else { - await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUserUUID, requestingUsername, options) + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) } } else { // Throw an Error instance so callers can catch and handle it properly @@ -873,12 +872,12 @@ class BaseOrgRepository extends BaseRepository { if (reviewObject) { await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options) } else { - await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, requestingUserUUID, requestingUsername, options) + await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, requestingUsername, options) } } else { // If no changes between org and new object but a review object exists, remove it since joint approval is no longer needed if (reviewObject) { - await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUserUUID, requestingUsername, options) + await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUsername, options) } } updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...protectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index faef04d58..0062da212 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -137,14 +137,13 @@ class ReviewObjectRepository extends BaseRepository { return reviewObject || null } - async createReviewOrgObject (orgBody, requesterUUID, requesterUsername, options = {}) { + async createReviewOrgObject (orgBody, requesterUsername, options = {}) { console.log('Creating review object for organization:', orgBody.UUID) const reviewObjectRaw = { uuid: uuid.v4(), target_object_uuid: orgBody.UUID, status: 'pending', requester: { - UUID: requesterUUID, username: requesterUsername }, new_review_data: orgBody || {} @@ -168,7 +167,7 @@ class ReviewObjectRepository extends BaseRepository { return result.toObject() } - async approveReviewOrgObject (UUID, approverUUID, approverUsername, options = {}) { + async approveReviewOrgObject (UUID, approverUsername, options = {}) { console.log('Approving review object with UUID:', UUID) const reviewObject = await this.findOneByUUID(UUID, options) if (!reviewObject) { @@ -176,9 +175,8 @@ class ReviewObjectRepository extends BaseRepository { } reviewObject.status = 'approved' - if (approverUUID && approverUsername) { + if (approverUsername) { reviewObject.approver = { - UUID: approverUUID, username: approverUsername } } @@ -243,7 +241,7 @@ class ReviewObjectRepository extends BaseRepository { return data } - async rejectReviewOrgObject (UUID, rejectorUUID, rejectorUsername, options = {}) { + async rejectReviewOrgObject (UUID, rejectorUsername, options = {}) { console.log('Rejecting review object with UUID:', UUID) const reviewObject = await this.findOneByUUID(UUID, options) if (!reviewObject) { @@ -251,9 +249,8 @@ class ReviewObjectRepository extends BaseRepository { } reviewObject.status = 'rejected' - if (rejectorUUID && rejectorUsername) { + if (rejectorUsername) { reviewObject.rejector = { - UUID: rejectorUUID, username: rejectorUsername } } diff --git a/test/integration-tests/review-object/review-object.repository.test.js b/test/integration-tests/review-object/review-object.repository.test.js index 6a1549a7f..8e2d15820 100644 --- a/test/integration-tests/review-object/review-object.repository.test.js +++ b/test/integration-tests/review-object/review-object.repository.test.js @@ -41,40 +41,34 @@ describe('ReviewObjectRepository Tests', function () { describe('create/approve/reject ReviewObject metadata', function () { const orgBody = { UUID: 'some-org-uuid', short_name: 'some-org' } - const requesterUUID = 'req-uuid-123' const requesterUsername = 'req-user' - const approverUUID = 'app-uuid-456' const approverUsername = 'app-user' let createdUUID - it('should store requester UUID and username on create', async () => { - const result = await repository.createReviewOrgObject(orgBody, requesterUUID, requesterUsername) + it('should store requester username on create', async () => { + const result = await repository.createReviewOrgObject(orgBody, requesterUsername) expect(result).to.exist expect(result.requester).to.exist - expect(result.requester.UUID).to.equal(requesterUUID) expect(result.requester.username).to.equal(requesterUsername) createdUUID = result.uuid }) - it('should store approver UUID and username on approve', async () => { - const result = await repository.approveReviewOrgObject(createdUUID, approverUUID, approverUsername) + it('should store approver username on approve', async () => { + const result = await repository.approveReviewOrgObject(createdUUID, approverUsername) expect(result).to.exist expect(result.approver).to.exist - expect(result.approver.UUID).to.equal(approverUUID) expect(result.approver.username).to.equal(approverUsername) expect(result.status).to.equal('approved') }) - it('should store rejector UUID and username on reject', async () => { + it('should store rejector username on reject', async () => { const rejectOrgBody = { UUID: 'another-org-uuid', short_name: 'another-org' } - const rejectCreated = await repository.createReviewOrgObject(rejectOrgBody, requesterUUID, requesterUsername) - const rejectorUUID = 'rej-uuid-789' + const rejectCreated = await repository.createReviewOrgObject(rejectOrgBody, requesterUsername) const rejectorUsername = 'rej-user' - const result = await repository.rejectReviewOrgObject(rejectCreated.uuid, rejectorUUID, rejectorUsername) + const result = await repository.rejectReviewOrgObject(rejectCreated.uuid, rejectorUsername) expect(result).to.exist expect(result.rejector).to.exist - expect(result.rejector.UUID).to.equal(rejectorUUID) expect(result.rejector.username).to.equal(rejectorUsername) expect(result.approver).to.not.exist expect(result.status).to.equal('rejected') From 926f576d0b137705bbdde0ea04ee571f01abddbb Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 17 Apr 2026 12:15:53 -0400 Subject: [PATCH 10/15] Fixing unit tests --- src/repositories/baseUserRepository.js | 2 +- test/unit-tests/org/orgCreateTest.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 9638e256b..6d1328d04 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -747,7 +747,7 @@ class BaseUserRepository extends BaseRepository { if (item.users && item.users.length > 0) { const populatedUsers = await Promise.all( item.users.map(async (uuid) => { - const user = await this.findOneByUUID(uuid) + const user = await this.findUserByUUID(uuid) return user ? user.toObject() : uuid // Return the user object if found, otherwise return the UUID }) ) diff --git a/test/unit-tests/org/orgCreateTest.js b/test/unit-tests/org/orgCreateTest.js index 9b9485ffd..70a83f0bf 100644 --- a/test/unit-tests/org/orgCreateTest.js +++ b/test/unit-tests/org/orgCreateTest.js @@ -136,6 +136,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { getBaseOrgRepository = sinon.stub().returns(baseOrgRepo) baseUserRepo = new BaseUserRepository() getBaseUserRepository = sinon.stub().returns(baseUserRepo) + sinon.stub(BaseUserRepository.prototype, 'findUserByUUID').resolves({ username: 'test_user' }) }) // Restore all stubs after each test From 520367c20bcf88f7b30d37d5f6266e0541cf36c9 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 17 Apr 2026 12:27:19 -0400 Subject: [PATCH 11/15] should no longer null out --- src/repositories/baseUserRepository.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 6d1328d04..be6164758 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -648,8 +648,8 @@ class BaseUserRepository extends BaseRepository { suffix: legacyUser.name?.suffix }, status: 'active', - created: legacyUser?.time?.created ?? null, - last_updated: legacyUser?.time?.modified ?? null + created: legacyUser?.time?.created, + last_updated: legacyUser?.time?.modified } } @@ -675,8 +675,8 @@ class BaseUserRepository extends BaseRepository { secret: registryUser.secret, active: registryUser.status === 'active', time: { - created: registryUser?.created ?? null, - modified: registryUser?.last_updated ?? null + created: registryUser?.created, + modified: registryUser?.last_updated } } } From 2c8ec9081b6942ebf39a4ee3c8ec4ac9d176b7de Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 17 Apr 2026 15:33:32 -0400 Subject: [PATCH 12/15] remove editor id --- .../registry-org.controller/registry-org.controller.js | 2 +- src/model/conversation.js | 3 +-- src/repositories/conversationRepository.js | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 341b27667..64da4d038 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -652,7 +652,7 @@ async function editConversationForOrg (req, res, next) { } // Make the edit - returnValue = await conversationRepo.editConversation(conversation.UUID, incomingParameters, userUUID, { session }) + returnValue = await conversationRepo.editConversation(conversation.UUID, incomingParameters, { session }) if (!isSecretariat && returnValue) { delete returnValue.author_id } diff --git a/src/model/conversation.js b/src/model/conversation.js index fe304bf8f..a88e92250 100644 --- a/src/model/conversation.js +++ b/src/model/conversation.js @@ -13,8 +13,7 @@ const schema = { visibility: String, body: String, posted_at: Date, - edited_at: Date, - editor_id: String + edited_at: Date } const ConversationSchema = new mongoose.Schema(schema, { collection: 'Conversation', timestamps: { createdAt: 'posted_at', updatedAt: 'last_updated' } }) diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 3d15dc737..02a61d807 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -79,7 +79,6 @@ class ConversationRepository extends BaseRepository { author_id: user.UUID, author_name: getUserFullName(user), author_role: isSecretariat ? 'Secretariat' : 'Partner', - editor_id: null, edited_at: null, visibility: !isSecretariat ? 'public' : (['public', 'private'].includes(body.visibility?.toLowerCase()) ? body.visibility.toLowerCase() : 'private'), body: body.body @@ -89,7 +88,7 @@ class ConversationRepository extends BaseRepository { return result.toObject() } - async editConversation (UUID, incomingParameters, userUUID, options = {}) { + async editConversation (UUID, incomingParameters, options = {}) { const conversation = await this.findOneByUUID(UUID, options) if (incomingParameters?.body) { conversation.body = incomingParameters.body @@ -97,7 +96,6 @@ class ConversationRepository extends BaseRepository { if (incomingParameters?.visibility) { conversation.visibility = incomingParameters.visibility } - conversation.editor_id = userUUID conversation.edited_at = Date.now() const result = await conversation.save(options) return result.toObject() From 61d21f23bc4eefea6b95fb73b3ead3f436c1d747 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 20 Apr 2026 11:18:14 -0400 Subject: [PATCH 13/15] Add changes to conversation tests --- .../conversation/editConversationTest.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 7adff82e6..54b91efc4 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -103,9 +103,6 @@ describe('Testing Conversation endpoints', () => { expect(res.body.updated).to.haveOwnProperty('body') expect(res.body.updated.body).to.equal('admin test updated') - expect(res.body.updated).to.haveOwnProperty('editor_id') - expect(res.body.updated.editor_id).to.equal(orgAdminUUID) - expect(res.body.updated).to.haveOwnProperty('edited_at') expect(res.body.updated.edited_at).to.not.be.null @@ -133,9 +130,6 @@ describe('Testing Conversation endpoints', () => { expect(res.body.updated).to.haveOwnProperty('body') expect(res.body.updated.body).to.equal('secretariat test updated') - expect(res.body.updated).to.haveOwnProperty('editor_id') - expect(res.body.updated.editor_id).to.equal(secUserUUID) - expect(res.body.updated).to.haveOwnProperty('edited_at') expect(res.body.updated.edited_at).to.not.be.null @@ -163,9 +157,6 @@ describe('Testing Conversation endpoints', () => { expect(res.body.updated).to.haveOwnProperty('body') expect(res.body.updated.body).to.equal('admin test updated by secretariat') - expect(res.body.updated).to.haveOwnProperty('editor_id') - expect(res.body.updated.editor_id).to.equal(secUserUUID) - expect(res.body.updated).to.haveOwnProperty('edited_at') expect(res.body.updated.edited_at).to.not.be.null From d974b4fdf2daf4e40379120902e3c810e7128603 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 20 Apr 2026 16:18:21 -0400 Subject: [PATCH 14/15] fixing lint issues --- test/integration-tests/conversation/editConversationTest.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 54b91efc4..4dc371ddb 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -16,8 +16,6 @@ const orgAdminHeaders = { describe('Testing Conversation endpoints', () => { let org - let secUserUUID - let orgAdminUUID // let rootConvoUUID before(async () => { @@ -38,7 +36,6 @@ describe('Testing Conversation endpoints', () => { .then((res, err) => { expect(err).to.be.undefined expect(res).to.have.status(200) - secUserUUID = res.body.UUID }) await chai @@ -48,7 +45,6 @@ describe('Testing Conversation endpoints', () => { .then((res, err) => { expect(err).to.be.undefined expect(res).to.have.status(200) - orgAdminUUID = res.body.UUID }) // Do org update to create conversation as admin From 343d04782a8c8efbc5fdcbffb9c9992c03d09aac Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 20 Apr 2026 16:21:34 -0400 Subject: [PATCH 15/15] Lock Endpoint --- src/controller/org.controller/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 1c5e6f110..f7805c496 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - // mw.onlySecretariat, + mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG