@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.
- package/migrations.json +11 -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/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/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
|
@@ -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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
-
{
|
|
173
|
+
{
|
|
174
|
+
enableWaf = true,
|
|
175
|
+
mfa = Mfa.REQUIRED,
|
|
176
|
+
mfaSecondFactor = { sms: true, otp: true },
|
|
177
|
+
}: UserIdentityProps = {},
|
|
159
178
|
) {
|
|
160
179
|
super(scope, id);
|
|
161
180
|
|
|
181
|
+
if (mfa === Mfa.REQUIRED && !mfaSecondFactor.sms && !mfaSecondFactor.otp) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
'UserIdentity: mfa is REQUIRED but mfaSecondFactor has no methods enabled (sms and otp are both false)',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
162
187
|
this.region = Stack.of(this).region;
|
|
163
|
-
this.userPool = this.createUserPool();
|
|
188
|
+
this.userPool = this.createUserPool(mfa, mfaSecondFactor);
|
|
164
189
|
|
|
165
190
|
if (enableWaf) {
|
|
166
191
|
this.webAcl = this.createWebAcl(
|
|
@@ -208,7 +233,12 @@ export class UserIdentity extends Construct {
|
|
|
208
233
|
});
|
|
209
234
|
}
|
|
210
235
|
|
|
211
|
-
private createUserPool = () => {
|
|
236
|
+
private createUserPool = (mfa: Mfa, mfaSecondFactor: MfaSecondFactor) => {
|
|
237
|
+
// Cognito rejects SmsConfiguration (emitted whenever phone is auto-verified) unless SMS is
|
|
238
|
+
// also an enabled MFA method, for any non-OFF MfaConfiguration. So phone verification via SMS
|
|
239
|
+
// can only be offered when SMS is actually usable as a second factor.
|
|
240
|
+
const phoneVerificationEnabled = mfa === Mfa.OFF || mfaSecondFactor.sms;
|
|
241
|
+
|
|
212
242
|
const userPool = new UserPool(this, 'UserPool', {
|
|
213
243
|
deletionProtection: true,
|
|
214
244
|
passwordPolicy: {
|
|
@@ -219,11 +249,11 @@ export class UserIdentity extends Construct {
|
|
|
219
249
|
requireSymbols: true,
|
|
220
250
|
tempPasswordValidity: Duration.days(3),
|
|
221
251
|
},
|
|
222
|
-
mfa
|
|
252
|
+
mfa,
|
|
223
253
|
featurePlan: FeaturePlan.PLUS,
|
|
224
254
|
// Audit-only logs threat assessments without blocking sign-in. Switch to FULL_FUNCTION to enforce automatic responses.
|
|
225
255
|
standardThreatProtectionMode: StandardThreatProtectionMode.AUDIT_ONLY,
|
|
226
|
-
mfaSecondFactor
|
|
256
|
+
mfaSecondFactor,
|
|
227
257
|
signInCaseSensitive: false,
|
|
228
258
|
signInAliases: { username: true, email: true },
|
|
229
259
|
accountRecovery: AccountRecovery.EMAIL_ONLY,
|
|
@@ -236,11 +266,11 @@ export class UserIdentity extends Construct {
|
|
|
236
266
|
},
|
|
237
267
|
autoVerify: {
|
|
238
268
|
email: true,
|
|
239
|
-
phone:
|
|
269
|
+
phone: phoneVerificationEnabled,
|
|
240
270
|
},
|
|
241
271
|
keepOriginal: {
|
|
242
272
|
email: true,
|
|
243
|
-
phone:
|
|
273
|
+
phone: phoneVerificationEnabled,
|
|
244
274
|
},
|
|
245
275
|
});
|
|
246
276
|
// Retain the SMS role alongside the pool so the pool can still be updated and deleted manually
|
|
@@ -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
|
|
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
|
|
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 =
|
|
138
|
-
kms_key_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 =
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
180
|
+
phone: phoneVerificationEnabled,
|
|
147
181
|
},
|
|
148
182
|
keepOriginal: {
|
|
149
183
|
email: true,
|
|
150
|
-
phone:
|
|
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
|