Skip to content

Commit 9ef5994

Browse files
committed
Migrate over state machine
1 parent 8d15827 commit 9ef5994

6 files changed

Lines changed: 411 additions & 21 deletions

File tree

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,17 @@ cdk-deploy:
257257
npm run cdk-deploy --workspace packages/cdk
258258

259259
cdk-synth:
260-
CDK_CONFIG_stackName=psu-api \
260+
CDK_CONFIG_stackName=psu-cdk \
261+
CDK_CONFIG_samStackName=psu \
262+
CDK_CONFIG_logRetentionInDays=30 \
263+
CDK_CONFIG_logLevel=DEBUG \
264+
CDK_CONFIG_environment=dev \
265+
CDK_CONFIG_forwardCsocLogs=false \
266+
CDK_CONFIG_deployCheckPrescriptionStatusUpdate=true \
267+
CDK_CONFIG_exposeGetStatusUpdates=false \
268+
CDK_CONFIG_enablePostDatedNotifications=false \
269+
CDK_CONFIG_requireApplicationName=false \
270+
CDK_CONFIG_enableBackup=false \
261271
npm run cdk-synth --workspace packages/cdk
262272

263273
cdk-diff:

packages/cdk/nagSuppressions.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,80 @@
1+
/* eslint-disable max-len */
12
import {Stack} from "aws-cdk-lib"
3+
import {safeAddNagSuppression, safeAddNagSuppressionGroup} from "@nhsdigital/eps-cdk-constructs"
24

