@aws/nx-plugin 1.0.0-rc.76 → 1.0.0-rc.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations.json +16 -1
- package/package.json +1 -1
- package/src/migrations/latest/dynamodb-configurable-encryption/__snapshots__/migration.spec.ts.snap +429 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/metadata.json +3 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.d.ts +6 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.js +325 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.js.map +1 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/metadata.json +3 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.d.ts +6 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js +418 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js.map +1 -0
- package/src/migrations/latest/user-identity-configurable-mfa/metadata.json +3 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.d.ts +6 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.js +362 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.js.map +1 -0
- package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
- package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +259 -48
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +37 -7
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +26 -0
- package/src/utils/dynamodb-constructs/files/cdk/core/dynamodb.ts.template +23 -3
- package/src/utils/dynamodb-constructs/files/terraform/app/dynamodb/__nameKebabCase__/__nameKebabCase__.tf.template +27 -1
- package/src/utils/dynamodb-constructs/files/terraform/core/dynamodb/dynamodb.tf.template +39 -5
- package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +41 -7
- package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +48 -13
- package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +26 -0
- package/src/utils/website-constructs/files/cdk/app/static-websites/__websiteNameKebabCase__.ts.template +11 -2
- package/src/utils/website-constructs/files/cdk/core/static-website.ts.template +47 -10
- package/src/utils/website-constructs/files/terraform/app/static-websites/__websiteNameKebabCase__/__websiteNameKebabCase__.tf.template +55 -0
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +57 -15
|
@@ -164,6 +164,41 @@ variable "acm_certificate_arn" {
|
|
|
164
164
|
default = null
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
variable "enable_waf" {
|
|
168
|
+
description = "Whether to protect the CloudFront distribution with an AWS WAF Web ACL."
|
|
169
|
+
type = bool
|
|
170
|
+
default = true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
variable "encryption" {
|
|
174
|
+
description = "Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED."
|
|
175
|
+
type = string
|
|
176
|
+
default = "KMS"
|
|
177
|
+
|
|
178
|
+
validation {
|
|
179
|
+
condition = contains(["KMS", "S3_MANAGED"], var.encryption)
|
|
180
|
+
error_message = "encryption must be one of KMS or S3_MANAGED."
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
variable "kms_key_arn" {
|
|
185
|
+
description = "ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy."
|
|
186
|
+
type = string
|
|
187
|
+
default = null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
variable "create_kms_key" {
|
|
191
|
+
description = "Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn."
|
|
192
|
+
type = bool
|
|
193
|
+
default = true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
variable "enable_key_rotation" {
|
|
197
|
+
description = "Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true."
|
|
198
|
+
type = bool
|
|
199
|
+
default = true
|
|
200
|
+
}
|
|
201
|
+
|
|
167
202
|
# Content-Security-Policy enforced on all responses. Restricts scripts and
|
|
168
203
|
# framing to mitigate XSS and clickjacking, while permitting HTTPS/WSS calls
|
|
169
204
|
# (connect-src) to AWS service endpoints such as API Gateway, Cognito and
|
|
@@ -175,6 +210,13 @@ locals {
|
|
|
175
210
|
# Delivery source/destination names have a 60 character maximum. Truncate the
|
|
176
211
|
# website name so the longest delivery name stays within the limit.
|
|
177
212
|
access_logs_name_prefix = substr(lower(var.website_name), 0, 16)
|
|
213
|
+
|
|
214
|
+
create_website_key = var.encryption == "KMS" && var.create_kms_key
|
|
215
|
+
website_kms_key_arn = (
|
|
216
|
+
var.encryption != "KMS" ? null :
|
|
217
|
+
local.create_website_key ? aws_kms_key.website_key[0].arn :
|
|
218
|
+
var.kms_key_arn
|
|
219
|
+
)
|
|
178
220
|
}
|
|
179
221
|
|
|
180
222
|
|
|
@@ -185,9 +227,11 @@ data "aws_region" "current" {}
|
|
|
185
227
|
|
|
186
228
|
# KMS Key for encryption
|
|
187
229
|
resource "aws_kms_key" "website_key" {
|
|
230
|
+
count = local.create_website_key ? 1 : 0
|
|
231
|
+
|
|
188
232
|
description = "KMS key for \${var.website_name} website encryption"
|
|
189
233
|
deletion_window_in_days = 7
|
|
190
|
-
enable_key_rotation =
|
|
234
|
+
enable_key_rotation = var.enable_key_rotation
|
|
191
235
|
|
|
192
236
|
policy = jsonencode({
|
|
193
237
|
Version = "2012-10-17"
|
|
@@ -260,8 +304,10 @@ resource "aws_kms_key" "website_key" {
|
|
|
260
304
|
}
|
|
261
305
|
|
|
262
306
|
resource "aws_kms_alias" "website_key_alias" {
|
|
307
|
+
count = local.create_website_key ? 1 : 0
|
|
308
|
+
|
|
263
309
|
name = "alias/\${lower(var.website_name)}-website-key-\${random_id.unique_suffix.hex}"
|
|
264
|
-
target_key_id = aws_kms_key.website_key.key_id
|
|
310
|
+
target_key_id = aws_kms_key.website_key[0].key_id
|
|
265
311
|
}
|
|
266
312
|
|
|
267
313
|
# CloudWatch log group receiving S3 server access logs for the website and
|
|
@@ -269,7 +315,7 @@ resource "aws_kms_alias" "website_key_alias" {
|
|
|
269
315
|
resource "aws_cloudwatch_log_group" "access_logs" {
|
|
270
316
|
name = "/aws/s3/\${lower(var.website_name)}-access-logs-\${random_id.bucket_suffix.hex}"
|
|
271
317
|
retention_in_days = 365
|
|
272
|
-
kms_key_id =
|
|
318
|
+
kms_key_id = local.website_kms_key_arn
|
|
273
319
|
}
|
|
274
320
|
|
|
275
321
|
resource "random_id" "unique_suffix" {
|
|
@@ -306,8 +352,8 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "website_encryptio
|
|
|
306
352
|
|
|
307
353
|
rule {
|
|
308
354
|
apply_server_side_encryption_by_default {
|
|
309
|
-
kms_master_key_id =
|
|
310
|
-
sse_algorithm = "aws:kms"
|
|
355
|
+
kms_master_key_id = var.encryption == "KMS" ? local.website_kms_key_arn : null
|
|
356
|
+
sse_algorithm = var.encryption == "KMS" ? "aws:kms" : "AES256"
|
|
311
357
|
}
|
|
312
358
|
}
|
|
313
359
|
}
|
|
@@ -371,8 +417,8 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "distribution_logs
|
|
|
371
417
|
|
|
372
418
|
rule {
|
|
373
419
|
apply_server_side_encryption_by_default {
|
|
374
|
-
kms_master_key_id =
|
|
375
|
-
sse_algorithm = "aws:kms"
|
|
420
|
+
kms_master_key_id = var.encryption == "KMS" ? local.website_kms_key_arn : null
|
|
421
|
+
sse_algorithm = var.encryption == "KMS" ? "aws:kms" : "AES256"
|
|
376
422
|
}
|
|
377
423
|
}
|
|
378
424
|
}
|
|
@@ -442,6 +488,8 @@ resource "aws_s3_bucket_policy" "distribution_logs_ssl_policy" {
|
|
|
442
488
|
|
|
443
489
|
# WAF Web ACL (must be in us-east-1 for CloudFront)
|
|
444
490
|
resource "aws_wafv2_web_acl" "cloudfront_waf" {
|
|
491
|
+
count = var.enable_waf ? 1 : 0
|
|
492
|
+
|
|
445
493
|
#checkov:skip=CKV2_AWS_31:WAF logging disabled
|
|
446
494
|
provider = aws.us_east_1
|
|
447
495
|
name = "\${lower(var.website_name)}-cloudfront-waf-\${random_id.unique_suffix.hex}"
|
|
@@ -566,7 +614,7 @@ resource "aws_cloudfront_distribution" "website" {
|
|
|
566
614
|
enabled = true
|
|
567
615
|
is_ipv6_enabled = true
|
|
568
616
|
default_root_object = "index.html"
|
|
569
|
-
web_acl_id = aws_wafv2_web_acl.cloudfront_waf.arn
|
|
617
|
+
web_acl_id = var.enable_waf ? aws_wafv2_web_acl.cloudfront_waf[0].arn : null
|
|
570
618
|
aliases = var.custom_domain_names
|
|
571
619
|
|
|
572
620
|
logging_config {
|
|
@@ -624,12 +672,6 @@ resource "aws_cloudfront_distribution" "website" {
|
|
|
624
672
|
Name = "\${lower(var.website_name)}-distribution-\${random_id.unique_suffix.hex}"
|
|
625
673
|
}
|
|
626
674
|
|
|
627
|
-
lifecycle {
|
|
628
|
-
replace_triggered_by = [
|
|
629
|
-
aws_wafv2_web_acl.cloudfront_waf
|
|
630
|
-
]
|
|
631
|
-
}
|
|
632
|
-
|
|
633
675
|
# CloudFront validates access to the distribution logs bucket when it
|
|
634
676
|
# creates the distribution.
|
|
635
677
|
depends_on = [
|
|
@@ -902,7 +944,7 @@ output "cloudfront_domain_name" {
|
|
|
902
944
|
|
|
903
945
|
output "waf_web_acl_arn" {
|
|
904
946
|
description = "ARN of the WAF Web ACL"
|
|
905
|
-
value = aws_wafv2_web_acl.cloudfront_waf.arn
|
|
947
|
+
value = var.enable_waf ? aws_wafv2_web_acl.cloudfront_waf[0].arn : null
|
|
906
948
|
}",
|
|
907
949
|
"test-app.tf": "terraform {
|
|
908
950
|
required_providers {
|
|
@@ -914,6 +956,53 @@ output "waf_web_acl_arn" {
|
|
|
914
956
|
}
|
|
915
957
|
}
|
|
916
958
|
|
|
959
|
+
variable "custom_domain_names" {
|
|
960
|
+
description = "Custom domain names (aliases) for the CloudFront distribution. Requires acm_certificate_arn."
|
|
961
|
+
type = list(string)
|
|
962
|
+
default = []
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
variable "acm_certificate_arn" {
|
|
966
|
+
description = "ARN of an ACM certificate (in us-east-1) for the custom domain names. When set, viewers are required to use TLS 1.2 or later."
|
|
967
|
+
type = string
|
|
968
|
+
default = null
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
variable "enable_waf" {
|
|
972
|
+
description = "Whether to protect the CloudFront distribution with an AWS WAF Web ACL."
|
|
973
|
+
type = bool
|
|
974
|
+
default = true
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
variable "encryption" {
|
|
978
|
+
description = "Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED."
|
|
979
|
+
type = string
|
|
980
|
+
default = "KMS"
|
|
981
|
+
|
|
982
|
+
validation {
|
|
983
|
+
condition = contains(["KMS", "S3_MANAGED"], var.encryption)
|
|
984
|
+
error_message = "encryption must be one of KMS or S3_MANAGED."
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
variable "kms_key_arn" {
|
|
989
|
+
description = "ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy."
|
|
990
|
+
type = string
|
|
991
|
+
default = null
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
variable "create_kms_key" {
|
|
995
|
+
description = "Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn."
|
|
996
|
+
type = bool
|
|
997
|
+
default = true
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
variable "enable_key_rotation" {
|
|
1001
|
+
description = "Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true."
|
|
1002
|
+
type = bool
|
|
1003
|
+
default = true
|
|
1004
|
+
}
|
|
1005
|
+
|
|
917
1006
|
# Static website module configured for the web package
|
|
918
1007
|
module "static_website" {
|
|
919
1008
|
source = "../../../core/static-website"
|
|
@@ -921,6 +1010,14 @@ module "static_website" {
|
|
|
921
1010
|
website_name = "test-app"
|
|
922
1011
|
website_file_path = "\${path.module}/../../../../../../../dist/test-app/bundle"
|
|
923
1012
|
|
|
1013
|
+
custom_domain_names = var.custom_domain_names
|
|
1014
|
+
acm_certificate_arn = var.acm_certificate_arn
|
|
1015
|
+
enable_waf = var.enable_waf
|
|
1016
|
+
encryption = var.encryption
|
|
1017
|
+
kms_key_arn = var.kms_key_arn
|
|
1018
|
+
create_kms_key = var.create_kms_key
|
|
1019
|
+
enable_key_rotation = var.enable_key_rotation
|
|
1020
|
+
|
|
924
1021
|
providers = {
|
|
925
1022
|
aws.us_east_1 = aws.us_east_1
|
|
926
1023
|
}
|
|
@@ -1299,11 +1396,17 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
1299
1396
|
exports[`react-website generator > Tanstack router integration > should generate website with no router correctly > packages/common/constructs/src/app/static-websites/test-app.ts 1`] = `
|
|
1300
1397
|
"import * as url from 'url';
|
|
1301
1398
|
import { Construct } from 'constructs';
|
|
1302
|
-
import { StaticWebsite } from '../../core/index.js';
|
|
1399
|
+
import { StaticWebsite, StaticWebsiteProps } from '../../core/index.js';
|
|
1400
|
+
|
|
1401
|
+
export type TestAppProps = Omit<
|
|
1402
|
+
StaticWebsiteProps,
|
|
1403
|
+
'websiteName' | 'websiteFilePath'
|
|
1404
|
+
>;
|
|
1303
1405
|
|
|
1304
1406
|
export class TestApp extends StaticWebsite {
|
|
1305
|
-
constructor(scope: Construct, id: string) {
|
|
1407
|
+
constructor(scope: Construct, id: string, props?: TestAppProps) {
|
|
1306
1408
|
super(scope, id, {
|
|
1409
|
+
...props,
|
|
1307
1410
|
websiteName: 'TestApp',
|
|
1308
1411
|
websiteFilePath: url.fileURLToPath(
|
|
1309
1412
|
new URL('../../../../../../dist/test-app/bundle', import.meta.url),
|
|
@@ -1641,7 +1744,7 @@ import {
|
|
|
1641
1744
|
} from 'aws-cdk-lib/aws-s3-deployment';
|
|
1642
1745
|
import { Construct } from 'constructs';
|
|
1643
1746
|
import { RuntimeConfig } from './runtime-config.js';
|
|
1644
|
-
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
1747
|
+
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
|
|
1645
1748
|
import {
|
|
1646
1749
|
CfnDelivery,
|
|
1647
1750
|
CfnDeliveryDestination,
|
|
@@ -1684,6 +1787,33 @@ export interface StaticWebsiteProps {
|
|
|
1684
1787
|
* When provided, viewers are required to use TLS 1.2 or later.
|
|
1685
1788
|
*/
|
|
1686
1789
|
readonly certificate?: ICertificate;
|
|
1790
|
+
/**
|
|
1791
|
+
* Whether to protect the CloudFront distribution with an AWS WAF Web ACL.
|
|
1792
|
+
*
|
|
1793
|
+
* @default true
|
|
1794
|
+
*/
|
|
1795
|
+
readonly enableWaf?: boolean;
|
|
1796
|
+
/**
|
|
1797
|
+
* Server-side encryption for the website and distribution log buckets.
|
|
1798
|
+
*
|
|
1799
|
+
* @default BucketEncryption.KMS
|
|
1800
|
+
*/
|
|
1801
|
+
readonly encryption?: BucketEncryption;
|
|
1802
|
+
/**
|
|
1803
|
+
* KMS key used to encrypt the website and distribution log buckets. Only used when \`encryption\` is
|
|
1804
|
+
* \`BucketEncryption.KMS\`. When not provided, a new key is created. Note that a key imported via
|
|
1805
|
+
* \`Key.fromKeyArn\` must already grant the CloudWatch Logs, S3 and CloudFront service principals the
|
|
1806
|
+
* necessary permissions in its own key policy - \`addToResourcePolicy\` is a no-op on an imported key,
|
|
1807
|
+
* so this construct cannot grant them on your behalf.
|
|
1808
|
+
*/
|
|
1809
|
+
readonly encryptionKey?: IKey;
|
|
1810
|
+
/**
|
|
1811
|
+
* Whether the automatically created KMS key has rotation enabled. Only applies when \`encryption\` is
|
|
1812
|
+
* \`BucketEncryption.KMS\` and no \`encryptionKey\` is supplied.
|
|
1813
|
+
*
|
|
1814
|
+
* @default true
|
|
1815
|
+
*/
|
|
1816
|
+
readonly enableKeyRotation?: boolean;
|
|
1687
1817
|
}
|
|
1688
1818
|
|
|
1689
1819
|
/**
|
|
@@ -1707,17 +1837,22 @@ export class StaticWebsite extends Construct {
|
|
|
1707
1837
|
websiteName,
|
|
1708
1838
|
domainNames,
|
|
1709
1839
|
certificate,
|
|
1840
|
+
enableWaf = true,
|
|
1841
|
+
encryption = BucketEncryption.KMS,
|
|
1842
|
+
encryptionKey,
|
|
1843
|
+
enableKeyRotation = true,
|
|
1710
1844
|
}: StaticWebsiteProps,
|
|
1711
1845
|
) {
|
|
1712
1846
|
super(scope, id);
|
|
1713
1847
|
|
|
1714
|
-
const websiteKey
|
|
1715
|
-
|
|
1716
|
-
|
|
1848
|
+
const websiteKey: IKey | undefined =
|
|
1849
|
+
encryption === BucketEncryption.KMS
|
|
1850
|
+
? (encryptionKey ?? new Key(this, 'WebsiteKey', { enableKeyRotation }))
|
|
1851
|
+
: undefined;
|
|
1717
1852
|
|
|
1718
1853
|
// Allow CloudWatch Logs to use the website key for server access log delivery.
|
|
1719
1854
|
const stack = Stack.of(this);
|
|
1720
|
-
websiteKey
|
|
1855
|
+
websiteKey?.addToResourcePolicy(
|
|
1721
1856
|
new PolicyStatement({
|
|
1722
1857
|
effect: Effect.ALLOW,
|
|
1723
1858
|
principals: [
|
|
@@ -1751,7 +1886,7 @@ export class StaticWebsite extends Construct {
|
|
|
1751
1886
|
enforceSSL: true,
|
|
1752
1887
|
autoDeleteObjects: true,
|
|
1753
1888
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
1754
|
-
encryption
|
|
1889
|
+
encryption,
|
|
1755
1890
|
encryptionKey: websiteKey,
|
|
1756
1891
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
1757
1892
|
publicReadAccess: false,
|
|
@@ -1768,7 +1903,7 @@ export class StaticWebsite extends Construct {
|
|
|
1768
1903
|
accessLogs,
|
|
1769
1904
|
);
|
|
1770
1905
|
// Web ACL
|
|
1771
|
-
const wafStack = new CloudfrontWebAcl(this, 'waf');
|
|
1906
|
+
const wafStack = enableWaf ? new CloudfrontWebAcl(this, 'waf') : undefined;
|
|
1772
1907
|
|
|
1773
1908
|
// Bucket holding CloudFront standard access logs. CloudFront delivers its
|
|
1774
1909
|
// own logs to S3 only, so this bucket is retained; its S3 server access
|
|
@@ -1777,7 +1912,7 @@ export class StaticWebsite extends Construct {
|
|
|
1777
1912
|
enforceSSL: true,
|
|
1778
1913
|
autoDeleteObjects: true,
|
|
1779
1914
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
1780
|
-
encryption
|
|
1915
|
+
encryption,
|
|
1781
1916
|
encryptionKey: websiteKey,
|
|
1782
1917
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
|
|
1783
1918
|
publicReadAccess: false,
|
|
@@ -1830,7 +1965,7 @@ export class StaticWebsite extends Construct {
|
|
|
1830
1965
|
this,
|
|
1831
1966
|
'CloudfrontDistribution',
|
|
1832
1967
|
{
|
|
1833
|
-
webAclId: wafStack
|
|
1968
|
+
webAclId: wafStack?.wafArn,
|
|
1834
1969
|
enableLogging: true,
|
|
1835
1970
|
logBucket: logBucket,
|
|
1836
1971
|
...(certificate
|
|
@@ -3124,11 +3259,17 @@ exports[`react-website generator > Tanstack router integration > should generate
|
|
|
3124
3259
|
exports[`react-website generator > Tanstack router integration > should generate website with router correctly > packages/common/constructs/src/app/static-websites/test-app.ts 1`] = `
|
|
3125
3260
|
"import * as url from 'url';
|
|
3126
3261
|
import { Construct } from 'constructs';
|
|
3127
|
-
import { StaticWebsite } from '../../core/index.js';
|
|
3262
|
+
import { StaticWebsite, StaticWebsiteProps } from '../../core/index.js';
|
|
3263
|
+
|
|
3264
|
+
export type TestAppProps = Omit<
|
|
3265
|
+
StaticWebsiteProps,
|
|
3266
|
+
'websiteName' | 'websiteFilePath'
|
|
3267
|
+
>;
|
|
3128
3268
|
|
|
3129
3269
|
export class TestApp extends StaticWebsite {
|
|
3130
|
-
constructor(scope: Construct, id: string) {
|
|
3270
|
+
constructor(scope: Construct, id: string, props?: TestAppProps) {
|
|
3131
3271
|
super(scope, id, {
|
|
3272
|
+
...props,
|
|
3132
3273
|
websiteName: 'TestApp',
|
|
3133
3274
|
websiteFilePath: url.fileURLToPath(
|
|
3134
3275
|
new URL('../../../../../../dist/test-app/bundle', import.meta.url),
|
|
@@ -3466,7 +3607,7 @@ import {
|
|
|
3466
3607
|
} from 'aws-cdk-lib/aws-s3-deployment';
|
|
3467
3608
|
import { Construct } from 'constructs';
|
|
3468
3609
|
import { RuntimeConfig } from './runtime-config.js';
|
|
3469
|
-
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
3610
|
+
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
|
|
3470
3611
|
import {
|
|
3471
3612
|
CfnDelivery,
|
|
3472
3613
|
CfnDeliveryDestination,
|
|
@@ -3509,6 +3650,33 @@ export interface StaticWebsiteProps {
|
|
|
3509
3650
|
* When provided, viewers are required to use TLS 1.2 or later.
|
|
3510
3651
|
*/
|
|
3511
3652
|
readonly certificate?: ICertificate;
|
|
3653
|
+
/**
|
|
3654
|
+
* Whether to protect the CloudFront distribution with an AWS WAF Web ACL.
|
|
3655
|
+
*
|
|
3656
|
+
* @default true
|
|
3657
|
+
*/
|
|
3658
|
+
readonly enableWaf?: boolean;
|
|
3659
|
+
/**
|
|
3660
|
+
* Server-side encryption for the website and distribution log buckets.
|
|
3661
|
+
*
|
|
3662
|
+
* @default BucketEncryption.KMS
|
|
3663
|
+
*/
|
|
3664
|
+
readonly encryption?: BucketEncryption;
|
|
3665
|
+
/**
|
|
3666
|
+
* KMS key used to encrypt the website and distribution log buckets. Only used when \`encryption\` is
|
|
3667
|
+
* \`BucketEncryption.KMS\`. When not provided, a new key is created. Note that a key imported via
|
|
3668
|
+
* \`Key.fromKeyArn\` must already grant the CloudWatch Logs, S3 and CloudFront service principals the
|
|
3669
|
+
* necessary permissions in its own key policy - \`addToResourcePolicy\` is a no-op on an imported key,
|
|
3670
|
+
* so this construct cannot grant them on your behalf.
|
|
3671
|
+
*/
|
|
3672
|
+
readonly encryptionKey?: IKey;
|
|
3673
|
+
/**
|
|
3674
|
+
* Whether the automatically created KMS key has rotation enabled. Only applies when \`encryption\` is
|
|
3675
|
+
* \`BucketEncryption.KMS\` and no \`encryptionKey\` is supplied.
|
|
3676
|
+
*
|
|
3677
|
+
* @default true
|
|
3678
|
+
*/
|
|
3679
|
+
readonly enableKeyRotation?: boolean;
|
|
3512
3680
|
}
|
|
3513
3681
|
|
|
3514
3682
|
/**
|
|
@@ -3532,17 +3700,22 @@ export class StaticWebsite extends Construct {
|
|
|
3532
3700
|
websiteName,
|
|
3533
3701
|
domainNames,
|
|
3534
3702
|
certificate,
|
|
3703
|
+
enableWaf = true,
|
|
3704
|
+
encryption = BucketEncryption.KMS,
|
|
3705
|
+
encryptionKey,
|
|
3706
|
+
enableKeyRotation = true,
|
|
3535
3707
|
}: StaticWebsiteProps,
|
|
3536
3708
|
) {
|
|
3537
3709
|
super(scope, id);
|
|
3538
3710
|
|
|
3539
|
-
const websiteKey
|
|
3540
|
-
|
|
3541
|
-
|
|
3711
|
+
const websiteKey: IKey | undefined =
|
|
3712
|
+
encryption === BucketEncryption.KMS
|
|
3713
|
+
? (encryptionKey ?? new Key(this, 'WebsiteKey', { enableKeyRotation }))
|
|
3714
|
+
: undefined;
|
|
3542
3715
|
|
|
3543
3716
|
// Allow CloudWatch Logs to use the website key for server access log delivery.
|
|
3544
3717
|
const stack = Stack.of(this);
|
|
3545
|
-
websiteKey
|
|
3718
|
+
websiteKey?.addToResourcePolicy(
|
|
3546
3719
|
new PolicyStatement({
|
|
3547
3720
|
effect: Effect.ALLOW,
|
|
3548
3721
|
principals: [
|
|
@@ -3576,7 +3749,7 @@ export class StaticWebsite extends Construct {
|
|
|
3576
3749
|
enforceSSL: true,
|
|
3577
3750
|
autoDeleteObjects: true,
|
|
3578
3751
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
3579
|
-
encryption
|
|
3752
|
+
encryption,
|
|
3580
3753
|
encryptionKey: websiteKey,
|
|
3581
3754
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
3582
3755
|
publicReadAccess: false,
|
|
@@ -3593,7 +3766,7 @@ export class StaticWebsite extends Construct {
|
|
|
3593
3766
|
accessLogs,
|
|
3594
3767
|
);
|
|
3595
3768
|
// Web ACL
|
|
3596
|
-
const wafStack = new CloudfrontWebAcl(this, 'waf');
|
|
3769
|
+
const wafStack = enableWaf ? new CloudfrontWebAcl(this, 'waf') : undefined;
|
|
3597
3770
|
|
|
3598
3771
|
// Bucket holding CloudFront standard access logs. CloudFront delivers its
|
|
3599
3772
|
// own logs to S3 only, so this bucket is retained; its S3 server access
|
|
@@ -3602,7 +3775,7 @@ export class StaticWebsite extends Construct {
|
|
|
3602
3775
|
enforceSSL: true,
|
|
3603
3776
|
autoDeleteObjects: true,
|
|
3604
3777
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
3605
|
-
encryption
|
|
3778
|
+
encryption,
|
|
3606
3779
|
encryptionKey: websiteKey,
|
|
3607
3780
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
|
|
3608
3781
|
publicReadAccess: false,
|
|
@@ -3655,7 +3828,7 @@ export class StaticWebsite extends Construct {
|
|
|
3655
3828
|
this,
|
|
3656
3829
|
'CloudfrontDistribution',
|
|
3657
3830
|
{
|
|
3658
|
-
webAclId: wafStack
|
|
3831
|
+
webAclId: wafStack?.wafArn,
|
|
3659
3832
|
enableLogging: true,
|
|
3660
3833
|
logBucket: logBucket,
|
|
3661
3834
|
...(certificate
|
|
@@ -5151,7 +5324,7 @@ import {
|
|
|
5151
5324
|
} from 'aws-cdk-lib/aws-s3-deployment';
|
|
5152
5325
|
import { Construct } from 'constructs';
|
|
5153
5326
|
import { RuntimeConfig } from './runtime-config.js';
|
|
5154
|
-
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
5327
|
+
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
|
|
5155
5328
|
import {
|
|
5156
5329
|
CfnDelivery,
|
|
5157
5330
|
CfnDeliveryDestination,
|
|
@@ -5194,6 +5367,33 @@ export interface StaticWebsiteProps {
|
|
|
5194
5367
|
* When provided, viewers are required to use TLS 1.2 or later.
|
|
5195
5368
|
*/
|
|
5196
5369
|
readonly certificate?: ICertificate;
|
|
5370
|
+
/**
|
|
5371
|
+
* Whether to protect the CloudFront distribution with an AWS WAF Web ACL.
|
|
5372
|
+
*
|
|
5373
|
+
* @default true
|
|
5374
|
+
*/
|
|
5375
|
+
readonly enableWaf?: boolean;
|
|
5376
|
+
/**
|
|
5377
|
+
* Server-side encryption for the website and distribution log buckets.
|
|
5378
|
+
*
|
|
5379
|
+
* @default BucketEncryption.KMS
|
|
5380
|
+
*/
|
|
5381
|
+
readonly encryption?: BucketEncryption;
|
|
5382
|
+
/**
|
|
5383
|
+
* KMS key used to encrypt the website and distribution log buckets. Only used when \`encryption\` is
|
|
5384
|
+
* \`BucketEncryption.KMS\`. When not provided, a new key is created. Note that a key imported via
|
|
5385
|
+
* \`Key.fromKeyArn\` must already grant the CloudWatch Logs, S3 and CloudFront service principals the
|
|
5386
|
+
* necessary permissions in its own key policy - \`addToResourcePolicy\` is a no-op on an imported key,
|
|
5387
|
+
* so this construct cannot grant them on your behalf.
|
|
5388
|
+
*/
|
|
5389
|
+
readonly encryptionKey?: IKey;
|
|
5390
|
+
/**
|
|
5391
|
+
* Whether the automatically created KMS key has rotation enabled. Only applies when \`encryption\` is
|
|
5392
|
+
* \`BucketEncryption.KMS\` and no \`encryptionKey\` is supplied.
|
|
5393
|
+
*
|
|
5394
|
+
* @default true
|
|
5395
|
+
*/
|
|
5396
|
+
readonly enableKeyRotation?: boolean;
|
|
5197
5397
|
}
|
|
5198
5398
|
|
|
5199
5399
|
/**
|
|
@@ -5217,17 +5417,22 @@ export class StaticWebsite extends Construct {
|
|
|
5217
5417
|
websiteName,
|
|
5218
5418
|
domainNames,
|
|
5219
5419
|
certificate,
|
|
5420
|
+
enableWaf = true,
|
|
5421
|
+
encryption = BucketEncryption.KMS,
|
|
5422
|
+
encryptionKey,
|
|
5423
|
+
enableKeyRotation = true,
|
|
5220
5424
|
}: StaticWebsiteProps,
|
|
5221
5425
|
) {
|
|
5222
5426
|
super(scope, id);
|
|
5223
5427
|
|
|
5224
|
-
const websiteKey
|
|
5225
|
-
|
|
5226
|
-
|
|
5428
|
+
const websiteKey: IKey | undefined =
|
|
5429
|
+
encryption === BucketEncryption.KMS
|
|
5430
|
+
? (encryptionKey ?? new Key(this, 'WebsiteKey', { enableKeyRotation }))
|
|
5431
|
+
: undefined;
|
|
5227
5432
|
|
|
5228
5433
|
// Allow CloudWatch Logs to use the website key for server access log delivery.
|
|
5229
5434
|
const stack = Stack.of(this);
|
|
5230
|
-
websiteKey
|
|
5435
|
+
websiteKey?.addToResourcePolicy(
|
|
5231
5436
|
new PolicyStatement({
|
|
5232
5437
|
effect: Effect.ALLOW,
|
|
5233
5438
|
principals: [
|
|
@@ -5261,7 +5466,7 @@ export class StaticWebsite extends Construct {
|
|
|
5261
5466
|
enforceSSL: true,
|
|
5262
5467
|
autoDeleteObjects: true,
|
|
5263
5468
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
5264
|
-
encryption
|
|
5469
|
+
encryption,
|
|
5265
5470
|
encryptionKey: websiteKey,
|
|
5266
5471
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
5267
5472
|
publicReadAccess: false,
|
|
@@ -5278,7 +5483,7 @@ export class StaticWebsite extends Construct {
|
|
|
5278
5483
|
accessLogs,
|
|
5279
5484
|
);
|
|
5280
5485
|
// Web ACL
|
|
5281
|
-
const wafStack = new CloudfrontWebAcl(this, 'waf');
|
|
5486
|
+
const wafStack = enableWaf ? new CloudfrontWebAcl(this, 'waf') : undefined;
|
|
5282
5487
|
|
|
5283
5488
|
// Bucket holding CloudFront standard access logs. CloudFront delivers its
|
|
5284
5489
|
// own logs to S3 only, so this bucket is retained; its S3 server access
|
|
@@ -5287,7 +5492,7 @@ export class StaticWebsite extends Construct {
|
|
|
5287
5492
|
enforceSSL: true,
|
|
5288
5493
|
autoDeleteObjects: true,
|
|
5289
5494
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
5290
|
-
encryption
|
|
5495
|
+
encryption,
|
|
5291
5496
|
encryptionKey: websiteKey,
|
|
5292
5497
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
|
|
5293
5498
|
publicReadAccess: false,
|
|
@@ -5340,7 +5545,7 @@ export class StaticWebsite extends Construct {
|
|
|
5340
5545
|
this,
|
|
5341
5546
|
'CloudfrontDistribution',
|
|
5342
5547
|
{
|
|
5343
|
-
webAclId: wafStack
|
|
5548
|
+
webAclId: wafStack?.wafArn,
|
|
5344
5549
|
enableLogging: true,
|
|
5345
5550
|
logBucket: logBucket,
|
|
5346
5551
|
...(certificate
|
|
@@ -5539,11 +5744,17 @@ export class CloudfrontWebAcl extends Stack {
|
|
|
5539
5744
|
exports[`react-website generator > should generate shared constructs > test-app.ts 1`] = `
|
|
5540
5745
|
"import * as url from 'url';
|
|
5541
5746
|
import { Construct } from 'constructs';
|
|
5542
|
-
import { StaticWebsite } from '../../core/index.js';
|
|
5747
|
+
import { StaticWebsite, StaticWebsiteProps } from '../../core/index.js';
|
|
5748
|
+
|
|
5749
|
+
export type TestAppProps = Omit<
|
|
5750
|
+
StaticWebsiteProps,
|
|
5751
|
+
'websiteName' | 'websiteFilePath'
|
|
5752
|
+
>;
|
|
5543
5753
|
|
|
5544
5754
|
export class TestApp extends StaticWebsite {
|
|
5545
|
-
constructor(scope: Construct, id: string) {
|
|
5755
|
+
constructor(scope: Construct, id: string, props?: TestAppProps) {
|
|
5546
5756
|
super(scope, id, {
|
|
5757
|
+
...props,
|
|
5547
5758
|
websiteName: 'TestApp',
|
|
5548
5759
|
websiteFilePath: url.fileURLToPath(
|
|
5549
5760
|
new URL('../../../../../../dist/test-app/bundle', import.meta.url),
|
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
FeaturePlan,
|
|
106
106
|
ManagedLoginVersion,
|
|
107
107
|
Mfa,
|
|
108
|
+
type MfaSecondFactor,
|
|
108
109
|
OAuthScope,
|
|
109
110
|
StandardThreatProtectionMode,
|
|
110
111
|
UserPool,
|
|
@@ -137,6 +138,20 @@ export interface UserIdentityProps {
|
|
|
137
138
|
* @default true
|
|
138
139
|
*/
|
|
139
140
|
readonly enableWaf?: boolean;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all.
|
|
144
|
+
*
|
|
145
|
+
* @default Mfa.REQUIRED
|
|
146
|
+
*/
|
|
147
|
+
readonly mfa?: Mfa;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The MFA methods available to users when MFA is not off.
|
|
151
|
+
*
|
|
152
|
+
* @default { sms: true, otp: true }
|
|
153
|
+
*/
|
|
154
|
+
readonly mfaSecondFactor?: MfaSecondFactor;
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
/**
|
|
@@ -155,12 +170,22 @@ export class UserIdentity extends Construct {
|
|
|
155
170
|
constructor(
|
|
156
171
|
scope: Construct,
|
|
157
172
|
id: string,
|
|
158
|
-
{
|
|
173
|
+
{
|
|
174
|
+
enableWaf = true,
|
|
175
|
+
mfa = Mfa.REQUIRED,
|
|
176
|
+
mfaSecondFactor = { sms: true, otp: true },
|
|
177
|
+
}: UserIdentityProps = {},
|
|
159
178
|
) {
|
|
160
179
|
super(scope, id);
|
|
161
180
|
|
|
181
|
+
if (mfa === Mfa.REQUIRED && !mfaSecondFactor.sms && !mfaSecondFactor.otp) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
'UserIdentity: mfa is REQUIRED but mfaSecondFactor has no methods enabled (sms and otp are both false)',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
162
187
|
this.region = Stack.of(this).region;
|
|
163
|
-
this.userPool = this.createUserPool();
|
|
188
|
+
this.userPool = this.createUserPool(mfa, mfaSecondFactor);
|
|
164
189
|
|
|
165
190
|
if (enableWaf) {
|
|
166
191
|
this.webAcl = this.createWebAcl(
|
|
@@ -208,7 +233,12 @@ export class UserIdentity extends Construct {
|
|
|
208
233
|
});
|
|
209
234
|
}
|
|
210
235
|
|
|
211
|
-
private createUserPool = () => {
|
|
236
|
+
private createUserPool = (mfa: Mfa, mfaSecondFactor: MfaSecondFactor) => {
|
|
237
|
+
// Cognito rejects SmsConfiguration (emitted whenever phone is auto-verified) unless SMS is
|
|
238
|
+
// also an enabled MFA method, for any non-OFF MfaConfiguration. So phone verification via SMS
|
|
239
|
+
// can only be offered when SMS is actually usable as a second factor.
|
|
240
|
+
const phoneVerificationEnabled = mfa === Mfa.OFF || mfaSecondFactor.sms;
|
|
241
|
+
|
|
212
242
|
const userPool = new UserPool(this, 'UserPool', {
|
|
213
243
|
deletionProtection: true,
|
|
214
244
|
passwordPolicy: {
|
|
@@ -219,11 +249,11 @@ export class UserIdentity extends Construct {
|
|
|
219
249
|
requireSymbols: true,
|
|
220
250
|
tempPasswordValidity: Duration.days(3),
|
|
221
251
|
},
|
|
222
|
-
mfa
|
|
252
|
+
mfa,
|
|
223
253
|
featurePlan: FeaturePlan.PLUS,
|
|
224
254
|
// Audit-only logs threat assessments without blocking sign-in. Switch to FULL_FUNCTION to enforce automatic responses.
|
|
225
255
|
standardThreatProtectionMode: StandardThreatProtectionMode.AUDIT_ONLY,
|
|
226
|
-
mfaSecondFactor
|
|
256
|
+
mfaSecondFactor,
|
|
227
257
|
signInCaseSensitive: false,
|
|
228
258
|
signInAliases: { username: true, email: true },
|
|
229
259
|
accountRecovery: AccountRecovery.EMAIL_ONLY,
|
|
@@ -236,11 +266,11 @@ export class UserIdentity extends Construct {
|
|
|
236
266
|
},
|
|
237
267
|
autoVerify: {
|
|
238
268
|
email: true,
|
|
239
|
-
phone:
|
|
269
|
+
phone: phoneVerificationEnabled,
|
|
240
270
|
},
|
|
241
271
|
keepOriginal: {
|
|
242
272
|
email: true,
|
|
243
|
-
phone:
|
|
273
|
+
phone: phoneVerificationEnabled,
|
|
244
274
|
},
|
|
245
275
|
});
|
|
246
276
|
// Retain the SMS role alongside the pool so the pool can still be updated and deleted manually
|