Skip to content

Commit d371eb3

Browse files
authored
Merge pull request #1835 from CVEProject/af-1715
Resolves Issue #1715, Add expanded user map to registry org response
2 parents a6d0939 + 15c4385 commit d371eb3

12 files changed

Lines changed: 849 additions & 26 deletions

File tree

api-docs/openapi.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2318,6 +2318,18 @@
23182318
},
23192319
"description": "The shortname or UUID of the registry organization"
23202320
},
2321+
{
2322+
"name": "expand",
2323+
"in": "query",
2324+
"required": false,
2325+
"schema": {
2326+
"type": "string",
2327+
"enum": [
2328+
"users"
2329+
]
2330+
},
2331+
"description": "Optional expanded related data. Accepted value: users."
2332+
},
23212333
{
23222334
"$ref": "#/components/parameters/apiEntityHeader"
23232335
},
@@ -7751,4 +7763,4 @@
77517763
}
77527764
}
77537765
}
7754-
}
7766+
}

schemas/registry-org/get-registry-org-response.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,49 @@
220220
}
221221
},
222222
"description": "List of conversation messages associated with the organization"
223+
},
224+
"_userMap": {
225+
"type": "object",
226+
"additionalProperties": {
227+
"type": "object",
228+
"properties": {
229+
"username": {
230+
"type": "string",
231+
"description": "User's identifier or username"
232+
},
233+
"name": {
234+
"type": "object",
235+
"properties": {
236+
"first": {
237+
"type": "string",
238+
"description": "User's first name"
239+
},
240+
"last": {
241+
"type": "string",
242+
"description": "User's last name"
243+
},
244+
"middle": {
245+
"type": "string",
246+
"description": "User's middle name"
247+
},
248+
"suffix": {
249+
"type": "string",
250+
"description": "User's name suffix"
251+
}
252+
}
253+
},
254+
"org": {
255+
"type": "object",
256+
"properties": {
257+
"short_name": {
258+
"type": "string",
259+
"description": "Short name of the organization associated with the user"
260+
}
261+
}
262+
}
263+
}
264+
},
265+
"description": "Map of expanded user UUIDs to display metadata, included when expand=users is requested"
223266
}
224267
}
225268
}

