Skip to content

Commit 09e58a6

Browse files
Merge pull request #1746 from CVEProject/dev
Updating Test to v2.7.4
2 parents 15238d1 + b55cef8 commit 09e58a6

11 files changed

Lines changed: 225 additions & 18 deletions

File tree

api-docs/openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"openapi": "3.0.2",
33
"info": {
4-
"version": "2.7.3",
4+
"version": "2.7.4",
55
"title": "CVE Services API",
66
"description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of <a href='https://www.cve.org/ProgramOrganization/CNAs'>CVE Numbering Authorities (CNAs)</a> should use one of the methods below to obtain credentials: <ul><li>If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials</li> <li>Contact your Root (<a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/Google'>Google</a>, <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/INCIBE'>INCIBE</a>, <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/jpcert'>JPCERT/CC</a>, or <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/redhat'>Red Hat</a>) or Top-Level Root (<a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/icscert'>CISA ICS</a> or <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/mitre'>MITRE</a>) to request credentials </ul> <p>CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located <a href='https://github.com/CVEProject/cve-schema/releases/tag/v5.2.0' target='_blank'>here</a>.</p> <a href='https://cveform.mitre.org/' class='link' target='_blank'>Contact the CVE Services team</a>",
77
"contact": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cve-services",
33
"author": "Automation Working Group",
4-
"version": "2.7.3",
4+
"version": "2.7.4",
55
"license": "(CC0)",
66
"devDependencies": {
77
"@faker-js/faker": "^7.6.0",

src/controller/registry-user.controller/registry-user.controller.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ async function updateUser (req, res, next) {
213213
214214
We need to make sure that either way we convert to one or the other. For now, I am going shortname / username
215215
*/
216-
const session = await mongoose.startSession()
216+
const session = await mongoose.startSession({ causalConsistency: false })
217217
// Check to see if identifier is set
218218
const identifier = req.ctx.params.identifier
219219

@@ -227,6 +227,11 @@ async function updateUser (req, res, next) {
227227

228228
const body = req.ctx.body
229229

230+
if ('secret' in body) {
231+
logger.info({ uuid: req.ctx.uuid, message: 'User attempted to update the secret.' })
232+
return res.status(400).json(error.secretUpdateNotAllowed())
233+
}
234+
230235
const requestingUserParameters = {
231236
org: req.ctx.org,
232237
username: req.ctx.user
@@ -248,12 +253,24 @@ async function updateUser (req, res, next) {
248253
: await userRepo.findOneByUsernameAndOrgShortname(userToEditParameters.username, userToEditParameters.org, { session })
249254

250255
const org = await orgRepo.findOneByShortName(userToEditParameters.org)
256+
if (!org) {
257+
logger.info({ uuid: req.ctx.uuid, message: `Target organization ${userToEditParameters.org} does not exist.` })
258+
return res.status(404).json(error.orgDnePathParam(userToEditParameters.org))
259+
}
251260

252261
if (body.org_short_name && !isSecretariat) {
253262
logger.info({ uuid: req.ctx.uuid, message: 'Only Secretariat can reassign user organization.' })
254263
return res.status(403).json(error.notAllowedToChangeOrganization())
255264
}
256265

266+
if (body.org_short_name) {
267+
const targetOrg = await orgRepo.findOneByShortName(body.org_short_name)
268+
if (!targetOrg) {
269+
logger.info({ uuid: req.ctx.uuid, message: `Target organization ${body.org_short_name} does not exist.` })
270+
return res.status(404).json(error.orgDnePathParam(body.org_short_name))
271+
}
272+
}
273+
257274
if (body.org_short_name && isSecretariat && userToEditParameters.org === org.short_name && body.org_short_name === org.short_name) {
258275
logger.info({ uuid: req.ctx.uuid, message: `User ${userToEditParameters.username} is already in organization ${userToEditParameters.org}.` })
259276
return res.status(403).json(error.alreadyInOrg(org.short_name, userToEditParameters.username))
@@ -298,6 +315,7 @@ async function updateUser (req, res, next) {
298315

299316
let result
300317
let updatedUser
318+
let updatedUserUUID
301319
try {
302320
session.startTransaction()
303321
try {
@@ -332,6 +350,8 @@ async function updateUser (req, res, next) {
332350
}
333351
}
334352

353+
// UUID of the user will not change, lets get it before we write to avoid read after write issues.
354+
updatedUserUUID = await userRepo.getUserUUID(req.ctx.user, org.UUID)
335355
updatedUser = await userRepo.updateUserFull(userToEdit.UUID, body, { session })
336356
await session.commitTransaction()
337357
} catch (error) {
@@ -346,9 +366,9 @@ async function updateUser (req, res, next) {
346366
change: userToEditParameters.username + ' was successfully updated.',
347367
req_UUID: req.ctx.uuid,
348368
org_UUID: org.UUID,
349-
user: updatedUser
369+
user: updatedUser,
370+
user_UUID: updatedUserUUID
350371
}
351-
payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID)
352372
logger.info(JSON.stringify(payload))
353373

354374
return res.status(200).json(

src/controller/user.controller/error.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ class UserControllerError extends idrErr.IDRError {
6464
err.message = 'This information can only be viewed or modified by the Secretariat, an Org Admin or if the requester is the user.'
6565
return err
6666
}
67+
68+
secretUpdateNotAllowed () {
69+
const err = {}
70+
err.error = 'SECRET_UPDATE_NOT_ALLOWED'
71+
err.message = 'The secret field must be updated through the reset_secret endpoint'
72+
return err
73+
}
6774
}
6875

6976
module.exports = {

src/middleware/middleware.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async function optionallyValidateUser (req, res, next) {
6161
authenticated = false
6262
} else {
6363
result = await userRepo.findOneByUserNameAndOrgUUID(user, orgUUID)
64-
if (!result || !result.active) {
64+
if (!result || result.status === 'inactive') {
6565
authenticated = false
6666
} else {
6767
const isPwd = await argon2.verify(result.secret, key)
@@ -133,7 +133,7 @@ async function validateUser (req, res, next) {
133133
return res.status(401).json(error.unauthorized())
134134
}
135135

136-
if (result.active === false || result.status === 'inactive') {
136+
if (result.status === 'inactive') {
137137
logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User deactivated. Authentication failed for ' + user }))
138138
return res.status(401).json(error.unauthorized())
139139
}

src/repositories/baseUserRepository.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ const BaseOrgModel = require('../model/baseorg')
77
const RegistryUser = require('../model/registryuser')
88
const cryptoRandomString = require('crypto-random-string')
99
const UserRepository = require('./userRepository')
10-
const _ = require('lodash')
1110
const getConstants = require('../constants').getConstants
11+
const _ = require('lodash')
12+
13+
const skipNulls = (objValue, srcValue) => {
14+
if (_.isArray(objValue)) {
15+
return srcValue
16+
}
17+
return undefined
18+
}
1219

1320
/**
1421
* @function setAggregateUserObj
@@ -514,19 +521,23 @@ class BaseUserRepository extends BaseRepository {
514521
throw new Error('Legacy user not found')
515522
}
516523

524+
const { ...incomingUserBody } = incomingUser
517525
let legacyObjectRaw
518526
let registryObjectRaw
519527

520528
if (!isRegistryObject) {
521-
legacyObjectRaw = incomingUser
522-
registryObjectRaw = this.convertLegacyToRegistry(incomingUser)
529+
legacyObjectRaw = incomingUserBody
530+
registryObjectRaw = this.convertLegacyToRegistry(incomingUserBody)
523531
} else {
524-
registryObjectRaw = incomingUser
525-
legacyObjectRaw = this.convertRegistryToLegacy(incomingUser)
532+
registryObjectRaw = incomingUserBody
533+
legacyObjectRaw = this.convertRegistryToLegacy(incomingUserBody)
526534
}
527535

528-
const updatedLegacyUser = _.merge(legacyUser, legacyObjectRaw)
529-
const updatedRegistryUser = _.merge(registryUser, registryObjectRaw)
536+
const protectedFieldsRegistry = ['_id', 'UUID', '__v', 'secret', 'created', 'last_updated']
537+
const protectedFieldsLegacy = ['_id', 'UUID', '__v', 'secret', 'time', 'org_UUID']
538+
539+
const updatedRegistryUser = registryUser.overwrite(_.mergeWith(_.pick(registryUser.toObject(), protectedFieldsRegistry), registryObjectRaw, skipNulls))
540+
const updatedLegacyUser = legacyUser.overwrite(_.mergeWith(_.pick(legacyUser.toObject(), protectedFieldsLegacy), legacyObjectRaw, skipNulls))
530541

531542
try {
532543
if (incomingUser.org_short_name) {

src/swagger.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re
2222
/* eslint-disable no-multi-str */
2323
const doc = {
2424
info: {
25-
version: '2.7.3',
25+
version: '2.7.4',
2626
title: 'CVE Services API',
2727
description: "The CVE Services API supports automation tooling for the CVE Program. Credentials are \
2828
required for most service endpoints. Representatives of \

test/integration-tests/cve-id/getCveIdTest.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,92 @@ describe('Testing Get CVE-ID endpoint', () => {
188188
expect(cveIdObject.requested_by.user).to.equal(constants.nonSecretariatUserHeaders['CVE-API-USER'])
189189
})
190190
})
191+
it('For Secretariat users, should return full information when getting a single RESERVED CVE ID', async () => {
192+
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')
193+
194+
await chai.request(app)
195+
.get(`/api/cve-id/${cveId}`)
196+
.set(constants.headers)
197+
.then(async (res, err) => {
198+
expect(err).to.be.undefined
199+
expect(res).to.have.status(200)
200+
expect(res.body.cve_id).to.equal(cveId)
201+
expect(res.body.state).to.equal('RESERVED')
202+
// Secretariat user should see owning org details
203+
expect(res.body.owning_cna).to.equal(constants.nonSecretariatUserHeaders['CVE-API-ORG'])
204+
})
205+
})
206+
207+
it('For owning CNA users, should return full information when getting a single RESERVED CVE ID', async () => {
208+
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')
209+
210+
await chai.request(app)
211+
.get(`/api/cve-id/${cveId}`)
212+
.set(constants.nonSecretariatUserHeaders)
213+
.then(async (res, err) => {
214+
expect(err).to.be.undefined
215+
expect(res).to.have.status(200)
216+
expect(res.body.cve_id).to.equal(cveId)
217+
expect(res.body.state).to.equal('RESERVED')
218+
// Non-secretariat user from owning org should see owning org details
219+
expect(res.body.owning_cna).to.equal(constants.nonSecretariatUserHeaders['CVE-API-ORG'])
220+
})
221+
})
222+
223+
it('For non-owning CNA users, should return partial information and redacted owning_cna when getting a single RESERVED CVE ID', async function () {
224+
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')
225+
226+
await chai.request(app)
227+
.get(`/api/cve-id/${cveId}`)
228+
.set(constants.nonSecretariatUserHeaders3) // evidence_15
229+
.then(async (res, err) => {
230+
expect(err).to.be.undefined
231+
expect(res).to.have.status(200)
232+
expect(res.body.cve_id).to.equal(cveId)
233+
expect(res.body.state).to.equal('RESERVED')
234+
expect(res.body.owning_cna).to.equal('[REDACTED]')
235+
expect(res.body).to.not.have.property('requested_by')
236+
})
237+
})
191238
})
192239
context('negative tests', () => {
240+
it('An inactive user should be treated as unauthenticated for optionallyValidateUser endpoints (GET /api/cve-id/:id)', async function () {
241+
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')
242+
243+
// Deactivate user
244+
await helpers.userDeactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])
245+
246+
await chai.request(app)
247+
.get(`/api/cve-id/${cveId}`)
248+
.set(constants.nonSecretariatUserHeaders)
249+
.then(async (res, err) => {
250+
expect(err).to.be.undefined
251+
expect(res).to.have.status(200)
252+
expect(res.body.cve_id).to.equal(cveId)
253+
expect(res.body.owning_cna).to.equal('[REDACTED]') // Should be redacted because user is treated as unauthenticated
254+
255+
// Reactivate user for other tests
256+
await helpers.userReactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])
257+
})
258+
})
259+
260+
it('An inactive user should be denied access for validateUser endpoints (GET /api/cve-id)', async function () {
261+
// Deactivate user
262+
await helpers.userDeactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])
263+
264+
await chai.request(app)
265+
.get('/api/cve-id')
266+
.set(constants.nonSecretariatUserHeaders)
267+
.then(async (res, err) => {
268+
expect(err).to.be.undefined
269+
expect(res).to.have.status(401)
270+
expect(res.body.error).to.equal('UNAUTHORIZED')
271+
272+
// Reactivate user for other tests
273+
await helpers.userReactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])
274+
})
275+
})
276+
193277
it('Feb 29 2100 should not be valid', async () => {
194278
await chai.request(app)
195279
.get('/api/cve-id?time_modified.gt=2100-02-29T00:00:00Z')

test/integration-tests/helpers.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,40 @@ async function updateOwningOrgAsSecHelper (cveId, newOrgShortName) {
116116
})
117117
}
118118

119+
async function userDeactivateAsSecHelper (userName, orgShortName) {
120+
const user = await chai.request(app)
121+
.get(`/api/registry/org/${orgShortName}/user/${userName}`)
122+
.set(constants.headers)
123+
.then(res => res.body)
124+
125+
await chai.request(app)
126+
.put(`/api/registry/org/${orgShortName}/user/${userName}`)
127+
.set(constants.headers)
128+
.send({ ...user, status: 'inactive' })
129+
.then((res, err) => {
130+
// Safety Expect
131+
expect(res).to.have.status(200)
132+
})
133+
}
134+
135+
async function userReactivateAsSecHelper (userName, orgShortName) {
136+
const user = await chai.request(app)
137+
.get(`/api/registry/org/${orgShortName}/user/${userName}`)
138+
.set(constants.headers)
139+
.then(res => res.body)
140+
141+
user.status = 'active'
142+
143+
await chai.request(app)
144+
.put(`/api/registry/org/${orgShortName}/user/${userName}`)
145+
.set(constants.headers)
146+
.send(user)
147+
.then((res, err) => {
148+
// Safety Expect
149+
expect(res).to.have.status(200)
150+
})
151+
}
152+
119153
module.exports = {
120154
cveIdReserveHelper,
121155
cveIdBulkReserveHelper,
@@ -126,5 +160,7 @@ module.exports = {
126160
cveUpdateAsSecHelper,
127161
cveUpdateAsCnaHelperWithAdpContainer,
128162
userOrgUpdateAsSecHelper,
129-
updateOwningOrgAsSecHelper
163+
updateOwningOrgAsSecHelper,
164+
userDeactivateAsSecHelper,
165+
userReactivateAsSecHelper
130166
}

test/integration-tests/user/updateUserTest.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,52 @@ describe('Testing Edit user endpoint', () => {
189189
expect(res.body.error).to.contain('USER_ALREADY_IN_ORG')
190190
})
191191
})
192+
it('Should return an error when attempting to update secret with registry enabled', async () => {
193+
let user
194+
await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
195+
await chai.request(app)
196+
.put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
197+
.set(constants.nonSecretariatUserHeaders)
198+
.send({
199+
...user,
200+
secret: 'some_new_secret_hash'
201+
})
202+
.then((res, err) => {
203+
expect(err).to.be.undefined
204+
expect(res).to.have.status(400)
205+
expect(res.body.error).to.equal('SECRET_UPDATE_NOT_ALLOWED')
206+
})
207+
})
208+
it('Should return 404 when target organization in path does not exist', async () => {
209+
const user = constants.headers['CVE-API-USER']
210+
await chai.request(app)
211+
.put(`/api/registry/org/non_existent_org/user/${user}`)
212+
.set(constants.headers)
213+
.send({
214+
name: {
215+
first: 'NewFirst',
216+
last: 'NewLast'
217+
}
218+
})
219+
.then((res) => {
220+
expect(res).to.have.status(404)
221+
expect(res.body.error).to.contain('ORG_DNE_PARAM')
222+
})
223+
})
224+
225+
it('Should return 404 when target organization in body does not exist', async () => {
226+
const user = constants.headers['CVE-API-USER']
227+
const org = constants.headers['CVE-API-ORG']
228+
await chai.request(app)
229+
.put(`/api/registry/org/${org}/user/${user}`)
230+
.set(constants.headers)
231+
.send({
232+
org_short_name: 'non_existent_org'
233+
})
234+
.then((res) => {
235+
expect(res).to.have.status(404)
236+
expect(res.body.error).to.contain('ORG_DNE_PARAM')
237+
})
238+
})
192239
})
193240
})

0 commit comments

Comments
 (0)