Skip to content

Commit c7b95e6

Browse files
authored
New: [AEA-5947] - delegated access (#2147)
## Summary - ✨ New Feature ### Details - Adapt expected delegated access headers to Spine API
1 parent 51d749a commit c7b95e6

File tree

7 files changed

+378
-18
lines changed

7 files changed

+378
-18
lines changed

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"timonwong.shellcheck",
3636
"mkhl.direnv",
3737
"github.vscode-github-actions",
38-
"Orta.vscode-jest"
38+
"Orta.vscode-jest",
39+
"jebbs.plantuml"
3940
],
4041
"settings": {
4142
"python.defaultInterpreterPath": "/workspaces/prescriptionsforpatients/.venv/bin/python",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
@startuml
2+
title: AEA-5947: (TO BE) Proxy Access Flow for Get My Prescriptions
3+
4+
participant User
5+
participant "NHS App" as App
6+
participant "NHS App Backend" as AppBackend
7+
participant "Apigee" as Apigee
8+
participant "ProxyRules" as ProxyRules
9+
participant "Step Functions StateMachine" as StateMachine
10+
participant "GetMyPrescriptions Lambda" as GmpLambda
11+
participant "psu Get Status Updates Lambda" as GsuLambda
12+
participant "EnrichPrescriptions Lambda" as EpLambda
13+
participant SpineClient
14+
participant Spine
15+
16+
User -> App: Request
17+
App -> AppBackend: Request API
18+
AppBackend -> Apigee: Call PfP API
19+
Apigee -> ProxyRules: Forward request
20+
ProxyRules -> ProxyRules: Preflow
21+
note right #FF9999
22+
Oauth token validation etc. is unchanged
23+
NEW: Sets delegated-access.enabled and IgnoreUnresolvedVariables to true
24+
end note
25+
ProxyRules -> ProxyRules: AddPatientAccessHeader
26+
note right
27+
Sets NHSD-NHSLogin-User to PX:JWT claim NHS number
28+
end note
29+
ProxyRules -> ProxyRules: AM-Add-Delegation-Headers
30+
note right #FF9999
31+
NEW: Sets new headers, completely separate to NHSD-NHSLogin-User
32+
end note
33+
ProxyRules -> ProxyRules: OverridePatientAccessHeader
34+
note right
35+
Overwrites NHSD-NHSLogin-User with P9:request header X-NHS-NUMBER
36+
end note
37+
ProxyRules -> StateMachine: Forward request
38+
StateMachine -> GmpLambda: Forward request
39+
activate GmpLambda
40+
GmpLambda -> GmpLambda: adaptHeadersToSpine(headers)
41+
note right #FF9999
42+
As well as the existing behaviour that sends spine the same values for
43+
both NHSD-NHSLogin-User (actor) and nhsNumber (subject)
44+
these are now separated if delegated-access.enabled is true
45+
end note
46+
GmpLambda -> SpineClient: getPrescriptions(*all* headers)
47+
SpineClient -> Spine: get request
48+
activate Spine
49+
Spine -> Spine: _createContext
50+
note right #FF9999
51+
NEW: Add actor to context
52+
end note
53+
== other calls, not least the actual query ==
54+
Spine -> Spine: auditSarAccessRequest
55+
note right #FF9999
56+
NEW: Add actor to SAR0001 log
57+
end note
58+
Spine -> SpineClient: Response
59+
deactivate Spine
60+
SpineClient -> GmpLambda
61+
GmpLambda -> StateMachine: Response
62+
deactivate GmpLambda
63+
StateMachine -> GsuLambda: Forward response
64+
GsuLambda -> SpineClient: getStatus(*all* headers)
65+
SpineClient -> Spine: get request
66+
Spine -> SpineClient: Response
67+
SpineClient -> StateMachine: Response
68+
GsuLambda -> StateMachine:
69+
StateMachine -> EpLambda: Forward response
70+
EpLambda -> StateMachine:
71+
StateMachine -> ProxyRules: Response
72+
note right #FF9999
73+
NEW: This is happy path but we must add RaiseFault flow too
74+
end note
75+
ProxyRules -> Apigee: Response
76+
Apigee -> App: Forward response
77+
App -> User: Display result
78+
@enduml

docs/pfp-AS-IS.puml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@startuml
2+
title: AEA-5947: (AS IS) Proxy Access Flow for Get My Prescriptions
3+
4+
participant User
5+
participant "NHS App" as App
6+
participant "NHS App Backend" as AppBackend
7+
participant "Apigee" as Apigee
8+
participant "ProxyRules" as ProxyRules
9+
participant "Step Functions StateMachine" as StateMachine
10+
participant "GetMyPrescriptions Lambda" as GmpLambda
11+
participant "psu Get Status Updates Lambda" as GsuLambda
12+
participant "EnrichPrescriptions Lambda" as EpLambda
13+
participant SpineClient
14+
participant "Spine Patient Facing Prescriptions" as Spine
15+
16+
User -> App: Request
17+
App -> AppBackend: Request API
18+
AppBackend -> Apigee: Call PfP API
19+
Apigee -> ProxyRules: Forward request
20+
ProxyRules -> ProxyRules: Preflow
21+
note right
22+
Includes OAuth token validation etc.
23+
end note
24+
ProxyRules -> ProxyRules: AddPatientAccessHeader
25+
note right
26+
Sets NHSD-NHSLogin-User to PX:JWT claim NHS number
27+
end note
28+
ProxyRules -> ProxyRules: OverridePatientAccessHeader
29+
note right
30+
Overwrites NHSD-NHSLogin-User with P9:request header X-NHS-NUMBER
31+
TODO who sets X-NHS-NUMBER? Is it trusted?
32+
TODO given this is within an Azure pipeline condition does it ever get called?
33+
end note
34+
ProxyRules -> StateMachine: Forward request
35+
StateMachine -> GmpLambda: Forward request
36+
activate GmpLambda
37+
GmpLambda -> GmpLambda: extractNHSNumber(headers["nhsd-nhslogin-user"])
38+
GmpLambda -> SpineClient: getPrescriptions(*all* headers)
39+
SpineClient -> Spine: get request
40+
activate Spine
41+
Spine -> Spine: _createContext
42+
== other calls, not least the actual query ==
43+
Spine -> Spine: auditSarAccessRequest
44+
Spine -> SpineClient: Response
45+
deactivate Spine
46+
SpineClient -> GmpLambda
47+
GmpLambda -> StateMachine: Response
48+
deactivate GmpLambda
49+
StateMachine -> GsuLambda: Forward response
50+
GsuLambda -> SpineClient: getStatus(*all* headers)
51+
SpineClient -> Spine: get request
52+
Spine -> SpineClient: Response
53+
SpineClient -> StateMachine: Response
54+
GsuLambda -> StateMachine:
55+
StateMachine -> EpLambda: Forward response
56+
EpLambda -> StateMachine:
57+
StateMachine -> ProxyRules: Response
58+
ProxyRules -> Apigee: Response
59+
Apigee -> App: Forward response
60+
App -> User: Display result
61+
@enduml

packages/getMyPrescriptions/src/extractNHSNumber.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function extractNHSNumber(nhsloginUser: string | undefined): string {
2020
if (authLevel !== "P9") {
2121
throw new NHSNumberValidationError("Identity proofing level is not P9")
2222
}
23+
return validateNHSNumber(nhsNumber)
24+
}
25+
26+
export function validateNHSNumber(nhsNumber: string): string {
2327
// convert numbers to strings, for internal consistency
2428
if (Number.isInteger(nhsNumber)) {
2529
nhsNumber = nhsNumber.toString()
@@ -42,7 +46,7 @@ export function extractNHSNumber(nhsloginUser: string | undefined): string {
4246

4347
// Do the check digits match?
4448
if (checkDigit !== Number(providedCheckDigit)) {
45-
throw new NHSNumberValidationError("invalid check digit in NHS number")
49+
throw new NHSNumberValidationError(`Invalid check digit in NHS number ${nhsNumber}`)
4650
}
4751
return nhsNumber
4852
}

packages/getMyPrescriptions/src/getMyPrescriptions.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
TraceIDs,
2424
ResponseFunc
2525
} from "./responses"
26-
import {extractNHSNumber, NHSNumberValidationError} from "./extractNHSNumber"
26+
import {extractNHSNumber, NHSNumberValidationError, validateNHSNumber} from "./extractNHSNumber"
2727
import {deepCopy, hasTimedOut, jobWithTimeout} from "./utils"
2828
import {buildStatusUpdateData, shouldGetStatusUpdates} from "./statusUpdate"
2929
import {extractOdsCodes, isolateOperationOutcome} from "./fhirUtils"
@@ -38,6 +38,8 @@ const servicesCache: ServicesCache = {}
3838
const LAMBDA_TIMEOUT_MS = 10_000
3939
const SPINE_TIMEOUT_MS = 9_000
4040
const SERVICE_SEARCH_TIMEOUT_MS = 5_000
41+
export const DELEGATED_ACCESS_HDR = "delegatedaccess"
42+
export const DELEGATED_ACCESS_SUB_HDR = "x-nhsd-subject-nhs-number"
4143

4244
type EventHeaders = Record<string, string | undefined>
4345

@@ -75,19 +77,16 @@ async function eventHandler(
7577
successResponse: ResponseFunc,
7678
includeStatusUpdateData: boolean = false
7779
): Promise<LambdaResult> {
78-
const xRequestId = headers["x-request-id"]
79-
const requestId = headers["apigw-request-id"]
80-
const spineClient = params.spineClient
81-
8280
const traceIDs: TraceIDs = {
8381
"nhsd-correlation-id": headers["nhsd-correlation-id"],
84-
"x-request-id": xRequestId,
82+
"x-request-id": headers["x-request-id"],
8583
"nhsd-request-id": headers["nhsd-request-id"],
8684
"x-correlation-id": headers["x-correlation-id"],
87-
"apigw-request-id": requestId
85+
"apigw-request-id": headers["apigw-request-id"]
8886
}
8987
logger.appendKeys(traceIDs)
9088

89+
const spineClient = params.spineClient
9190
const applicationName = headers["nhsd-application-name"] ?? "unknown"
9291

9392
try {
@@ -96,10 +95,8 @@ async function eventHandler(
9695
return SPINE_CERT_NOT_CONFIGURED_RESPONSE
9796
}
9897

99-
const nhsNumber = extractNHSNumber(headers["nhsd-nhslogin-user"])
100-
logger.info(`nhsNumber: ${nhsNumber}`, {nhsNumber})
101-
headers["nhsNumber"] = nhsNumber
102-
if (await params.pfpConfig.isTC008(nhsNumber)) {
98+
headers = adaptHeadersToSpine(headers)
99+
if (await params.pfpConfig.isTC008(headers["nhsNumber"]!)) {
103100
logger.info("Test NHS number corresponding to TC008 has been received. Returning a 500 response")
104101
return TC008_ERROR_RESPONSE
105102
}
@@ -111,7 +108,7 @@ async function eventHandler(
111108
return TIMEOUT_RESPONSE
112109
}
113110
const searchsetBundle: Bundle = response.data
114-
searchsetBundle.id = xRequestId
111+
searchsetBundle.id = traceIDs["x-request-id"] || "unknown"
115112

116113
const operationOutcomes = isolateOperationOutcome(searchsetBundle)
117114
operationOutcomes.forEach((operationOutcome) => {
@@ -124,7 +121,8 @@ async function eventHandler(
124121
+ "They have these relevant ODS codes, and the PfP request was made via this apigee application.",
125122
{
126123
ODSCodes,
127-
nhsNumber,
124+
actorNhsNumber: headers["nhsd-nhslogin-user"],
125+
subjectNhsNumber: headers["nhsNumber"],
128126
applicationName
129127
}
130128
)
@@ -141,10 +139,15 @@ async function eventHandler(
141139
timeout: SERVICE_SEARCH_TIMEOUT_MS,
142140
message: `The request to the distance selling service timed out after ${SERVICE_SEARCH_TIMEOUT_MS}ms.`
143141
})
144-
return await successResponse(logger, nhsNumber, searchsetBundle, traceIDs, params.pfpConfig, statusUpdateData)
142+
return await successResponse(
143+
logger, headers["nhsNumber"]!, searchsetBundle, traceIDs, params.pfpConfig, statusUpdateData
144+
)
145145
}
146146

147-
return await successResponse(logger, nhsNumber, distanceSellingBundle, traceIDs, params.pfpConfig, statusUpdateData)
147+
return await successResponse(
148+
logger, headers["nhsNumber"]!, distanceSellingBundle, traceIDs,
149+
params.pfpConfig, statusUpdateData
150+
)
148151
} catch (error) {
149152
if (error instanceof NHSNumberValidationError) {
150153
return INVALID_NHS_NUMBER_RESPONSE
@@ -154,6 +157,28 @@ async function eventHandler(
154157
}
155158
}
156159

160+
export function adaptHeadersToSpine(headers: EventHeaders): EventHeaders {
161+
// AEA-3344 introduces delegated access using different headers
162+
logger.debug("Testing if delegated access enabled", {headers})
163+
if (!headers[DELEGATED_ACCESS_HDR] || headers[DELEGATED_ACCESS_HDR].toLowerCase() !== "true") {
164+
logger.info("Subject access request detected")
165+
headers["nhsNumber"] = extractNHSNumber(headers["nhsd-nhslogin-user"])
166+
} else {
167+
logger.info("Delegated access request detected")
168+
let subjectNHSNumber = headers[DELEGATED_ACCESS_SUB_HDR]
169+
if (!subjectNHSNumber) {
170+
throw new NHSNumberValidationError(`${DELEGATED_ACCESS_SUB_HDR} header not present for delegated access`)
171+
}
172+
if (subjectNHSNumber.indexOf(":") > -1) {
173+
logger.warn(`${DELEGATED_ACCESS_SUB_HDR} is not expected to be prefixed by proofing level, but is, removing it`)
174+
subjectNHSNumber = subjectNHSNumber.split(":")[1]
175+
}
176+
headers["nhsNumber"] = validateNHSNumber(subjectNHSNumber)
177+
}
178+
logger.info(`after setting subject nhsNumber`, {headers})
179+
return headers
180+
}
181+
157182
type HandlerConfig<T> = {
158183
handlerFunction: (event: T, config: HandlerParams) => Promise<LambdaResult>
159184
middleware: Array<middy.MiddlewareObj>

0 commit comments

Comments
 (0)