src/controller/org.controller/index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,15 @@ router.get('/registry/org/:identifier',
280280
<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>
281281
<p><b>Secretariat:</b> Retrieves information about any registry organization</p>"
282282
#swagger.parameters['identifier'] = { description: 'The shortname or UUID of the registry organization' }
283+
#swagger.parameters['expand'] = {
284+
in: 'query',
285+
description: 'Optional expanded related data. Accepted value: users.',
286+
required: false,
287+
schema: {
288+
type: 'string',
289+
enum: ['users']
290+
}
291+
}
283292
#swagger.parameters['$ref'] = [
284293
'#/components/parameters/apiEntityHeader',
285294
'#/components/parameters/apiUserHeader',
@@ -338,7 +347,9 @@ router.get('/registry/org/:identifier',
338347
*/
339348
mw.useRegistry(),
340349
mw.validateUser,
341-
query().custom((query) => { return mw.validateQueryParameterNames(query, ['']) }),
350+
query().custom((query) => { return mw.validateQueryParameterNames(query, ['expand']) }),
351+
query(['expand']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
352+
query(['expand']).optional().isIn(['users']),
342353
parseError,
343354
parseGetParams,
344355
registryOrgController.SINGLE_ORG

src/controller/org.controller/org.middleware.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ function parsePostParams (req, res, next) {
376376

377377
function parseGetParams (req, res, next) {
378378
utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier'])
379-
utils.reqCtxMapping(req, 'query', ['page'])
379+
utils.reqCtxMapping(req, 'query', ['page', 'expand'])
380380
next()
381381
}
382382

src/controller/registry-org.controller/registry-org.controller.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,89 @@ const convoError = new conversationErrors.ConversationControllerError()
99
const validateUUID = require('uuid').validate
1010
const authContext = require('../../utils/authContext')
1111

12+
function addUUIDsToSet (uuidSet, values) {
13+
if (!Array.isArray(values)) return
14+
values.filter(Boolean).forEach(uuid => uuidSet.add(uuid))
15+
}
16+
17+
function addConversationAuthorUUIDsToSet (uuidSet, conversations) {
18+
if (!Array.isArray(conversations)) return
19+
conversations.forEach(conversation => {
20+
if (conversation.author_id) {
21+
uuidSet.add(conversation.author_id)
22+
}
23+
})
24+
}
25+
26+
function asPlainObject (value) {
27+
return typeof value?.toObject === 'function' ? value.toObject() : value
28+
}
29+
30+
function buildOrgShortNameByUserUUID (orgs, requestedUserUUIDs) {
31+
const orgShortNameByUserUUID = {}
32+
orgs.forEach(org => {
33+
const orgObject = asPlainObject(org)
34+
if (!Array.isArray(orgObject.users)) return
35+
36+
orgObject.users.forEach(userUUID => {
37+
if (requestedUserUUIDs.has(userUUID)) {
38+
orgShortNameByUserUUID[userUUID] = orgObject.short_name
39+
}
40+
})
41+
})
42+
return orgShortNameByUserUUID
43+
}
44+
45+
async function buildUserMapForUUIDs (userUUIDs, userRepo, orgRepo) {
46+
if (userUUIDs.size === 0) {
47+
return {}
48+
}
49+
50+
const userUUIDList = Array.from(userUUIDs)
51+
const users = await userRepo.findUsersByUUIDs(userUUIDList)
52+
const orgs = await orgRepo.findOrgsByUserUUIDs(userUUIDList)
53+
const orgShortNameByUserUUID = buildOrgShortNameByUserUUID(orgs, userUUIDs)
54+
55+
return users.reduce((userMap, user) => {
56+
const userObject = asPlainObject(user)
57+
if (!userUUIDs.has(userObject.UUID)) return userMap
58+
const orgShortName = orgShortNameByUserUUID[userObject.UUID]
59+
userMap[userObject.UUID] = _.omitBy({
60+
username: userObject.username,
61+
name: userObject.name,
62+
org: orgShortName ? { short_name: orgShortName } : undefined
63+
}, _.isUndefined)
64+
return userMap
65+
}, {})
66+
}
67+
68+
async function buildOrgUserMap (org, userRepo, orgRepo) {
69+
const orgObject = asPlainObject(org)
70+
const userUUIDs = new Set()
71+
addUUIDsToSet(userUUIDs, orgObject.users)
72+
addUUIDsToSet(userUUIDs, orgObject.admins)
73+
addUUIDsToSet(userUUIDs, orgObject.contact_info?.additional_contacts)
74+
75+
return buildUserMapForUUIDs(userUUIDs, userRepo, orgRepo)
76+
}
77+
78+
async function buildUserMap (org, userRepo, orgRepo) {
79+
const orgObject = asPlainObject(org)
80+
const userUUIDs = new Set()
81+
addUUIDsToSet(userUUIDs, orgObject.users)
82+
addUUIDsToSet(userUUIDs, orgObject.admins)
83+
addUUIDsToSet(userUUIDs, orgObject.contact_info?.additional_contacts)
84+
addConversationAuthorUUIDsToSet(userUUIDs, orgObject.conversation)
85+
86+
return buildUserMapForUUIDs(userUUIDs, userRepo, orgRepo)
87+
}
88+
89+
function removeUserUUIDFields (org) {
90+
delete org.users
91+
delete org.admins
92+
_.unset(org, 'contact_info.additional_contacts')
93+
}
94+
1295
/**
1396
* Retrieves information about all registry organizations.
1497
*
@@ -101,7 +184,21 @@ async function getOrg (req, res, next) {
101184
if (isSecretariat) {
102185
returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid'])) : undefined
103186
} else {
104-
returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid', 'visibility'])) : undefined
187+
returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid', 'visibility', 'author_id'])) : undefined
188+
}
189+
190+
if (req.ctx.query?.expand === 'users') {
191+
const userRepo = req.ctx.repositories.getBaseUserRepository()
192+
const isAdminOfRequestedOrg = !isSecretariat && await userRepo.isAdmin(req.ctx.user, returnValue.short_name, {})
193+
if (isSecretariat) {
194+
returnValue._userMap = await buildUserMap(returnValue, userRepo, repo)
195+
} else if (isAdminOfRequestedOrg) {
196+
returnValue._userMap = await buildOrgUserMap(requesterOrg, userRepo, repo)
197+
}
198+
}
199+
200+
if (!isSecretariat) {
201+
removeUserUUIDFields(returnValue)
105202
}
106203
}
107204
} catch (error) {

src/repositories/baseOrgRepository.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,18 @@ class BaseOrgRepository extends BaseRepository {
243243
return null
244244
}
245245

246+
async findOrgsByUserUUIDs (userUUIDs, options = {}) {
247+
if (!Array.isArray(userUUIDs) || userUUIDs.length === 0) {
248+
return []
249+
}
250+
251+
return await BaseOrgModel.find(
252+
{ users: { $in: userUUIDs } },
253+
{ _id: 0, UUID: 1, short_name: 1, users: 1 },
254+
options
255+
)
256+
}
257+
246258
/**
247259
* @function hasRole
248260
* @description Checks if an organization object has the requested role.

src/repositories/baseUserRepository.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,18 @@ class BaseUserRepository extends BaseRepository {
204204
return user || null
205205
}
206206

207+
async findUsersByUUIDs (uuids, options = {}) {
208+
if (!Array.isArray(uuids) || uuids.length === 0) {
209+
return []
210+
}
211+
212+
return await BaseUser.find(
213+
{ UUID: { $in: uuids } },
214+
{ _id: 0, UUID: 1, username: 1, name: 1 },
215+
options
216+
)
217+
}
218+
207219
/**
208220
* @async
209221
* @function isUserAdminOfOrgUUID

src/repositories/conversationRepository.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ const uuid = require('uuid')
22
const ConversationModel = require('../model/conversation')
33
const BaseRepository = require('./baseRepository')
44

5+
const SECRETARIAT_AUTHOR_NAME = 'Secretariat'
6+
7+
function normalizeConversationAuthorName (conversation) {
8+
if (conversation.author_role === 'Secretariat') {
9+
conversation.author_name = SECRETARIAT_AUTHOR_NAME
10+
}
11+
return conversation
12+
}
13+
514
class ConversationRepository extends BaseRepository {
615
constructor () {
716
super(ConversationModel)
@@ -23,7 +32,11 @@ class ConversationRepository extends BaseRepository {
2332
}
2433
]
2534
const pg = await this.aggregatePaginate(agt, options)
26-
const data = { conversations: pg.itemsList }
35+
const data = {
36+
conversations: pg.itemsList.map(conversation => normalizeConversationAuthorName(
37+
typeof conversation.toObject === 'function' ? conversation.toObject() : conversation
38+
))
39+
}
2740
if (pg.itemCount >= options.limit) {
2841
data.totalCount = pg.itemCount
2942
data.itemsPerPage = pg.itemsPerPage
@@ -48,6 +61,7 @@ class ConversationRepository extends BaseRepository {
4861
}
4962
})
5063
return conversations.map(convo => convo.toObject()).filter(conv => isSecretariat || conv.visibility === 'public').map(conv => {
64+
normalizeConversationAuthorName(conv)
5165
if (!isSecretariat && conv.author_role === 'Secretariat') {
5266
delete conv.author_id
5367
delete conv.author_name
@@ -89,7 +103,7 @@ class ConversationRepository extends BaseRepository {
89103
previous_conversation_uuid: latestConversation?.UUID || null,
90104
next_conversation_uuid: null,
91105
author_id: user.UUID,
92-
author_name: getUserFullName(user),
106+
author_name: isSecretariat ? SECRETARIAT_AUTHOR_NAME : getUserFullName(user),
93107
author_role: isSecretariat ? 'Secretariat' : 'Partner',
94108
edited_at: null,
95109
visibility: !isSecretariat ? 'public' : (['public', 'private'].includes(body.visibility?.toLowerCase()) ? body.visibility.toLowerCase() : 'private'),

0 commit comments

Comments
 (0)