diff --git a/api-docs/openapi.json b/api-docs/openapi.json
index 2b1571467..e077a9476 100644
--- a/api-docs/openapi.json
+++ b/api-docs/openapi.json
@@ -3546,7 +3546,7 @@
"Registry Organization"
],
"summary": "Retrieves information about the registry organization specified by short name or UUID (accessible to same-organization users or Secretariat)",
- "description": "
Access Control Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.
Expected Behavior Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization
Secretariat: Retrieves information about any registry organization
The reports_to, top_level_root, and oversees relationship fields contain organization short names.
The _hierarchy array contains the ancestor path from the top-level Root to the requested organization and that organization's complete subtree. Ancestors include only the child on that path. Nodes contain short_name, long_name, authority, and children; UUID is omitted. ROOT nodes also contain is_top_level_root, and CNA nodes contain is_last_resort. An organization without a parent begins its own tree.
",
+ "description": " Access Control Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.
Expected Behavior Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization
Secretariat: Retrieves information about any registry organization
The reports_to, top_level_root, and oversees relationship fields contain organization short names.
The _hierarchy array contains the ancestor path from the top-level Root to the requested organization and that organization's complete subtree. Ancestors include only the child on that path. Nodes contain UUID, short_name, long_name, authority, and children. ROOT nodes also contain is_top_level_root, and CNA nodes contain is_last_resort. An organization without a parent begins its own tree.
",
"operationId": "registryOrgSingle",
"parameters": [
{
diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json
index 298526872..31dd07ad2 100644
--- a/schemas/registry-org/get-registry-org-response.json
+++ b/schemas/registry-org/get-registry-org-response.json
@@ -293,6 +293,9 @@
"hierarchyNode": {
"type": "object",
"properties": {
+ "UUID": {
+ "type": "string"
+ },
"short_name": {
"type": "string"
},
@@ -320,7 +323,7 @@
}
}
},
- "required": ["short_name", "long_name", "authority", "children"],
+ "required": ["UUID", "short_name", "long_name", "authority", "children"],
"allOf": [
{
"if": {
diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json
index 734ff18dd..d72d8780d 100644
--- a/schemas/registry-org/list-registry-orgs-response.json
+++ b/schemas/registry-org/list-registry-orgs-response.json
@@ -70,10 +70,6 @@
},
"description": "The organization's function within the CVE program"
},
- "top_level_root": {
- "type": "string",
- "description": "Short name of the top-level ROOT organization in this organization's reporting chain"
- },
"is_top_level_root": {
"type": "boolean",
"description": "Indicates whether a ROOT organization is top-level"
@@ -87,14 +83,14 @@
"string",
"null"
],
- "description": "Short name of the parent organization, if any"
+ "description": "UUID of the parent organization, if any"
},
"oversees": {
"type": "array",
"items": {
"type": "string"
},
- "description": "Short names of organizations overseen by this organization"
+ "description": "UUIDs of organizations overseen by this organization"
},
"users": {
"type": "array",
diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js
index 776b449ee..51d051ed0 100644
--- a/src/controller/registry.controller/index.js
+++ b/src/controller/registry.controller/index.js
@@ -397,7 +397,7 @@ router.get('/registry/org/:identifier',
Regular, CNA & Admin Users: Retrieves registry organization record for the specified shortname or UUID if it is the user's organization
Secretariat: Retrieves information about any registry organization
The reports_to, top_level_root, and oversees relationship fields contain organization short names.
- The _hierarchy array contains the ancestor path from the top-level Root to the requested organization and that organization's complete subtree. Ancestors include only the child on that path. Nodes contain short_name, long_name, authority, and children; UUID is omitted. ROOT nodes also contain is_top_level_root, and CNA nodes contain is_last_resort. An organization without a parent begins its own tree.
"
+ The _hierarchy array contains the ancestor path from the top-level Root to the requested organization and that organization's complete subtree. Ancestors include only the child on that path. Nodes contain UUID, short_name, long_name, authority, and children. ROOT nodes also contain is_top_level_root, and CNA nodes contain is_last_resort. An organization without a parent begins its own tree.
"
#swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' }
#swagger.parameters['expand'] = {
in: 'query',
diff --git a/src/controller/registry.controller/org.registry.controller.js b/src/controller/registry.controller/org.registry.controller.js
index 63eeb4ec7..4c288eaa8 100644
--- a/src/controller/registry.controller/org.registry.controller.js
+++ b/src/controller/registry.controller/org.registry.controller.js
@@ -140,7 +140,7 @@ async function getAllOrgs (req, res, next) {
if (error.message && error.message.includes('Unknown Org type requested')) {
return res.status(400).json({ message: error.message })
}
- return res.status(500).json({ message: 'Error fetching orgs' })
+ return res.status(500).json({ message: error.message })
}
logger.info({ uuid: req.ctx.uuid, message: 'The orgs were sent to the user.' })
diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js
index 38935911f..fcdf0b837 100644
--- a/src/repositories/baseOrgRepository.js
+++ b/src/repositories/baseOrgRepository.js
@@ -70,7 +70,6 @@ function decorateCnaRelationships (activeOrgs, hierarchyOrgs) {
}
function buildOrgHierarchy (orgs, orgUUID) {
- const includeUUID = orgUUID === undefined
const orgsByUUID = new Map(orgs.filter(org => org.UUID).map(org => [org.UUID, org]))
const childrenByUUID = new Map()
const parentByUUID = new Map()
@@ -116,7 +115,7 @@ function buildOrgHierarchy (orgs, orgUUID) {
const org = orgsByUUID.get(UUID)
const authority = Array.isArray(org.authority) ? org.authority : []
const node = {
- ...(includeUUID ? { UUID: org.UUID } : {}),
+ UUID: org.UUID,
short_name: org.short_name || '',
long_name: org.long_name || '',
authority,
@@ -214,9 +213,10 @@ function setAggregateOrgObj (query) {
* @description Constructs the aggregation pipeline for registry organization objects and their reporting relationships.
* @param {object} query - The query object to match.
* @param {boolean} [isSecretariat=false] - Whether restricted organization fields may be returned.
+ * @param {boolean} [resolveRelationshipNames=false] - Whether relationship UUIDs should be resolved to short names.
* @returns {Array} The aggregation pipeline.
*/
-function setAggregateRegistryOrgObj (query, isSecretariat = false) {
+function setAggregateRegistryOrgObj (query, isSecretariat = false, resolveRelationshipNames = false) {
const CONSTANTS = getConstants()
const projection = {
_id: false,
@@ -225,21 +225,26 @@ function setAggregateRegistryOrgObj (query, isSecretariat = false) {
inUse: false,
in_use: false,
parentOrg: false,
- grandparentOrg: false,
- overseenOrgDocuments: false,
relatedOrgUUIDs: false,
relatedOrgDocuments: false,
_relatedOrganizations: false,
_hierarchy: false
}
+ if (resolveRelationshipNames) {
+ projection.grandparentOrg = false
+ projection.overseenOrgDocuments = false
+ } else {
+ projection.top_level_root = false
+ }
+
if (!isSecretariat) {
CONSTANTS.ORG_RESTRICTED_FIELDS.forEach(field => {
projection[field] = false
})
}
- return [
+ const pipeline = [
{
$match: query
},
@@ -251,62 +256,81 @@ function setAggregateRegistryOrgObj (query, isSecretariat = false) {
foreignField: 'oversees',
as: 'parentOrg'
}
- },
- {
- $lookup: {
- from: 'BaseOrg',
- localField: 'parentOrg.UUID',
- foreignField: 'oversees',
- as: 'grandparentOrg'
- }
- },
- {
- $lookup: {
- from: 'BaseOrg',
- localField: 'oversees',
- foreignField: 'UUID',
- as: 'overseenOrgDocuments'
- }
- },
- {
- $addFields: {
- reports_to: {
- $cond: {
- if: { $gt: [{ $size: '$parentOrg' }, 0] },
- then: { $arrayElemAt: ['$parentOrg.short_name', 0] },
- else: null
- }
- },
- top_level_root: {
- $cond: {
- if: { $eq: ['$is_top_level_root', true] },
- then: '$short_name',
- else: {
- $cond: {
- if: { $eq: [{ $arrayElemAt: ['$parentOrg.is_top_level_root', 0] }, true] },
- then: { $arrayElemAt: ['$parentOrg.short_name', 0] },
- else: {
- $cond: {
- if: { $eq: [{ $arrayElemAt: ['$grandparentOrg.is_top_level_root', 0] }, true] },
- then: { $arrayElemAt: ['$grandparentOrg.short_name', 0] },
- else: null
+ }
+ ]
+
+ if (resolveRelationshipNames) {
+ pipeline.push(
+ {
+ $lookup: {
+ from: 'BaseOrg',
+ localField: 'parentOrg.UUID',
+ foreignField: 'oversees',
+ as: 'grandparentOrg'
+ }
+ },
+ {
+ $lookup: {
+ from: 'BaseOrg',
+ localField: 'oversees',
+ foreignField: 'UUID',
+ as: 'overseenOrgDocuments'
+ }
+ },
+ {
+ $addFields: {
+ reports_to: {
+ $cond: {
+ if: { $gt: [{ $size: '$parentOrg' }, 0] },
+ then: { $arrayElemAt: ['$parentOrg.short_name', 0] },
+ else: null
+ }
+ },
+ top_level_root: {
+ $cond: {
+ if: { $eq: ['$is_top_level_root', true] },
+ then: '$short_name',
+ else: {
+ $cond: {
+ if: { $eq: [{ $arrayElemAt: ['$parentOrg.is_top_level_root', 0] }, true] },
+ then: { $arrayElemAt: ['$parentOrg.short_name', 0] },
+ else: {
+ $cond: {
+ if: { $eq: [{ $arrayElemAt: ['$grandparentOrg.is_top_level_root', 0] }, true] },
+ then: { $arrayElemAt: ['$grandparentOrg.short_name', 0] },
+ else: null
+ }
}
}
}
}
+ },
+ oversees: {
+ $cond: {
+ if: { $isArray: '$oversees' },
+ then: '$overseenOrgDocuments.short_name',
+ else: '$oversees'
+ }
}
- },
- oversees: {
+ }
+ }
+ )
+ } else {
+ pipeline.push({
+ $addFields: {
+ reports_to: {
$cond: {
- if: { $isArray: '$oversees' },
- then: '$overseenOrgDocuments.short_name',
- else: '$oversees'
+ if: { $gt: [{ $size: '$parentOrg' }, 0] },
+ then: { $arrayElemAt: ['$parentOrg.UUID', 0] },
+ else: null
}
}
}
- },
- { $project: projection }
- ]
+ })
+ }
+
+ pipeline.push({ $project: projection })
+ return pipeline
}
function getOrgProjection (isSecretariat = false) {
@@ -787,7 +811,7 @@ class BaseOrgRepository extends BaseRepository {
* The UUID projection assembles relationships and identifies nodes in full forests.
*
* @param {string} [orgUUID] - Restricts the forest to this org's ancestor path and subtree.
- * @returns {Promise} Organization names, roles, role flags, and nested children. Full forests also include UUIDs.
+ * @returns {Promise} Organization UUIDs, names, roles, role flags, and nested children.
*/
async getOrgHierarchy (orgUUID) {
const orgs = await BaseOrgModel.find({})
@@ -891,7 +915,7 @@ class BaseOrgRepository extends BaseRepository {
: await this.findOneByShortName(identifier, { ...options, lean: true }, true, projection)
} else {
const query = identifierIsUUID ? { UUID: identifier } : { short_name: identifier }
- const organizations = await this.aggregate(setAggregateRegistryOrgObj(query, isSecretariat), options)
+ const organizations = await this.aggregate(setAggregateRegistryOrgObj(query, isSecretariat, true), options)
data = organizations[0]
}
diff --git a/test/integration-tests/registry-org/orgHierarchyTest.js b/test/integration-tests/registry-org/orgHierarchyTest.js
index 8f084f5b1..d7e405f68 100644
--- a/test/integration-tests/registry-org/orgHierarchyTest.js
+++ b/test/integration-tests/registry-org/orgHierarchyTest.js
@@ -33,15 +33,7 @@ function addUUIDs (nodes, organizations) {
return nodes.map(addUUID)
}
-function removeUUIDs (nodes) {
- return nodes.map(node => {
- const projected = { ...node, children: removeUUIDs(node.children) }
- delete projected.UUID
- return projected
- })
-}
-
-function expectProjectedNode (node, includeUUID = false) {
+function expectProjectedNode (node, includeUUID = true) {
const expectedKeys = ['short_name', 'long_name', 'authority', 'children']
if (includeUUID) expectedKeys.unshift('UUID')
if (node.authority.includes('ROOT')) expectedKeys.push('is_top_level_root')
@@ -160,12 +152,12 @@ describe('Registry organization hierarchy', () => {
expect(res).to.have.status(200)
expect(res.body).to.not.have.property('_relatedOrganizations')
- expect(res.body._hierarchy).to.deep.equal([projectNode(organizations.top, [
+ expect(res.body._hierarchy).to.deep.equal(addUUIDs([projectNode(organizations.top, [
projectNode(organizations.root, [
projectNode(organizations.branch, [projectNode(organizations.cna)]),
projectNode(organizations.adp)
])
- ])])
+ ])], organizations))
flattenHierarchy(res.body._hierarchy).forEach(node => expectProjectedNode(node))
})
@@ -175,11 +167,11 @@ describe('Registry organization hierarchy', () => {
.set(secretariatHeaders)
expect(res).to.have.status(200)
- expect(res.body._hierarchy).to.deep.equal([projectNode(organizations.top, [
+ expect(res.body._hierarchy).to.deep.equal(addUUIDs([projectNode(organizations.top, [
projectNode(organizations.root, [
projectNode(organizations.branch, [projectNode(organizations.cna)])
])
- ])])
+ ])], organizations))
})
it('returns a requested top-level Root with its full subtree and a standalone org by itself', async () => {
@@ -189,16 +181,16 @@ describe('Registry organization hierarchy', () => {
.set(secretariatHeaders)
expect(topRes).to.have.status(200)
- expect(topRes.body._hierarchy).to.deep.equal(removeUUIDs([
+ expect(topRes.body._hierarchy).to.deep.equal([
hierarchy.find(node => node.short_name === organizations.top.short_name)
- ]))
+ ])
const standaloneRes = await chai.request(app)
.get(`/api/registry/org/${organizations.detached.short_name}`)
.set(secretariatHeaders)
expect(standaloneRes).to.have.status(200)
- expect(standaloneRes.body._hierarchy).to.deep.equal([projectNode(organizations.detached)])
+ expect(standaloneRes.body._hierarchy).to.deep.equal(addUUIDs([projectNode(organizations.detached)], organizations))
})
it('provides own-org users a trimmed hierarchy without exposing sibling organizations', async () => {
@@ -243,7 +235,7 @@ describe('Registry organization hierarchy', () => {
definitions: schemaRes.body.definitions
})
expect(validate(orgRes.body._hierarchy), JSON.stringify(validate.errors)).to.equal(true)
- expect(validate([{ UUID: organizations.top.UUID, ...projectNode(organizations.top) }])).to.equal(false)
+ expect(validate([{ UUID: organizations.top.UUID, ...projectNode(organizations.top) }])).to.equal(true)
const invalid = [projectNode(organizations.top, [
{ ...projectNode(organizations.root), private_contacts: [] }
])]
@@ -323,11 +315,11 @@ describe('Registry organization hierarchy', () => {
.set(secretariatHeaders)
expect(detail).to.have.status(200)
expect(detail.body.reports_to).to.equal(organizations.branch.short_name)
- expect(detail.body._hierarchy).to.deep.equal([projectNode(organizations.top, [
+ expect(detail.body._hierarchy).to.deep.equal(addUUIDs([projectNode(organizations.top, [
projectNode(organizations.root, [
projectNode(organizations.branch, [projectNode(organizations.cna)])
])
- ])])
+ ])], organizations))
const parentDetail = await chai.request(app)
.get(`/api/registry/org/${organizations.branch.short_name}`)
@@ -362,10 +354,10 @@ describe('Registry organization hierarchy', () => {
.get(`/api/registry/org/${organizations.cna.short_name}`)
.set(secretariatHeaders)
expect(detail).to.have.status(200)
- expect(detail.body._hierarchy).to.deep.equal([projectNode(organizations.alternate, [
+ expect(detail.body._hierarchy).to.deep.equal(addUUIDs([projectNode(organizations.alternate, [
projectNode(organizations.root, [
projectNode(organizations.branch, [projectNode(organizations.cna)])
])
- ])])
+ ])], organizations))
})
})
diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js
index 1ab9881da..54d062833 100644
--- a/test/integration-tests/registry-org/rootOrgTest.js
+++ b/test/integration-tests/registry-org/rootOrgTest.js
@@ -224,16 +224,19 @@ describe('Testing ROOT Organization Type', () => {
expect(res.body).to.not.have.property('_relatedOrganizations')
expect(res.body._hierarchy).to.deep.equal([{
+ UUID: topLevelRootOrg.UUID,
short_name: topLevelRootOrg.short_name,
long_name: topLevelRootOrg.long_name,
authority: topLevelRootOrg.authority,
is_top_level_root: true,
children: [{
+ UUID: secondRootOrg.UUID,
short_name: secondRootOrg.short_name,
long_name: secondRootOrg.long_name,
authority: secondRootOrg.authority,
is_top_level_root: false,
children: [{
+ UUID: reportingOrg.UUID,
short_name: reportingOrg.short_name,
long_name: reportingOrg.long_name,
authority: reportingOrg.authority,
@@ -276,14 +279,14 @@ describe('Testing ROOT Organization Type', () => {
})
})
- it('Omits related organizations from registry organization pages', async () => {
+ it('Returns stored relationship UUIDs on registry organization pages', async () => {
const { organization, response } = await getRegistryOrgListEntry(
org => org.UUID === reportingOrg.UUID
)
expect(organization).to.not.be.undefined
- expect(organization.reports_to).to.equal(secondRootOrg.short_name)
- expect(organization.top_level_root).to.equal(topLevelRootOrg.short_name)
+ expect(organization.reports_to).to.equal(secondRootOrg.UUID)
+ expect(organization).to.not.have.property('top_level_root')
expect(response.body).to.not.have.property('_relatedOrganizations')
response.body.organizations.forEach(org => {
expect(org).to.not.have.property('_relatedOrganizations')
@@ -325,11 +328,13 @@ describe('Testing ROOT Organization Type', () => {
expect(res.body.reports_to).to.equal(topLevelRootOrg.short_name)
expect(res.body.top_level_root).to.equal(topLevelRootOrg.short_name)
expect(res.body._hierarchy).to.deep.equal([{
+ UUID: topLevelRootOrg.UUID,
short_name: topLevelRootOrg.short_name,
long_name: topLevelRootOrg.long_name,
authority: topLevelRootOrg.authority,
is_top_level_root: true,
children: [{
+ UUID: reportingOrg.UUID,
short_name: reportingOrg.short_name,
long_name: reportingOrg.long_name,
authority: reportingOrg.authority,
diff --git a/test/unit-tests/org/orgHierarchyTest.js b/test/unit-tests/org/orgHierarchyTest.js
index e2cef70ba..fa6740d1d 100644
--- a/test/unit-tests/org/orgHierarchyTest.js
+++ b/test/unit-tests/org/orgHierarchyTest.js
@@ -15,7 +15,7 @@ function org (UUID, fields = {}) {
return { UUID, short_name: UUID, long_name: `${UUID} organization`, ...fields }
}
-function flattenHierarchy (hierarchy, includeUUID = false) {
+function flattenHierarchy (hierarchy, includeUUID = true) {
const nodes = []
const visited = new Set()
const pending = [...hierarchy]
@@ -35,6 +35,14 @@ function flattenHierarchy (hierarchy, includeUUID = false) {
return nodes
}
+function addUUIDs (nodes) {
+ return nodes.map(node => ({
+ UUID: node.short_name,
+ ...node,
+ children: addUUIDs(node.children)
+ }))
+}
+
describe('Organization hierarchy repository', () => {
afterEach(() => sinon.restore())
@@ -119,7 +127,7 @@ describe('Organization hierarchy repository', () => {
const result = await new BaseOrgRepository().getOrgHierarchy('requested')
- expect(result).to.deep.equal([{
+ expect(result).to.deep.equal(addUUIDs([{
short_name: 'top',
long_name: 'top organization',
authority: ['ROOT'],
@@ -143,7 +151,7 @@ describe('Organization hierarchy repository', () => {
}, { short_name: 'child', long_name: 'child organization', authority: [], children: [] }]
}]
}]
- }])
+ }]))
expect(find.calledOnceWithExactly({})).to.equal(true)
expect(flattenHierarchy(result)).to.have.lengthOf(6)
})
@@ -160,7 +168,7 @@ describe('Organization hierarchy repository', () => {
expect(flattenHierarchy(topHierarchy).map(node => node.long_name))
.to.have.members(['top organization', 'child organization'])
expect(await repository.getOrgHierarchy('standalone')).to.deep.equal([{
- short_name: 'same-name', long_name: 'standalone organization', authority: [], children: []
+ UUID: 'standalone', short_name: 'same-name', long_name: 'standalone organization', authority: [], children: []
}])
expect(await repository.getOrgHierarchy('missing')).to.deep.equal([])
})
@@ -186,7 +194,7 @@ describe('Organization hierarchy repository', () => {
expect(branch[0].short_name).to.equal(`root-${index}`)
branch = branch[0].children
}
- expect(branch).to.deep.equal([{ short_name: 'leaf', long_name: 'leaf organization', authority: [], children: [] }])
+ expect(branch).to.deep.equal([{ UUID: 'leaf', short_name: 'leaf', long_name: 'leaf organization', authority: [], children: [] }])
expect(flattenHierarchy(result)).to.have.lengthOf(depth + 1)
})
diff --git a/test/unit-tests/repository/leanRepositoryQueriesTest.js b/test/unit-tests/repository/leanRepositoryQueriesTest.js
index 9720e673b..7ad5ca640 100644
--- a/test/unit-tests/repository/leanRepositoryQueriesTest.js
+++ b/test/unit-tests/repository/leanRepositoryQueriesTest.js
@@ -181,7 +181,7 @@ describe('Lean repository queries', () => {
expect(relationshipProjection.oversees.$cond.then).to.equal('$overseenOrgDocuments.short_name')
})
- it('omits related organizations and their hydration lookup from a registry organization page', async () => {
+ it('keeps relationship UUIDs and omits relationship hydration from a registry organization page', async () => {
const repository = new BaseOrgRepository()
const aggregatePaginate = sinon.stub(repository, 'aggregatePaginate').resolves({
itemsList: [
@@ -217,10 +217,16 @@ describe('Lean repository queries', () => {
expect(result).to.not.have.property('_relatedOrganizations')
expect(result.totalCount).to.equal(2)
const pipeline = aggregatePaginate.firstCall.args[0]
- expect(pipeline.some(stage => stage.$lookup?.as === 'overseenOrgDocuments')).to.equal(true)
+ expect(pipeline.some(stage => stage.$lookup?.as === 'grandparentOrg')).to.equal(false)
+ expect(pipeline.some(stage => stage.$lookup?.as === 'overseenOrgDocuments')).to.equal(false)
expect(pipeline.some(stage => stage.$lookup?.as === 'relatedOrgDocuments')).to.equal(false)
expect(pipeline.some(stage => stage.$addFields?.relatedOrgUUIDs)).to.equal(false)
expect(pipeline.some(stage => stage.$addFields?._relatedOrganizations)).to.equal(false)
+ const relationshipProjection = pipeline.find(stage => stage.$addFields?.reports_to).$addFields
+ expect(relationshipProjection.reports_to.$cond.then)
+ .to.deep.equal({ $arrayElemAt: ['$parentOrg.UUID', 0] })
+ expect(relationshipProjection).to.not.have.property('top_level_root')
+ expect(relationshipProjection).to.not.have.property('oversees')
})
it('preserves hydrated empty-array defaults in sparse registry organization responses', async () => {