@aws/nx-plugin 1.0.0-rc.76 → 1.0.0-rc.77

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 (21) hide show
  1. package/migrations.json +11 -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/user-identity-configurable-mfa/metadata.json +3 -0
  9. package/src/migrations/latest/user-identity-configurable-mfa/migration.d.ts +6 -0
  10. package/src/migrations/latest/user-identity-configurable-mfa/migration.js +362 -0
  11. package/src/migrations/latest/user-identity-configurable-mfa/migration.js.map +1 -0
  12. package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
  13. package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
  14. package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +37 -7
  15. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +26 -0
  16. package/src/utils/dynamodb-constructs/files/cdk/core/dynamodb.ts.template +23 -3
  17. package/src/utils/dynamodb-constructs/files/terraform/app/dynamodb/__nameKebabCase__/__nameKebabCase__.tf.template +27 -1
  18. package/src/utils/dynamodb-constructs/files/terraform/core/dynamodb/dynamodb.tf.template +39 -5
  19. package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +41 -7
  20. package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +48 -13
  21. package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +26 -0
@@ -63,8 +63,31 @@ variable "deletion_protection_enabled" {
63
63
  default = true
64
64
  }
65
65
 
66
+ variable "encryption" {
67
+ description = "Server-side encryption for the table. One of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT (the AWS owned key, at no cost and with no key to manage). Changing this on an already-deployed table away from CUSTOMER_MANAGED requires a manual step first - see the ts#dynamodb generator's docs."
68
+ type = string
69
+ default = "CUSTOMER_MANAGED"
70
+
71
+ validation {
72
+ condition = contains(["CUSTOMER_MANAGED", "AWS_MANAGED", "DEFAULT"], var.encryption)
73
+ error_message = "encryption must be one of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT."
74
+ }
75
+ }
76
+
77
+ variable "kms_key_arn" {
78
+ description = "ARN of an existing KMS key used to encrypt the table when encryption is CUSTOMER_MANAGED. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the DynamoDB service the necessary permissions in its own key policy."
79
+ type = string
80
+ default = null
81
+ }
82
+
83
+ variable "create_kms_key" {
84
+ description = "Whether to create a KMS key for the table. Only applies when encryption is CUSTOMER_MANAGED. Set to false when supplying kms_key_arn."
85
+ type = bool
86
+ default = true
87
+ }
88
+
66
89
  variable "enable_key_rotation" {
67
- description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table."
90
+ description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table. Only applies when encryption is CUSTOMER_MANAGED and create_kms_key is true."
68
91
  type = bool
69
92
  default = true
70
93
  }
@@ -84,12 +107,22 @@ locals {
84
107
  gsi_attribute_names = distinct(flatten([
85
108
  for gsi in var.global_secondary_indexes : [gsi.hash_key, gsi.range_key]
86
109
  ]))
110
+
111
+ create_table_key = var.encryption == "CUSTOMER_MANAGED" && var.create_kms_key
112
+ table_kms_key_arn = (
113
+ var.encryption == "CUSTOMER_MANAGED" ? (local.create_table_key ? aws_kms_key.table[0].arn : var.kms_key_arn) :
114
+ var.encryption == "AWS_MANAGED" ? "arn:\${data.aws_partition.current.partition}:kms:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:alias/aws/dynamodb" :
115
+ null
116
+ )
87
117
  }
88
118
 
89
119
  data "aws_caller_identity" "current" {}
90
120
  data "aws_partition" "current" {}
121
+ data "aws_region" "current" {}
91
122
 
