Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/cdkConstructs/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ export const LAMBDA_RESOURCES = {
SplunkDeliveryStream: Fn.importValue("lambda-resources:SplunkDeliveryStream"),
SplunkSubscriptionFilterRole: Fn.importValue("lambda-resources:SplunkSubscriptionFilterRole")
}

/** Shared cfn-guard rule identifiers used for metadata suppressions. */
export const CFN_GUARD_RULES = {
LogGroupRetentionPeriodCheck: "CW_LOGGROUP_RETENTION_PERIOD_CHECK"
} as const
88 changes: 57 additions & 31 deletions packages/cdkConstructs/src/constructs/RestApiGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ import {accessLogFormat} from "./RestApiGateway/accessLogFormat.js"
import {Certificate, CertificateValidation} from "aws-cdk-lib/aws-certificatemanager"
import {Bucket} from "aws-cdk-lib/aws-s3"
import {BucketDeployment, Source} from "aws-cdk-lib/aws-s3-deployment"
import {ARecord, HostedZone, RecordTarget} from "aws-cdk-lib/aws-route53"
import {
ARecord,
AaaaRecord,
HostedZone,
IHostedZone,
RecordTarget
} from "aws-cdk-lib/aws-route53"
import {ApiGateway as ApiGatewayTarget} from "aws-cdk-lib/aws-route53-targets"
import {NagSuppressions} from "cdk-nag"
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
import {addSuppressions} from "../utils/helpers"

