@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
|
@@ -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
|
|
@@ -28,6 +28,29 @@ variable "enable_waf" {
|
|
|
28
28
|
default = true
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
variable "mfa" {
|
|
32
|
+
description = "Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all"
|
|
33
|
+
type = string
|
|
34
|
+
default = "ON"
|
|
35
|
+
|
|
36
|
+
validation {
|
|
37
|
+
condition = contains(["OFF", "OPTIONAL", "ON"], var.mfa)
|
|
38
|
+
error_message = "mfa must be one of OFF, OPTIONAL, ON"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
variable "mfa_second_factor_otp" {
|
|
43
|
+
description = "Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method"
|
|
44
|
+
type = bool
|
|
45
|
+
default = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
variable "mfa_second_factor_sms" {
|
|
49
|
+
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"
|
|
50
|
+
type = bool
|
|
51
|
+
default = true
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
variable "callback_urls" {
|
|
32
55
|
description = "Additional callback URLs for the user pool client"
|
|
33
56
|
type = list(string)
|
|
@@ -58,10 +81,14 @@ resource "random_id" "unique_suffix" {
|
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
# Generate a random external ID for SMS role security
|
|
61
|
-
resource "random_uuid" "sms_external_id" {
|
|
84
|
+
resource "random_uuid" "sms_external_id" {
|
|
85
|
+
count = var.mfa_second_factor_sms ? 1 : 0
|
|
86
|
+
}
|
|
62
87
|
|
|
63
88
|
# IAM role for SMS MFA
|
|
64
89
|
resource "aws_iam_role" "cognito_sms_role" {
|
|
90
|
+
count = var.mfa_second_factor_sms ? 1 : 0
|
|
91
|
+
|
|
65
92
|
name = "cognito-sms-role-${random_id.unique_suffix.hex}"
|
|
66
93
|
|
|
67
94
|
assume_role_policy = jsonencode({
|
|
@@ -75,7 +102,7 @@ resource "aws_iam_role" "cognito_sms_role" {
|
|
|
75
102
|
}
|
|
76
103
|
Condition = {
|
|
77
104
|
StringEquals = {
|
|
78
|
-
"sts:ExternalId" = random_uuid.sms_external_id.result
|
|
105
|
+
"sts:ExternalId" = random_uuid.sms_external_id[0].result
|
|
79
106
|
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
|
|
80
107
|
}
|
|
81
108
|
ArnLike = {
|
|
@@ -90,8 +117,10 @@ resource "aws_iam_role" "cognito_sms_role" {
|
|
|
90
117
|
resource "aws_iam_role_policy" "cognito_sms_policy" {
|
|
91
118
|
# checkov:skip=CKV_AWS_290:Cognito SMS requires sns:Publish with wildcard resource
|
|
92
119
|
# checkov:skip=CKV_AWS_355:Cognito SMS requires sns:Publish with wildcard resource
|
|
120
|
+
count = var.mfa_second_factor_sms ? 1 : 0
|
|
121
|
+
|
|
93
122
|
name = "cognito-sms-policy-${random_id.unique_suffix.hex}"
|
|
94
|
-
role = aws_iam_role.cognito_sms_role.id
|
|
123
|
+
role = aws_iam_role.cognito_sms_role[0].id
|
|
95
124
|
|
|
96
125
|
policy = jsonencode({
|
|
97
126
|
Version = "2012-10-17"
|
|
@@ -127,18 +156,23 @@ resource "aws_cognito_user_pool" "user_pool" {
|
|
|
127
156
|
}
|
|
128
157
|
|
|
129
158
|
# MFA configuration
|
|
130
|
-
mfa_configuration =
|
|
159
|
+
mfa_configuration = var.mfa
|
|
131
160
|
|
|
132
161
|
# Software token MFA configuration
|
|
133
162
|
software_token_mfa_configuration {
|
|
134
|
-
enabled =
|
|
163
|
+
enabled = var.mfa_second_factor_otp
|
|
135
164
|
}
|
|
136
165
|
|
|
137
|
-
# SMS MFA configuration
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
166
|
+
# SMS MFA configuration. Due to a Terraform AWS provider limitation, this also gates
|
|
167
|
+
# SMS-based phone number verification below - see the mfa_second_factor_sms variable.
|
|
168
|
+
dynamic "sms_configuration" {
|
|
169
|
+
for_each = var.mfa_second_factor_sms ? [1] : []
|
|
170
|
+
|
|
171
|
+
content {
|
|
172
|
+
external_id = random_uuid.sms_external_id[0].result
|
|
173
|
+
sns_caller_arn = aws_iam_role.cognito_sms_role[0].arn
|
|
174
|
+
sns_region = data.aws_region.current.region
|
|
175
|
+
}
|
|
142
176
|
}
|
|
143
177
|
|
|
144
178
|
depends_on = [aws_iam_role_policy.cognito_sms_policy]
|
|
@@ -158,8 +192,9 @@ resource "aws_cognito_user_pool" "user_pool" {
|
|
|
158
192
|
}
|
|
159
193
|
}
|
|
160
194
|
|
|
161
|
-
# Auto verification
|
|
162
|
-
|
|
195
|
+
# Auto verification. Phone number verification requires sms_configuration above, so it
|
|
196
|
+
# follows mfa_second_factor_sms - see that variable's description.
|
|
197
|
+
auto_verified_attributes = var.mfa_second_factor_sms ? ["email", "phone_number"] : ["email"]
|
|
163
198
|
|
|
164
199
|
# User pool tier (equivalent to FeaturePlan.PLUS)
|
|
165
200
|
user_pool_tier = "PLUS"
|
|
@@ -220,7 +255,7 @@ resource "aws_cognito_user_pool" "user_pool" {
|
|
|
220
255
|
|
|
221
256
|
# User attribute update settings - require verification before update
|
|
222
257
|
user_attribute_update_settings {
|
|
223
|
-
attributes_require_verification_before_update = ["email", "phone_number"]
|
|
258
|
+
attributes_require_verification_before_update = var.mfa_second_factor_sms ? ["email", "phone_number"] : ["email"]
|
|
224
259
|
}
|
|
225
260
|
|
|
226
261
|
# Verification message templates
|
|
@@ -4,12 +4,38 @@ variable "enable_waf" {
|
|
|
4
4
|
default = true
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
variable "mfa" {
|
|
8
|
+
description = "Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all"
|
|
9
|
+
type = string
|
|
10
|
+
default = "ON"
|
|
11
|
+
|
|
12
|
+
validation {
|
|
13
|
+
condition = contains(["OFF", "OPTIONAL", "ON"], var.mfa)
|
|
14
|
+
error_message = "mfa must be one of OFF, OPTIONAL, ON"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
variable "mfa_second_factor_otp" {
|
|
19
|
+
description = "Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method"
|
|
20
|
+
type = bool
|
|
21
|
+
default = true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
variable "mfa_second_factor_sms" {
|
|
25
|
+
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"
|
|
26
|
+
type = bool
|
|
27
|
+
default = true
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
module "identity" {
|
|
8
31
|
source = "./identity"
|
|
9
32
|
|
|
10
33
|
user_pool_domain_prefix = "<%= cognitoDomain %>"
|
|
11
34
|
allow_signup = <%= allowSignup %>
|
|
12
35
|
enable_waf = var.enable_waf
|
|
36
|
+
mfa = var.mfa
|
|
37
|
+
mfa_second_factor_otp = var.mfa_second_factor_otp
|
|
38
|
+
mfa_second_factor_sms = var.mfa_second_factor_sms
|
|
13
39
|
}
|
|
14
40
|
|
|
15
41
|
# Outputs
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import * as url from 'url';
|
|
2
2
|
import { Construct } from 'constructs';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
StaticWebsite,
|
|
5
|
+
StaticWebsiteProps,
|
|
6
|
+
} from '../../core/index<% if (esm) { %>.js<% } %>';
|
|
7
|
+
|
|
8
|
+
export type <%= websiteNameClassName %>Props = Omit<
|
|
9
|
+
StaticWebsiteProps,
|
|
10
|
+
'websiteName' | 'websiteFilePath'
|
|
11
|
+
>;
|
|
4
12
|
|
|
5
13
|
export class <%= websiteNameClassName %> extends StaticWebsite {
|
|
6
|
-
constructor(scope: Construct, id: string) {
|
|
14
|
+
constructor(scope: Construct, id: string, props?: <%= websiteNameClassName %>Props) {
|
|
7
15
|
super(scope, id, {
|
|
16
|
+
...props,
|
|
8
17
|
websiteName: '<%= websiteNameClassName %>',
|
|
9
18
|
websiteFilePath: url.fileURLToPath(
|
|
10
19
|
new URL('../../../../../../<%= websiteContentPath %>/bundle', <% if (esm) { %>import.meta.url<% } else { %>require('url').pathToFileURL(__filename).href<% } %>)
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from 'aws-cdk-lib/aws-s3-deployment';
|
|
32
32
|
import { Construct } from 'constructs';
|
|
33
33
|
import { RuntimeConfig } from './runtime-config<% if (esm) { %>.js<% } %>';
|
|
34
|
-
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
34
|
+
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
|
|
35
35
|
import {
|
|
36
36
|
CfnDelivery,
|
|
37
37
|
CfnDeliveryDestination,
|
|
@@ -74,6 +74,33 @@ export interface StaticWebsiteProps {
|
|
|
74
74
|
* When provided, viewers are required to use TLS 1.2 or later.
|
|
75
75
|
*/
|
|
76
76
|
readonly certificate?: ICertificate;
|
|
77
|
+
/**
|
|
78
|
+
* Whether to protect the CloudFront distribution with an AWS WAF Web ACL.
|
|
79
|
+
*
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
readonly enableWaf?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Server-side encryption for the website and distribution log buckets.
|
|
85
|
+
*
|
|
86
|
+
* @default BucketEncryption.KMS
|
|
87
|
+
*/
|
|
88
|
+
readonly encryption?: BucketEncryption;
|
|
89
|
+
/**
|
|
90
|
+
* KMS key used to encrypt the website and distribution log buckets. Only used when `encryption` is
|
|
91
|
+
* `BucketEncryption.KMS`. When not provided, a new key is created. Note that a key imported via
|
|
92
|
+
* `Key.fromKeyArn` must already grant the CloudWatch Logs, S3 and CloudFront service principals the
|
|
93
|
+
* necessary permissions in its own key policy - `addToResourcePolicy` is a no-op on an imported key,
|
|
94
|
+
* so this construct cannot grant them on your behalf.
|
|
95
|
+
*/
|
|
96
|
+
readonly encryptionKey?: IKey;
|
|
97
|
+
/**
|
|
98
|
+
* Whether the automatically created KMS key has rotation enabled. Only applies when `encryption` is
|
|
99
|
+
* `BucketEncryption.KMS` and no `encryptionKey` is supplied.
|
|
100
|
+
*
|
|
101
|
+
* @default true
|
|
102
|
+
*/
|
|
103
|
+
readonly enableKeyRotation?: boolean;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
/**
|
|
@@ -92,17 +119,27 @@ export class StaticWebsite extends Construct {
|
|
|
92
119
|
constructor(
|
|
93
120
|
scope: Construct,
|
|
94
121
|
id: string,
|
|
95
|
-
{
|
|
122
|
+
{
|
|
123
|
+
websiteFilePath,
|
|
124
|
+
websiteName,
|
|
125
|
+
domainNames,
|
|
126
|
+
certificate,
|
|
127
|
+
enableWaf = true,
|
|
128
|
+
encryption = BucketEncryption.KMS,
|
|
129
|
+
encryptionKey,
|
|
130
|
+
enableKeyRotation = true,
|
|
131
|
+
}: StaticWebsiteProps
|
|
96
132
|
) {
|
|
97
133
|
super(scope, id);
|
|
98
134
|
|
|
99
|
-
const websiteKey
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
const websiteKey: IKey | undefined =
|
|
136
|
+
encryption === BucketEncryption.KMS
|
|
137
|
+
? (encryptionKey ?? new Key(this, 'WebsiteKey', { enableKeyRotation }))
|
|
138
|
+
: undefined;
|
|
102
139
|
|
|
103
140
|
// Allow CloudWatch Logs to use the website key for server access log delivery.
|
|
104
141
|
const stack = Stack.of(this);
|
|
105
|
-
websiteKey
|
|
142
|
+
websiteKey?.addToResourcePolicy(
|
|
106
143
|
new PolicyStatement({
|
|
107
144
|
effect: Effect.ALLOW,
|
|
108
145
|
principals: [
|
|
@@ -136,7 +173,7 @@ export class StaticWebsite extends Construct {
|
|
|
136
173
|
enforceSSL: true,
|
|
137
174
|
autoDeleteObjects: true,
|
|
138
175
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
139
|
-
encryption
|
|
176
|
+
encryption,
|
|
140
177
|
encryptionKey: websiteKey,
|
|
141
178
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
|
|
142
179
|
publicReadAccess: false,
|
|
@@ -153,7 +190,7 @@ export class StaticWebsite extends Construct {
|
|
|
153
190
|
accessLogs,
|
|
154
191
|
);
|
|
155
192
|
// Web ACL
|
|
156
|
-
const wafStack = new CloudfrontWebAcl(this, 'waf');
|
|
193
|
+
const wafStack = enableWaf ? new CloudfrontWebAcl(this, 'waf') : undefined;
|
|
157
194
|
|
|
158
195
|
// Bucket holding CloudFront standard access logs. CloudFront delivers its
|
|
159
196
|
// own logs to S3 only, so this bucket is retained; its S3 server access
|
|
@@ -162,7 +199,7 @@ export class StaticWebsite extends Construct {
|
|
|
162
199
|
enforceSSL: true,
|
|
163
200
|
autoDeleteObjects: true,
|
|
164
201
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
165
|
-
encryption
|
|
202
|
+
encryption,
|
|
166
203
|
encryptionKey: websiteKey,
|
|
167
204
|
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
|
|
168
205
|
publicReadAccess: false,
|
|
@@ -214,7 +251,7 @@ export class StaticWebsite extends Construct {
|
|
|
214
251
|
this,
|
|
215
252
|
'CloudfrontDistribution',
|
|
216
253
|
{
|
|
217
|
-
webAclId: wafStack
|
|
254
|
+
webAclId: wafStack?.wafArn,
|
|
218
255
|
enableLogging: true,
|
|
219
256
|
logBucket: logBucket,
|
|
220
257
|
...(certificate
|
|
@@ -8,6 +8,53 @@ terraform {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
variable "custom_domain_names" {
|
|
12
|
+
description = "Custom domain names (aliases) for the CloudFront distribution. Requires acm_certificate_arn."
|
|
13
|
+
type = list(string)
|
|
14
|
+
default = []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
variable "acm_certificate_arn" {
|
|
18
|
+
description = "ARN of an ACM certificate (in us-east-1) for the custom domain names. When set, viewers are required to use TLS 1.2 or later."
|
|
19
|
+
type = string
|
|
20
|
+
default = null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
variable "enable_waf" {
|
|
24
|
+
description = "Whether to protect the CloudFront distribution with an AWS WAF Web ACL."
|
|
25
|
+
type = bool
|
|
26
|
+
default = true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
variable "encryption" {
|
|
30
|
+
description = "Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED."
|
|
31
|
+
type = string
|
|
32
|
+
default = "KMS"
|
|
33
|
+
|
|
34
|
+
validation {
|
|
35
|
+
condition = contains(["KMS", "S3_MANAGED"], var.encryption)
|
|
36
|
+
error_message = "encryption must be one of KMS or S3_MANAGED."
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
variable "kms_key_arn" {
|
|
41
|
+
description = "ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy."
|
|
42
|
+
type = string
|
|
43
|
+
default = null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
variable "create_kms_key" {
|
|
47
|
+
description = "Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn."
|
|
48
|
+
type = bool
|
|
49
|
+
default = true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
variable "enable_key_rotation" {
|
|
53
|
+
description = "Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true."
|
|
54
|
+
type = bool
|
|
55
|
+
default = true
|
|
56
|
+
}
|
|
57
|
+
|
|
11
58
|
# Static website module configured for the web package
|
|
12
59
|
module "static_website" {
|
|
13
60
|
source = "../../../core/static-website"
|
|
@@ -15,6 +62,14 @@ module "static_website" {
|
|
|
15
62
|
website_name = "<%= websiteNameKebabCase %>"
|
|
16
63
|
website_file_path = "${path.module}/../../../../../../../<%= websiteContentPath %>/bundle"
|
|
17
64
|
|
|
65
|
+
custom_domain_names = var.custom_domain_names
|
|
66
|
+
acm_certificate_arn = var.acm_certificate_arn
|
|
67
|
+
enable_waf = var.enable_waf
|
|
68
|
+
encryption = var.encryption
|
|
69
|
+
kms_key_arn = var.kms_key_arn
|
|
70
|
+
create_kms_key = var.create_kms_key
|
|
71
|
+
enable_key_rotation = var.enable_key_rotation
|
|
72
|
+
|
|
18
73
|
providers = {
|
|
19
74
|
aws.us_east_1 = aws.us_east_1
|
|
20
75
|
}
|