Skip to content

Commit

Permalink
Make Cognito use SES for notifications (#817)
Browse files Browse the repository at this point in the history
## Ticket

Relates to #690

## Changes

Fixes two bugs that were causing notifications not to work:

- updated the terraform AWS provider version
- swapped `local.dash_domain` and `var.domain_name`

Changes the identity provide module to require SES, and then use SES for
email notifications:

Makes notifications rely on two variables being defined for a given
environment: `hosted_zone` and `domain_name`

Moves the Congito IDP config out of `infra/app/service/main.tf` and into
its own file

## Context for reviewers

Despite the smallish diff, this took dang forever

## Testing

Tested with notifications both on and off via
navapbc/pfml-starter-kit-app#237

---------

Co-authored-by: Loren Yu <[email protected]>
  • Loading branch information
coilysiren and lorenyu authored Jan 2, 2025
1 parent 1c7b9c2 commit 3f079aa
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 72 deletions.
6 changes: 6 additions & 0 deletions infra/app/app-config/env-config/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ locals {
prefix = terraform.workspace == "default" ? "" : "${terraform.workspace}-"

bucket_name = "${local.prefix}${var.project_name}-${var.app_name}-${var.environment}"

network_config = module.project_config.network_configs[var.network_name]
}

module "project_config" {
source = "../../../project-config"
}
2 changes: 1 addition & 1 deletion infra/app/app-config/env-config/notifications.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Notifications configuration
locals {
notifications_config = var.enable_notifications ? {
notifications_config = var.enable_notifications && var.domain_name != null && local.network_config.domain_config.hosted_zone != null ? {
# Pinpoint app name.
name = "${var.app_name}-${var.environment}"

Expand Down
9 changes: 8 additions & 1 deletion infra/app/app-config/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ locals {
# 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
# Whether or not the application should deploy a notification service.
#
# To use this in a particular environment, domain_name must also be set.
# The domain name is set in infra/<APP_NAME>/app-config/<ENVIRONMENT>.tf
# The domain name is the same domain as, or a subdomain of, the hosted zone in that environment.
# The hosted zone is set in infra/project-config/networks.tf
# If either (domain name or hosted zone) is not set in an environment, notifications will not actually be enabled.
#
# If enabled:
# 1. Creates an AWS Pinpoint application
# 2. Configures email notifications using AWS SES
Expand Down
53 changes: 53 additions & 0 deletions infra/app/service/identity_provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
locals {
# If this is a temporary environment, re-use an existing Cognito user pool. Otherwise, create a new one.
identity_provider_user_pool_id = module.app_config.enable_identity_provider ? (
local.is_temporary ? module.existing_identity_provider[0].user_pool_id : module.identity_provider[0].user_pool_id
) : null
identity_provider_environment_variables = module.app_config.enable_identity_provider ? {
COGNITO_USER_POOL_ID = local.identity_provider_user_pool_id,
COGNITO_CLIENT_ID = module.identity_provider_client[0].client_id
} : {}
}

# If the app has `enable_identity_provider` set to true AND this is not a temporary
# environment, then create a new identity provider.
module "identity_provider" {
count = module.app_config.enable_identity_provider && !local.is_temporary ? 1 : 0
source = "../../modules/identity-provider/resources"

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
domain_name = local.network_config.domain_config.hosted_zone
domain_identity_arn = local.notifications_config == null ? null : local.domain_identity_arn
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
}

# If the app has `enable_identity_provider` set to true AND this *is* a temporary
# environment, then use an existing identity provider.
module "existing_identity_provider" {
count = module.app_config.enable_identity_provider && local.is_temporary ? 1 : 0
source = "../../modules/identity-provider/data"

name = local.identity_provider_config.identity_provider_name
}

# If the app has `enable_identity_provider` set to true, create a new identity provider
# client for the service. A new client is created for all environments, including
# temporary environments.
module "identity_provider_client" {
count = module.app_config.enable_identity_provider ? 1 : 0
source = "../../modules/identity-provider-client/resources"

callback_urls = local.identity_provider_config.client.callback_urls
logout_urls = local.identity_provider_config.client.logout_urls
name = "${local.prefix}${local.identity_provider_config.identity_provider_name}"

user_pool_id = local.identity_provider_user_pool_id
}
55 changes: 1 addition & 54 deletions infra/app/service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,6 @@ locals {
notifications_config = local.environment_config.notifications_config

network_config = module.project_config.network_configs[local.environment_config.network_name]

# Identity provider locals.
# If this is a temporary environment, re-use an existing Cognito user pool.
# Otherwise, create a new one.
identity_provider_user_pool_id = module.app_config.enable_identity_provider ? (
local.is_temporary ? module.existing_identity_provider[0].user_pool_id : module.identity_provider[0].user_pool_id
) : null
identity_provider_environment_variables = module.app_config.enable_identity_provider ? {
COGNITO_USER_POOL_ID = local.identity_provider_user_pool_id,
COGNITO_CLIENT_ID = module.identity_provider_client[0].client_id
} : {}
}

terraform {
Expand All @@ -69,7 +58,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.35.0, < 6.0.0"
version = ">= 5.81.0, < 6.0.0"
}
}

Expand Down Expand Up @@ -225,45 +214,3 @@ module "storage" {
name = local.storage_config.bucket_name
is_temporary = local.is_temporary
}

# If the app has `enable_identity_provider` set to true AND this is not a temporary
# environment, then create a new identity provider.
module "identity_provider" {
count = module.app_config.enable_identity_provider && !local.is_temporary ? 1 : 0
source = "../../modules/identity-provider/resources"

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
}

# If the app has `enable_identity_provider` set to true AND this *is* a temporary
# environment, then use an existing identity provider.
module "existing_identity_provider" {
count = module.app_config.enable_identity_provider && local.is_temporary ? 1 : 0
source = "../../modules/identity-provider/data"

name = local.identity_provider_config.identity_provider_name
}

# If the app has `enable_identity_provider` set to true, create a new identity provider
# client for the service. A new client is created for all environments, including
# temporary environments.
module "identity_provider_client" {
count = module.app_config.enable_identity_provider ? 1 : 0
source = "../../modules/identity-provider-client/resources"

callback_urls = local.identity_provider_config.client.callback_urls
logout_urls = local.identity_provider_config.client.logout_urls
name = "${local.prefix}${local.identity_provider_config.identity_provider_name}"

user_pool_id = local.identity_provider_user_pool_id
}
16 changes: 8 additions & 8 deletions infra/app/service/notifications.tf
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
locals {
# If this is a temporary environment, re-use an existing email identity. Otherwise, create a new one.
domain_identity_arn = module.app_config.enable_notifications ? (
domain_identity_arn = local.notifications_config != null ? (
!local.is_temporary ?
module.notifications_email_domain[0].domain_identity_arn :
module.existing_notifications_email_domain[0].domain_identity_arn
) : null
notifications_environment_variables = module.app_config.enable_notifications ? {
notifications_environment_variables = local.notifications_config != null ? {
AWS_PINPOINT_APP_ID = module.notifications[0].app_id,
AWS_PINPOINT_SENDER_EMAIL = local.notifications_config.sender_email
} : {}
notifications_app_name = module.app_config.enable_notifications ? "${local.prefix}${local.notifications_config.name}" : ""
notifications_app_name = local.notifications_config != null ? "${local.prefix}${local.notifications_config.name}" : ""
}

# If the app has `enable_notifications` set to true AND this is not a temporary
# environment, then create a email notification identity.
module "notifications_email_domain" {
count = module.app_config.enable_notifications && !local.is_temporary ? 1 : 0
count = local.notifications_config != null && !local.is_temporary ? 1 : 0
source = "../../modules/notifications-email-domain/resources"

domain_name = local.service_config.domain_name
domain_name = local.network_config.domain_config.hosted_zone
}

# If the app has `enable_notifications` set to true AND this *is* a temporary
# environment, then create a email notification identity.
module "existing_notifications_email_domain" {
count = module.app_config.enable_notifications && local.is_temporary ? 1 : 0
count = local.notifications_config != null && local.is_temporary ? 1 : 0
source = "../../modules/notifications-email-domain/data"

domain_name = local.service_config.domain_name
domain_name = local.network_config.domain_config.hosted_zone
}

# If the app has `enable_notifications` set to true, create a new email notification
# AWS Pinpoint app for the service. A new app is created for all environments, including
# temporary environments.
module "notifications" {
count = module.app_config.enable_notifications ? 1 : 0
count = local.notifications_config != null ? 1 : 0
source = "../../modules/notifications/resources"

name = local.notifications_app_name
Expand Down
11 changes: 8 additions & 3 deletions infra/modules/identity-provider/resources/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
## - Configures MFA
############################################################################################

locals {
dash_domain = var.domain_name != null ? replace(var.domain_name, ".", "-") : null
}

resource "aws_cognito_user_pool" "main" {
name = var.name

Expand All @@ -29,11 +33,12 @@ resource "aws_cognito_user_pool" "main" {
# 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.email_identity_arn
email_sending_account = var.email_identity_arn != null ? "DEVELOPER" : "COGNITO_DEFAULT"
source_arn = var.domain_identity_arn
configuration_set = local.dash_domain
email_sending_account = var.domain_identity_arn != 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.email_identity_arn != null ? (var.sender_display_name != null ? "${var.sender_display_name} <${var.sender_email}>" : var.sender_email) : null
from_email_address = var.domain_identity_arn != 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
}

Expand Down
10 changes: 8 additions & 2 deletions infra/modules/identity-provider/resources/variables.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
variable "email_identity_arn" {
variable "domain_name" {
description = "The domain name to configure SES"
type = string
default = null
}

variable "domain_identity_arn" {
description = "The ARN of the domain identity to in use for SES"
type = string
description = "The arn of the SESv2 email identity to use to send emails"
default = null
}

Expand Down
7 changes: 4 additions & 3 deletions infra/modules/notifications-email-domain/resources/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ locals {
# Verify email sender identity.
# Docs: https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-email-manage-verify.html
resource "aws_sesv2_email_identity" "sender_domain" {
email_identity = local.dash_domain
email_identity = var.domain_name
configuration_set_name = aws_sesv2_configuration_set.email.configuration_set_name
}

# The configuration set applied to messages that is sent through this email channel.
resource "aws_sesv2_configuration_set" "email" {
configuration_set_name = var.domain_name
configuration_set_name = local.dash_domain

delivery_options {
tls_policy = "REQUIRE"
max_delivery_seconds = 300
tls_policy = "REQUIRE"
}

reputation_options {
Expand Down

0 comments on commit 3f079aa

Please sign in to comment.