Skip to content
Merged
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
@@ -1,7 +1,7 @@
{
"openapi": "3.0.2",
"info": {
"version": "2.7.3",
"version": "2.7.4",
"title": "CVE Services API",
"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>",
"contact": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cve-services",
"author": "Automation Working Group",
"version": "2.7.3",
"version": "2.7.4",
"license": "(CC0)",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ async function updateUser (req, res, next) {

We need to make sure that either way we convert to one or the other. For now, I am going shortname / username
*/
const session = await mongoose.startSession()
const session = await mongoose.startSession({ causalConsistency: false })
// Check to see if identifier is set
const identifier = req.ctx.params.identifier

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

const body = req.ctx.body

if ('secret' in body) {
logger.info({ uuid: req.ctx.uuid, message: 'User attempted to update the secret.' })
return res.status(400).json(error.secretUpdateNotAllowed())
}

const requestingUserParameters = {
org: req.ctx.org,
username: req.ctx.user
Expand All @@ -248,12 +253,24 @@ async function updateUser (req, res, next) {
: await userRepo.findOneByUsernameAndOrgShortname(userToEditParameters.username, userToEditParameters.org, { session })

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

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

if (body.org_short_name) {
const targetOrg = await orgRepo.findOneByShortName(body.org_short_name)
if (!targetOrg) {
logger.info({ uuid: req.ctx.uuid, message: `Target organization ${body.org_short_name} does not exist.` })
return res.status(404).json(error.orgDnePathParam(body.org_short_name))
}
}

if (body.org_short_name && isSecretariat && userToEditParameters.org === org.short_name && body.org_short_name === org.short_name) {
logger.info({ uuid: req.ctx.uuid, message: `User ${userToEditParameters.username} is already in organization ${userToEditParameters.org}.` })
return res.status(403).json(error.alreadyInOrg(org.short_name, userToEditParameters.username))
Expand Down Expand Up @@ -298,6 +315,7 @@ async function updateUser (req, res, next) {

let result
let updatedUser
let updatedUserUUID
try {
session.startTransaction()
try {
Expand Down Expand Up @@ -332,6 +350,8 @@ async function updateUser (req, res, next) {
}
}

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

return res.status(200).json(
Expand Down
7 changes: 7 additions & 0 deletions src/controller/user.controller/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ class UserControllerError extends idrErr.IDRError {
err.message = 'This information can only be viewed or modified by the Secretariat, an Org Admin or if the requester is the user.'
return err
}

secretUpdateNotAllowed () {
const err = {}
err.error = 'SECRET_UPDATE_NOT_ALLOWED'
err.message = 'The secret field must be updated through the reset_secret endpoint'
return err
}
}

module.exports = {
Expand Down
4 changes: 2 additions & 2 deletions src/middleware/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async function optionallyValidateUser (req, res, next) {
authenticated = false
} else {
result = await userRepo.findOneByUserNameAndOrgUUID(user, orgUUID)
if (!result || !result.active) {
if (!result || result.status === 'inactive') {
authenticated = false
} else {
const isPwd = await argon2.verify(result.secret, key)
Expand Down Expand Up @@ -133,7 +133,7 @@ async function validateUser (req, res, next) {
return res.status(401).json(error.unauthorized())
}

if (result.active === false || result.status === 'inactive') {
if (result.status === 'inactive') {
logger.warn(JSON.stringify({ uuid: req.ctx.uuid, message: 'User deactivated. Authentication failed for ' + user }))
return res.status(401).json(error.unauthorized())
}
Expand Down
25 changes: 18 additions & 7 deletions src/repositories/baseUserRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ const BaseOrgModel = require('../model/baseorg')
const RegistryUser = require('../model/registryuser')
const cryptoRandomString = require('crypto-random-string')
const UserRepository = require('./userRepository')
const _ = require('lodash')
const getConstants = require('../constants').getConstants
const _ = require('lodash')

const skipNulls = (objValue, srcValue) => {
if (_.isArray(objValue)) {
return srcValue
}
return undefined
}

/**
* @function setAggregateUserObj
Expand Down Expand Up @@ -514,19 +521,23 @@ class BaseUserRepository extends BaseRepository {
throw new Error('Legacy user not found')
}

const { ...incomingUserBody } = incomingUser
let legacyObjectRaw
let registryObjectRaw

if (!isRegistryObject) {
legacyObjectRaw = incomingUser
registryObjectRaw = this.convertLegacyToRegistry(incomingUser)
legacyObjectRaw = incomingUserBody
registryObjectRaw = this.convertLegacyToRegistry(incomingUserBody)
} else {
registryObjectRaw = incomingUser
legacyObjectRaw = this.convertRegistryToLegacy(incomingUser)
registryObjectRaw = incomingUserBody
legacyObjectRaw = this.convertRegistryToLegacy(incomingUserBody)
}

const updatedLegacyUser = _.merge(legacyUser, legacyObjectRaw)
const updatedRegistryUser = _.merge(registryUser, registryObjectRaw)
const protectedFieldsRegistry = ['_id', 'UUID', '__v', 'secret', 'created', 'last_updated']
const protectedFieldsLegacy = ['_id', 'UUID', '__v', 'secret', 'time', 'org_UUID']

const updatedRegistryUser = registryUser.overwrite(_.mergeWith(_.pick(registryUser.toObject(), protectedFieldsRegistry), registryObjectRaw, skipNulls))
const updatedLegacyUser = legacyUser.overwrite(_.mergeWith(_.pick(legacyUser.toObject(), protectedFieldsLegacy), legacyObjectRaw, skipNulls))

try {
if (incomingUser.org_short_name) {
Expand Down
2 changes: 1 addition & 1 deletion src/swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re
/* eslint-disable no-multi-str */
const doc = {
info: {
version: '2.7.3',
version: '2.7.4',
title: 'CVE Services API',
description: "The CVE Services API supports automation tooling for the CVE Program. Credentials are \
required for most service endpoints. Representatives of \
Expand Down
84 changes: 84 additions & 0 deletions test/integration-tests/cve-id/getCveIdTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,92 @@ describe('Testing Get CVE-ID endpoint', () => {
expect(cveIdObject.requested_by.user).to.equal(constants.nonSecretariatUserHeaders['CVE-API-USER'])
})
})
it('For Secretariat users, should return full information when getting a single RESERVED CVE ID', async () => {
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')

await chai.request(app)
.get(`/api/cve-id/${cveId}`)
.set(constants.headers)
.then(async (res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.cve_id).to.equal(cveId)
expect(res.body.state).to.equal('RESERVED')
// Secretariat user should see owning org details
expect(res.body.owning_cna).to.equal(constants.nonSecretariatUserHeaders['CVE-API-ORG'])
})
})

it('For owning CNA users, should return full information when getting a single RESERVED CVE ID', async () => {
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')

await chai.request(app)
.get(`/api/cve-id/${cveId}`)
.set(constants.nonSecretariatUserHeaders)
.then(async (res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.cve_id).to.equal(cveId)
expect(res.body.state).to.equal('RESERVED')
// Non-secretariat user from owning org should see owning org details
expect(res.body.owning_cna).to.equal(constants.nonSecretariatUserHeaders['CVE-API-ORG'])
})
})

it('For non-owning CNA users, should return partial information and redacted owning_cna when getting a single RESERVED CVE ID', async function () {
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')

await chai.request(app)
.get(`/api/cve-id/${cveId}`)
.set(constants.nonSecretariatUserHeaders3) // evidence_15
.then(async (res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.cve_id).to.equal(cveId)
expect(res.body.state).to.equal('RESERVED')
expect(res.body.owning_cna).to.equal('[REDACTED]')
expect(res.body).to.not.have.property('requested_by')
})
})
})
context('negative tests', () => {
it('An inactive user should be treated as unauthenticated for optionallyValidateUser endpoints (GET /api/cve-id/:id)', async function () {
const cveId = await helpers.cveIdReserveHelper(1, '2023', constants.nonSecretariatUserHeaders['CVE-API-ORG'], 'non-sequential')

// Deactivate user
await helpers.userDeactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])

await chai.request(app)
.get(`/api/cve-id/${cveId}`)
.set(constants.nonSecretariatUserHeaders)
.then(async (res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.cve_id).to.equal(cveId)
expect(res.body.owning_cna).to.equal('[REDACTED]') // Should be redacted because user is treated as unauthenticated

// Reactivate user for other tests
await helpers.userReactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])
})
})

it('An inactive user should be denied access for validateUser endpoints (GET /api/cve-id)', async function () {
// Deactivate user
await helpers.userDeactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])

await chai.request(app)
.get('/api/cve-id')
.set(constants.nonSecretariatUserHeaders)
.then(async (res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(401)
expect(res.body.error).to.equal('UNAUTHORIZED')

// Reactivate user for other tests
await helpers.userReactivateAsSecHelper(constants.nonSecretariatUserHeaders['CVE-API-USER'], constants.nonSecretariatUserHeaders['CVE-API-ORG'])
})
})

it('Feb 29 2100 should not be valid', async () => {
await chai.request(app)
.get('/api/cve-id?time_modified.gt=2100-02-29T00:00:00Z')
Expand Down
38 changes: 37 additions & 1 deletion test/integration-tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,40 @@ async function updateOwningOrgAsSecHelper (cveId, newOrgShortName) {
})
}

async function userDeactivateAsSecHelper (userName, orgShortName) {
const user = await chai.request(app)
.get(`/api/registry/org/${orgShortName}/user/${userName}`)
.set(constants.headers)
.then(res => res.body)

await chai.request(app)
.put(`/api/registry/org/${orgShortName}/user/${userName}`)
.set(constants.headers)
.send({ ...user, status: 'inactive' })
.then((res, err) => {
// Safety Expect
expect(res).to.have.status(200)
})
}

async function userReactivateAsSecHelper (userName, orgShortName) {
const user = await chai.request(app)
.get(`/api/registry/org/${orgShortName}/user/${userName}`)
.set(constants.headers)
.then(res => res.body)

user.status = 'active'

await chai.request(app)
.put(`/api/registry/org/${orgShortName}/user/${userName}`)
.set(constants.headers)
.send(user)
.then((res, err) => {
// Safety Expect
expect(res).to.have.status(200)
})
}

module.exports = {
cveIdReserveHelper,
cveIdBulkReserveHelper,
Expand All @@ -126,5 +160,7 @@ module.exports = {
cveUpdateAsSecHelper,
cveUpdateAsCnaHelperWithAdpContainer,
userOrgUpdateAsSecHelper,
updateOwningOrgAsSecHelper
updateOwningOrgAsSecHelper,
userDeactivateAsSecHelper,
userReactivateAsSecHelper
}
47 changes: 47 additions & 0 deletions test/integration-tests/user/updateUserTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,52 @@ describe('Testing Edit user endpoint', () => {
expect(res.body.error).to.contain('USER_ALREADY_IN_ORG')
})
})
it('Should return an error when attempting to update secret with registry enabled', async () => {
let user
await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body })
await chai.request(app)
.put('/api/registry/org/win_5/user/jasminesmith@win_5.com')
.set(constants.nonSecretariatUserHeaders)
.send({
...user,
secret: 'some_new_secret_hash'
})
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(400)
expect(res.body.error).to.equal('SECRET_UPDATE_NOT_ALLOWED')
})
})
it('Should return 404 when target organization in path does not exist', async () => {
const user = constants.headers['CVE-API-USER']
await chai.request(app)
.put(`/api/registry/org/non_existent_org/user/${user}`)
.set(constants.headers)
.send({
name: {
first: 'NewFirst',
last: 'NewLast'
}
})
.then((res) => {
expect(res).to.have.status(404)
expect(res.body.error).to.contain('ORG_DNE_PARAM')
})
})

