|
| 1 | +import {Fn, RemovalPolicy} from "aws-cdk-lib" |
| 2 | +import { |
| 3 | + CfnStage, |
| 4 | + EndpointType, |
| 5 | + LogGroupLogDestination, |
| 6 | + MethodLoggingLevel, |
| 7 | + MTLSConfig, |
| 8 | + RestApi, |
| 9 | + SecurityPolicy |
| 10 | +} from "aws-cdk-lib/aws-apigateway" |
| 11 | +import { |
| 12 | + IManagedPolicy, |
| 13 | + IRole, |
| 14 | + ManagedPolicy, |
| 15 | + PolicyStatement, |
| 16 | + Role, |
| 17 | + ServicePrincipal |
| 18 | +} from "aws-cdk-lib/aws-iam" |
| 19 | +import {Stream} from "aws-cdk-lib/aws-kinesis" |
| 20 | +import {Key} from "aws-cdk-lib/aws-kms" |
| 21 | +import {CfnSubscriptionFilter, LogGroup} from "aws-cdk-lib/aws-logs" |
| 22 | +import {Construct} from "constructs" |
| 23 | +import {accessLogFormat} from "./RestApiGateway/accessLogFormat.js" |
| 24 | +import {Certificate, CertificateValidation} from "aws-cdk-lib/aws-certificatemanager" |
| 25 | +import {Bucket} from "aws-cdk-lib/aws-s3" |
| 26 | +import {BucketDeployment, Source} from "aws-cdk-lib/aws-s3-deployment" |
| 27 | +import {ARecord, HostedZone, RecordTarget} from "aws-cdk-lib/aws-route53" |
| 28 | +import {ApiGateway as ApiGatewayTarget} from "aws-cdk-lib/aws-route53-targets" |
| 29 | +import {NagSuppressions} from "cdk-nag" |
| 30 | + |
| 31 | +export interface RestApiGatewayProps { |
| 32 | + readonly stackName: string |
| 33 | + readonly logRetentionInDays: number |
| 34 | + readonly mutualTlsTrustStoreKey: string | undefined |
| 35 | + readonly forwardCsocLogs: boolean |
| 36 | + readonly csocApiGatewayDestination: string |
| 37 | + readonly executionPolicies: Array<IManagedPolicy> |
| 38 | +} |
| 39 | + |
| 40 | +export class RestApiGateway extends Construct { |
| 41 | + public readonly api: RestApi |
| 42 | + public readonly role: IRole |
| 43 | + |
| 44 | + public constructor(scope: Construct, id: string, props: RestApiGatewayProps) { |
| 45 | + super(scope, id) |
| 46 | + |
| 47 | + // Imports |
| 48 | + const cloudWatchLogsKmsKey = Key.fromKeyArn( |
| 49 | + this, "cloudWatchLogsKmsKey", Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn")) |
| 50 | + |
| 51 | + const splunkDeliveryStream = Stream.fromStreamArn( |
| 52 | + this, "SplunkDeliveryStream", Fn.importValue("lambda-resources:SplunkDeliveryStream")) |
| 53 | + |
| 54 | + const splunkSubscriptionFilterRole = Role.fromRoleArn( |
| 55 | + this, "splunkSubscriptionFilterRole", Fn.importValue("lambda-resources:SplunkSubscriptionFilterRole")) |
| 56 | + |
| 57 | + const trustStoreBucket = Bucket.fromBucketArn( |
| 58 | + this, "TrustStoreBucket", Fn.importValue("account-resources:TrustStoreBucket")) |
| 59 | + |
| 60 | + const trustStoreDeploymentBucket = Bucket.fromBucketArn( |
| 61 | + this, "TrustStoreDeploymentBucket", Fn.importValue("account-resources:TrustStoreDeploymentBucket")) |
| 62 | + |
| 63 | + const trustStoreBucketKmsKey = Key.fromKeyArn( |
| 64 | + this, "TrustStoreBucketKmsKey", Fn.importValue("account-resources:TrustStoreBucketKMSKey")) |
| 65 | + |
| 66 | + const epsDomainName: string = Fn.importValue("eps-route53-resources:EPS-domain") |
| 67 | + const hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", { |
| 68 | + hostedZoneId: Fn.importValue("eps-route53-resources:EPS-ZoneID"), |
| 69 | + zoneName: epsDomainName |
| 70 | + }) |
| 71 | + const serviceDomainName = `${props.stackName}.${epsDomainName}` |
| 72 | + |
| 73 | + // Resources |
| 74 | + const logGroup = new LogGroup(this, "ApiGatewayAccessLogGroup", { |
| 75 | + encryptionKey: cloudWatchLogsKmsKey, |
| 76 | + logGroupName: `/aws/apigateway/${props.stackName}-apigw`, |
| 77 | + retention: props.logRetentionInDays, |
| 78 | + removalPolicy: RemovalPolicy.DESTROY |
| 79 | + }) |
| 80 | + |
| 81 | + new CfnSubscriptionFilter(this, "ApiGatewayAccessLogsSplunkSubscriptionFilter", { |
| 82 | + destinationArn: splunkDeliveryStream.streamArn, |
| 83 | + filterPattern: "", |
| 84 | + logGroupName: logGroup.logGroupName, |
| 85 | + roleArn: splunkSubscriptionFilterRole.roleArn |
| 86 | + }) |
| 87 | + |
| 88 | + if (props.forwardCsocLogs) { |
| 89 | + new CfnSubscriptionFilter(this, "ApiGatewayAccessLogsCSOCSubscriptionFilter", { |
| 90 | + destinationArn: props.csocApiGatewayDestination, |
| 91 | + filterPattern: "", |
| 92 | + logGroupName: logGroup.logGroupName, |
| 93 | + roleArn: splunkSubscriptionFilterRole.roleArn |
| 94 | + }) |
| 95 | + } |
| 96 | + |
| 97 | + const certificate = new Certificate(this, "Certificate", { |
| 98 | + domainName: serviceDomainName, |
| 99 | + validation: CertificateValidation.fromDns(hostedZone) |
| 100 | + }) |
| 101 | + |
| 102 | + let mtlsConfig: MTLSConfig | undefined |
| 103 | + |
| 104 | + if (props.mutualTlsTrustStoreKey) { |
| 105 | + const trustStoreKeyPrefix = `cpt-api/${props.stackName}-truststore` |
| 106 | + const logGroup = new LogGroup(scope, "LambdaLogGroup", { |
| 107 | + encryptionKey: cloudWatchLogsKmsKey, |
| 108 | + logGroupName: `/aws/lambda/${props.stackName}-truststore-deployment`, |
| 109 | + retention: props.logRetentionInDays, |
| 110 | + removalPolicy: RemovalPolicy.DESTROY |
| 111 | + }) |
| 112 | + const trustStoreDeploymentPolicy = new ManagedPolicy(this, "TrustStoreDeploymentPolicy", { |
| 113 | + statements: [ |
| 114 | + new PolicyStatement({ |
| 115 | + actions: [ |
| 116 | + "s3:ListBucket" |
| 117 | + ], |
| 118 | + resources: [ |
| 119 | + trustStoreBucket.bucketArn, |
| 120 | + trustStoreDeploymentBucket.bucketArn |
| 121 | + ] |
| 122 | + }), |
| 123 | + new PolicyStatement({ |
| 124 | + actions: [ |
| 125 | + "s3:GetObject" |
| 126 | + ], |
| 127 | + resources: [trustStoreBucket.arnForObjects(props.mutualTlsTrustStoreKey)] |
| 128 | + }), |
| 129 | + new PolicyStatement({ |
| 130 | + actions: [ |
| 131 | + "s3:DeleteObject", |
| 132 | + "s3:PutObject" |
| 133 | + ], |
| 134 | + resources: [ |
| 135 | + trustStoreDeploymentBucket.arnForObjects(trustStoreKeyPrefix + "/" + props.mutualTlsTrustStoreKey) |
| 136 | + ] |
| 137 | + }), |
| 138 | + new PolicyStatement({ |
| 139 | + actions: [ |
| 140 | + "kms:Decrypt", |
| 141 | + "kms:Encrypt", |
| 142 | + "kms:GenerateDataKey" |
| 143 | + ], |
| 144 | + resources: [trustStoreBucketKmsKey.keyArn] |
| 145 | + }), |
| 146 | + new PolicyStatement({ |
| 147 | + actions: [ |
| 148 | + "logs:CreateLogStream", |
| 149 | + "logs:PutLogEvents" |
| 150 | + ], |
| 151 | + resources: [ |
| 152 | + logGroup.logGroupArn, |
| 153 | + `${logGroup.logGroupArn}:log-stream:*` |
| 154 | + ] |
| 155 | + }) |
| 156 | + ] |
| 157 | + }) |
| 158 | + NagSuppressions.addResourceSuppressions(trustStoreDeploymentPolicy, [ |
| 159 | + { |
| 160 | + id: "AwsSolutions-IAM5", |
| 161 | + // eslint-disable-next-line max-len |
| 162 | + reason: "Suppress error for not having wildcards in permissions. This is a fine as we need to have permissions on all log streams under path" |
| 163 | + } |
| 164 | + ]) |
| 165 | + const trustStoreDeploymentRole = new Role(this, "TrustStoreDeploymentRole", { |
| 166 | + assumedBy: new ServicePrincipal("lambda.amazonaws.com"), |
| 167 | + managedPolicies: [trustStoreDeploymentPolicy] |
| 168 | + }).withoutPolicyUpdates() |
| 169 | + const deployment = new BucketDeployment(this, "TrustStoreDeployment", { |
| 170 | + sources: [Source.bucket(trustStoreBucket, props.mutualTlsTrustStoreKey)], |
| 171 | + destinationBucket: trustStoreDeploymentBucket, |
| 172 | + destinationKeyPrefix: trustStoreKeyPrefix, |
| 173 | + extract: false, |
| 174 | + retainOnDelete: false, |
| 175 | + role: trustStoreDeploymentRole, |
| 176 | + logGroup: logGroup |
| 177 | + }) |
| 178 | + mtlsConfig = { |
| 179 | + bucket: deployment.deployedBucket, |
| 180 | + key: trustStoreKeyPrefix + "/" + props.mutualTlsTrustStoreKey |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + const apiGateway = new RestApi(this, "ApiGateway", { |
| 185 | + restApiName: `${props.stackName}-apigw`, |
| 186 | + domainName: { |
| 187 | + domainName: serviceDomainName, |
| 188 | + certificate: certificate, |
| 189 | + securityPolicy: SecurityPolicy.TLS_1_2, |
| 190 | + endpointType: EndpointType.REGIONAL, |
| 191 | + mtls: mtlsConfig |
| 192 | + }, |
| 193 | + disableExecuteApiEndpoint: mtlsConfig ? true : false, |
| 194 | + endpointConfiguration: { |
| 195 | + types: [EndpointType.REGIONAL] |
| 196 | + }, |
| 197 | + deploy: true, |
| 198 | + deployOptions: { |
| 199 | + accessLogDestination: new LogGroupLogDestination(logGroup), |
| 200 | + accessLogFormat: accessLogFormat(), |
| 201 | + loggingLevel: MethodLoggingLevel.INFO, |
| 202 | + metricsEnabled: true |
| 203 | + } |
| 204 | + }) |
| 205 | + |
| 206 | + const role = new Role(this, "ApiGatewayRole", { |
| 207 | + assumedBy: new ServicePrincipal("apigateway.amazonaws.com"), |
| 208 | + managedPolicies: props.executionPolicies |
| 209 | + }).withoutPolicyUpdates() |
| 210 | + |
| 211 | + new ARecord(this, "ARecord", { |
| 212 | + recordName: props.stackName, |
| 213 | + target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)), |
| 214 | + zone: hostedZone |
| 215 | + }) |
| 216 | + |
| 217 | + const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage |
| 218 | + cfnStage.cfnOptions.metadata = { |
| 219 | + guard: { |
| 220 | + SuppressedRules: [ |
| 221 | + "API_GW_CACHE_ENABLED_AND_ENCRYPTED" |
| 222 | + ] |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + // Outputs |
| 227 | + this.api = apiGateway |
| 228 | + this.role = role |
| 229 | + } |
| 230 | +} |
0 commit comments