92
123
  resource "aws_kms_key" "table" {
124
+ count = local.create_table_key ? 1 : 0
125
+
93
126
  description = "KMS key for DynamoDB table \${var.name}"
94
127
  enable_key_rotation = var.enable_key_rotation
95
128
 
@@ -126,6 +159,7 @@ resource "aws_kms_key" "table" {
126
159
  }
127
160
 
128
161
  resource "aws_dynamodb_table" "table" {
162
+ #checkov:skip=CKV_AWS_119:Encryption is configurable via var.encryption; checkov cannot resolve the conditional server_side_encryption.enabled expression
129
163
  name = var.name
130
164
  billing_mode = var.billing_mode
131
165
  hash_key = "pk"
@@ -165,8 +199,8 @@ resource "aws_dynamodb_table" "table" {
165
199
  }
166
200
 
167
201
  server_side_encryption {
168
- enabled = true
169
- kms_key_arn = aws_kms_key.table.arn
202
+ enabled = var.encryption != "DEFAULT"
203
+ kms_key_arn = local.table_kms_key_arn
170
204
  }
171
205
  }
172
206
 
@@ -181,8 +215,8 @@ output "table_arn" {
181
215
  }
182
216
 
183
217
  output "kms_key_arn" {
184
- description = "ARN of the KMS key used to encrypt the DynamoDB table."
185
- value = aws_kms_key.table.arn
218
+ description = "ARN of the KMS key used to encrypt the DynamoDB table, or null when using the AWS owned key (encryption = DEFAULT)."
219
+ value = local.table_kms_key_arn
186
220
  }
187
221
  "
188
222
  `;
@@ -219,8 +253,31 @@ variable "deletion_protection_enabled" {
219
253
  default = true
220
254
  }
221
255
 
256
+ variable "encryption" {
257
+ description = "Server-side encryption for the table. One of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT (the AWS owned key, at no cost and with no key to manage). Changing this on an already-deployed table away from CUSTOMER_MANAGED requires a manual step first - see the ts#dynamodb generator's docs."
258
+ type = string
259
+ default = "CUSTOMER_MANAGED"
260
+
261
+ validation {
262
+ condition = contains(["CUSTOMER_MANAGED", "AWS_MANAGED", "DEFAULT"], var.encryption)
263
+ error_message = "encryption must be one of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT."
264
+ }
265
+ }
266
+
267
+ variable "kms_key_arn" {
268
+ description = "ARN of an existing KMS key used to encrypt the table when encryption is CUSTOMER_MANAGED. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the DynamoDB service the necessary permissions in its own key policy."
269
+ type = string
270
+ default = null
271
+ }
272
+
273
+ variable "create_kms_key" {
274
+ description = "Whether to create a KMS key for the table. Only applies when encryption is CUSTOMER_MANAGED. Set to false when supplying kms_key_arn."
275
+ type = bool
276
+ default = true
277
+ }
278
+
222
279
  variable "enable_key_rotation" {
223
- description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table."
280
+ description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table. Only applies when encryption is CUSTOMER_MANAGED and create_kms_key is true."
224
281
  type = bool
225
282
  default = true
226
283
  }
@@ -247,6 +304,9 @@ module "dynamodb_table" {
247
304
  billing_mode = var.billing_mode
248
305
  point_in_time_recovery_enabled = var.point_in_time_recovery_enabled
249
306
  deletion_protection_enabled = var.deletion_protection_enabled
307
+ encryption = var.encryption
308
+ kms_key_arn = var.kms_key_arn
309
+ create_kms_key = var.create_kms_key
250
310
  enable_key_rotation = var.enable_key_rotation
251
311
  global_secondary_indexes = local.global_secondary_indexes
252
312
  }
@@ -747,7 +807,7 @@ import {
747
807
  } from 'aws-cdk-lib/aws-dynamodb';
748
808
  import { RemovalPolicy } from 'aws-cdk-lib';
749
809
  import { Grant, IGrantable } from 'aws-cdk-lib/aws-iam';
750
- import { Key } from 'aws-cdk-lib/aws-kms';
810
+ import { IKey, Key } from 'aws-cdk-lib/aws-kms';
751
811
  import { RuntimeConfig } from './runtime-config.js';
752
812
 
753
813
  type _DynamoDBTableProps = Omit<
@@ -767,8 +827,23 @@ export interface DynamoDBTableProps extends _DynamoDBTableProps {
767
827
  */
768
828
  readonly runtimeConfigKey: string;
769
829
 
830
+ /**
831
+ * Server-side encryption for the table.
832
+ *
833
+ * @default TableEncryption.CUSTOMER_MANAGED
834
+ */
835
+ readonly encryption?: TableEncryption;
836
+
837
+ /**
838
+ * KMS key used to encrypt the table. Only used when \`encryption\` is
839
+ * \`TableEncryption.CUSTOMER_MANAGED\`. When not provided, a new key is created.
840
+ */
841
+ readonly encryptionKey?: IKey;
842
+
770
843
  /**
771
844
  * Whether to enable automatic key rotation on the KMS key used to encrypt the table.
845
+ * Only applies when \`encryption\` is \`TableEncryption.CUSTOMER_MANAGED\` and no
846
+ * \`encryptionKey\` is supplied.
772
847
  *
773
848
  * @default true
774
849
  */
@@ -788,13 +863,19 @@ export abstract class DynamoDBTable extends Construct {
788
863
  pointInTimeRecoverySpecification = { pointInTimeRecoveryEnabled: true },
789
864
  deletionProtection = true,
790
865
  removalPolicy = RemovalPolicy.RETAIN,
866
+ encryption = TableEncryption.CUSTOMER_MANAGED,
867
+ encryptionKey,
791
868
  enableKeyRotation = true,
792
869
  ...rest
793
870
  }: DynamoDBTableProps,
794
871
  ) {
795
872
  super(scope, id);
796
873
 
797
- const key = new Key(this, 'EncryptionKey', { enableKeyRotation });
874
+ const key: IKey | undefined =
875
+ encryption === TableEncryption.CUSTOMER_MANAGED
876
+ ? (encryptionKey ??
877
+ new Key(this, 'EncryptionKey', { enableKeyRotation }))
878
+ : undefined;
798
879
 
799
880
  this.table = new Table(this, runtimeConfigKey, {
800
881
  ...rest,
@@ -805,7 +886,7 @@ export abstract class DynamoDBTable extends Construct {
805
886
  pointInTimeRecoverySpecification,
806
887
  deletionProtection,
807
888
  encryptionKey: key,
808
- encryption: TableEncryption.CUSTOMER_MANAGED,
889
+ encryption,
809
890
  removalPolicy,
810
891
  });
811
892
 
@@ -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
@@ -153,12 +153,38 @@ except Exception as e:
153
153
  default = true
154
154
  }
155
155
 
156
+ variable "mfa" {
157
+ description = "Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all"
158
+ type = string
159
+ default = "ON"
160
+
161
+ validation {
162
+ condition = contains(["OFF", "OPTIONAL", "ON"], var.mfa)
163
+ error_message = "mfa must be one of OFF, OPTIONAL, ON"
164
+ }
165
+ }
166
+
167
+ variable "mfa_second_factor_otp" {
168
+ description = "Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method"
169
+ type = bool
170
+ default = true
171
+ }
172
+
173
+ variable "mfa_second_factor_sms" {
174
+ description = "Whether users can use SMS to their verified phone numbers as an MFA method. Due to a Terraform AWS provider limitation, disabling this also disables SMS-based phone number verification, unlike the equivalent CDK construct prop"
175
+ type = bool
176
+ default = true
177
+ }
178
+
156
179
  module "identity" {
157
180
  source = "./identity"
158
181
 
159
182
  user_pool_domain_prefix = "test"
160
183
  allow_signup = true
161
184
  enable_waf = var.enable_waf
185
+ mfa = var.mfa
186
+ mfa_second_factor_otp = var.mfa_second_factor_otp
187
+ mfa_second_factor_sms = var.mfa_second_factor_sms
162
188
  }
163
189
 
164
190
  # Outputs
@@ -8,7 +8,7 @@ import {
8
8
  } from 'aws-cdk-lib/aws-dynamodb';
9
9
  import { RemovalPolicy } from 'aws-cdk-lib';
10
10
  import { Grant, IGrantable } from 'aws-cdk-lib/aws-iam';
11
- import { Key } from 'aws-cdk-lib/aws-kms';
11
+ import { IKey, Key } from 'aws-cdk-lib/aws-kms';
12
12
  import { RuntimeConfig } from './runtime-config<% if (esm) { %>.js<% } %>';
13
13
 
14
14
  type _DynamoDBTableProps = Omit<TableProps, 'tableName' | 'partitionKey' | 'sortKey' | 'encryption' | 'encryptionKey'>;
@@ -25,8 +25,23 @@ export interface DynamoDBTableProps extends _DynamoDBTableProps {
25
25
  */
26
26
  readonly runtimeConfigKey: string;
27
27
 
28
+ /**
29
+ * Server-side encryption for the table.
30
+ *
31
+ * @default TableEncryption.CUSTOMER_MANAGED
32
+ */
33
+ readonly encryption?: TableEncryption;
34
+
35
+ /**
36
+ * KMS key used to encrypt the table. Only used when `encryption` is
37
+ * `TableEncryption.CUSTOMER_MANAGED`. When not provided, a new key is created.
38
+ */
39
+ readonly encryptionKey?: IKey;
40
+
28
41
  /**
29
42
  * Whether to enable automatic key rotation on the KMS key used to encrypt the table.
43
+ * Only applies when `encryption` is `TableEncryption.CUSTOMER_MANAGED` and no
44
+ * `encryptionKey` is supplied.
30
45
  *
31
46
  * @default true
32
47
  */
@@ -46,13 +61,18 @@ export abstract class DynamoDBTable extends Construct {
46
61
  pointInTimeRecoverySpecification = { pointInTimeRecoveryEnabled: true },
47
62
  deletionProtection = true,
48
63
  removalPolicy = RemovalPolicy.RETAIN,
64
+ encryption = TableEncryption.CUSTOMER_MANAGED,
65
+ encryptionKey,
49
66
  enableKeyRotation = true,
50
67
  ...rest
51
68
  }: DynamoDBTableProps,
52
69
  ) {
53
70
  super(scope, id);
54
71
 
55
- const key = new Key(this, 'EncryptionKey', { enableKeyRotation });
72
+ const key: IKey | undefined =
73
+ encryption === TableEncryption.CUSTOMER_MANAGED
74
+ ? (encryptionKey ?? new Key(this, 'EncryptionKey', { enableKeyRotation }))
75
+ : undefined;
56
76
 
57
77
  this.table = new Table(this, runtimeConfigKey, {
58
78
  ...rest,
@@ -63,7 +83,7 @@ export abstract class DynamoDBTable extends Construct {
63
83
  pointInTimeRecoverySpecification,
64
84
  deletionProtection,
65
85
  encryptionKey: key,
66
- encryption: TableEncryption.CUSTOMER_MANAGED,
86
+ encryption,
67
87
  removalPolicy,
68
88
  });
69
89
 
@@ -29,8 +29,31 @@ variable "deletion_protection_enabled" {
29
29
  default = true
30
30
  }
31
31
 
32
+ variable "encryption" {
33
+ description = "Server-side encryption for the table. One of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT (the AWS owned key, at no cost and with no key to manage). Changing this on an already-deployed table away from CUSTOMER_MANAGED requires a manual step first - see the ts#dynamodb generator's docs."
34
+ type = string
35
+ default = "CUSTOMER_MANAGED"
36
+
37
+ validation {
38
+ condition = contains(["CUSTOMER_MANAGED", "AWS_MANAGED", "DEFAULT"], var.encryption)
39
+ error_message = "encryption must be one of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT."
40
+ }
41
+ }
42
+
43
+ variable "kms_key_arn" {
44
+ description = "ARN of an existing KMS key used to encrypt the table when encryption is CUSTOMER_MANAGED. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the DynamoDB service the necessary permissions in its own key policy."
45
+ type = string
46
+ default = null
47
+ }
48
+
49
+ variable "create_kms_key" {
50
+ description = "Whether to create a KMS key for the table. Only applies when encryption is CUSTOMER_MANAGED. Set to false when supplying kms_key_arn."
51
+ type = bool
52
+ default = true
53
+ }
54
+
32
55
  variable "enable_key_rotation" {
33
- description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table."
56
+ description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table. Only applies when encryption is CUSTOMER_MANAGED and create_kms_key is true."
34
57
  type = bool
35
58
  default = true
36
59
  }
@@ -57,6 +80,9 @@ module "dynamodb_table" {
57
80
  billing_mode = var.billing_mode
58
81
  point_in_time_recovery_enabled = var.point_in_time_recovery_enabled
59
82
  deletion_protection_enabled = var.deletion_protection_enabled
83
+ encryption = var.encryption
84
+ kms_key_arn = var.kms_key_arn
85
+ create_kms_key = var.create_kms_key
60
86
  enable_key_rotation = var.enable_key_rotation
61
87
  global_secondary_indexes = local.global_secondary_indexes
62
88
  }
@@ -32,8 +32,31 @@ variable "deletion_protection_enabled" {
32
32
  default = true
33
33
  }
34
34
 
35
+ variable "encryption" {
36
+ description = "Server-side encryption for the table. One of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT (the AWS owned key, at no cost and with no key to manage). Changing this on an already-deployed table away from CUSTOMER_MANAGED requires a manual step first - see the ts#dynamodb generator's docs."
37
+ type = string
38
+ default = "CUSTOMER_MANAGED"
39
+
40
+ validation {
41
+ condition = contains(["CUSTOMER_MANAGED", "AWS_MANAGED", "DEFAULT"], var.encryption)
42
+ error_message = "encryption must be one of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT."
43
+ }
44
+ }
45
+
46
+ variable "kms_key_arn" {
47
+ description = "ARN of an existing KMS key used to encrypt the table when encryption is CUSTOMER_MANAGED. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the DynamoDB service the necessary permissions in its own key policy."
48
+ type = string
49
+ default = null
50
+ }
51
+
52
+ variable "create_kms_key" {
53
+ description = "Whether to create a KMS key for the table. Only applies when encryption is CUSTOMER_MANAGED. Set to false when supplying kms_key_arn."
54
+ type = bool
55
+ default = true
56
+ }
57
+
35
58
  variable "enable_key_rotation" {
36
- description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table."
59
+ description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table. Only applies when encryption is CUSTOMER_MANAGED and create_kms_key is true."
37
60
  type = bool
38
61
  default = true
39
62
  }
@@ -53,12 +76,22 @@ locals {
53
76
  gsi_attribute_names = distinct(flatten([
54
77
  for gsi in var.global_secondary_indexes : [gsi.hash_key, gsi.range_key]
55
78
  ]))
79
+
80
+ create_table_key = var.encryption == "CUSTOMER_MANAGED" && var.create_kms_key
81
+ table_kms_key_arn = (
82
+ var.encryption == "CUSTOMER_MANAGED" ? (local.create_table_key ? aws_kms_key.table[0].arn : var.kms_key_arn) :
83
+ var.encryption == "AWS_MANAGED" ? "arn:${data.aws_partition.current.partition}:kms:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:alias/aws/dynamodb" :
84
+ null
85
+ )
56
86
  }
57
87
 
58
88
  data "aws_caller_identity" "current" {}
59
89
  data "aws_partition" "current" {}
90
+ data "aws_region" "current" {}
60
91
 
61
92
  resource "aws_kms_key" "table" {
93
+ count = local.create_table_key ? 1 : 0
94
+
62
95
  description = "KMS key for DynamoDB table ${var.name}"
63
96
  enable_key_rotation = var.enable_key_rotation
64
97
 
@@ -95,6 +128,7 @@ resource "aws_kms_key" "table" {
95
128
  }
96
129
 
97
130
  resource "aws_dynamodb_table" "table" {
131
+ #checkov:skip=CKV_AWS_119:Encryption is configurable via var.encryption; checkov cannot resolve the conditional server_side_encryption.enabled expression
98
132
  name = var.name
99
133
  billing_mode = var.billing_mode
100
134
  hash_key = "pk"
@@ -134,8 +168,8 @@ resource "aws_dynamodb_table" "table" {
134
168
  }
135
169
 
136
170
  server_side_encryption {
137
- enabled = true
138
- kms_key_arn = aws_kms_key.table.arn
171
+ enabled = var.encryption != "DEFAULT"
172
+ kms_key_arn = local.table_kms_key_arn
139
173
  }
140
174
  }
141
175
 
@@ -150,6 +184,6 @@ output "table_arn" {
150
184
  }
151
185
 
152
186
  output "kms_key_arn" {
153
- description = "ARN of the KMS key used to encrypt the DynamoDB table."
154
- value = aws_kms_key.table.arn
187
+ description = "ARN of the KMS key used to encrypt the DynamoDB table, or null when using the AWS owned key (encryption = DEFAULT)."
188
+ value = local.table_kms_key_arn
155
189
  }
@@ -16,6 +16,7 @@ import {
16
16
  FeaturePlan,
17
17
  ManagedLoginVersion,
18
18
  Mfa,
19
+ type MfaSecondFactor,
19
20
  OAuthScope,
20
21
  StandardThreatProtectionMode,
21
22
  UserPool,
@@ -48,6 +49,20 @@ export interface UserIdentityProps {
48
49
  * @default true
49
50
  */
50
51
  readonly enableWaf?: boolean;
52
+
53
+ /**
54
+ * Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all.
55
+ *
56
+ * @default Mfa.REQUIRED
57
+ */
58
+ readonly mfa?: Mfa;
59
+
60
+ /**
61
+ * The MFA methods available to users when MFA is not off.
62
+ *
63
+ * @default { sms: true, otp: true }
64
+ */
65
+ readonly mfaSecondFactor?: MfaSecondFactor;
51
66
  }
52
67
 
53
68
  /**
@@ -63,11 +78,25 @@ export class UserIdentity extends Construct {
63
78
  /** The WAFv2 Web ACL associated with the user pool, if WAF is enabled */
64
79
  public readonly webAcl?: CfnWebACL;
65
80
 
66
- constructor(scope: Construct, id: string, { enableWaf = true }: UserIdentityProps = {}) {
81
+ constructor(
82
+ scope: Construct,
83
+ id: string,
84
+ {
85
+ enableWaf = true,
86
+ mfa = Mfa.REQUIRED,
87
+ mfaSecondFactor = { sms: true, otp: true },
88
+ }: UserIdentityProps = {},
89
+ ) {
67
90
  super(scope, id);
68
91
 
92
+ if (mfa === Mfa.REQUIRED && !mfaSecondFactor.sms && !mfaSecondFactor.otp) {
93
+ throw new Error(
94
+ 'UserIdentity: mfa is REQUIRED but mfaSecondFactor has no methods enabled (sms and otp are both false)',
95
+ );
96
+ }
97
+
69
98
  this.region = Stack.of(this).region;
70
- this.userPool = this.createUserPool();
99
+ this.userPool = this.createUserPool(mfa, mfaSecondFactor);
71
100
 
72
101
  if (enableWaf) {
73
102
  this.webAcl = this.createWebAcl(
@@ -115,7 +144,12 @@ export class UserIdentity extends Construct {
115
144
  });
116
145
  }
117
146
 
118
- private createUserPool = () => {
147
+ private createUserPool = (mfa: Mfa, mfaSecondFactor: MfaSecondFactor) => {
148
+ // Cognito rejects SmsConfiguration (emitted whenever phone is auto-verified) unless SMS is
149
+ // also an enabled MFA method, for any non-OFF MfaConfiguration. So phone verification via SMS
150
+ // can only be offered when SMS is actually usable as a second factor.
151
+ const phoneVerificationEnabled = mfa === Mfa.OFF || mfaSecondFactor.sms;
152
+
119
153
  const userPool = new UserPool(this, 'UserPool', {
120
154
  deletionProtection: true,
121
155
  passwordPolicy: {
@@ -126,11 +160,11 @@ export class UserIdentity extends Construct {
126
160
  requireSymbols: true,
127
161
  tempPasswordValidity: Duration.days(3),
128
162
  },
129
- mfa: Mfa.REQUIRED,
163
+ mfa,
130
164
  featurePlan: FeaturePlan.PLUS,
131
165
  // Audit-only logs threat assessments without blocking sign-in. Switch to FULL_FUNCTION to enforce automatic responses.
132
166
  standardThreatProtectionMode: StandardThreatProtectionMode.AUDIT_ONLY,
133
- mfaSecondFactor: { sms: true, otp: true },
167
+ mfaSecondFactor,
134
168
  signInCaseSensitive: false,
135
169
  signInAliases: { username: true, email: true },
136
170
  accountRecovery: AccountRecovery.EMAIL_ONLY,
@@ -143,11 +177,11 @@ export class UserIdentity extends Construct {
143
177
  },
144
178
  autoVerify: {
145
179
  email: true,
146
- phone: true,
180
+ phone: phoneVerificationEnabled,
147
181
  },
148
182
  keepOriginal: {
149
183
  email: true,
150
- phone: true,
184
+ phone: phoneVerificationEnabled,
151
185
  },
152
186
  });
153
187
  // Retain the SMS role alongside the pool so the pool can still be updated and deleted manually