it('Should return 404 when target organization in body does not exist', async () => {
const user = constants.headers['CVE-API-USER']
const org = constants.headers['CVE-API-ORG']
await chai.request(app)
.put(`/api/registry/org/${org}/user/${user}`)
.set(constants.headers)
.send({
org_short_name: 'non_existent_org'
})
.then((res) => {
expect(res).to.have.status(404)
expect(res.body.error).to.contain('ORG_DNE_PARAM')
})
})
})
})
6 changes: 4 additions & 2 deletions test/unit-tests/middleware/mockObjects.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const existentUser = {
},
secret: '$argon2i$v=19$m=4096,t=3,p=1$+qGHEfH5h4/tk404iWBxFw$xV96/b4NvQVvlZIq57wTS8s7gfKzsfMXRiOyf3ffgcw',
username: 'cpadro',
active: true
active: true,
status: 'active'
}

const deactivatedUser = {
Expand All @@ -49,7 +50,8 @@ const deactivatedUser = {
},
secret: '$argon2i$v=19$m=4096,t=3,p=1$+qGHEfH5h4/tk404iWBxFw$xV96/b4NvQVvlZIq57wTS8s7gfKzsfMXRiOyf3ffgcw',
username: 'flast',
active: false
active: false,
status: 'inactive'
}

module.exports = {
Expand Down
Loading