Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
16 changes: 8 additions & 8 deletions packages/cdkConstructs/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {StandardStackProps} from "../apps/createApp"

export function getConfigFromEnvVar(
varName: string,
prefix: string = "CDK_CONFIG_",
defaultValue: string | undefined = undefined
defaultValue?: string,
Comment thread
tstephen-nhs marked this conversation as resolved.
Outdated
prefix: string = "CDK_CONFIG_"
): string {
const value = process.env[prefix + varName]
if (!value) {
Expand All @@ -19,19 +19,19 @@ export function getConfigFromEnvVar(

export function getBooleanConfigFromEnvVar(
varName: string,
prefix: string = "CDK_CONFIG_",
defaultValue: string | undefined = undefined
defaultValue?: string,
Comment thread
tstephen-nhs marked this conversation as resolved.
Outdated
prefix: string = "CDK_CONFIG_"
): boolean {
const value = getConfigFromEnvVar(varName, prefix, defaultValue)
const value = getConfigFromEnvVar(varName, defaultValue, prefix)
return value.toLowerCase().trim() === "true"
}

export function getNumberConfigFromEnvVar(
varName: string,
prefix: string = "CDK_CONFIG_",
defaultValue: string | undefined = undefined
defaultValue?: string,
Comment thread
tstephen-nhs marked this conversation as resolved.
Outdated
prefix: string = "CDK_CONFIG_"
): number {
const value = getConfigFromEnvVar(varName, prefix, defaultValue)
const value = getConfigFromEnvVar(varName, defaultValue, prefix)
return Number(value)
}

Expand Down
12 changes: 12 additions & 0 deletions packages/cdkConstructs/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export const ACCOUNT_RESOURCES = {
CloudwatchLogsKmsKeyArn: Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn"),
EpsDomainName: Fn.importValue("eps-route53-resources:EPS-domain"),
EpsZoneId: Fn.importValue("eps-route53-resources:EPS-ZoneID"),
LambdaAccessSecretsPolicy: Fn.importValue("account-resources:LambdaAccessSecretsPolicy"),
LambdaDecryptSecretsKMSPolicy: Fn.importValue("account-resources:LambdaDecryptSecretsKMSPolicy"),
SpinePrivateKeyARN: Fn.importValue("account-resources:SpinePrivateKey"),
SpinePublicCertificateARN: Fn.importValue("account-resources:SpinePublicCertificate"),
SpineASIDARN: Fn.importValue("account-resources:SpineASID"),
SpinePartyKeyARN: Fn.importValue("account-resources:SpinePartyKey"),
SpineCAChainARN: Fn.importValue("account-resources:SpineCAChain"),
TrustStoreBucket: Fn.importValue("account-resources:TrustStoreBucket"),
TrustStoreBucketKMSKey: Fn.importValue("account-resources:TrustStoreBucketKMSKey"),
TrustStoreDeploymentBucket: Fn.importValue("account-resources:TrustStoreDeploymentBucket")
Expand All @@ -17,3 +24,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
96 changes: 63 additions & 33 deletions packages/cdkConstructs/src/constructs/RestApiGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,36 @@ 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 {
/** Stack name, used as prefix for resource naming and DNS records. */
readonly stackName: string
/** Shared retention period for API and deployment-related log groups. */
readonly logRetentionInDays: number
/** Truststore object key to enable mTLS; leave undefined to disable mTLS. */
/** Truststore object key to enable mTLS; leave undefined to disable mTLS or when enableServiceDomain is false. */
readonly mutualTlsTrustStoreKey: string | undefined
/** Enables creation of a second subscription filter to forward logs to CSOC. */
readonly forwardCsocLogs: boolean
/** Destination ARN used by the optional CSOC subscription filter. */
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 @@ -63,18 +74,25 @@ export class RestApiGateway extends Construct {
* mutualTlsTrustStoreKey: "truststore.pem",
* forwardCsocLogs: true,
* csocApiGatewayDestination: "arn:aws:logs:eu-west-2:123456789012:destination:csoc",
* executionPolicies: [myLambdaInvokePolicy]
* executionPolicies: [myLambdaInvokePolicy],
* 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")
}

if (!enableServiceDomain && props.mutualTlsTrustStoreKey) {
throw new Error("mutualTlsTrustStoreKey should not be provided when enableServiceDomain is false")
}

// Imports
const cloudWatchLogsKmsKey = Key.fromKeyArn(
this, "cloudWatchLogsKmsKey", ACCOUNT_RESOURCES.CloudwatchLogsKmsKeyArn)
Expand All @@ -94,12 +112,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 +148,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 +239,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 +267,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
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
12 changes: 6 additions & 6 deletions packages/cdkConstructs/tests/config/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe("config helpers", () => {
test("getConfigFromEnvVar returns the default value when env var is not set", () => {
delete process.env.CDK_CONFIG_MISSING

expect(getConfigFromEnvVar("MISSING", "CDK_CONFIG_", "fallback")).toBe("fallback")
expect(getConfigFromEnvVar("MISSING", "fallback")).toBe("fallback")
})

test("getConfigFromEnvVar throws when value is missing", () => {
Expand All @@ -109,7 +109,7 @@ describe("config helpers", () => {
test("getConfigFromEnvVar supports alternate prefixes", () => {
process.env.APP_CUSTOM_VALUE = "alt"

expect(getConfigFromEnvVar("CUSTOM_VALUE", "APP_")).toBe("alt")
expect(getConfigFromEnvVar("CUSTOM_VALUE", undefined, "APP_")).toBe("alt")
})

test("getBooleanConfigFromEnvVar maps string booleans", () => {
Expand All @@ -123,8 +123,8 @@ describe("config helpers", () => {
test("getBooleanConfigFromEnvVar uses default value when env var is not set", () => {
delete process.env.CDK_CONFIG_BOOL_MISSING

expect(getBooleanConfigFromEnvVar("BOOL_MISSING", "CDK_CONFIG_", "true")).toBe(true)
expect(getBooleanConfigFromEnvVar("BOOL_MISSING", "CDK_CONFIG_", "false")).toBe(false)
expect(getBooleanConfigFromEnvVar("BOOL_MISSING", "true")).toBe(true)
expect(getBooleanConfigFromEnvVar("BOOL_MISSING", "false")).toBe(false)
})

test("getNumberConfigFromEnvVar parses numeric strings", () => {
Expand All @@ -136,13 +136,13 @@ describe("config helpers", () => {
test("getNumberConfigFromEnvVar uses default value when env var is not set", () => {
delete process.env.CDK_CONFIG_NUM_MISSING

expect(getNumberConfigFromEnvVar("NUM_MISSING", "CDK_CONFIG_", "99")).toBe(99)
expect(getNumberConfigFromEnvVar("NUM_MISSING", "99")).toBe(99)
})

test("getConfigFromEnvVar ignores default value when env var is set", () => {
process.env.CDK_CONFIG_STACK_NAME = "primary"

expect(getConfigFromEnvVar("STACK_NAME", "CDK_CONFIG_", "ignored")).toBe("primary")
expect(getConfigFromEnvVar("STACK_NAME", "ignored")).toBe("primary")
})

test("getTrustStoreVersion returns the version ID from S3", async () => {
Expand Down
Loading
Loading