@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.
Files changed (30) hide show
  1. package/migrations.json +16 -1
  2. package/package.json +1 -1
  3. package/src/migrations/latest/dynamodb-configurable-encryption/__snapshots__/migration.spec.ts.snap +429 -0
  4. package/src/migrations/latest/dynamodb-configurable-encryption/metadata.json +3 -0
  5. package/src/migrations/latest/dynamodb-configurable-encryption/migration.d.ts +6 -0
  6. package/src/migrations/latest/dynamodb-configurable-encryption/migration.js +325 -0
  7. package/src/migrations/latest/dynamodb-configurable-encryption/migration.js.map +1 -0
  8. package/src/migrations/latest/static-website-configurable-waf-encryption/metadata.json +3 -0
  9. package/src/migrations/latest/static-website-configurable-waf-encryption/migration.d.ts +6 -0
  10. package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js +418 -0
  11. package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js.map +1 -0
  12. package/src/migrations/latest/user-identity-configurable-mfa/metadata.json +3 -0
  13. package/src/migrations/latest/user-identity-configurable-mfa/migration.d.ts +6 -0
  14. package/src/migrations/latest/user-identity-configurable-mfa/migration.js +362 -0
  15. package/src/migrations/latest/user-identity-configurable-mfa/migration.js.map +1 -0
  16. package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
  17. package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
  18. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +259 -48
  19. package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +37 -7
  20. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +26 -0
  21. package/src/utils/dynamodb-constructs/files/cdk/core/dynamodb.ts.template +23 -3
  22. package/src/utils/dynamodb-constructs/files/terraform/app/dynamodb/__nameKebabCase__/__nameKebabCase__.tf.template +27 -1
  23. package/src/utils/dynamodb-constructs/files/terraform/core/dynamodb/dynamodb.tf.template +39 -5
  24. package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +41 -7
  25. package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +48 -13
  26. package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +26 -0
  27. package/src/utils/website-constructs/files/cdk/app/static-websites/__websiteNameKebabCase__.ts.template +11 -2
  28. package/src/utils/website-constructs/files/cdk/core/static-website.ts.template +47 -10
  29. package/src/utils/website-constructs/files/terraform/app/static-websites/__websiteNameKebabCase__/__websiteNameKebabCase__.tf.template +55 -0
  30. 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 = true
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 = aws_kms_key.website_key.arn
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 = aws_kms_key.website_key.arn
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 = aws_kms_key.website_key.arn
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 = new Key(this, 'WebsiteKey', {
1715
- enableKeyRotation: true,
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.addToResourcePolicy(
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: BucketEncryption.KMS,
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: BucketEncryption.KMS,
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.wafArn,
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 = new Key(this, 'WebsiteKey', {
3540
- enableKeyRotation: true,
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.addToResourcePolicy(
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: BucketEncryption.KMS,
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: BucketEncryption.KMS,
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.wafArn,
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 = new Key(this, 'WebsiteKey', {
5225
- enableKeyRotation: true,
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.addToResourcePolicy(
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: BucketEncryption.KMS,
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: BucketEncryption.KMS,
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.wafArn,
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
- { enableWaf = true }: UserIdentityProps = {},
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: Mfa.REQUIRED,
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: { sms: true, otp: true },
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: true,
269
+ phone: phoneVerificationEnabled,
240
270
  },
241
271
  keepOriginal: {
242
272
  email: true,
243
- phone: true,
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