Skip to content

Commit afc616b

Browse files
authored
Merge pull request #1552 from CVEProject/emathew/1531-audit-collection-model
Resolves issue 1531, 1532, 1533, creating Audit Collection
2 parents 2593cc6 + 75344a0 commit afc616b

File tree

12 files changed

+1131
-5
lines changed

12 files changed

+1131
-5
lines changed
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
const mongoose = require('mongoose')
2+
const logger = require('../../middleware/logger')
3+
const errors = require('./error')
4+
const error = new errors.AuditControllerError()
5+
const validateUUID = require('uuid').validate
6+
7+
/**
8+
* Create a new audit document
9+
* Called by POST /api/audit/org/
10+
*/
11+
async function createAuditDocument (req, res, next) {
12+
try {
13+
const session = await mongoose.startSession()
14+
const repo = req.ctx.repositories.getAuditRepository()
15+
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
16+
const body = req.ctx.body
17+
let returnValue
18+
19+
if (body?.uuid ?? null) {
20+
return res.status(400).json(error.uuidProvided('audit'))
21+
}
22+
23+
if (!body.target_uuid) {
24+
logger.info({ uuid: req.ctx.uuid, message: 'Missing required field: target_uuid' })
25+
return res.status(400).json(error.missingRequiredField('target_uuid'))
26+
}
27+
28+
if (!validateUUID(body.target_uuid)) {
29+
logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' })
30+
return res.status(400).json(error.invalidUUID('target_uuid'))
31+
}
32+
33+
try {
34+
session.startTransaction()
35+
36+
// Validate the audit document against the schema
37+
const auditValidation = await repo.validateAudit(body, { session })
38+
if (!auditValidation.isValid) {
39+
logger.error({ uuid: req.ctx.uuid, message: 'Audit document validation FAILED' })
40+
await session.abortTransaction()
41+
return res.status(400).json(
42+
error.invalidAuditObject()
43+
)
44+
}
45+
46+
// Check if audit document already exists
47+
const exists = await repo.findOneByTargetUUID(body.target_uuid, { session })
48+
if (exists) {
49+
logger.info({ uuid: req.ctx.uuid, message: `Audit document was not created because one already exists for target_uuid: ${body.target_uuid}` })
50+
await session.abortTransaction()
51+
return res.status(400).json(error.auditExists(body.target_uuid))
52+
}
53+
54+
// Check if target org exists first
55+
const targetOrg = await orgRepo.getOrg(body.target_uuid, true, { session })
56+
if (!targetOrg) {
57+
logger.info({ uuid: req.ctx.uuid, message: `No organization found with UUID ${body.target_uuid}` })
58+
await session.abortTransaction()
59+
return res.status(404).json(error.orgDne(body.target_uuid))
60+
}
61+
62+
// Validate initial history entries if provided
63+
if (body.history && body.history.length > 0) {
64+
for (const entry of body.history) {
65+
if (!entry.audit_object) {
66+
logger.info({ uuid: req.ctx.uuid, message: 'Missing audit_object in history entry' })
67+
await session.abortTransaction()
68+
return res.status(400).json(error.missingRequiredField('audit_object'))
69+
}
70+
if (!entry.change_author) {
71+
logger.info({ uuid: req.ctx.uuid, message: 'Missing change_author in history entry' })
72+
await session.abortTransaction()
73+
return res.status(400).json(error.missingRequiredField('change_author'))
74+
}
75+
}
76+
}
77+
returnValue = await repo.createAuditDocument(body, { session })
78+
await session.commitTransaction()
79+
80+
logger.info({
81+
uuid: req.ctx.uuid,
82+
message: `Audit document created for target_uuid ${body.target_uuid}`,
83+
audit_uuid: returnValue.uuid
84+
})
85+
} catch (err) {
86+
await session.abortTransaction()
87+
throw err
88+
} finally {
89+
await session.endSession()
90+
}
91+
92+
return res.status(200).json({ message: 'Audit ' + returnValue.uuid + ' was successfully created.', created: returnValue })
93+
} catch (err) {
94+
next(err)
95+
}
96+
}
97+
98+
/**
99+
* Append a new entry to the audit history (Secretariat only)
100+
* Called by PUT /api/audit/org/
101+
* Allows for multiple appends in a single request
102+
*/
103+
async function appendToAuditHistory (req, res, next) {
104+
try {
105+
const session = await mongoose.startSession()
106+
const repo = req.ctx.repositories.getAuditRepository()
107+
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
108+
const body = req.ctx.body
109+
let returnValue
110+
111+
// Requiring target_uuid to validate audit_object easily.
112+
// TODO: will need to query by uuid instead if target_uuid should be optional in the future
113+
if (!body.target_uuid) {
114+
logger.info({ uuid: req.ctx.uuid, message: 'Missing required field: target_uuid' })
115+
return res.status(400).json(error.missingRequiredField('target_uuid'))
116+
}
117+
118+
if (!validateUUID(body.target_uuid)) {
119+
logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' })
120+
return res.status(400).json(error.invalidUUID('target_uuid'))
121+
}
122+
123+
try {
124+
session.startTransaction()
125+
126+
// Validate the audit document against the schema
127+
const auditValidation = await repo.validateAudit(body, { session })
128+
if (!auditValidation.isValid) {
129+
logger.error({ uuid: req.ctx.uuid, message: 'Audit document validation FAILED' })
130+
await session.abortTransaction()
131+
return res.status(400).json(
132+
error.invalidAuditObject()
133+
)
134+
}
135+
136+
// Check if target org exists first
137+
const targetOrg = await orgRepo.getOrg(body.target_uuid, true, { session })
138+
if (!targetOrg) {
139+
logger.info({ uuid: req.ctx.uuid, message: `No organization found with UUID ${body.target_uuid}` })
140+
await session.abortTransaction()
141+
return res.status(404).json(error.orgDne(body.target_uuid))
142+
}
143+
// Process each history entry
144+
for (const entry of body.history) {
145+
if (!entry.audit_object) {
146+
logger.info({ uuid: req.ctx.uuid, message: 'Missing audit_object in history entry' })
147+
await session.abortTransaction()
148+
return res.status(400).json(error.missingRequiredField('audit_object'))
149+
}
150+
151+
// Append this history entry
152+
returnValue = await repo.appendToAuditHistory(
153+
body.target_uuid,
154+
entry.audit_object,
155+
entry.change_author,
156+
{ session }
157+
)
158+
159+
if (!returnValue) {
160+
logger.info({ uuid: req.ctx.uuid, message: `No audit document found for target_uuid ${body.target_uuid}` })
161+
await session.abortTransaction()
162+
return res.status(404).json(error.auditDneByTarget(body.target_uuid))
163+
}
164+
}
165+
166+
await session.commitTransaction()
167+
168+
logger.info({
169+
uuid: req.ctx.uuid,
170+
message: `${body.history.length} audit entry(ies) appended for target_uuid ${body.target_uuid}`,
171+
change_author: body.change_author
172+
})
173+
} catch (err) {
174+
await session.abortTransaction()
175+
throw err
176+
} finally {
177+
await session.endSession()
178+
}
179+
180+
return res.status(200).json({
181+
message: `${body.history.length} audit entry(ies) for ${body.target_uuid} was successfully appended.`,
182+
updated: returnValue
183+
})
184+
} catch (err) {
185+
next(err)
186+
}
187+
}
188+
189+
/**
190+
* Get all audit documents
191+
* Called by GET /api/audit/org/
192+
*/
193+
async function getAllAuditDocuments (req, res, next) {
194+
try {
195+
const session = await mongoose.startSession()
196+
const repo = req.ctx.repositories.getAuditRepository()
197+
let returnValue
198+
199+
try {
200+
returnValue = await repo.findAllAuditDocuments({ session })
201+
} finally {
202+
await session.endSession()
203+
}
204+
205+
logger.info({ uuid: req.ctx.uuid, message: 'All audit documents sent to user' })
206+
return res.status(200).json(returnValue)
207+
} catch (err) {
208+
next(err)
209+
}
210+
}
211+
212+
/**
213+
* Get audit document by its document UUID
214+
* Called by GET /api/audit/org/document/:document_uuid
215+
*/
216+
async function getAuditByDocumentUUID (req, res, next) {
217+
try {
218+
const session = await mongoose.startSession()
219+
const repo = req.ctx.repositories.getAuditRepository()
220+
const documentUUID = req.ctx.params.document_uuid
221+
let returnValue
222+
223+
if (!documentUUID) {
224+
logger.info({ uuid: req.ctx.uuid, message: 'Missing audit uuid parameter' })
225+
return res.status(400).json(error.missingRequiredField('document_uuid'))
226+
}
227+
228+
if (!validateUUID(documentUUID)) {
229+
logger.info({ uuid: req.ctx.uuid, message: 'Invalid document_uuid format' })
230+
return res.status(400).json(error.invalidUUID('document_uuid'))
231+
}
232+
233+
try {
234+
returnValue = await repo.findOneByUUID(documentUUID, { session })
235+
236+
if (!returnValue) {
237+
logger.info({ uuid: req.ctx.uuid, message: `No audit document found with UUID ${documentUUID}` })
238+
return res.status(404).json(error.auditDneByDocument(documentUUID))
239+
}
240+
} finally {
241+
await session.endSession()
242+
}
243+
244+
logger.info({ uuid: req.ctx.uuid, message: `Audit document ${documentUUID} sent to user` })
245+
return res.status(200).json(returnValue)
246+
} catch (err) {
247+
next(err)
248+
}
249+
}
250+
/**
251+
* Get audit history by target UUID
252+
* Called by GET /api/audit/org/:target_uuid
253+
* TODO: remove comment-> I changed parameter name from org_identifier to target_uuid to be more generic.
254+
*/
255+
async function getAuditByTargetUUID (req, res, next) {
256+
try {
257+
const session = await mongoose.startSession()
258+
const repo = req.ctx.repositories.getAuditRepository()
259+
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
260+
const targetUUID = req.ctx.params.target_uuid
261+
let returnValue
262+
263+
if (!targetUUID) {
264+
logger.info({ uuid: req.ctx.uuid, message: 'Missing target_uuid parameter' })
265+
return res.status(400).json(error.missingRequiredField('target_uuid'))
266+
}
267+
268+
if (!validateUUID(targetUUID)) {
269+
logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' })
270+
return res.status(400).json(error.invalidUUID('target_uuid'))
271+
}
272+
273+
try {
274+
session.startTransaction()
275+
276+
// Find the target organization
277+
const targetOrg = await orgRepo.findOneByUUID(targetUUID, { session })
278+
if (!targetOrg) {
279+
logger.info({ uuid: req.ctx.uuid, message: `No organization found with UUID ${targetUUID}` })
280+
await session.abortTransaction()
281+
return res.status(404).json(error.orgDne(targetUUID))
282+
}
283+
284+
// TODO: confirm middleware is checking admin and secretariat permissions properly
285+
286+
returnValue = await repo.findOneByTargetUUID(targetUUID, { session })
287+
288+
if (!returnValue) {
289+
logger.info({ uuid: req.ctx.uuid, message: `No audit history found for target UUID ${targetUUID}` })
290+
await session.abortTransaction()
291+
return res.status(404).json(error.auditDneByTarget(targetUUID))
292+
}
293+
294+
await session.commitTransaction()
295+
} catch (err) {
296+
await session.abortTransaction()
297+
throw err
298+
} finally {
299+
await session.endSession()
300+
}
301+
302+
logger.info({
303+
uuid: req.ctx.uuid,
304+
message: `Audit history for target UUID ${targetUUID} sent to user ${req.ctx.user}`
305+
})
306+
return res.status(200).json(returnValue)
307+
} catch (err) {
308+
next(err)
309+
}
310+
}
311+
312+
/**
313+
* Get last X changes for an organization
314+
* Called by GET /api/audit/org/:target_uuid/:number_of_changes
315+
*/
316+
async function getLastXChanges (req, res, next) {
317+
try {
318+
const session = await mongoose.startSession()
319+
const repo = req.ctx.repositories.getAuditRepository()
320+
const targetUUID = req.ctx.params.target_uuid
321+
const numberOfChanges = parseInt(req.ctx.params.number_of_changes)
322+
let returnValue
323+
324+
if (!targetUUID) {
325+
logger.info({ uuid: req.ctx.uuid, message: 'Missing org_identifier parameter' })
326+
return res.status(400).json(error.missingRequiredField('org_identifier'))
327+
}
328+
329+
if (!validateUUID(targetUUID)) {
330+
logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' })
331+
return res.status(400).json(error.invalidUUID('target_uuid'))
332+
}
333+
334+
if (isNaN(numberOfChanges) || numberOfChanges < 1) {
335+
logger.info({ uuid: req.ctx.uuid, message: 'Invalid number_of_changes parameter' })
336+
return res.status(400).json(error.invalidNumberOfChanges())
337+
}
338+
339+
try {
340+
session.startTransaction()
341+
342+
const lastChanges = await repo.getLastXChanges(targetUUID, numberOfChanges, { session })
343+
344+
if (!lastChanges || lastChanges.length === 0) {
345+
logger.info({ uuid: req.ctx.uuid, message: `No audit history found for organization ${targetUUID}` })
346+
await session.abortTransaction()
347+
return res.status(404).json(error.auditDneByTarget(targetUUID))
348+
}
349+
350+
returnValue = {
351+
target_uuid: targetUUID,
352+
changes: lastChanges
353+
}
354+
355+
await session.commitTransaction()
356+
} catch (err) {
357+
await session.abortTransaction()
358+
throw err
359+
} finally {
360+
await session.endSession()
361+
}
362+
363+
logger.info({
364+
uuid: req.ctx.uuid,
365+
message: `Last ${numberOfChanges} changes for ${targetUUID} sent to user ${req.ctx.user}`
366+
})
367+
return res.status(200).json(returnValue)
368+
} catch (err) {
369+
next(err)
370+
}
371+
}
372+
373+
module.exports = {
374+
AUDIT_CREATE_SINGLE: createAuditDocument,
375+
AUDIT_UPDATE: appendToAuditHistory,
376+
AUDIT_GET_ALL: getAllAuditDocuments,
377+
AUDIT_GET_BY_UUID: getAuditByDocumentUUID,
378+
AUDIT_GET_BY_TARGET_UUID: getAuditByTargetUUID,
379+
AUDIT_GET_LAST: getLastXChanges
380+
}

0 commit comments

Comments
 (0)