Skip to content

Commit

Permalink
chore: get audit events working for page view events (#342)
Browse files Browse the repository at this point in the history
* chore: get audit events working for page view events

* chore: make typing better
  • Loading branch information
ReedSoftware authored Nov 25, 2024
1 parent 33609b3 commit e1e2b07
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ generic-service:
# serviceAccountName: "hmpps-challenge-support-intervention-plan-ui"

replicaCount: 4
serviceAccountName: hmpps-csip-ui

image:
repository: quay.io/hmpps/hmpps-challenge-support-intervention-plan-ui
Expand Down Expand Up @@ -54,9 +55,9 @@ generic-service:
csip-ui-sentry:
SENTRY_DSN: "SENTRY_DSN"
SENTRY_LOADER_SCRIPT_ID: "SENTRY_LOADER_SCRIPT_ID"
#sqs-hmpps-audit-secret:
# AUDIT_SQS_QUEUE_URL: "sqs_queue_url"
# AUDIT_SQS_QUEUE_NAME: "sqs_queue_name"
sqs-hmpps-audit-secret:
AUDIT_SQS_QUEUE_URL: "sqs_queue_url"
AUDIT_SQS_QUEUE_NAME: "sqs_queue_name"

allowlist:
groups:
Expand Down
5 changes: 4 additions & 1 deletion helm_deploy/values-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ generic-service:
DPS_HOME_PAGE_URL: "https://digital-dev.prison.service.justice.gov.uk"
PRISONER_PROFILE_URL: "https://prisoner-dev.digital.prison.service.justice.gov.uk"
ENVIRONMENT_NAME: DEV # Phase banner tag label
AUDIT_ENABLED: "false"
AUDIT_ENABLED: "true"
SENTRY_ENVIRONMENT: DEV
AUDIT_SQS_REGION: "eu-west-2"
AUDIT_SERVICE_NAME: "hmpps-challenge-support-intervention-plan-ui"


generic-prometheus-alerts:
alertSeverity: move-and-improve-alerts-non-prod
Expand Down
2 changes: 2 additions & 0 deletions helm_deploy/values-preprod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ generic-service:
ENVIRONMENT_NAME: PRE-PRODUCTION # Phase banner tag label
AUDIT_ENABLED: "false"
SENTRY_ENVIRONMENT: PRE-PRODUCTION
AUDIT_SQS_REGION: "eu-west-2"
AUDIT_SERVICE_NAME: "hmpps-challenge-support-intervention-plan-ui"

generic-prometheus-alerts:
alertSeverity: move-and-improve-alerts-non-prod
Expand Down
2 changes: 2 additions & 0 deletions helm_deploy/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ generic-service:
PRISONER_PROFILE_URL: "https://prisoner.digital.prison.service.justice.gov.uk"
AUDIT_ENABLED: "false"
SENTRY_ENVIRONMENT: PRODUCTION
AUDIT_SQS_REGION: "eu-west-2"
AUDIT_SERVICE_NAME: "hmpps-challenge-support-intervention-plan-ui"

generic-prometheus-alerts:
alertSeverity: move-and-improve-alerts
8 changes: 8 additions & 0 deletions server/@types/express/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ export declare global {
validationErrors?: fieldErrors
digitalPrisonServicesUrl: string
breadcrumbs: Breadcrumbs
auditEvent: {
pageNameSuffix: string
who: string
subjectId?: string
subjectType?: string
correlationId?: string
details?: Record<string, unknown>
}
prisoner?: PrisonerSummary
feComponentsMeta?: {
activeCaseLoad: CaseLoad
Expand Down
36 changes: 36 additions & 0 deletions server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dpsComponents from '@ministryofjustice/hmpps-connect-dps-components'
import * as Sentry from '@sentry/node'
// @ts-expect-error Import untyped middleware for cypress coverage
import cypressCoverage from '@cypress/code-coverage/middleware/express'
import { validate } from 'uuid'
import nunjucksSetup from './utils/nunjucksSetup'
import errorHandler from './errorHandler'
import { appInsightsMiddleware } from './utils/azureAppInsights'
Expand Down Expand Up @@ -53,6 +54,41 @@ export default function createApp(services: Services): express.Application {
app.use(setUpStaticResources())
nunjucksSetup(app)
app.use(setUpAuthentication())
app.use((req, res, next) => {
const hasJourneyId = validate(req.originalUrl.split('/')[1])
res.locals.auditEvent = {
pageNameSuffix: hasJourneyId
? `${req.originalUrl.split('/')[1]}/${req.originalUrl.replace(/\?.*/, '').split('/').slice(2).join('/')}` // JOURNEYID_PAGE
: req.originalUrl.replace(/\?.*/, ''), // PAGE
who: res.locals.user.username,
correlationId: req.id,
}

res.prependOnceListener('close', async () => {
await services.auditService.logPageView(
req.originalUrl,
req.journeyData,
req.query,
res.locals.auditEvent,
`ACCESS_ATTEMPT_`,
)
})

type resRenderCb = (view: string, options?: object, callback?: (err: Error, html: string) => void) => void
const resRender = res.render as resRenderCb
res.render = (view: string, options?) => {
resRender.call(res, view, options, async (err: Error, html: string) => {
if (err) {
res.status(500).send(err)
return
}
await services.auditService.logPageView(req.originalUrl, req.journeyData, req.query, res.locals.auditEvent)
res.send(html)
})
}
next()
})

app.use(authorisationMiddleware())
app.use(setUpCsrf())
app.use(setUpCurrentUser())
Expand Down
1 change: 1 addition & 0 deletions server/middleware/insertJourneyIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default function insertJourneyIdentifier() {
if (!validate(uuid)) {
return res.redirect(`${req.baseUrl}/${uuidV4()}${req.url}`)
}
res.locals.auditEvent.pageNameSuffix = `${req.url.split('/')[1]}/${req.url.replace(/\?.*/, '').split('/').slice(2).join('/')}` // JOURNEYID_PAGE
return next()
}
}
2 changes: 2 additions & 0 deletions server/middleware/populatePrisonerSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export default function populatePrisonerSummary(): RequestHandler {
return async (req, res, next) => {
if (req.journeyData.prisoner) {
res.locals['prisoner'] = req.journeyData.prisoner
res.locals.auditEvent.subjectId = req.journeyData.prisoner.prisonerNumber
res.locals.auditEvent.subjectType = req.journeyData.prisoner.prisonerNumber
}
next()
}
Expand Down
6 changes: 2 additions & 4 deletions server/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { type RequestHandler, Router } from 'express'

import asyncMiddleware from '../middleware/asyncMiddleware'
import type { Services } from '../services'
import insertJourneyIdentifier from '../middleware/insertJourneyIdentifier'
Expand All @@ -15,9 +14,6 @@ export default function routes(services: Services): Router {
const controller = new HomePageController(services.csipApiService)
const get = (path: string | string[], handler: RequestHandler) => router.get(path, asyncMiddleware(handler))

// TODO: determine whether we need to implement audit service
// import { Page } from '../services/auditService'
// await services.auditService.logPageView(Page.EXAMPLE_PAGE, { who: res.locals.user.username, correlationId: req.id })
get('/', controller.GET)

router.use('/csip-records/:recordUuid', CsipRecordRoutes(services))
Expand All @@ -27,6 +23,7 @@ export default function routes(services: Services): Router {
)

router.use(insertJourneyIdentifier())

router.use(
redirectCheckAnswersMiddleware([
/on-behalf-of$/,
Expand All @@ -46,6 +43,7 @@ export default function routes(services: Services): Router {
/record-review\/next-review-date$/,
]),
)

router.use(journeyStateMachine())
router.use('/:journeyId', JourneyRoutes(services))

Expand Down
23 changes: 13 additions & 10 deletions server/services/auditService.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import AuditService, { Page } from './auditService'
import AuditService from './auditService'
import HmppsAuditClient from '../data/hmppsAuditClient'

jest.mock('../data/hmppsAuditClient')
Expand Down Expand Up @@ -41,19 +41,22 @@ describe('Audit service', () => {

describe('logPageView', () => {
it('sends page view event audit message using audit client', async () => {
await auditService.logPageView(Page.EXAMPLE_PAGE, {
who: 'user1',
subjectId: 'subject123',
subjectType: 'exampleType',
correlationId: 'request123',
details: { extraDetails: 'example' },
})
await auditService.logPageView(
'/csip-records/0192d2fb-6920-749f-86fb-f0c6c2deaec8',
{},
{ extraDetails: 'example' },
{
pageNameSuffix: 'EXAMPLE_PAGE',
who: 'user1',
correlationId: 'request123',
},
)

expect(hmppsAuditClient.sendMessage).toHaveBeenCalledWith({
what: 'PAGE_VIEW_EXAMPLE_PAGE',
who: 'user1',
subjectId: 'subject123',
subjectType: 'exampleType',
subjectId: '0192d2fb-6920-749f-86fb-f0c6c2deaec8',
subjectType: '0192d2fb-6920-749f-86fb-f0c6c2deaec8',
correlationId: 'request123',
details: { extraDetails: 'example' },
})
Expand Down
22 changes: 19 additions & 3 deletions server/services/auditService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Request, Response } from 'express'
import HmppsAuditClient, { AuditEvent } from '../data/hmppsAuditClient'
import { JourneyData } from '../@types/express'

export enum Page {
EXAMPLE_PAGE = 'EXAMPLE_PAGE',
Expand All @@ -19,10 +21,24 @@ export default class AuditService {
await this.hmppsAuditClient.sendMessage(event)
}

async logPageView(page: Page, eventDetails: PageViewEventDetails) {
async logPageView(
requestUrl: string,
journeyData: Partial<JourneyData>,
query: Request['query'],
auditEvent: Response['locals']['auditEvent'],
pagePrefix: string = '',
) {
const { pageNameSuffix, ...auditEventProperties } = auditEvent

const csipFromUrl = requestUrl.includes('csip-record') ? requestUrl.split('/').filter(Boolean)[1] : undefined
const csipIdInRequest = csipFromUrl || journeyData?.csipRecord?.recordUuid

const event: AuditEvent = {
...eventDetails,
what: `PAGE_VIEW_${page}`,
...auditEventProperties,
...(query ? { details: query } : {}),
...(csipIdInRequest ? { subjectId: csipIdInRequest } : {}),
...(csipIdInRequest ? { subjectType: csipIdInRequest } : {}),
what: `PAGE_VIEW_${pagePrefix + pageNameSuffix}`,
}
await this.hmppsAuditClient.sendMessage(event)
}
Expand Down

0 comments on commit e1e2b07

Please sign in to comment.