diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 78cb846f8..326830e81 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -120,7 +120,7 @@ async function getUser (req, res, next) { return res.status(404).json(error.userDne(identifier)) } - org = await repo.getOrg(orgUUID, true) + org = await repo.findOneByUUID(orgUUID) userToGetParameters = { org: org.short_name, @@ -133,7 +133,7 @@ async function getUser (req, res, next) { return res.status(404).json(error.userDne(userToGetParameters.username)) } - org = await repo.getOrg(req.ctx.params.shortname) + org = await repo.findOneByShortName(req.ctx.params.shortname) userToGetParameters = { org: org.short_name, username: result.username diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 63981888a..3654f4018 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -17,6 +17,43 @@ const { handleAuthorityModelChange } = require('./baseOrgRepositoryHelpers') const { normalizeOrgCveWebsiteUpdateDate } = require('../utils/dateOnly') +const RegistryOrgResponseSchema = require('../../schemas/registry-org/get-registry-org-response.json') + +const INTERNAL_UNDERSCORE_FIELDS = ['_id', '__t', '__v'] + +function isResponseExtensionField (key) { + return key.startsWith('_') && !INTERNAL_UNDERSCORE_FIELDS.includes(key) +} + +function maskObjectToSchemaProperties (obj, schema) { + if (!_.isPlainObject(obj) || !schema?.properties) return obj + + const maskedObj = {} + Object.keys(obj).forEach(key => { + const propertySchema = schema.properties[key] + if (!propertySchema) return + + if (_.isPlainObject(obj[key]) && propertySchema.properties) { + maskedObj[key] = maskObjectToSchemaProperties(obj[key], propertySchema) + } else { + maskedObj[key] = obj[key] + } + }) + + return maskedObj +} + +function maskOrgForResponseSchema (orgObj, fieldsToPreserve = []) { + const preservedFields = _.pick(orgObj, fieldsToPreserve) + const extensionFields = _.pickBy(orgObj, (_value, key) => isResponseExtensionField(key)) + const maskedOrg = maskObjectToSchemaProperties(_.cloneDeep(orgObj), RegistryOrgResponseSchema) + + return { + ...maskedOrg, + ...extensionFields, + ...preservedFields + } +} /** * @function setAggregateOrgObj @@ -109,7 +146,7 @@ function getOrgProjection (isSecretariat = false) { return projection } -function filterOrg (orgObj, isSecretariat = false) { +function filterOrg (orgObj, isSecretariat = false, applyResponseMask = false, fieldsToPreserve = []) { const CONSTANTS = getConstants() const _ = require('lodash') normalizeOrgCveWebsiteUpdateDate(orgObj, { output: true }) @@ -117,7 +154,8 @@ function filterOrg (orgObj, isSecretariat = false) { if (!isSecretariat) { fieldsToOmit = [...fieldsToOmit, ...CONSTANTS.ORG_RESTRICTED_FIELDS] } - return _.omit(orgObj, fieldsToOmit) + const filteredOrg = _.omit(orgObj, fieldsToOmit) + return applyResponseMask ? maskOrgForResponseSchema(filteredOrg, fieldsToPreserve) : filteredOrg } class BaseOrgRepository extends BaseRepository { @@ -436,11 +474,12 @@ class BaseOrgRepository extends BaseRepository { // Strip nulls returned by DocumentDB to prevent schema validation errors if (pg.itemsList) { - pg.itemsList.forEach(org => { + pg.itemsList = pg.itemsList.map(org => { if (org.reports_to === null) { delete org.reports_to } normalizeOrgCveWebsiteUpdateDate(org, { output: true }) + return returnLegacyFormat ? org : filterOrg(org, isSecretariat, true) }) } @@ -517,8 +556,11 @@ class BaseOrgRepository extends BaseRepository { } } - normalizeOrgCveWebsiteUpdateDate(result, { output: true }) - return deepRemoveEmpty(result) + if (returnLegacyFormat) { + normalizeOrgCveWebsiteUpdateDate(result, { output: true }) + return deepRemoveEmpty(result) + } + return deepRemoveEmpty(filterOrg(result, isSecretariat, true)) } /** @@ -737,7 +779,7 @@ class BaseOrgRepository extends BaseRepository { } const rawRegistryOrgObject = registryObject.toObject() - return filterOrg(deepRemoveEmpty(rawRegistryOrgObject), isSecretariat) + return filterOrg(deepRemoveEmpty(rawRegistryOrgObject), isSecretariat, true) } /** @@ -928,7 +970,7 @@ class BaseOrgRepository extends BaseRepository { return filterOrg(deepRemoveEmpty(legacyOrg.toObject()), isSecretariat) } - return filterOrg(deepRemoveEmpty(registryOrg.toObject()), isSecretariat) + return filterOrg(deepRemoveEmpty(registryOrg.toObject()), isSecretariat, true) } /** @@ -1051,7 +1093,7 @@ class BaseOrgRepository extends BaseRepository { const plainJavascriptRegistryOrg = updatedRegistryOrg.toObject() plainJavascriptRegistryOrg.conversation = conversationArray plainJavascriptRegistryOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) - return filterOrg(deepRemoveEmpty(plainJavascriptRegistryOrg), isSecretariat) + return filterOrg(deepRemoveEmpty(plainJavascriptRegistryOrg), isSecretariat, true, ['joint_approval_required']) } /** diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 678bd6b78..1205a4baa 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -668,6 +668,7 @@ class BaseUserRepository extends BaseRepository { const currentOrgUUID = legacyUser ? legacyUser.org_UUID : registryUser?.org_UUID // Fallback if schema supports it const currentOrg = currentOrgUUID ? await baseOrgRepository.findOneByUUID(currentOrgUUID) : null const newOrg = await baseOrgRepository.findOneByShortName(incomingUser.org_short_name) + const wasAdmin = currentOrg?.admins?.includes(identifier) ?? false if (!newOrg) { throw new Error(`Organization ${incomingUser.org_short_name} not found`) @@ -687,7 +688,7 @@ class BaseUserRepository extends BaseRepository { if (!Array.isArray(newOrg.users)) newOrg.users = [] newOrg.users.addToSet(identifier) - const isAdmin = updatedRegistryUser?.role === 'ADMIN' || (updatedLegacyUser?.authority?.active_roles?.includes('ADMIN')) + const isAdmin = wasAdmin || updatedRegistryUser?.role === 'ADMIN' || (updatedLegacyUser?.authority?.active_roles?.includes('ADMIN')) if (isAdmin) { if (!Array.isArray(newOrg.admins)) { newOrg.admins = [] diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 27630db8d..aafe9d094 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -373,6 +373,7 @@ describe('Testing /registryOrg endpoints', () => { .set(constants.nonSecretariatUserHeaders) .then((res) => { expect(res).to.have.status(200) + expect(postedConvoUUID).to.not.be.undefined 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. @@ -381,6 +382,72 @@ describe('Testing /registryOrg endpoints', () => { expect(convo).to.not.have.property('author_id') }) }) + it('Masks stale schema fields from registry org GET responses', async () => { + const BaseOrg = require('../../../src/model/baseorg') + const staleOrg = { + ...testRegistryOrg, + short_name: 'registry_org_stale_mask', + long_name: 'Registry Org Stale Mask' + } + + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(staleOrg) + .then((res) => { + expect(res).to.have.status(200) + }) + + await BaseOrg.collection.updateOne( + { short_name: staleOrg.short_name }, + { + $set: { + 'program_data.advisory_location_require_credentials': true, + 'program_data.vulnerability_advisory_location_for_web_scraping': ['https://example.com/stale'], + users: ['d41d8cd9-8f00-3204-a980-0998ecf8427e'], + admins: ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + } + } + ) + + let orgFromList + await chai.request(app) + .get('/api/registry/org') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + orgFromList = res.body.organizations.find(org => org.short_name === staleOrg.short_name) + expect(orgFromList).to.not.be.undefined + expect(orgFromList).to.have.property('created') + expect(orgFromList).to.have.property('last_updated') + expect(orgFromList.users).to.deep.equal(['d41d8cd9-8f00-3204-a980-0998ecf8427e']) + expect(orgFromList.admins).to.deep.equal(['d41d8cd9-8f00-3204-a980-0998ecf8427e']) + expect(orgFromList.program_data).to.not.have.property('advisory_location_require_credentials') + expect(orgFromList.program_data).to.not.have.property('vulnerability_advisory_location_for_web_scraping') + }) + + const putBody = { ...orgFromList } + delete putBody.created + delete putBody.last_updated + delete putBody.users + delete putBody.admins + delete putBody.conversation + delete putBody.reports_to + + await chai.request(app) + .put(`/api/registry/org/${staleOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...putBody, + long_name: 'Registry Org Stale Mask Updated' + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.updated.long_name).to.equal('Registry Org Stale Mask Updated') + expect(res.body.updated.program_data).to.not.have.property('advisory_location_require_credentials') + expect(res.body.updated.program_data).to.not.have.property('vulnerability_advisory_location_for_web_scraping') + }) + }) }) context('Negative Tests', () => { it('Fails to get a registry organization that does not exist', async () => { @@ -637,6 +704,10 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated.oversees).to.be.an('array').that.includes(createdSubOrgUUID) }) + const BaseOrg = require('../../../src/model/baseorg') + const registryOrgCheck = await BaseOrg.findOne({ short_name: createdOrg.short_name }) + expect(registryOrgCheck.oversees).to.be.an('array').that.includes(createdSubOrgUUID) + // Assert that the sub org dynamically returns reports_to matching the main org's UUID await chai.request(app) .get(`/api/registry/org/${subOrg.short_name}`)