Skip to content

Commit c3525e5

Browse files
New: [AEA-6523] - Allow unique stack ID as well as stack name (#669)
## Summary - ✨ New Feature ### Details Allow a mTLS trust store key UUID addition to the rest API naming prefex, to work around API GW mTLS implementation limitation --------- Signed-off-by: Connor Avery <214469360+connoravo-nhs@users.noreply.github.com>
1 parent 615765c commit c3525e5

File tree

2 files changed

+133
-2
lines changed

2 files changed

+133
-2
lines changed

packages/cdkConstructs/src/constructs/RestApiGateway.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export interface RestApiGatewayProps {
4444
readonly logRetentionInDays: number
4545
/** Truststore object key to enable mTLS; leave undefined to disable mTLS or when enableServiceDomain is false. */
4646
readonly mutualTlsTrustStoreKey: string | undefined
47+
/** Required with mutualTlsTrustStoreKey. Service name, used as prefix for trust store key */
48+
readonly serviceName: string | undefined
49+
/** Optional stack UUID. If set, included in the mTLS trust store key prefix to prevent collisions
50+
* when deploying multiple stacks with the same name, avoiding AWS API Gateway mTLS key caching issues. */
51+
readonly trustStoreUuuid: string | undefined
4752
/** Enables creation of a second subscription filter to forward logs to CSOC. */
4853
readonly forwardCsocLogs: boolean
4954
/** Destination ARN used by the optional CSOC subscription filter. */
@@ -56,6 +61,16 @@ export interface RestApiGatewayProps {
5661
readonly enableServiceDomain?: boolean
5762
}
5863

64+
const getTrustStoreKeyPrefix = (stackName: string,
65+
serviceName: string | undefined,
66+
trustStoreUuuid: string | undefined) => {
67+
if (trustStoreUuuid) {
68+
return `${serviceName}/${stackName}-${trustStoreUuuid}-truststore`
69+
} else {
70+
return `${serviceName}/${stackName}-truststore`
71+
}
72+
}
73+
5974
/** Creates a regional REST API with standard logging, DNS, and optional mTLS/CSOC integration. */
6075
export class RestApiGateway extends Construct {
6176
/** Created API Gateway instance. */
@@ -69,9 +84,11 @@ export class RestApiGateway extends Construct {
6984
* @example
7085
* ```ts
7186
* const api = new RestApiGateway(this, "MyApi", {
72-
* stackName: "my-service",
87+
* stackName: "v1.3",
7388
* logRetentionInDays: 30,
7489
* mutualTlsTrustStoreKey: "truststore.pem",
90+
* serviceName: "my-service",
91+
* trustStoreUuuid: "abc123",
7592
* forwardCsocLogs: true,
7693
* csocApiGatewayDestination: "arn:aws:logs:eu-west-2:123456789012:destination:csoc",
7794
* executionPolicies: [myLambdaInvokePolicy],
@@ -93,6 +110,10 @@ export class RestApiGateway extends Construct {
93110
throw new Error("mutualTlsTrustStoreKey should not be provided when enableServiceDomain is false")
94111
}
95112

113+
if (props.mutualTlsTrustStoreKey && !props.serviceName) {
114+
throw new Error("serviceName must be provided when mTLS is set")
115+
}
116+
96117
// Imports
97118
const cloudWatchLogsKmsKey = Key.fromKeyArn(
98119
this, "cloudWatchLogsKmsKey", ACCOUNT_RESOURCES.CloudwatchLogsKmsKeyArn)
@@ -158,7 +179,11 @@ export class RestApiGateway extends Construct {
158179
let mtlsConfig: MTLSConfig | undefined
159180

160181
if (enableServiceDomain && props.mutualTlsTrustStoreKey) {
161-
const trustStoreKeyPrefix = `cpt-api/${props.stackName}-truststore`
182+
const trustStoreKeyPrefix = getTrustStoreKeyPrefix(
183+
props.stackName,
184+
props.serviceName,
185+
props.trustStoreUuuid
186+
)
162187
const logGroup = new LogGroup(this, "LambdaLogGroup", {
163188
encryptionKey: cloudWatchLogsKmsKey,
164189
logGroupName: `/aws/lambda/${props.stackName}-truststore-deployment`,

packages/cdkConstructs/tests/constructs/RestApiGateway.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ describe("RestApiGateway with mTLS", () => {
253253
stackName: "test-stack",
254254
logRetentionInDays: 30,
255255
mutualTlsTrustStoreKey: "truststore.pem",
256+
serviceName: "cpt-api",
256257
forwardCsocLogs: false,
257258
csocApiGatewayDestination: "",
258259
executionPolicies: [testPolicy],
@@ -321,6 +322,12 @@ describe("RestApiGateway with mTLS", () => {
321322
expect(Object.keys(customResources).length).toBeGreaterThan(0)
322323
})
323324

325+
test("uses serviceName in trust store deployment key prefix", () => {
326+
template.hasResourceProperties("Custom::CDKBucketDeployment", {
327+
DestinationBucketKeyPrefix: "cpt-api/test-stack-truststore"
328+
})
329+
})
330+
324331
test("disables execute-api endpoint when mTLS is enabled", () => {
325332
template.hasResourceProperties("AWS::ApiGateway::RestApi", {
326333
Name: "test-stack-apigw",
@@ -344,6 +351,80 @@ describe("RestApiGateway with mTLS", () => {
344351
})
345352
})
346353

354+
describe("RestApiGateway with mTLS and trustStoreUuuid", () => {
355+
test("uses trustStoreUuuid in trust store deployment key prefix", () => {
356+
interface ManagedPolicyResource {
357+
Properties?: {
358+
PolicyDocument?: {
359+
Statement?: Array<{
360+
Action?: Array<string>
361+
Resource?: unknown | Array<unknown>
362+
}>
363+
}
364+
}
365+
}
366+
367+
const app = new App()
368+
const stack = new Stack(app, "RestApiGatewayStackWithUuid")
369+
370+
const testPolicy = new ManagedPolicy(stack, "TestPolicy", {
371+
description: "test execution policy",
372+
statements: [
373+
new PolicyStatement({
374+
actions: ["lambda:InvokeFunction"],
375+
resources: ["arn:aws:lambda:eu-west-2:123456789012:function:test-function"]
376+
})
377+
]
378+
})
379+
380+
const apiGateway = new RestApiGateway(stack, "TestApiGateway", {
381+
stackName: "test-stack",
382+
logRetentionInDays: 30,
383+
mutualTlsTrustStoreKey: "truststore.pem",
384+
serviceName: "cpt-api",
385+
trustStoreUuuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
386+
forwardCsocLogs: false,
387+
csocApiGatewayDestination: "",
388+
executionPolicies: [testPolicy],
389+
enableServiceDomain: true
390+
})
391+
392+
apiGateway.api.root.addMethod("GET")
393+
394+
const template = Template.fromStack(stack)
395+
template.hasResourceProperties("Custom::CDKBucketDeployment", {
396+
DestinationBucketKeyPrefix: "cpt-api/test-stack-f47ac10b-58cc-4372-a567-0e02b2c3d479-truststore"
397+
})
398+
399+
const policies = template.findResources("AWS::IAM::ManagedPolicy")
400+
const expectedTrustStoreObjectPath =
401+
"cpt-api/test-stack-f47ac10b-58cc-4372-a567-0e02b2c3d479-truststore/truststore.pem"
402+
403+
const hasExpectedTrustStorePath = Object.values(policies).some((policy) => {
404+
const statements = (policy as ManagedPolicyResource).Properties?.PolicyDocument?.Statement ?? []
405+
return statements.some((statement) => {
406+
if (!statement.Action?.includes("s3:PutObject")) {
407+
return false
408+
}
409+
410+
const resources = Array.isArray(statement.Resource)
411+
? statement.Resource
412+
: (statement.Resource ? [statement.Resource] : [])
413+
414+
return resources.some((resource) => {
415+
if (typeof resource === "string") {
416+
return resource.includes(expectedTrustStoreObjectPath)
417+
}
418+
419+
return JSON.stringify(resource).includes(expectedTrustStoreObjectPath)
420+
})
421+
})
422+
})
423+
424+
expect(hasExpectedTrustStorePath).toBe(true)
425+
})
426+
})
427+
347428
describe("RestApiGateway validation errors", () => {
348429
test("throws when forwardCsocLogs is true and csocApiGatewayDestination is empty string", () => {
349430
const app = new App()
@@ -385,12 +466,37 @@ describe("RestApiGateway validation errors", () => {
385466
stackName: "test-stack",
386467
logRetentionInDays: 30,
387468
mutualTlsTrustStoreKey: "truststore.pem",
469+
serviceName: "cpt-api",
388470
forwardCsocLogs: false,
389471
csocApiGatewayDestination: "",
390472
executionPolicies: [testPolicy],
391473
enableServiceDomain: false
392474
})).toThrow("mutualTlsTrustStoreKey should not be provided when enableServiceDomain is false")
393475
})
476+
477+
test("throws when mutualTlsTrustStoreKey is set and serviceName is missing", () => {
478+
const app = new App()
479+
const stack = new Stack(app, "ValidationStack3")
480+
const testPolicy = new ManagedPolicy(stack, "TestPolicy", {
481+
description: "test execution policy",
482+
statements: [
483+
new PolicyStatement({
484+
actions: ["lambda:InvokeFunction"],
485+
resources: ["arn:aws:lambda:eu-west-2:123456789012:function:test-function"]
486+
})
487+
]
488+
})
489+
490+
expect(() => new RestApiGateway(stack, "TestApiGateway", {
491+
stackName: "test-stack",
492+
logRetentionInDays: 30,
493+
mutualTlsTrustStoreKey: "truststore.pem",
494+
forwardCsocLogs: false,
495+
csocApiGatewayDestination: "",
496+
executionPolicies: [testPolicy],
497+
enableServiceDomain: true
498+
})).toThrow("serviceName must be provided when mTLS is set")
499+
})
394500
})
395501

396502
describe("RestApiGateway enableServiceDomain default behaviour", () => {

0 commit comments

Comments
 (0)