@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
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { addDestructuredImport, applyGritQL, matchGritQL } from "../../../utils/ast.js";
5
+ import { formatFilesInSubtree } from "../../../utils/format.js";
6
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../../../utils/shared-constructs-constants.js";
7
+ /**
8
+ * Surface mfa and mfaSecondFactor on the vended UserIdentity construct/module
9
+ *
10
+ * CDK's `mfa` / `mfaSecondFactor` props default to what the construct already
11
+ * hard-coded (`Mfa.REQUIRED` / `{ sms: true, otp: true }`), so this just makes
12
+ * them overridable, matching the existing `enableWaf` prop pattern.
13
+ *
14
+ * Terraform has no equivalent of CloudFormation's `EnabledMfas` - SMS MFA is
15
+ * inseparable from the `sms_configuration` block, which phone number
16
+ * verification also relies on. So `mfa_second_factor_sms` gates
17
+ * `sms_configuration` (and, with it, SMS phone verification) rather than only
18
+ * MFA, unlike the CDK prop.
19
+ *
20
+ * CDK has the same underlying coupling for any non-OFF `mfa`: Cognito rejects
21
+ * `SmsConfiguration` (which CDK emits whenever phone is auto-verified) unless
22
+ * SMS is also an enabled MFA method. So `phoneVerificationEnabled` gates
23
+ * `autoVerify`/`keepOriginal`'s `phone` flag on whether SMS is actually usable
24
+ * as a second factor, and a `mfa: REQUIRED` with neither `sms` nor `otp`
25
+ * enabled - a pool nobody could ever sign into - is rejected at synth time.
26
+ *
27
+ * How to write a migration:
28
+ * - https://nx.dev/docs/kb/migration-generators
29
+ * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject
30
+ *
31
+ * Guardrails:
32
+ * - Pattern-match before writing: skip files that have diverged from the shape
33
+ * your generators produce and report them via `nextSteps`, rather than
34
+ * clobbering the user's changes.
35
+ * - Idempotent: re-running must be a no-op.
36
+ * - Format what you write: finish with `formatFilesInSubtree` so the files your
37
+ * migration wrote are formatted correctly.
38
+ */ const CDK_USER_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/user-identity.ts`;
39
+ const TERRAFORM_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/identity/identity.tf`;
40
+ const TERRAFORM_MAIN_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/main.tf`;
41
+ const NEW_TERRAFORM_VARIABLES = `variable "mfa" {
42
+ description = "Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all"
43
+ type = string
44
+ default = "ON"
45
+
46
+ validation {
47
+ condition = contains(["OFF", "OPTIONAL", "ON"], var.mfa)
48
+ error_message = "mfa must be one of OFF, OPTIONAL, ON"
49
+ }
50
+ }
51
+
52
+ variable "mfa_second_factor_otp" {
53
+ description = "Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method"
54
+ type = bool
55
+ default = true
56
+ }
57
+
58
+ variable "mfa_second_factor_sms" {
59
+ 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"
60
+ type = bool
61
+ default = true
62
+ }`;
63
+ // Guards each file: present only once it has been migrated.
64
+ const CDK_MIGRATED_PATTERN = '`mfa === Mfa.OFF || mfaSecondFactor.sms`';
65
+ const TERRAFORM_IDENTITY_MIGRATED_PATTERN = 'language hcl\n`mfa_configuration = var.mfa`';
66
+ const TERRAFORM_MAIN_MIGRATED_PATTERN = 'language hcl\n`mfa = var.mfa`';
67
+ // Interface members don't parse as standalone snippets, so this anchors on the
68
+ // whole interface body (preserved via $body) rather than the enableWaf member.
69
+ const CDK_INTERFACE_BODY_PATTERN = '`export interface UserIdentityProps { $body }`';
70
+ // A parameter's type annotation blocks matching the destructure sub-pattern in
71
+ // isolation, so this anchors on the full constructor signature instead
72
+ // (literal params so a customised signature is correctly seen as diverged).
73
+ const CDK_CONSTRUCTOR_PATTERN = '`constructor(scope: Construct, id: string, { enableWaf = true }: UserIdentityProps = {}) { $body }`';
74
+ const CDK_CREATE_CALL_PATTERN = '`this.createUserPool()`';
75
+ const CDK_CREATE_SIGNATURE_PATTERN = '`private createUserPool = () => $body`';
76
+ // Trailing commas block matching a bare object property in isolation; the
77
+ // separator is outside the matched span either way, so it survives untouched.
78
+ const CDK_MFA_FIELD_PATTERN = '`mfa: Mfa.REQUIRED`';
79
+ const CDK_MFA_SECOND_FACTOR_FIELD_PATTERN = '`mfaSecondFactor: { sms: true, otp: true }`';
80
+ const CDK_SUPER_CALL_PATTERN = '`super(scope, id);`';
81
+ // $props preserves the whole (large) UserPool props object untouched while
82
+ // inserting the new const declaration immediately before it.
83
+ const CDK_NEW_USER_POOL_PATTERN = "`const userPool = new UserPool(this, 'UserPool', { $props })`";
84
+ // Matches both the autoVerify.phone and keepOriginal.phone occurrences in one pass.
85
+ const CDK_PHONE_TRUE_PATTERN = '`phone: true`';
86
+ const CDK_EDITS = [
87
+ [
88
+ CDK_INTERFACE_BODY_PATTERN,
89
+ `${CDK_INTERFACE_BODY_PATTERN} => \`export interface UserIdentityProps {
90
+ $body
91
+
92
+ /**
93
+ * Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all.
94
+ *
95
+ * @default Mfa.REQUIRED
96
+ */
97
+ readonly mfa?: Mfa;
98
+
99
+ /**
100
+ * The MFA methods available to users when MFA is not off.
101
+ *
102
+ * @default { sms: true, otp: true }
103
+ */
104
+ readonly mfaSecondFactor?: MfaSecondFactor;
105
+ }\``
106
+ ],
107
+ [
108
+ CDK_CONSTRUCTOR_PATTERN,
109
+ `${CDK_CONSTRUCTOR_PATTERN} => \`constructor(
110
+ scope: Construct,
111
+ id: string,
112
+ {
113
+ enableWaf = true,
114
+ mfa = Mfa.REQUIRED,
115
+ mfaSecondFactor = { sms: true, otp: true },
116
+ }: UserIdentityProps = {},
117
+ ) { $body }\``
118
+ ],
119
+ [
120
+ CDK_CREATE_CALL_PATTERN,
121
+ `${CDK_CREATE_CALL_PATTERN} => \`this.createUserPool(mfa, mfaSecondFactor)\``
122
+ ],
123
+ [
124
+ CDK_CREATE_SIGNATURE_PATTERN,
125
+ `${CDK_CREATE_SIGNATURE_PATTERN} => \`private createUserPool = (mfa: Mfa, mfaSecondFactor: MfaSecondFactor) => $body\``
126
+ ],
127
+ [
128
+ CDK_MFA_FIELD_PATTERN,
129
+ `${CDK_MFA_FIELD_PATTERN} => \`mfa\``
130
+ ],
131
+ [
132
+ CDK_MFA_SECOND_FACTOR_FIELD_PATTERN,
133
+ `${CDK_MFA_SECOND_FACTOR_FIELD_PATTERN} => \`mfaSecondFactor\``
134
+ ],
135
+ [
136
+ CDK_SUPER_CALL_PATTERN,
137
+ `${CDK_SUPER_CALL_PATTERN} => \`super(scope, id);
138
+
139
+ if (mfa === Mfa.REQUIRED && !mfaSecondFactor.sms && !mfaSecondFactor.otp) {
140
+ throw new Error(
141
+ 'UserIdentity: mfa is REQUIRED but mfaSecondFactor has no methods enabled (sms and otp are both false)',
142
+ );
143
+ }\``
144
+ ],
145
+ [
146
+ CDK_NEW_USER_POOL_PATTERN,
147
+ `${CDK_NEW_USER_POOL_PATTERN} => \`const phoneVerificationEnabled = mfa === Mfa.OFF || mfaSecondFactor.sms;
148
+
149
+ const userPool = new UserPool(this, 'UserPool', { $props })\``
150
+ ],
151
+ [
152
+ CDK_PHONE_TRUE_PATTERN,
153
+ `${CDK_PHONE_TRUE_PATTERN} => \`phone: phoneVerificationEnabled\``
154
+ ]
155
+ ];
156
+ const TERRAFORM_IDENTITY_ENABLE_WAF_VARIABLE_ANCHOR = [
157
+ 'language hcl',
158
+ '`variable "enable_waf" {',
159
+ ' description = "Whether to enable AWS WAFv2 with the default managed ruleset (AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet) and associate it with the user pool"',
160
+ ' type = bool',
161
+ ' default = true',
162
+ '}`'
163
+ ].join('\n');
164
+ const TERRAFORM_MFA_CONFIGURATION_PATTERN = [
165
+ 'language hcl',
166
+ '`mfa_configuration = "ON"`'
167
+ ].join('\n');
168
+ const TERRAFORM_SOFTWARE_TOKEN_MFA_PATTERN = [
169
+ 'language hcl',
170
+ '`software_token_mfa_configuration {',
171
+ ' enabled = true',
172
+ ' }`'
173
+ ].join('\n');
174
+ const TERRAFORM_SMS_CONFIGURATION_PATTERN = [
175
+ 'language hcl',
176
+ '`sms_configuration {',
177
+ ' external_id = random_uuid.sms_external_id.result',
178
+ ' sns_caller_arn = aws_iam_role.cognito_sms_role.arn',
179
+ ' sns_region = data.aws_region.current.region',
180
+ ' }`'
181
+ ].join('\n');
182
+ const TERRAFORM_RANDOM_UUID_PATTERN = [
183
+ 'language hcl',
184
+ '`resource "random_uuid" "sms_external_id" {}`'
185
+ ].join('\n');
186
+ const TERRAFORM_SMS_ROLE_PATTERN = [
187
+ 'language hcl',
188
+ '`resource "aws_iam_role" "cognito_sms_role" { $body }`'
189
+ ].join('\n');
190
+ const TERRAFORM_SMS_POLICY_PATTERN = [
191
+ 'language hcl',
192
+ '`resource "aws_iam_role_policy" "cognito_sms_policy" { $body }`'
193
+ ].join('\n');
194
+ const TERRAFORM_SMS_ROLE_ID_REF_PATTERN = [
195
+ 'language hcl',
196
+ '`aws_iam_role.cognito_sms_role.id`'
197
+ ].join('\n');
198
+ const TERRAFORM_EXTERNAL_ID_REF_PATTERN = [
199
+ 'language hcl',
200
+ '`random_uuid.sms_external_id.result`'
201
+ ].join('\n');
202
+ const TERRAFORM_AUTO_VERIFIED_ATTRIBUTES_PATTERN = [
203
+ 'language hcl',
204
+ '`auto_verified_attributes = ["email", "phone_number"]`'
205
+ ].join('\n');
206
+ const TERRAFORM_ATTRIBUTES_REQUIRE_VERIFICATION_PATTERN = [
207
+ 'language hcl',
208
+ '`attributes_require_verification_before_update = ["email", "phone_number"]`'
209
+ ].join('\n');
210
+ const TERRAFORM_IDENTITY_EDITS = [
211
+ [
212
+ TERRAFORM_IDENTITY_ENABLE_WAF_VARIABLE_ANCHOR,
213
+ `${TERRAFORM_IDENTITY_ENABLE_WAF_VARIABLE_ANCHOR} => \`variable "enable_waf" {
214
+ description = "Whether to enable AWS WAFv2 with the default managed ruleset (AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet) and associate it with the user pool"
215
+ type = bool
216
+ default = true
217
+ }
218
+
219
+ ${NEW_TERRAFORM_VARIABLES}\``
220
+ ],
221
+ [
222
+ TERRAFORM_RANDOM_UUID_PATTERN,
223
+ `${TERRAFORM_RANDOM_UUID_PATTERN} => \`resource "random_uuid" "sms_external_id" {
224
+ count = var.mfa_second_factor_sms ? 1 : 0
225
+ }\``
226
+ ],
227
+ [
228
+ TERRAFORM_SMS_ROLE_PATTERN,
229
+ `${TERRAFORM_SMS_ROLE_PATTERN} => \`resource "aws_iam_role" "cognito_sms_role" {
230
+ count = var.mfa_second_factor_sms ? 1 : 0
231
+
232
+ $body
233
+ }\``
234
+ ],
235
+ [
236
+ TERRAFORM_SMS_POLICY_PATTERN,
237
+ `${TERRAFORM_SMS_POLICY_PATTERN} => \`resource "aws_iam_role_policy" "cognito_sms_policy" {
238
+ count = var.mfa_second_factor_sms ? 1 : 0
239
+
240
+ $body
241
+ }\``
242
+ ],
243
+ [
244
+ TERRAFORM_MFA_CONFIGURATION_PATTERN,
245
+ `${TERRAFORM_MFA_CONFIGURATION_PATTERN} => \`mfa_configuration = var.mfa\``
246
+ ],
247
+ [
248
+ TERRAFORM_SOFTWARE_TOKEN_MFA_PATTERN,
249
+ `${TERRAFORM_SOFTWARE_TOKEN_MFA_PATTERN} => \`software_token_mfa_configuration {
250
+ enabled = var.mfa_second_factor_otp
251
+ }\``
252
+ ],
253
+ [
254
+ TERRAFORM_SMS_CONFIGURATION_PATTERN,
255
+ `${TERRAFORM_SMS_CONFIGURATION_PATTERN} => \`# SMS MFA configuration. Due to a Terraform AWS provider limitation, this also gates
256
+ # SMS-based phone number verification below - see the mfa_second_factor_sms variable.
257
+ dynamic "sms_configuration" {
258
+ for_each = var.mfa_second_factor_sms ? [1] : []
259
+
260
+ content {
261
+ external_id = random_uuid.sms_external_id[0].result
262
+ sns_caller_arn = aws_iam_role.cognito_sms_role[0].arn
263
+ sns_region = data.aws_region.current.region
264
+ }
265
+ }\``
266
+ ],
267
+ [
268
+ TERRAFORM_SMS_ROLE_ID_REF_PATTERN,
269
+ `${TERRAFORM_SMS_ROLE_ID_REF_PATTERN} => \`aws_iam_role.cognito_sms_role[0].id\``
270
+ ],
271
+ [
272
+ TERRAFORM_EXTERNAL_ID_REF_PATTERN,
273
+ `${TERRAFORM_EXTERNAL_ID_REF_PATTERN} => \`random_uuid.sms_external_id[0].result\``
274
+ ],
275
+ [
276
+ TERRAFORM_AUTO_VERIFIED_ATTRIBUTES_PATTERN,
277
+ `${TERRAFORM_AUTO_VERIFIED_ATTRIBUTES_PATTERN} => \`auto_verified_attributes = var.mfa_second_factor_sms ? ["email", "phone_number"] : ["email"]\``
278
+ ],
279
+ [
280
+ TERRAFORM_ATTRIBUTES_REQUIRE_VERIFICATION_PATTERN,
281
+ `${TERRAFORM_ATTRIBUTES_REQUIRE_VERIFICATION_PATTERN} => \`attributes_require_verification_before_update = var.mfa_second_factor_sms ? ["email", "phone_number"] : ["email"]\``
282
+ ]
283
+ ];
284
+ const TERRAFORM_MAIN_ENABLE_WAF_VARIABLE_ANCHOR = [
285
+ 'language hcl',
286
+ '`variable "enable_waf" {',
287
+ ' description = "Whether to enable AWS WAFv2 with the default managed ruleset and associate it with the user pool"',
288
+ ' type = bool',
289
+ ' default = true',
290
+ '}`'
291
+ ].join('\n');
292
+ const TERRAFORM_MAIN_MODULE_PATTERN = [
293
+ 'language hcl',
294
+ '`module "identity" {',
295
+ ' source = "./identity"',
296
+ '',
297
+ ' user_pool_domain_prefix = $prefix',
298
+ ' allow_signup = $allowSignup',
299
+ ' enable_waf = var.enable_waf',
300
+ '}`'
301
+ ].join('\n');
302
+ const TERRAFORM_MAIN_EDITS = [
303
+ [
304
+ TERRAFORM_MAIN_ENABLE_WAF_VARIABLE_ANCHOR,
305
+ `${TERRAFORM_MAIN_ENABLE_WAF_VARIABLE_ANCHOR} => \`variable "enable_waf" {
306
+ description = "Whether to enable AWS WAFv2 with the default managed ruleset and associate it with the user pool"
307
+ type = bool
308
+ default = true
309
+ }
310
+
311
+ ${NEW_TERRAFORM_VARIABLES}\``
312
+ ],
313
+ [
314
+ TERRAFORM_MAIN_MODULE_PATTERN,
315
+ `${TERRAFORM_MAIN_MODULE_PATTERN} => \`module "identity" {
316
+ source = "./identity"
317
+
318
+ user_pool_domain_prefix = $prefix
319
+ allow_signup = $allowSignup
320
+ enable_waf = var.enable_waf
321
+ mfa = var.mfa
322
+ mfa_second_factor_otp = var.mfa_second_factor_otp
323
+ mfa_second_factor_sms = var.mfa_second_factor_sms
324
+ }\``
325
+ ]
326
+ ];
327
+ const divergedNextStep = (filePath)=>`${filePath}: the UserIdentity construct/module has diverged from the generated shape - left untouched. To make MFA configurable, add mfa and mfaSecondFactor (CDK) / mfa, mfa_second_factor_otp and mfa_second_factor_sms (Terraform) yourself, following the same pattern as the existing enableWaf prop/variable.`;
328
+ const applyEdits = async (tree, filePath, migratedPattern, edits, nextSteps, beforeApply)=>{
329
+ if (!tree.exists(filePath)) {
330
+ // This workspace doesn't use this IaC provider, or has no UserIdentity.
331
+ return;
332
+ }
333
+ if (await matchGritQL(tree, filePath, migratedPattern)) {
334
+ // Already migrated - silent skip keeps re-runs a no-op.
335
+ return;
336
+ }
337
+ // Confirm every edit site is present before writing any of them, so a file
338
+ // that only partly matches is left whole rather than half-edited.
339
+ const allSitesPresent = (await Promise.all(edits.map(([match])=>matchGritQL(tree, filePath, match)))).every(Boolean);
340
+ if (!allSitesPresent) {
341
+ nextSteps.push(divergedNextStep(filePath));
342
+ return;
343
+ }
344
+ await beforeApply?.();
345
+ for (const [, rewrite] of edits){
346
+ await applyGritQL(tree, filePath, rewrite);
347
+ }
348
+ };
349
+ export default async function migration(tree) {
350
+ const nextSteps = [];
351
+ await applyEdits(tree, CDK_USER_IDENTITY_FILE, CDK_MIGRATED_PATTERN, CDK_EDITS, nextSteps, ()=>addDestructuredImport(tree, CDK_USER_IDENTITY_FILE, [
352
+ 'type MfaSecondFactor'
353
+ ], 'aws-cdk-lib/aws-cognito'));
354
+ await applyEdits(tree, TERRAFORM_IDENTITY_FILE, TERRAFORM_IDENTITY_MIGRATED_PATTERN, TERRAFORM_IDENTITY_EDITS, nextSteps);
355
+ await applyEdits(tree, TERRAFORM_MAIN_FILE, TERRAFORM_MAIN_MIGRATED_PATTERN, TERRAFORM_MAIN_EDITS, nextSteps);
356
+ await formatFilesInSubtree(tree);
357
+ return {
358
+ nextSteps
359
+ };
360
+ }
361
+
362
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/user-identity-configurable-mfa/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport {\n addDestructuredImport,\n applyGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Surface mfa and mfaSecondFactor on the vended UserIdentity construct/module\n *\n * CDK's `mfa` / `mfaSecondFactor` props default to what the construct already\n * hard-coded (`Mfa.REQUIRED` / `{ sms: true, otp: true }`), so this just makes\n * them overridable, matching the existing `enableWaf` prop pattern.\n *\n * Terraform has no equivalent of CloudFormation's `EnabledMfas` - SMS MFA is\n * inseparable from the `sms_configuration` block, which phone number\n * verification also relies on. So `mfa_second_factor_sms` gates\n * `sms_configuration` (and, with it, SMS phone verification) rather than only\n * MFA, unlike the CDK prop.\n *\n * CDK has the same underlying coupling for any non-OFF `mfa`: Cognito rejects\n * `SmsConfiguration` (which CDK emits whenever phone is auto-verified) unless\n * SMS is also an enabled MFA method. So `phoneVerificationEnabled` gates\n * `autoVerify`/`keepOriginal`'s `phone` flag on whether SMS is actually usable\n * as a second factor, and a `mfa: REQUIRED` with neither `sms` nor `otp`\n * enabled - a pool nobody could ever sign into - is rejected at synth time.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CDK_USER_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/user-identity.ts`;\nconst TERRAFORM_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/identity/identity.tf`;\nconst TERRAFORM_MAIN_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/main.tf`;\n\nconst NEW_TERRAFORM_VARIABLES = `variable \"mfa\" {\n description = \"Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all\"\n type = string\n default = \"ON\"\n\n validation {\n condition = contains([\"OFF\", \"OPTIONAL\", \"ON\"], var.mfa)\n error_message = \"mfa must be one of OFF, OPTIONAL, ON\"\n }\n}\n\nvariable \"mfa_second_factor_otp\" {\n description = \"Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method\"\n type = bool\n default = true\n}\n\nvariable \"mfa_second_factor_sms\" {\n 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\"\n type = bool\n default = true\n}`;\n\n// Guards each file: present only once it has been migrated.\nconst CDK_MIGRATED_PATTERN = '`mfa === Mfa.OFF || mfaSecondFactor.sms`';\nconst TERRAFORM_IDENTITY_MIGRATED_PATTERN =\n 'language hcl\\n`mfa_configuration = var.mfa`';\nconst TERRAFORM_MAIN_MIGRATED_PATTERN = 'language hcl\\n`mfa = var.mfa`';\n\n// Interface members don't parse as standalone snippets, so this anchors on the\n// whole interface body (preserved via $body) rather than the enableWaf member.\nconst CDK_INTERFACE_BODY_PATTERN =\n '`export interface UserIdentityProps { $body }`';\n// A parameter's type annotation blocks matching the destructure sub-pattern in\n// isolation, so this anchors on the full constructor signature instead\n// (literal params so a customised signature is correctly seen as diverged).\nconst CDK_CONSTRUCTOR_PATTERN =\n '`constructor(scope: Construct, id: string, { enableWaf = true }: UserIdentityProps = {}) { $body }`';\nconst CDK_CREATE_CALL_PATTERN = '`this.createUserPool()`';\nconst CDK_CREATE_SIGNATURE_PATTERN = '`private createUserPool = () => $body`';\n// Trailing commas block matching a bare object property in isolation; the\n// separator is outside the matched span either way, so it survives untouched.\nconst CDK_MFA_FIELD_PATTERN = '`mfa: Mfa.REQUIRED`';\nconst CDK_MFA_SECOND_FACTOR_FIELD_PATTERN =\n '`mfaSecondFactor: { sms: true, otp: true }`';\nconst CDK_SUPER_CALL_PATTERN = '`super(scope, id);`';\n// $props preserves the whole (large) UserPool props object untouched while\n// inserting the new const declaration immediately before it.\nconst CDK_NEW_USER_POOL_PATTERN =\n \"`const userPool = new UserPool(this, 'UserPool', { $props })`\";\n// Matches both the autoVerify.phone and keepOriginal.phone occurrences in one pass.\nconst CDK_PHONE_TRUE_PATTERN = '`phone: true`';\n\nconst CDK_EDITS: Array<[string, string]> = [\n [\n CDK_INTERFACE_BODY_PATTERN,\n `${CDK_INTERFACE_BODY_PATTERN} => \\`export interface UserIdentityProps {\n $body\n\n /**\n * Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all.\n *\n * @default Mfa.REQUIRED\n */\n readonly mfa?: Mfa;\n\n /**\n * The MFA methods available to users when MFA is not off.\n *\n * @default { sms: true, otp: true }\n */\n readonly mfaSecondFactor?: MfaSecondFactor;\n}\\``,\n ],\n [\n CDK_CONSTRUCTOR_PATTERN,\n `${CDK_CONSTRUCTOR_PATTERN} => \\`constructor(\n scope: Construct,\n id: string,\n {\n enableWaf = true,\n mfa = Mfa.REQUIRED,\n mfaSecondFactor = { sms: true, otp: true },\n }: UserIdentityProps = {},\n ) { $body }\\``,\n ],\n [\n CDK_CREATE_CALL_PATTERN,\n `${CDK_CREATE_CALL_PATTERN} => \\`this.createUserPool(mfa, mfaSecondFactor)\\``,\n ],\n [\n CDK_CREATE_SIGNATURE_PATTERN,\n `${CDK_CREATE_SIGNATURE_PATTERN} => \\`private createUserPool = (mfa: Mfa, mfaSecondFactor: MfaSecondFactor) => $body\\``,\n ],\n [CDK_MFA_FIELD_PATTERN, `${CDK_MFA_FIELD_PATTERN} => \\`mfa\\``],\n [\n CDK_MFA_SECOND_FACTOR_FIELD_PATTERN,\n `${CDK_MFA_SECOND_FACTOR_FIELD_PATTERN} => \\`mfaSecondFactor\\``,\n ],\n [\n CDK_SUPER_CALL_PATTERN,\n `${CDK_SUPER_CALL_PATTERN} => \\`super(scope, id);\n\n if (mfa === Mfa.REQUIRED && !mfaSecondFactor.sms && !mfaSecondFactor.otp) {\n throw new Error(\n 'UserIdentity: mfa is REQUIRED but mfaSecondFactor has no methods enabled (sms and otp are both false)',\n );\n }\\``,\n ],\n [\n CDK_NEW_USER_POOL_PATTERN,\n `${CDK_NEW_USER_POOL_PATTERN} => \\`const phoneVerificationEnabled = mfa === Mfa.OFF || mfaSecondFactor.sms;\n\n const userPool = new UserPool(this, 'UserPool', { $props })\\``,\n ],\n [\n CDK_PHONE_TRUE_PATTERN,\n `${CDK_PHONE_TRUE_PATTERN} => \\`phone: phoneVerificationEnabled\\``,\n ],\n];\n\nconst TERRAFORM_IDENTITY_ENABLE_WAF_VARIABLE_ANCHOR = [\n 'language hcl',\n '`variable \"enable_waf\" {',\n ' description = \"Whether to enable AWS WAFv2 with the default managed ruleset (AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet) and associate it with the user pool\"',\n ' type = bool',\n ' default = true',\n '}`',\n].join('\\n');\n\nconst TERRAFORM_MFA_CONFIGURATION_PATTERN = [\n 'language hcl',\n '`mfa_configuration = \"ON\"`',\n].join('\\n');\n\nconst TERRAFORM_SOFTWARE_TOKEN_MFA_PATTERN = [\n 'language hcl',\n '`software_token_mfa_configuration {',\n ' enabled = true',\n ' }`',\n].join('\\n');\n\nconst TERRAFORM_SMS_CONFIGURATION_PATTERN = [\n 'language hcl',\n '`sms_configuration {',\n ' external_id = random_uuid.sms_external_id.result',\n ' sns_caller_arn = aws_iam_role.cognito_sms_role.arn',\n ' sns_region = data.aws_region.current.region',\n ' }`',\n].join('\\n');\n\nconst TERRAFORM_RANDOM_UUID_PATTERN = [\n 'language hcl',\n '`resource \"random_uuid\" \"sms_external_id\" {}`',\n].join('\\n');\n\nconst TERRAFORM_SMS_ROLE_PATTERN = [\n 'language hcl',\n '`resource \"aws_iam_role\" \"cognito_sms_role\" { $body }`',\n].join('\\n');\n\nconst TERRAFORM_SMS_POLICY_PATTERN = [\n 'language hcl',\n '`resource \"aws_iam_role_policy\" \"cognito_sms_policy\" { $body }`',\n].join('\\n');\n\nconst TERRAFORM_SMS_ROLE_ID_REF_PATTERN = [\n 'language hcl',\n '`aws_iam_role.cognito_sms_role.id`',\n].join('\\n');\n\nconst TERRAFORM_EXTERNAL_ID_REF_PATTERN = [\n 'language hcl',\n '`random_uuid.sms_external_id.result`',\n].join('\\n');\n\nconst TERRAFORM_AUTO_VERIFIED_ATTRIBUTES_PATTERN = [\n 'language hcl',\n '`auto_verified_attributes = [\"email\", \"phone_number\"]`',\n].join('\\n');\n\nconst TERRAFORM_ATTRIBUTES_REQUIRE_VERIFICATION_PATTERN = [\n 'language hcl',\n '`attributes_require_verification_before_update = [\"email\", \"phone_number\"]`',\n].join('\\n');\n\nconst TERRAFORM_IDENTITY_EDITS: Array<[string, string]> = [\n [\n TERRAFORM_IDENTITY_ENABLE_WAF_VARIABLE_ANCHOR,\n `${TERRAFORM_IDENTITY_ENABLE_WAF_VARIABLE_ANCHOR} => \\`variable \"enable_waf\" {\n description = \"Whether to enable AWS WAFv2 with the default managed ruleset (AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet) and associate it with the user pool\"\n type = bool\n default = true\n}\n\n${NEW_TERRAFORM_VARIABLES}\\``,\n ],\n [\n TERRAFORM_RANDOM_UUID_PATTERN,\n `${TERRAFORM_RANDOM_UUID_PATTERN} => \\`resource \"random_uuid\" \"sms_external_id\" {\n count = var.mfa_second_factor_sms ? 1 : 0\n}\\``,\n ],\n [\n TERRAFORM_SMS_ROLE_PATTERN,\n `${TERRAFORM_SMS_ROLE_PATTERN} => \\`resource \"aws_iam_role\" \"cognito_sms_role\" {\n count = var.mfa_second_factor_sms ? 1 : 0\n\n $body\n}\\``,\n ],\n [\n TERRAFORM_SMS_POLICY_PATTERN,\n `${TERRAFORM_SMS_POLICY_PATTERN} => \\`resource \"aws_iam_role_policy\" \"cognito_sms_policy\" {\n count = var.mfa_second_factor_sms ? 1 : 0\n\n $body\n}\\``,\n ],\n [\n TERRAFORM_MFA_CONFIGURATION_PATTERN,\n `${TERRAFORM_MFA_CONFIGURATION_PATTERN} => \\`mfa_configuration = var.mfa\\``,\n ],\n [\n TERRAFORM_SOFTWARE_TOKEN_MFA_PATTERN,\n `${TERRAFORM_SOFTWARE_TOKEN_MFA_PATTERN} => \\`software_token_mfa_configuration {\n enabled = var.mfa_second_factor_otp\n }\\``,\n ],\n [\n TERRAFORM_SMS_CONFIGURATION_PATTERN,\n `${TERRAFORM_SMS_CONFIGURATION_PATTERN} => \\`# SMS MFA configuration. Due to a Terraform AWS provider limitation, this also gates\n # SMS-based phone number verification below - see the mfa_second_factor_sms variable.\n dynamic \"sms_configuration\" {\n for_each = var.mfa_second_factor_sms ? [1] : []\n\n content {\n external_id = random_uuid.sms_external_id[0].result\n sns_caller_arn = aws_iam_role.cognito_sms_role[0].arn\n sns_region = data.aws_region.current.region\n }\n }\\``,\n ],\n [\n TERRAFORM_SMS_ROLE_ID_REF_PATTERN,\n `${TERRAFORM_SMS_ROLE_ID_REF_PATTERN} => \\`aws_iam_role.cognito_sms_role[0].id\\``,\n ],\n [\n TERRAFORM_EXTERNAL_ID_REF_PATTERN,\n `${TERRAFORM_EXTERNAL_ID_REF_PATTERN} => \\`random_uuid.sms_external_id[0].result\\``,\n ],\n [\n TERRAFORM_AUTO_VERIFIED_ATTRIBUTES_PATTERN,\n `${TERRAFORM_AUTO_VERIFIED_ATTRIBUTES_PATTERN} => \\`auto_verified_attributes = var.mfa_second_factor_sms ? [\"email\", \"phone_number\"] : [\"email\"]\\``,\n ],\n [\n TERRAFORM_ATTRIBUTES_REQUIRE_VERIFICATION_PATTERN,\n `${TERRAFORM_ATTRIBUTES_REQUIRE_VERIFICATION_PATTERN} => \\`attributes_require_verification_before_update = var.mfa_second_factor_sms ? [\"email\", \"phone_number\"] : [\"email\"]\\``,\n ],\n];\n\nconst TERRAFORM_MAIN_ENABLE_WAF_VARIABLE_ANCHOR = [\n 'language hcl',\n '`variable \"enable_waf\" {',\n ' description = \"Whether to enable AWS WAFv2 with the default managed ruleset and associate it with the user pool\"',\n ' type = bool',\n ' default = true',\n '}`',\n].join('\\n');\n\nconst TERRAFORM_MAIN_MODULE_PATTERN = [\n 'language hcl',\n '`module \"identity\" {',\n ' source = \"./identity\"',\n '',\n ' user_pool_domain_prefix = $prefix',\n ' allow_signup = $allowSignup',\n ' enable_waf = var.enable_waf',\n '}`',\n].join('\\n');\n\nconst TERRAFORM_MAIN_EDITS: Array<[string, string]> = [\n [\n TERRAFORM_MAIN_ENABLE_WAF_VARIABLE_ANCHOR,\n `${TERRAFORM_MAIN_ENABLE_WAF_VARIABLE_ANCHOR} => \\`variable \"enable_waf\" {\n description = \"Whether to enable AWS WAFv2 with the default managed ruleset and associate it with the user pool\"\n type = bool\n default = true\n}\n\n${NEW_TERRAFORM_VARIABLES}\\``,\n ],\n [\n TERRAFORM_MAIN_MODULE_PATTERN,\n `${TERRAFORM_MAIN_MODULE_PATTERN} => \\`module \"identity\" {\n source = \"./identity\"\n\n user_pool_domain_prefix = $prefix\n allow_signup = $allowSignup\n enable_waf = var.enable_waf\n mfa = var.mfa\n mfa_second_factor_otp = var.mfa_second_factor_otp\n mfa_second_factor_sms = var.mfa_second_factor_sms\n}\\``,\n ],\n];\n\nconst divergedNextStep = (filePath: string) =>\n `${filePath}: the UserIdentity construct/module has diverged from the generated shape - left untouched. To make MFA configurable, add mfa and mfaSecondFactor (CDK) / mfa, mfa_second_factor_otp and mfa_second_factor_sms (Terraform) yourself, following the same pattern as the existing enableWaf prop/variable.`;\n\nconst applyEdits = async (\n tree: Tree,\n filePath: string,\n migratedPattern: string,\n edits: Array<[string, string]>,\n nextSteps: string[],\n beforeApply?: () => Promise<void>,\n) => {\n if (!tree.exists(filePath)) {\n // This workspace doesn't use this IaC provider, or has no UserIdentity.\n return;\n }\n\n if (await matchGritQL(tree, filePath, migratedPattern)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n return;\n }\n\n // Confirm every edit site is present before writing any of them, so a file\n // that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n edits.map(([match]) => matchGritQL(tree, filePath, match)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(divergedNextStep(filePath));\n return;\n }\n\n await beforeApply?.();\n\n for (const [, rewrite] of edits) {\n await applyGritQL(tree, filePath, rewrite);\n }\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n await applyEdits(\n tree,\n CDK_USER_IDENTITY_FILE,\n CDK_MIGRATED_PATTERN,\n CDK_EDITS,\n nextSteps,\n () =>\n addDestructuredImport(\n tree,\n CDK_USER_IDENTITY_FILE,\n ['type MfaSecondFactor'],\n 'aws-cdk-lib/aws-cognito',\n ),\n );\n\n await applyEdits(\n tree,\n TERRAFORM_IDENTITY_FILE,\n TERRAFORM_IDENTITY_MIGRATED_PATTERN,\n TERRAFORM_IDENTITY_EDITS,\n nextSteps,\n );\n\n await applyEdits(\n tree,\n TERRAFORM_MAIN_FILE,\n TERRAFORM_MAIN_MIGRATED_PATTERN,\n TERRAFORM_MAIN_EDITS,\n nextSteps,\n );\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["addDestructuredImport","applyGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","CDK_USER_IDENTITY_FILE","TERRAFORM_IDENTITY_FILE","TERRAFORM_MAIN_FILE","NEW_TERRAFORM_VARIABLES","CDK_MIGRATED_PATTERN","TERRAFORM_IDENTITY_MIGRATED_PATTERN","TERRAFORM_MAIN_MIGRATED_PATTERN","CDK_INTERFACE_BODY_PATTERN","CDK_CONSTRUCTOR_PATTERN","CDK_CREATE_CALL_PATTERN","CDK_CREATE_SIGNATURE_PATTERN","CDK_MFA_FIELD_PATTERN","CDK_MFA_SECOND_FACTOR_FIELD_PATTERN","CDK_SUPER_CALL_PATTERN","CDK_NEW_USER_POOL_PATTERN","CDK_PHONE_TRUE_PATTERN","CDK_EDITS","TERRAFORM_IDENTITY_ENABLE_WAF_VARIABLE_ANCHOR","join","TERRAFORM_MFA_CONFIGURATION_PATTERN","TERRAFORM_SOFTWARE_TOKEN_MFA_PATTERN","TERRAFORM_SMS_CONFIGURATION_PATTERN","TERRAFORM_RANDOM_UUID_PATTERN","TERRAFORM_SMS_ROLE_PATTERN","TERRAFORM_SMS_POLICY_PATTERN","TERRAFORM_SMS_ROLE_ID_REF_PATTERN","TERRAFORM_EXTERNAL_ID_REF_PATTERN","TERRAFORM_AUTO_VERIFIED_ATTRIBUTES_PATTERN","TERRAFORM_ATTRIBUTES_REQUIRE_VERIFICATION_PATTERN","TERRAFORM_IDENTITY_EDITS","TERRAFORM_MAIN_ENABLE_WAF_VARIABLE_ANCHOR","TERRAFORM_MAIN_MODULE_PATTERN","TERRAFORM_MAIN_EDITS","divergedNextStep","filePath","applyEdits","tree","migratedPattern","edits","nextSteps","beforeApply","exists","allSitesPresent","Promise","all","map","match","every","Boolean","push","rewrite","migration"],"mappings":"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"}
@@ -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
  }
