From fdcebadb5da1c95516a9ed83755c66b15aebb309 Mon Sep 17 00:00:00 2001 From: Rocket Date: Thu, 1 Aug 2024 16:15:04 -0700 Subject: [PATCH] Manually update template to 7fcff11e1ebcddaeaa3c73a5f273acb4bd551106 --- .template-version | 2 +- infra/app/app-config/dev.tf | 7 ++ .../env-config/identity-provider.tf | 45 ++++++++++ .../app-config/env-config/notifications.tf | 16 ++++ infra/app/app-config/env-config/outputs.tf | 8 ++ infra/app/app-config/env-config/variables.tf | 24 ++++++ infra/app/app-config/main.tf | 12 +++ infra/app/app-config/outputs.tf | 4 + infra/app/app-config/prod.tf | 1 + infra/app/app-config/staging.tf | 1 + infra/app/service/main.tf | 72 +++++++++++++--- .../access-control.tf | 15 ++++ .../modules/identity-provider-client/main.tf | 37 ++++++++ .../identity-provider-client/outputs.tf | 13 +++ .../identity-provider-client/variables.tf | 21 +++++ infra/modules/identity-provider/main.tf | 85 +++++++++++++++++++ infra/modules/identity-provider/outputs.tf | 4 + infra/modules/identity-provider/variables.tf | 52 ++++++++++++ 18 files changed, 404 insertions(+), 15 deletions(-) create mode 100644 infra/app/app-config/env-config/identity-provider.tf create mode 100644 infra/app/app-config/env-config/notifications.tf create mode 100644 infra/modules/identity-provider-client/access-control.tf create mode 100644 infra/modules/identity-provider-client/main.tf create mode 100644 infra/modules/identity-provider-client/outputs.tf create mode 100644 infra/modules/identity-provider-client/variables.tf create mode 100644 infra/modules/identity-provider/main.tf create mode 100644 infra/modules/identity-provider/outputs.tf create mode 100644 infra/modules/identity-provider/variables.tf diff --git a/.template-version b/.template-version index 26863967..c38de925 100644 --- a/.template-version +++ b/.template-version @@ -1 +1 @@ -4d6c144a080edf19d5711d7a0a5cc7ac68d20fa2 +7fcff11e1ebcddaeaa3c73a5f273acb4bd551106 diff --git a/infra/app/app-config/dev.tf b/infra/app/app-config/dev.tf index 8cf29e4b..995009aa 100644 --- a/infra/app/app-config/dev.tf +++ b/infra/app/app-config/dev.tf @@ -10,6 +10,13 @@ module "dev_config" { has_database = local.has_database has_incident_management_service = local.has_incident_management_service + # Enable and configure identity provider. + enable_identity_provider = local.enable_identity_provider + + # Support local development against the dev instance. + extra_identity_provider_callback_urls = ["http://localhost"] + extra_identity_provider_logout_urls = ["http://localhost"] + # Enables ECS Exec access for debugging or jump access. # See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html # Defaults to `false`. Uncomment the next line to enable. diff --git a/infra/app/app-config/env-config/identity-provider.tf b/infra/app/app-config/env-config/identity-provider.tf new file mode 100644 index 00000000..e3d8ba3f --- /dev/null +++ b/infra/app/app-config/env-config/identity-provider.tf @@ -0,0 +1,45 @@ +# Identity provider configuration. +# If the notification service is configured, the identity provider will use the +# SES-verified email to send notifications. +locals { + # If your application should redirect users, after successful authentication, to a + # page other than the homepage, specify the path fragment here. + # Example: "profile" + # Docs: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html + callback_url_path = "" + + # If your application should redirect users, after signing out, to a page other than + # the homepage, specify the path fragment here. + # Example: "logout" + # Docs: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html + logout_url_path = "" + + identity_provider_config = var.enable_identity_provider ? { + identity_provider_name = "${local.prefix}${var.app_name}-${var.environment}" + + password_policy = { + password_minimum_length = 12 + temporary_password_validity_days = 7 + } + + # Optionally configure email template for resetting a password. + # Set any attribute to a non-null value to override AWS Cognito defaults. + # Docs: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-customizations.html + verification_email = { + verification_email_message = null + verification_email_subject = null + } + + # Do not modify this block directly. + client = { + callback_urls = concat( + var.domain_name != null ? ["https://${var.domain_name}/${local.callback_url_path}"] : [], + var.extra_identity_provider_callback_urls + ) + logout_urls = concat( + var.domain_name != null ? ["https://${var.domain_name}/${local.logout_url_path}"] : [], + var.extra_identity_provider_logout_urls + ) + } + } : null +} diff --git a/infra/app/app-config/env-config/notifications.tf b/infra/app/app-config/env-config/notifications.tf new file mode 100644 index 00000000..d4f2bb42 --- /dev/null +++ b/infra/app/app-config/env-config/notifications.tf @@ -0,0 +1,16 @@ +# Notifications configuration +locals { + notifications_config = var.enable_notifications ? { + # Set to an SES-verified email address to be used when sending emails. + # Docs: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html + sender_email = null + + # Configure the name that users see in the "From" section of their inbox, so that it's + # clearer who the email is from. + sender_display_name = null + + # Configure the REPLY-TO email address if it should be different from the sender. + # Note: Only used by the identity-provider service. + reply_to_email = null + } : null +} diff --git a/infra/app/app-config/env-config/outputs.tf b/infra/app/app-config/env-config/outputs.tf index ab66d9b0..065ee876 100644 --- a/infra/app/app-config/env-config/outputs.tf +++ b/infra/app/app-config/env-config/outputs.tf @@ -38,6 +38,14 @@ output "service_config" { } } +output "identity_provider_config" { + value = local.identity_provider_config +} + +output "notifications_config" { + value = local.notifications_config +} + output "storage_config" { value = { # Include project name in bucket name since buckets need to be globally unique across AWS diff --git a/infra/app/app-config/env-config/variables.tf b/infra/app/app-config/env-config/variables.tf index efe1e8cb..bdeb1c08 100644 --- a/infra/app/app-config/env-config/variables.tf +++ b/infra/app/app-config/env-config/variables.tf @@ -31,11 +31,35 @@ variable "enable_https" { default = false } +variable "enable_identity_provider" { + type = bool + description = "Enables identity provider" + default = false +} + +variable "enable_notifications" { + type = bool + description = "Enables notifications" + default = false +} + variable "environment" { description = "name of the application environment (e.g. dev, staging, prod)" type = string } +variable "extra_identity_provider_callback_urls" { + type = list(string) + description = "List of additional URLs that the identity provider will redirect the user to after a successful sign-in. Used for local development." + default = [] +} + +variable "extra_identity_provider_logout_urls" { + type = list(string) + description = "List of additional URLs that the identity provider will redirect the user to after signing out. Used for local development." + default = [] +} + variable "has_database" { type = bool } diff --git a/infra/app/app-config/main.tf b/infra/app/app-config/main.tf index 8f79e88b..bc5b1ac2 100644 --- a/infra/app/app-config/main.tf +++ b/infra/app/app-config/main.tf @@ -24,6 +24,18 @@ locals { has_incident_management_service = true + # Whether or not the application should deploy an identity provider + # If enabled: + # 1. Creates a Cognito user pool + # 2. Creates a Cognito user pool app client + # 3. Adds environment variables for the app client to the service + enable_identity_provider = false + + # Whether or not the application should deploy a notification service + # Note: This is not yet ready for use. + # TODO(https://github.com/navapbc/template-infra/issues/567) + enable_notifications = false + environment_configs = { dev = module.dev_config staging = module.staging_config diff --git a/infra/app/app-config/outputs.tf b/infra/app/app-config/outputs.tf index 6af0f385..cdcedc32 100644 --- a/infra/app/app-config/outputs.tf +++ b/infra/app/app-config/outputs.tf @@ -30,6 +30,10 @@ output "has_incident_management_service" { value = local.has_incident_management_service } +output "enable_identity_provider" { + value = local.enable_identity_provider +} + output "image_repository_name" { value = local.image_repository_name } diff --git a/infra/app/app-config/prod.tf b/infra/app/app-config/prod.tf index 3cc9bc78..fcc7d70d 100644 --- a/infra/app/app-config/prod.tf +++ b/infra/app/app-config/prod.tf @@ -9,6 +9,7 @@ module "prod_config" { enable_https = false has_database = local.has_database has_incident_management_service = local.has_incident_management_service + enable_identity_provider = local.enable_identity_provider # These numbers are a starting point based on this article # Update the desired instance size and counts based on the project's specific needs diff --git a/infra/app/app-config/staging.tf b/infra/app/app-config/staging.tf index dad61db0..342716d3 100644 --- a/infra/app/app-config/staging.tf +++ b/infra/app/app-config/staging.tf @@ -9,6 +9,7 @@ module "staging_config" { enable_https = false has_database = local.has_database has_incident_management_service = local.has_incident_management_service + enable_identity_provider = local.enable_identity_provider # Enables ECS Exec access for debugging or jump access. # See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html diff --git a/infra/app/service/main.tf b/infra/app/service/main.tf index 27c6ae12..3631a4a8 100644 --- a/infra/app/service/main.tf +++ b/infra/app/service/main.tf @@ -35,6 +35,8 @@ locals { database_config = local.environment_config.database_config storage_config = local.environment_config.storage_config incident_management_service_integration_config = local.environment_config.incident_management_service_integration + identity_provider_config = local.environment_config.identity_provider_config + notifications_config = local.environment_config.notifications_config network_config = module.project_config.network_configs[local.environment_config.network_name] } @@ -150,22 +152,38 @@ module "service" { } } : null - extra_environment_variables = merge({ - FEATURE_FLAGS_PROJECT = module.feature_flags.evidently_project_name - BUCKET_NAME = local.storage_config.bucket_name - }, local.service_config.extra_environment_variables) - - secrets = [ - for secret_name in keys(local.service_config.secrets) : { + extra_environment_variables = merge( + { + FEATURE_FLAGS_PROJECT = module.feature_flags.evidently_project_name + BUCKET_NAME = local.storage_config.bucket_name + }, + module.app_config.enable_identity_provider ? { + COGNITO_USER_POOL_ID = module.identity_provider[0].user_pool_id + COGNITO_CLIENT_ID = module.identity_provider_client[0].client_id + } : {}, + local.service_config.extra_environment_variables + ) + + secrets = concat( + [for secret_name in keys(local.service_config.secrets) : { name = secret_name valueFrom = module.secrets[secret_name].secret_arn - } - ] - - extra_policies = { - feature_flags_access = module.feature_flags.access_policy_arn, - storage_access = module.storage.access_policy_arn - } + }], + module.app_config.enable_identity_provider ? [{ + name = "COGNITO_CLIENT_SECRET" + valueFrom = module.identity_provider_client[0].client_secret_arn + }] : [] + ) + + extra_policies = merge( + { + feature_flags_access = module.feature_flags.access_policy_arn, + storage_access = module.storage.access_policy_arn + }, + module.app_config.enable_identity_provider ? { + identity_provider_access = module.identity_provider_client[0].access_policy_arn, + } : {} + ) is_temporary = local.is_temporary } @@ -192,3 +210,29 @@ module "storage" { name = local.storage_config.bucket_name is_temporary = local.is_temporary } + +module "identity_provider" { + count = module.app_config.enable_identity_provider ? 1 : 0 + source = "../../modules/identity-provider" + is_temporary = local.is_temporary + + name = local.identity_provider_config.identity_provider_name + password_minimum_length = local.identity_provider_config.password_policy.password_minimum_length + temporary_password_validity_days = local.identity_provider_config.password_policy.temporary_password_validity_days + verification_email_message = local.identity_provider_config.verification_email.verification_email_message + verification_email_subject = local.identity_provider_config.verification_email.verification_email_subject + + sender_email = local.notifications_config == null ? null : local.notifications_config.sender_email + sender_display_name = local.notifications_config == null ? null : local.notifications_config.sender_display_name + reply_to_email = local.notifications_config == null ? null : local.notifications_config.reply_to_email +} + +module "identity_provider_client" { + count = module.app_config.enable_identity_provider ? 1 : 0 + source = "../../modules/identity-provider-client" + + name = local.identity_provider_config.identity_provider_name + cognito_user_pool_id = module.identity_provider[0].user_pool_id + callback_urls = local.identity_provider_config.client.callback_urls + logout_urls = local.identity_provider_config.client.logout_urls +} diff --git a/infra/modules/identity-provider-client/access-control.tf b/infra/modules/identity-provider-client/access-control.tf new file mode 100644 index 00000000..db1421df --- /dev/null +++ b/infra/modules/identity-provider-client/access-control.tf @@ -0,0 +1,15 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +resource "aws_iam_policy" "cognito_access" { + name = "${var.name}-cognito-access" + policy = data.aws_iam_policy_document.cognito_access.json +} + +data "aws_iam_policy_document" "cognito_access" { + statement { + actions = ["cognito-idp:*"] + effect = "Allow" + resources = ["arn:aws:cognito-idp:${data.aws_region.current.name}:${data.aws_caller_identity.current.id}:userpool/${var.cognito_user_pool_id}"] + } +} diff --git a/infra/modules/identity-provider-client/main.tf b/infra/modules/identity-provider-client/main.tf new file mode 100644 index 00000000..d8110d52 --- /dev/null +++ b/infra/modules/identity-provider-client/main.tf @@ -0,0 +1,37 @@ +resource "aws_cognito_user_pool_client" "client" { + name = var.name + user_pool_id = var.cognito_user_pool_id + + callback_urls = var.callback_urls + logout_urls = var.logout_urls + supported_identity_providers = ["COGNITO"] + refresh_token_validity = 1 + access_token_validity = 60 + id_token_validity = 60 + token_validity_units { + refresh_token = "days" + access_token = "minutes" + id_token = "minutes" + } + + generate_secret = true + allowed_oauth_flows_user_pool_client = true + allowed_oauth_flows = ["code"] + allowed_oauth_scopes = ["phone", "email", "openid", "profile"] + explicit_auth_flows = ["ALLOW_ADMIN_USER_PASSWORD_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"] + + # Avoid security issue where error messages indicate when a user doesn't exist + prevent_user_existence_errors = "ENABLED" + + enable_token_revocation = true + enable_propagate_additional_user_context_data = false + + read_attributes = ["email", "email_verified", "phone_number", "phone_number_verified", "updated_at"] + write_attributes = ["email", "updated_at", "phone_number"] +} + +resource "aws_ssm_parameter" "client_secret" { + name = "/${var.name}/identity-provider/client-secret" + type = "SecureString" + value = aws_cognito_user_pool_client.client.client_secret +} diff --git a/infra/modules/identity-provider-client/outputs.tf b/infra/modules/identity-provider-client/outputs.tf new file mode 100644 index 00000000..2bf81ddf --- /dev/null +++ b/infra/modules/identity-provider-client/outputs.tf @@ -0,0 +1,13 @@ +output "access_policy_arn" { + value = aws_iam_policy.cognito_access.arn +} + +output "client_id" { + description = "The ID of the user pool client" + value = aws_cognito_user_pool_client.client.id +} + +output "client_secret_arn" { + description = "The arn for the SSM parameter storing the user pool client secret" + value = aws_ssm_parameter.client_secret.arn +} diff --git a/infra/modules/identity-provider-client/variables.tf b/infra/modules/identity-provider-client/variables.tf new file mode 100644 index 00000000..f75f5296 --- /dev/null +++ b/infra/modules/identity-provider-client/variables.tf @@ -0,0 +1,21 @@ +variable "callback_urls" { + type = list(string) + description = "The URL(s) that the identity provider will redirect to after a successful login" + default = [] +} + +variable "cognito_user_pool_id" { + type = string + description = "The ID of the user pool that the client will be associated with" +} + +variable "logout_urls" { + type = list(string) + description = "The URL that the identity provider will redirect to after a successful logout" + default = [] +} + +variable "name" { + type = string + description = "Name of the application or service that will act as a client to the identity provider" +} diff --git a/infra/modules/identity-provider/main.tf b/infra/modules/identity-provider/main.tf new file mode 100644 index 00000000..70096f4f --- /dev/null +++ b/infra/modules/identity-provider/main.tf @@ -0,0 +1,85 @@ +############################################################################################ +## A module for configuring a Cognito User Pool +## - Configures for email, but not SMS +## - Configures MFA +############################################################################################ + +data "aws_ses_email_identity" "sender" { + count = var.sender_email != null ? 1 : 0 + email = var.sender_email +} + +resource "aws_cognito_user_pool" "main" { + name = var.name + + # Use a separate line to support automated terraform destroy commands + deletion_protection = var.is_temporary ? "INACTIVE" : "ACTIVE" + + username_attributes = ["email"] + auto_verified_attributes = ["email"] + + account_recovery_setting { + recovery_mechanism { + name = "verified_email" + priority = 1 + } + } + + device_configuration { + challenge_required_on_new_device = true + device_only_remembered_on_user_prompt = true + } + + email_configuration { + # Use this SES email to send cognito emails. If we're not using SES for emails then use null. + # Optionally configures the FROM address and the REPLY-TO address. + # Optionally configures using the Cognito default email or using SES. + source_arn = var.sender_email != null ? data.aws_ses_email_identity.sender[0].arn : null + email_sending_account = var.sender_email != null ? "DEVELOPER" : "COGNITO_DEFAULT" + # Customize the name that users see in the "From" section of their inbox, so that it's clearer who the email is from. + # This name also needs to be updated manually in the Cognito console for each environment's Advanced Security emails. + from_email_address = var.sender_email != null ? (var.sender_display_name != null ? "${var.sender_display_name} <${var.sender_email}>" : var.sender_email) : null + reply_to_email_address = var.reply_to_email != null ? var.reply_to_email : null + } + + password_policy { + minimum_length = var.password_minimum_length + temporary_password_validity_days = var.temporary_password_validity_days + } + + mfa_configuration = "OPTIONAL" + software_token_mfa_configuration { + enabled = true + } + + user_pool_add_ons { + advanced_security_mode = "AUDIT" + } + + username_configuration { + case_sensitive = false + } + + user_attribute_update_settings { + attributes_require_verification_before_update = ["email"] + } + + schema { + name = "email" + attribute_data_type = "String" + mutable = "true" + required = "true" + + string_attribute_constraints { + max_length = 2048 + min_length = 0 + } + } + + # Optionally configures email template for resetting a password + verification_message_template { + default_email_option = "CONFIRM_WITH_CODE" + email_message = var.verification_email_message != null ? var.verification_email_message : null + email_subject = var.verification_email_subject != null ? var.verification_email_subject : null + } +} diff --git a/infra/modules/identity-provider/outputs.tf b/infra/modules/identity-provider/outputs.tf new file mode 100644 index 00000000..03914275 --- /dev/null +++ b/infra/modules/identity-provider/outputs.tf @@ -0,0 +1,4 @@ +output "user_pool_id" { + description = "The ID of the user pool." + value = aws_cognito_user_pool.main.id +} diff --git a/infra/modules/identity-provider/variables.tf b/infra/modules/identity-provider/variables.tf new file mode 100644 index 00000000..f980c705 --- /dev/null +++ b/infra/modules/identity-provider/variables.tf @@ -0,0 +1,52 @@ +variable "is_temporary" { + description = "Whether the service is meant to be spun up temporarily (e.g. for automated infra tests). This is used to disable deletion protection." + type = bool + default = false +} + +variable "name" { + type = string + description = "The name of the Cognito User Pool" +} + +variable "password_minimum_length" { + type = number + description = "The password minimum length" + default = 12 +} + +variable "reply_to_email" { + type = string + description = "Email address used as the REPLY-TO for identity service emails" + default = null +} + +variable "sender_display_name" { + type = string + description = "The display name for the identity service's emails. Only used if sender_email is provided" + default = null +} + +variable "sender_email" { + type = string + description = "Email address to use to send identity provider emails. If none is provided, the identity service will be configured to use Cognito's default email functionality, which should only be relied on outside of production." + default = null +} + +variable "temporary_password_validity_days" { + type = number + description = "The number of days a temporary password is valid for" + default = 7 +} + +variable "verification_email_message" { + type = string + description = "The email body for a password reset email. Must contain the {####} placeholder." + default = null +} + +variable "verification_email_subject" { + type = string + description = "The email subject for a password reset email" + default = null +}