Skip to content
Open
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
14 changes: 13 additions & 1 deletion api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2318,6 +2318,18 @@
},
"description": "The shortname or UUID of the registry organization"
},
{
"name": "expand",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"users"
]
},
"description": "Optional expanded related data. Accepted value: users."
},
{
"$ref": "#/components/parameters/apiEntityHeader"
},
Expand Down Expand Up @@ -7744,4 +7756,4 @@
}
}
}
}
}
43 changes: 43 additions & 0 deletions schemas/registry-org/get-registry-org-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,49 @@
}
},
"description": "List of conversation messages associated with the organization"
},
"_userMap": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "User's identifier or username"
},
"name": {
"type": "object",
"properties": {
"first": {
"type": "string",
"description": "User's first name"
},
"last": {
"type": "string",
"description": "User's last name"
},
"middle": {
"type": "string",
"description": "User's middle name"
},
"suffix": {
"type": "string",
"description": "User's name suffix"
}
}
},
"org": {
"type": "object",
"properties": {
"short_name": {
"type": "string",
"description": "Short name of the organization associated with the user"
}
}
}
}
},
"description": "Map of expanded user UUIDs to display metadata, included when expand=users is requested"
}
}
}
13 changes: 12 additions & 1 deletion src/controller/org.controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,15 @@ 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>"
#swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' }
#swagger.parameters['expand'] = {
in: 'query',
description: 'Optional expanded related data. Accepted value: users.',
required: false,
schema: {
type: 'string',
enum: ['users']
}
}
#swagger.parameters['$ref'] = [
'#/components/parameters/apiEntityHeader',
'#/components/parameters/apiUserHeader',
Expand Down Expand Up @@ -338,7 +347,9 @@ router.get('/registry/org/:identifier',
*/
mw.useRegistry(),
mw.validateUser,
query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }),
query().custom((query) => { return mw.validateQueryParameterNames(query, ['expand']) }),
query(['expand']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
query(['expand']).optional().isIn(['users']),
parseError,
parseGetParams,
registryOrgController.SINGLE_ORG
Expand Down
2 changes: 1 addition & 1 deletion src/controller/org.controller/org.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ function parsePostParams (req, res, next) {

function parseGetParams (req, res, next) {
utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier'])
utils.reqCtxMapping(req, 'query', ['page'])
utils.reqCtxMapping(req, 'query', ['page', 'expand'])
next()
}

Expand Down
62 changes: 62 additions & 0 deletions src/controller/registry-org.controller/registry-org.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,63 @@ const conversationErrors = require('../conversation.controller/error')
const convoError = new conversationErrors.ConversationControllerError()
const validateUUID = require('uuid').validate

function addUUIDsToSet (uuidSet, values) {
if (!Array.isArray(values)) return
values.filter(Boolean).forEach(uuid => uuidSet.add(uuid))
}

function addConversationAuthorUUIDsToSet (uuidSet, conversations) {
if (!Array.isArray(conversations)) return
conversations.forEach(conversation => {
if (conversation.author_id) {
uuidSet.add(conversation.author_id)
}
})
}

function buildOrgShortNameByUserUUID (orgs, requestedUserUUIDs) {
const orgShortNameByUserUUID = {}
orgs.forEach(org => {
const orgObject = typeof org.toObject === 'function' ? org.toObject() : org
if (!Array.isArray(orgObject.users)) return

orgObject.users.forEach(userUUID => {
if (requestedUserUUIDs.has(userUUID)) {
orgShortNameByUserUUID[userUUID] = orgObject.short_name
}
})
})
return orgShortNameByUserUUID
}

async function buildUserMap (org, userRepo, orgRepo) {
const userUUIDs = new Set()
addUUIDsToSet(userUUIDs, org.users)
addUUIDsToSet(userUUIDs, org.admins)
addUUIDsToSet(userUUIDs, org.contact_info?.additional_contacts)
addConversationAuthorUUIDsToSet(userUUIDs, org.conversation)

if (userUUIDs.size === 0) {
return {}
}

const userUUIDList = Array.from(userUUIDs)
const users = await userRepo.findUsersByUUIDs(userUUIDList)
const orgs = await orgRepo.findOrgsByUserUUIDs(userUUIDList)
const orgShortNameByUserUUID = buildOrgShortNameByUserUUID(orgs, userUUIDs)

return users.reduce((userMap, user) => {
const userObject = typeof user.toObject === 'function' ? user.toObject() : user
const orgShortName = orgShortNameByUserUUID[userObject.UUID]
userMap[userObject.UUID] = _.omitBy({
username: userObject.username,
name: userObject.name,
org: orgShortName ? { short_name: orgShortName } : undefined
}, _.isUndefined)
return userMap
}, {})
}

/**
* Retrieves information about all registry organizations.
*
Expand Down Expand Up @@ -101,6 +158,11 @@ async function getOrg (req, res, next) {
} else {
returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid', 'visibility'])) : undefined
}

if (req.ctx.query?.expand === 'users') {
const userRepo = req.ctx.repositories.getBaseUserRepository()
returnValue._userMap = await buildUserMap(returnValue, userRepo, repo)
}
}
} catch (error) {
// Handle the specific error thrown by BaseOrgRepository.createOrg
Expand Down
12 changes: 12 additions & 0 deletions src/repositories/baseOrgRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,18 @@ class BaseOrgRepository extends BaseRepository {
return null
}

async findOrgsByUserUUIDs (userUUIDs, options = {}) {
if (!Array.isArray(userUUIDs) || userUUIDs.length === 0) {
return []
}

return await BaseOrgModel.find(
{ users: { $in: userUUIDs } },
{ _id: 0, UUID: 1, short_name: 1, users: 1 },
options
)
}

/**
* @async
* @function orgExists
Expand Down
12 changes: 12 additions & 0 deletions src/repositories/baseUserRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ class BaseUserRepository extends BaseRepository {
return user || null
}

async findUsersByUUIDs (uuids, options = {}) {
if (!Array.isArray(uuids) || uuids.length === 0) {
return []
}

return await BaseUser.find(
{ UUID: { $in: uuids } },
{ _id: 0, UUID: 1, username: 1, name: 1 },
options
)
}

/**
* @async
* @function deleteUserByUUID
Expand Down
76 changes: 76 additions & 0 deletions test/integration-tests/registry-org/registryOrgCRUDTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const chai = require('chai')
const expect = chai.expect
chai.use(require('chai-http'))
const { v4: uuidv4 } = require('uuid')

const constants = require('../constants.js')
const app = require('../../../src/index.js')
Expand Down Expand Up @@ -296,6 +297,81 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.advisory_locations).to.deep.equal(createdOrg.advisory_locations)
})
})
it('Gets a registry organization with expanded user metadata', async () => {
const uniqueSuffix = uuidv4().replace(/-/g, '').slice(0, 20)
const orgShortName = `expand_${uniqueSuffix}`
const username = `${orgShortName}@example.com`
let orgUUID
let createdUserUUID

await chai.request(app)
.post('/api/registry/org')
.set(secretariatHeaders)
.send({
short_name: orgShortName,
long_name: 'Expanded User Map Test Org',
authority: ['CNA'],
hard_quota: 1000
})
.then((res) => {
expect(res).to.have.status(200)
orgUUID = res.body.created.UUID
})

await chai.request(app)
.post(`/api/registry/org/${orgShortName}/user`)
.set(secretariatHeaders)
.send({
username,
name: {
first: 'Expanded',
last: 'User'
}
})
.then((res) => {
expect(res).to.have.status(200)
createdUserUUID = res.body.created.UUID
})

const conversationRes = await chai.request(app)
.post(`/api/conversation/target/${orgUUID}`)
.set(secretariatHeaders)
.send({
visibility: 'public',
body: 'This is a test conversation for expanded user metadata'
})

expect(conversationRes).to.have.status(200)
const secretariatUserUUID = conversationRes.body.author_id

await chai.request(app)
.get(`/api/registry/org/${orgShortName}?expand=users`)
.set(secretariatHeaders)
.then((res) => {
expect(res).to.have.status(200)
expect(res.body).to.have.property('_userMap')

expect(res.body._userMap).to.have.property(createdUserUUID)
expect(res.body._userMap[createdUserUUID]).to.deep.equal({
username,
name: {
first: 'Expanded',
last: 'User'
},
org: {
short_name: orgShortName
}
})

expect(res.body._userMap).to.have.property(secretariatUserUUID)
expect(res.body._userMap[secretariatUserUUID]).to.include({
username: 'test_secretariat_0@mitre.org'
})
expect(res.body._userMap[secretariatUserUUID].org).to.deep.equal({
short_name: 'mitre'
})
})
})
it('Strips author_id from conversations for non-secretariats', async () => {
// 1. Get win_5 org UUID
let win5UUID
Expand Down
Loading
Loading