Skip to content
Merged

RC3 #2016

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": " <h2>Access Control</h2> <p>Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.</p> <h2>Expected Behavior</h2> <p><b>Regular, CNA & Admin Users:</b> Retrieves registry organization record for the specified shortname or UUID if it is the user's organization</p> <p><b>Secretariat:</b> Retrieves information about any registry organization</p> <p>The reports_to, top_level_root, and oversees relationship fields contain organization short names.</p> <p>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.</p>",
"description": " <h2>Access Control</h2> <p>Authenticated users can access this endpoint only for their own organization. Secretariat users can access any organization.</p> <h2>Expected Behavior</h2> <p><b>Regular, CNA & Admin Users:</b> Retrieves registry organization record for the specified shortname or UUID if it is the user's organization</p> <p><b>Secretariat:</b> Retrieves information about any registry organization</p> <p>The reports_to, top_level_root, and oversees relationship fields contain organization short names.</p> <p>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.</p>",
"operationId": "registryOrgSingle",
"parameters": [
{
Expand Down
5 changes: 4 additions & 1 deletion schemas/registry-org/get-registry-org-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@
"hierarchyNode": {
"type": "object",
"properties": {
"UUID": {
"type": "string"
},
"short_name": {
"type": "string"
},
Expand Down Expand Up @@ -320,7 +323,7 @@
}
}
},
"required": ["short_name", "long_name", "authority", "children"],
"required": ["UUID", "short_name", "long_name", "authority", "children"],
"allOf": [
{
"if": {
Expand Down
8 changes: 2 additions & 6 deletions schemas/registry-org/list-registry-orgs-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/controller/registry.controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ router.get('/registry/org/:identifier',
<p><b>Regular, CNA & Admin Users:</b> Retrieves registry organization record for the specified shortname or UUID if it is the user's organization</p>
<p><b>Secretariat:</b> Retrieves information about any registry organization</p>
<p>The reports_to, top_level_root, and oversees relationship fields contain organization short names.</p>
<p>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.</p>"
<p>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.</p>"
#swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' }
#swagger.parameters['expand'] = {
in: 'query',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.' })
Expand Down
134 changes: 79 additions & 55 deletions src/repositories/baseOrgRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
},
Expand All @@ -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) {
Expand Down Expand Up @@ -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<object[]>} Organization names, roles, role flags, and nested children. Full forests also include UUIDs.
* @returns {Promise<object[]>} Organization UUIDs, names, roles, role flags, and nested children.
*/
async getOrgHierarchy (orgUUID) {
const orgs = await BaseOrgModel.find({})
Expand Down Expand Up @@ -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]
}

Expand Down
34 changes: 13 additions & 21 deletions test/integration-tests/registry-org/orgHierarchyTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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))
})

Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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: [] }
])]
Expand Down Expand Up @@ -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}`)
Expand Down Expand Up @@ -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))
})
})
Loading
Loading