/** Configuration for creating a REST API with optional mTLS and log forwarding integrations. */
export interface RestApiGatewayProps {
Expand All @@ -43,6 +50,10 @@ export interface RestApiGatewayProps {
readonly csocApiGatewayDestination: string
/** Managed policies attached to the API Gateway execution role. */
readonly executionPolicies: Array<IManagedPolicy>
/**
* When true (default), creates the custom service domain, ACM certificate, and Route53 records.
*/
readonly enableServiceDomain?: boolean
}

/** Creates a regional REST API with standard logging, DNS, and optional mTLS/CSOC integration. */
Expand All @@ -64,13 +75,16 @@ export class RestApiGateway extends Construct {
* forwardCsocLogs: true,
* csocApiGatewayDestination: "arn:aws:logs:eu-west-2:123456789012:destination:csoc",
* executionPolicies: [myLambdaInvokePolicy]
Comment thread
tstephen-nhs marked this conversation as resolved.
Outdated
* enableServiceDomain: true
* })
* api.api.root.addResource("patients")
* ```
*/
public constructor(scope: Construct, id: string, props: RestApiGatewayProps) {
super(scope, id)

const enableServiceDomain = (props.enableServiceDomain ?? true)

if (props.forwardCsocLogs && props.csocApiGatewayDestination === "") {
throw new Error("csocApiGatewayDestination must be provided when forwardCsocLogs is true")
}
Expand All @@ -94,12 +108,17 @@ export class RestApiGateway extends Construct {
const trustStoreBucketKmsKey = Key.fromKeyArn(
this, "TrustStoreBucketKmsKey", ACCOUNT_RESOURCES.TrustStoreBucketKMSKey)

const epsDomainName: string = ACCOUNT_RESOURCES.EpsDomainName
const hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
hostedZoneId: ACCOUNT_RESOURCES.EpsZoneId,
zoneName: epsDomainName
})
const serviceDomainName = `${props.stackName}.${epsDomainName}`
let hostedZone: IHostedZone | undefined
let serviceDomainName: string | undefined

if (enableServiceDomain) {
const epsDomainName: string = ACCOUNT_RESOURCES.EpsDomainName
hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
hostedZoneId: ACCOUNT_RESOURCES.EpsZoneId,
zoneName: epsDomainName
})
serviceDomainName = `${props.stackName}.${epsDomainName}`
}

// Resources
const logGroup = new LogGroup(this, "ApiGatewayAccessLogGroup", {
Expand All @@ -125,14 +144,16 @@ export class RestApiGateway extends Construct {
})
}

const certificate = new Certificate(this, "Certificate", {
domainName: serviceDomainName,
validation: CertificateValidation.fromDns(hostedZone)
})
const certificate = enableServiceDomain && hostedZone && serviceDomainName
? new Certificate(this, "Certificate", {
domainName: serviceDomainName,
validation: CertificateValidation.fromDns(hostedZone)
})
: undefined

let mtlsConfig: MTLSConfig | undefined

Comment thread
tstephen-nhs marked this conversation as resolved.
if (props.mutualTlsTrustStoreKey) {
if (enableServiceDomain && props.mutualTlsTrustStoreKey) {
const trustStoreKeyPrefix = `cpt-api/${props.stackName}-truststore`
const logGroup = new LogGroup(this, "LambdaLogGroup", {
encryptionKey: cloudWatchLogsKmsKey,
Expand Down Expand Up @@ -214,13 +235,16 @@ export class RestApiGateway extends Construct {

const apiGateway = new RestApi(this, "ApiGateway", {
restApiName: `${props.stackName}-apigw`,
domainName: {
domainName: serviceDomainName,
certificate: certificate,
securityPolicy: SecurityPolicy.TLS_1_2,
endpointType: EndpointType.REGIONAL,
mtls: mtlsConfig
},
...(enableServiceDomain
? {
domainName: {
domainName: serviceDomainName!,
certificate: certificate!,
securityPolicy: SecurityPolicy.TLS_1_2,
endpointType: EndpointType.REGIONAL,
mtls: mtlsConfig
}
} : {}),
disableExecuteApiEndpoint: mtlsConfig ? true : false, // NOSONAR
endpointConfiguration: {
types: [EndpointType.REGIONAL]
Expand All @@ -239,21 +263,23 @@ export class RestApiGateway extends Construct {
managedPolicies: props.executionPolicies
}).withoutPolicyUpdates()

new ARecord(this, "ARecord", {
recordName: props.stackName,
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
zone: hostedZone
})
if (enableServiceDomain && hostedZone) {
new ARecord(this, "ARecord", {
recordName: props.stackName,
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
zone: hostedZone
})

const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage
cfnStage.cfnOptions.metadata = {
guard: {
SuppressedRules: [
"API_GW_CACHE_ENABLED_AND_ENCRYPTED"
]
}
new AaaaRecord(this, "AaaaRecord", {
recordName: props.stackName,
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
zone: hostedZone
})
}

const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage
addSuppressions([cfnStage], ["API_GW_CACHE_ENABLED_AND_ENCRYPTED"])

// Outputs
this.api = apiGateway
this.role = role
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class StateMachineEndpoint extends Construct {
methodResponses: [
{statusCode: "200"},
{statusCode: "400"},
{statusCode: "408"},
Comment thread
tstephen-nhs marked this conversation as resolved.
Outdated
{statusCode: "500"}
]
})
Expand Down
11 changes: 3 additions & 8 deletions packages/cdkConstructs/src/constructs/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
} from "aws-cdk-lib/aws-stepfunctions"
import {Construct} from "constructs"
import {CfnDeliveryStream} from "aws-cdk-lib/aws-kinesisfirehose"
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
import {ACCOUNT_RESOURCES, CFN_GUARD_RULES, LAMBDA_RESOURCES} from "../constants"
import {addSuppressions} from "../utils/helpers"

/**
* Configuration for provisioning an Express Step Functions state machine
Expand Down Expand Up @@ -109,13 +110,7 @@ export class ExpressStateMachine extends Construct {
})

const cfnLogGroup = logGroup.node.defaultChild as CfnLogGroup
cfnLogGroup.cfnOptions.metadata = {
guard: {
SuppressedRules: [
"CW_LOGGROUP_RETENTION_PERIOD_CHECK"
]
}
}
addSuppressions([cfnLogGroup], [CFN_GUARD_RULES.LogGroupRetentionPeriodCheck])

if (addSplunkSubscriptionFilter) {
if (splunkDeliveryStream) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "aws-cdk-lib/aws-iam"
import {NagSuppressions} from "cdk-nag"
import {LAMBDA_INSIGHTS_LAYER_ARNS} from "../config"
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
import {ACCOUNT_RESOURCES, CFN_GUARD_RULES, LAMBDA_RESOURCES} from "../constants"
import {addSuppressions} from "../utils/helpers"
import {CfnDeliveryStream} from "aws-cdk-lib/aws-kinesisfirehose"
import {Stream} from "aws-cdk-lib/aws-kinesis"
Expand Down Expand Up @@ -74,7 +74,7 @@ export const createSharedLambdaResources = (
})

const cfnlogGroup = logGroup.node.defaultChild as CfnLogGroup
addSuppressions([cfnlogGroup], ["CW_LOGGROUP_RETENTION_PERIOD_CHECK"])
addSuppressions([cfnlogGroup], [CFN_GUARD_RULES.LogGroupRetentionPeriodCheck])

if (addSplunkSubscriptionFilter) {
// This is in an if statement to ensure correct value is used
Expand Down
41 changes: 38 additions & 3 deletions packages/cdkConstructs/tests/constructs/RestApiGateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ describe("RestApiGateway without mTLS", () => {
mutualTlsTrustStoreKey: undefined,
forwardCsocLogs: false,
csocApiGatewayDestination: "",
executionPolicies: [testPolicy]
executionPolicies: [testPolicy],
enableServiceDomain: true
})

// Add a dummy method to satisfy API Gateway validation
Expand Down Expand Up @@ -192,7 +193,8 @@ describe("RestApiGateway with CSOC logs", () => {
mutualTlsTrustStoreKey: undefined,
forwardCsocLogs: true,
csocApiGatewayDestination: "arn:aws:logs:eu-west-2:123456789012:destination:csoc-destination",
executionPolicies: [testPolicy]
executionPolicies: [testPolicy],
enableServiceDomain: true
})

// Add a dummy method to satisfy API Gateway validation
Expand Down Expand Up @@ -240,7 +242,8 @@ describe("RestApiGateway with mTLS", () => {
mutualTlsTrustStoreKey: "truststore.pem",
forwardCsocLogs: false,
csocApiGatewayDestination: "",
executionPolicies: [testPolicy]
executionPolicies: [testPolicy],
enableServiceDomain: true
})

// Add a dummy method to satisfy API Gateway validation
Expand Down Expand Up @@ -352,3 +355,35 @@ describe("RestApiGateway validation errors", () => {
})).toThrow("csocApiGatewayDestination must be provided when forwardCsocLogs is true")
})
})

describe("RestApiGateway enableServiceDomain default behaviour", () => {
test("creates custom domain resources when enableServiceDomain is omitted", () => {
const app = new App()
const stack = new Stack(app, "EnableServiceDomainDefaultStack")
const testPolicy = new ManagedPolicy(stack, "TestPolicy", {
description: "test execution policy",
statements: [
new PolicyStatement({
actions: ["lambda:InvokeFunction"],
resources: ["arn:aws:lambda:eu-west-2:123456789012:function:test-function"]
})
]
})

const apiGateway = new RestApiGateway(stack, "TestApiGateway", {
stackName: "test-stack",
logRetentionInDays: 30,
mutualTlsTrustStoreKey: undefined,
forwardCsocLogs: false,
csocApiGatewayDestination: "",
executionPolicies: [testPolicy]
})

apiGateway.api.root.addMethod("GET")

const template = Template.fromStack(stack)
template.resourceCountIs("AWS::CertificateManager::Certificate", 1)
template.resourceCountIs("AWS::ApiGateway::DomainName", 1)
template.resourceCountIs("AWS::Route53::RecordSet", 2)
})
Comment thread
tstephen-nhs marked this conversation as resolved.
})
Loading