@aws/nx-plugin 1.0.0-rc.76 → 1.0.0-rc.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations.json +16 -1
- package/package.json +1 -1
- package/src/migrations/latest/dynamodb-configurable-encryption/__snapshots__/migration.spec.ts.snap +429 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/metadata.json +3 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.d.ts +6 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.js +325 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.js.map +1 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/metadata.json +3 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.d.ts +6 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js +418 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js.map +1 -0
- package/src/migrations/latest/user-identity-configurable-mfa/metadata.json +3 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.d.ts +6 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.js +362 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.js.map +1 -0
- package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
- package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +259 -48
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +37 -7
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +26 -0
- package/src/utils/dynamodb-constructs/files/cdk/core/dynamodb.ts.template +23 -3
- package/src/utils/dynamodb-constructs/files/terraform/app/dynamodb/__nameKebabCase__/__nameKebabCase__.tf.template +27 -1
- package/src/utils/dynamodb-constructs/files/terraform/core/dynamodb/dynamodb.tf.template +39 -5
- package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +41 -7
- package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +48 -13
- package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +26 -0
- package/src/utils/website-constructs/files/cdk/app/static-websites/__websiteNameKebabCase__.ts.template +11 -2
- package/src/utils/website-constructs/files/cdk/core/static-website.ts.template +47 -10
- package/src/utils/website-constructs/files/terraform/app/static-websites/__websiteNameKebabCase__/__websiteNameKebabCase__.tf.template +55 -0
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +57 -15
|
@@ -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 =
|
|
169
|
-
kms_key_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 =
|
|
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
|
|
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
|
|
957
|
+
encryption,
|
|
877
958
|
removalPolicy,
|
|
878
959
|
});
|
|
879
960
|
|