3-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4-
export const nagSuppressions = (_stack: Stack) => {
5-
// Nag suppressions will be added here as resources are migrated
5+
export const nagSuppressions = (stack: Stack) => {
6+
// State machine log policies require wildcard for log streams and log delivery actions
7+
safeAddNagSuppressionGroup(
8+
stack,
9+
[
10+
"/PsuStatelessStack/StateMachines/UpdatePrescriptionStatusStateMachine/StateMachinePutLogsManagedPolicy/Resource",
11+
"/PsuStatelessStack/StateMachines/Format1UpdatePrescriptionsStatusStateMachine/StateMachinePutLogsManagedPolicy/Resource"
12+
],
13+
[
14+
{
15+
id: "AwsSolutions-IAM5",
16+
reason: "Wildcard on log-stream is required to write to any log stream under the log group. Wildcard on Resource::* is required for log delivery management actions (DescribeLogGroups, ListLogDeliveries, etc.) which do not support resource-level permissions."
17+
}
18+
]
19+
)
20+
21+
// API Gateway does not use request validation — validation is handled by service logic
22+
safeAddNagSuppression(
23+
stack,
24+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/Resource",
25+
[
26+
{
27+
id: "AwsSolutions-APIG2",
28+
reason: "Request validation is handled by the backend service logic and FHIR validation state machine, not at the API Gateway level."
29+
}
30+
]
31+
)
32+
33+
// API Gateway CloudWatch role uses AWS managed policy
34+
safeAddNagSuppression(
35+
stack,
36+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/CloudWatchRole/Resource",
37+
[
38+
{
39+
id: "AwsSolutions-IAM4",
40+
reason: "AWS managed policy AmazonAPIGatewayPushToCloudWatchLogs is the standard approach for API Gateway logging and is maintained by AWS.",
41+
appliesTo: ["Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"]
42+
}
43+
]
44+
)
45+
46+
// API Gateway stage is not associated with WAFv2 — WAF is managed externally via Apigee
47+
safeAddNagSuppression(
48+
stack,
49+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/DeploymentStage.prod/Resource",
50+
[
51+
{
52+
id: "AwsSolutions-APIG3",
53+
reason: "WAF is managed externally via the Apigee proxy layer, not at the API Gateway level."
54+
}
55+
]
56+
)
57+
58+
// API methods do not use authorization — mTLS and Apigee handle auth externally
59+
safeAddNagSuppressionGroup(
60+
stack,
61+
[
62+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/Default/POST/Resource",
63+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/Default/format-1/POST/Resource",
64+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/Default/notification-delivery-status-callback/POST/Resource",
65+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/Default/_status/GET/Resource",
66+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/Default/metadata/GET/Resource",
67+
"/PsuStatelessStack/Apis/RestApiGateway/ApiGateway/Default/checkprescriptionstatusupdates/GET/Resource"
68+
],
69+
[
70+
{
71+
id: "AwsSolutions-APIG4",
72+
reason: "Authorization is handled externally via mutual TLS and the Apigee API gateway proxy. API Gateway methods do not require an additional authorizer."
73+
},
74+
{
75+
id: "AwsSolutions-COG4",
76+
reason: "This API does not use Cognito for authentication. Auth is handled via mutual TLS and the Apigee API gateway proxy."
77+
}
78+
]
79+
)
680
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {IFunction} from "aws-cdk-lib/aws-lambda"
2+
import {LambdaInvoke} from "aws-cdk-lib/aws-stepfunctions-tasks"
3+
import {Construct} from "constructs"
4+
import {
5+
Chain,
6+
Choice,
7+
Condition,
8+
IChainable,
9+
Pass,
10+
TaskInput
11+
} from "aws-cdk-lib/aws-stepfunctions"
12+
13+
export interface Format1UpdatePrescriptionsStatusDefinitionProps {
14+
readonly convertRequestToFhirFormatFunction: IFunction
15+
readonly updatePrescriptionStatusFunction: IFunction
16+
}
17+
18+
export class Format1UpdatePrescriptionsStatusDefinition extends Construct {
19+
public readonly definition: IChainable
20+
21+
public constructor(
22+
scope: Construct, id: string, props: Format1UpdatePrescriptionsStatusDefinitionProps
23+
) {
24+
super(scope, id)
25+
26+
const catchAllError = new Pass(this, "CatchAllError", {
27+
result: {
28+
value: {
29+
Payload: {
30+
statusCode: 500,
31+
headers: {
32+
"Content-Type": "application/fhir+json",
33+
"Cache-Control": "no-cache"
34+
},
35+
body: JSON.stringify({
36+
resourceType: "OperationOutcome",
37+
issue: [{severity: "error", code: "processing", diagnostics: "System error"}]
38+
})
39+
}
40+
}
41+
}
42+
})
43+
44+
const callConvertRequestToFhirFormat = new LambdaInvoke(
45+
this, "Call Convert Request To Fhir Format", {
46+
lambdaFunction: props.convertRequestToFhirFormatFunction,
47+
assign: {
48+
convertStatusCode: "{% $states.result.Payload.statusCode %}",
49+
convertHeaders: "{% $states.result.Payload.headers %}",
50+
convertBody: "{% $parse($states.result.Payload.body) %}"
51+
}
52+
}
53+
)
54+
callConvertRequestToFhirFormat.addCatch(catchAllError)
55+
56+
const failedConvertRequestToFhir = new Pass(this, "Failed Convert Request to FHIR", {
57+
outputs: {
58+
Payload: {
59+
statusCode: "{% $convertStatusCode %}",
60+
headers: "{% $convertHeaders %}",
61+
body: "{% $string($convertBody) %}"
62+
}
63+
}
64+
})
65+
66+
const callUpdatePrescriptionStatus = new LambdaInvoke(
67+
this, "Call Update Prescription Status", {
68+
lambdaFunction: props.updatePrescriptionStatusFunction,
69+
payload: TaskInput.fromObject({
70+
body: "{% $string($convertBody) %}",
71+
headers: "{% $convertHeaders %}"
72+
}),
73+
assign: {
74+
updateStatusCode: "{% $states.result.Payload.statusCode %}",
75+
updatePayload: "{% $states.result.Payload %}"
76+
}
77+
}
78+
)
79+
callUpdatePrescriptionStatus.addCatch(catchAllError)
80+
81+
const translate409To202 = new Pass(this, "Translate 409 to 202", {
82+
result: {
83+
value: {
84+
Payload: {
85+
statusCode: 202,
86+
headers: {
87+
"Content-Type": "application/fhir+json",
88+
"Cache-Control": "no-cache"
89+
},
90+
body: JSON.stringify({
91+
resourceType: "OperationOutcome",
92+
issue: [{
93+
severity: "information",
94+
code: "informational",
95+
diagnostics:
96+
"Duplicate update detected. The message was valid but did not result in an update."
97+
}]
98+
})
99+
}
100+
}
101+
}
102+
})
103+
104+
const endState = new Pass(this, "End State")
105+
106+
const checkConvertResult = new Choice(this, "Convert Request to FHIR result")
107+
const convertNotOk = Condition.jsonata("{% $convertStatusCode != 200 %}")
108+
109+
const checkUpdateResult = new Choice(this, "Check Update Prescription Status Result")
110+
const updateIs409 = Condition.jsonata("{% $updateStatusCode = 409 %}")
111+
112+
this.definition = Chain
113+
.start(callConvertRequestToFhirFormat)
114+
.next(
115+
checkConvertResult
116+
.when(convertNotOk, failedConvertRequestToFhir)
117+
.otherwise(
118+
callUpdatePrescriptionStatus
119+
.next(
120+
checkUpdateResult
121+
.when(updateIs409, translate409To202)
122+
.otherwise(endState)
123+
)
124+
)
125+
)
126+
}
127+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {IFunction} from "aws-cdk-lib/aws-lambda"
2+
import {LambdaInvoke} from "aws-cdk-lib/aws-stepfunctions-tasks"
3+
import {Construct} from "constructs"
4+
import {
5+
Chain,
6+
Choice,
7+
Condition,
8+
IChainable,
9+
Pass
10+
} from "aws-cdk-lib/aws-stepfunctions"
11+
12+
export interface UpdatePrescriptionStatusDefinitionProps {
13+
readonly fhirValidationFunction: IFunction
14+
readonly updatePrescriptionStatusFunction: IFunction
15+
}
16+
17+
export class UpdatePrescriptionStatusDefinition extends Construct {
18+
public readonly definition: IChainable
19+
20+
public constructor(
21+
scope: Construct, id: string, props: UpdatePrescriptionStatusDefinitionProps
22+
) {
23+
super(scope, id)
24+
25+
const catchAllError = new Pass(this, "CatchAllError", {
26+
result: {
27+
value: {
28+
Payload: {
29+
statusCode: 500,
30+
headers: {
31+
"Content-Type": "application/fhir+json",
32+
"Cache-Control": "no-cache"
33+
},
34+
body: JSON.stringify({
35+
resourceType: "OperationOutcome",
36+
issue: [{severity: "error", code: "processing", diagnostics: "System error"}]
37+
})
38+
}
39+
}
40+
}
41+
})
42+
43+
const callFhirValidation = new LambdaInvoke(this, "Call FHIR Validation", {
44+
lambdaFunction: props.fhirValidationFunction,
45+
assign: {
46+
fhirValidationResponse: "{% $states.result.Payload %}",
47+
fhirValidationErrorCount:
48+
"{% $count($states.result.Payload.issue[severity = 'error']) %}"
49+
}
50+
})
51+
callFhirValidation.addCatch(catchAllError)
52+
53+
const returnFailedFhirValidationErrors = new Pass(this, "Return Failed FHIR Validation Errors", {
54+
outputs: {
55+
Payload: {
56+
statusCode: 400,
57+
headers: {
58+
"Content-Type": "application/fhir+json",
59+
"Cache-Control": "no-cache"
60+
},
61+
body: "{% $string($fhirValidationResponse) %}"
62+
}
63+
}
64+
})
65+
66+
const callUpdatePrescriptionStatus = new LambdaInvoke(
67+
this, "Call Update Prescription Status", {
68+
lambdaFunction: props.updatePrescriptionStatusFunction
69+
}
70+
)
71+
callUpdatePrescriptionStatus.addCatch(catchAllError)
72+
73+
const doFhirValidationErrorsExist = new Choice(this, "Do FHIR Validation Errors Exist")
74+
const hasErrors = Condition.jsonata("{% $fhirValidationErrorCount > 0 %}")
75+
76+
this.definition = Chain
77+
.start(callFhirValidation)
78+
.next(
79+
doFhirValidationErrorsExist
80+
.when(hasErrors, returnFailedFhirValidationErrors)
81+
.otherwise(callUpdatePrescriptionStatus)
82+
)
83+
}
84+
}

0 commit comments

Comments
 (0)