@@ -815,7 +875,7 @@ import {
815
875
  } from 'aws-cdk-lib/aws-dynamodb';
816
876
  import { RemovalPolicy } from 'aws-cdk-lib';
817
877
  import { Grant, IGrantable } from 'aws-cdk-lib/aws-iam';
818
- import { Key } from 'aws-cdk-lib/aws-kms';
878
+ import { IKey, Key } from 'aws-cdk-lib/aws-kms';
819
879
  import { RuntimeConfig } from './runtime-config.js';
820
880
 
821
881
  type _DynamoDBTableProps = Omit<
@@ -835,8 +895,23 @@ export interface DynamoDBTableProps extends _DynamoDBTableProps {
835
895
  */
836
896
  readonly runtimeConfigKey: string;
837
897
 
898
+ /**
899
+ * Server-side encryption for the table.
900
+ *
901
+ * @default TableEncryption.CUSTOMER_MANAGED
902
+ */
903
+ readonly encryption?: TableEncryption;
904
+
905
+ /**
906
+ * KMS key used to encrypt the table. Only used when \`encryption\` is
907
+ * \`TableEncryption.CUSTOMER_MANAGED\`. When not provided, a new key is created.
908
+ */
909
+ readonly encryptionKey?: IKey;
910
+
838
911
  /**
839
912
  * Whether to enable automatic key rotation on the KMS key used to encrypt the table.
913
+ * Only applies when \`encryption\` is \`TableEncryption.CUSTOMER_MANAGED\` and no
914
+ * \`encryptionKey\` is supplied.
840
915
  *
841
916
  * @default true
842
917
  */
@@ -856,13 +931,19 @@ export abstract class DynamoDBTable extends Construct {
856
931
  pointInTimeRecoverySpecification = { pointInTimeRecoveryEnabled: true },
857
932
  deletionProtection = true,
858
933
  removalPolicy = RemovalPolicy.RETAIN,
934
+ encryption = TableEncryption.CUSTOMER_MANAGED,
935
+ encryptionKey,
859
936
  enableKeyRotation = true,
860
937
  ...rest
861
938
  }: DynamoDBTableProps,
862
939
  ) {
863
940
  super(scope, id);
864
941
 
865
- const key = new Key(this, 'EncryptionKey', { enableKeyRotation });
942
+ const key: IKey | undefined =
943
+ encryption === TableEncryption.CUSTOMER_MANAGED
944
+ ? (encryptionKey ??
945
+ new Key(this, 'EncryptionKey', { enableKeyRotation }))
946
+ : undefined;
866
947
 
867
948
  this.table = new Table(this, runtimeConfigKey, {
868
949
  ...rest,
@@ -873,7 +954,7 @@ export abstract class DynamoDBTable extends Construct {
873
954
  pointInTimeRecoverySpecification,
874
955
  deletionProtection,
875
956
  encryptionKey: key,
876
- encryption: TableEncryption.CUSTOMER_MANAGED,
957
+ encryption,
877
958
  removalPolicy,
878
959
  });
879
960