Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email Notifications [mimics main branch] #140

Closed
wants to merge 106 commits into from
Closed
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
ab8e5a4
Bring over WIP from template-infra branch
rocketnova Aug 7, 2024
9ab6473
Enable notifications
rocketnova Aug 7, 2024
e772c1e
Restrict access
rocketnova Aug 8, 2024
18ee918
Conditionally return values based on verification method
rocketnova Aug 8, 2024
d62b062
Deal with pinpoint_email_channel role_arn
rocketnova Aug 8, 2024
391b603
Separate a verified sender identity from a pinpoint app
rocketnova Aug 8, 2024
9a607bb
Refactor outputs and variables
rocketnova Aug 8, 2024
0c417ad
Add code for aws_sesv2_email_identity_policy
rocketnova Aug 8, 2024
179b385
Drop workspace prefix for notifications config
rocketnova Aug 8, 2024
79f1bb9
Connect notifications and email-identity modules with app-config and …
rocketnova Aug 9, 2024
c07cb33
Rework idp to remove dependency on data output which would otherwise …
rocketnova Aug 9, 2024
11eee11
Add test email to satisfy tests
rocketnova Aug 9, 2024
a3e203b
Fix IAM role name collision
rocketnova Aug 12, 2024
c649240
Grant Github Actions auth for notifications: ses & mobiletargeting
rocketnova Aug 12, 2024
5dbcda6
Remove deprecated pinpoint identity role
rocketnova Aug 12, 2024
f0a9759
Move email identity into resources subdir
rocketnova Aug 12, 2024
7330a31
Refactor to support re-using existing email identities
rocketnova Aug 12, 2024
d7b5e35
Fix depends_on syntax
rocketnova Aug 12, 2024
3a80dbf
Simplify
rocketnova Aug 14, 2024
f6a57ed
Simplify and use domain name
rocketnova Aug 14, 2024
06bae3a
Auto-add dns records for domain verification
rocketnova Aug 14, 2024
9a53c2c
Merge branch 'main' into rocket/notifications
coilysiren Oct 4, 2024
1e24692
diff reduction
coilysiren Oct 4, 2024
64268b1
spacing and docs
coilysiren Oct 4, 2024
46dd4cd
use jsonencode
coilysiren Oct 4, 2024
0e4eb4c
module path
coilysiren Oct 4, 2024
a3834ee
prefix
coilysiren Oct 4, 2024
2b7bda9
try to fix verification records
coilysiren Oct 5, 2024
d2c2bf3
try using count
coilysiren Oct 7, 2024
33e654d
use route53 config from truss
coilysiren Oct 7, 2024
e3bbbdd
fix vars
coilysiren Oct 7, 2024
044b13c
trigger ci
coilysiren Oct 7, 2024
c38a5ec
fix regex
coilysiren Oct 11, 2024
bb99fa7
use 'mail' subdomain
coilysiren Oct 11, 2024
939f6fd
try subdomain again
coilysiren Oct 11, 2024
ca0a0d4
try subdomain again again
coilysiren Oct 11, 2024
a9e50da
revert to the original stuff
coilysiren Oct 11, 2024
bda7066
strip last
coilysiren Oct 11, 2024
09b6f08
fix incorrect var
coilysiren Oct 11, 2024
bd3f474
add back TLD
coilysiren Oct 12, 2024
11b8712
mail dot
coilysiren Oct 12, 2024
c45728a
mail_from_domain
coilysiren Oct 12, 2024
9c967c9
outbound subdomain
coilysiren Oct 12, 2024
f52cdf5
try again
coilysiren Oct 12, 2024
ab4da73
use trussworks patterns
coilysiren Oct 12, 2024
60edf04
update vars
coilysiren Oct 12, 2024
359f8ef
pass in seperate domain names
coilysiren Oct 12, 2024
c487417
workon mail from domain
coilysiren Oct 12, 2024
802536f
update vars
coilysiren Oct 12, 2024
5580c74
typo
coilysiren Oct 12, 2024
a7df4f9
route53 is on the wrong domain
coilysiren Oct 12, 2024
57bd7c7
small TXT change
coilysiren Oct 12, 2024
e688f90
move folders, remove some complexity around PR envs
coilysiren Oct 19, 2024
bea59b1
use existing zone
coilysiren Oct 19, 2024
3ab84b2
fix validation errors
coilysiren Oct 19, 2024
6de293c
dont double up on notes services
coilysiren Oct 19, 2024
5e212a0
fix conditional
coilysiren Oct 19, 2024
27fda90
Merge branch 'main' into kai/notifications
coilysiren Oct 25, 2024
6557098
more files means less merge conflicts
coilysiren Oct 25, 2024
23e653b
move module around
coilysiren Oct 25, 2024
e667aee
setup notifications client
coilysiren Oct 26, 2024
d5aedaa
add vars to service
coilysiren Oct 26, 2024
cc8e679
swap to real versions
coilysiren Oct 26, 2024
4fdb991
update output
coilysiren Oct 26, 2024
1f00ad9
remove errant module
coilysiren Oct 26, 2024
5add212
Revert "remove errant module"
coilysiren Oct 26, 2024
7f2ebe4
remove correct stuff
coilysiren Oct 26, 2024
f36741a
Merge remote-tracking branch 'origin' into kai/notifications-2
coilysiren Nov 1, 2024
2366ca9
Merge remote-tracking branch 'origin' into kai/notifications
coilysiren Nov 1, 2024
d8ecbfe
Revert "swap to real versions"
coilysiren Nov 1, 2024
3df9175
name based on PR apps
coilysiren Nov 1, 2024
7ba2cdb
Merge branch 'kai/notifications' into kai/notifications-2
coilysiren Nov 1, 2024
67710e8
Revert "Revert "swap to real versions""
coilysiren Nov 1, 2024
2483ed3
string concat
coilysiren Nov 1, 2024
42f781d
string concat
coilysiren Nov 1, 2024
bd6e835
auto code review
coilysiren Nov 1, 2024
75e7bc4
typo
coilysiren Nov 1, 2024
31ff496
narrow env var scope
coilysiren Nov 8, 2024
d421603
domain_identity_arn
coilysiren Nov 8, 2024
52643a5
cleanup some vars
coilysiren Nov 8, 2024
f2d43bf
validation + test
coilysiren Nov 8, 2024
a6d6d83
Revert "validation + test"
coilysiren Nov 8, 2024
d8b89e2
use domain name as name
coilysiren Nov 8, 2024
8a10e95
remove some name
coilysiren Nov 8, 2024
7081ed2
comment
coilysiren Nov 8, 2024
d1218bb
move stuff around
coilysiren Nov 8, 2024
98d8036
ignore a bunch of stuff
coilysiren Nov 8, 2024
e3379d2
fix domain vars
coilysiren Nov 8, 2024
8a72123
testing
coilysiren Nov 8, 2024
592a188
pull from correct var hopefully
coilysiren Nov 8, 2024
09a192d
use default email config, instead of explicit one
coilysiren Nov 8, 2024
9794f45
various renames
coilysiren Nov 14, 2024
1fa2229
revert idp stuff
coilysiren Nov 14, 2024
5910294
delete app first
coilysiren Nov 14, 2024
31eb7b2
delete it please, thanks
coilysiren Nov 14, 2024
fcfa6f4
update some var refs
coilysiren Nov 14, 2024
b8cd1fe
put everything back
coilysiren Nov 14, 2024
103f386
update vars
coilysiren Nov 14, 2024
fe2678d
reorg
coilysiren Nov 14, 2024
e1c6c2f
a var name
coilysiren Nov 14, 2024
e46c6b7
Merge branch 'kai/notifications-2' into kai/notifications
coilysiren Nov 15, 2024
cd3088a
typo
coilysiren Nov 15, 2024
545c659
bool
coilysiren Nov 15, 2024
b155ab2
testing env
coilysiren Nov 15, 2024
0584221
fix ifs
coilysiren Nov 15, 2024
e820044
fix domain input
coilysiren Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions infra/app/app-config/dev.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module "dev_config" {
enable_https = true
has_database = local.has_database
has_incident_management_service = local.has_incident_management_service
enable_notifications = local.enable_notifications

# Enable and configure identity provider.
enable_identity_provider = local.enable_identity_provider
Expand Down
25 changes: 20 additions & 5 deletions infra/app/app-config/env-config/notifications.tf
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
# 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
# Pinpoint app name.
name = "${var.app_name}-${var.environment}"

# The method to use to verify the sender email address.
# - Must be 'email' or 'domain'.
# - If method is 'email', AWS will send you an email with a one-time link. Click on
# the link to verify the email address.
# - If method is 'domain', then custom domains are required and the domain used will
# match the one set in the env-config. See /docs/infra/set-up-custom-domains.md
# Docs: https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-email-manage-verify.html
email_verification_method = "domain"

# 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
sender_display_name = "coilysiren"

# Set to the email address to be used when sending emails.
# - If enable_notifications is true, this is required.
# - If email_verification_method is set to 'domain', make sure the domain name of the
# sender_email matches the domain provided in the env-config.
# Docs: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html
sender_email = "[email protected]"
Copy link
Collaborator

@lorenyu lorenyu Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like the email should end in @platform-test-dev.navapbc.com

  • If email_verification_method is set to 'domain', make sure the domain name of the
    sender_email matches the domain provided in the env-config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah


# 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
reply_to_email = "[email protected]"
} : null
}
7 changes: 4 additions & 3 deletions infra/app/app-config/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ locals {
enable_identity_provider = true

# 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
# If enabled:
# 1. Creates an AWS Pinpoint application
# 2. Configures email notifications using AWS SES
enable_notifications = true

environment_configs = {
dev = module.dev_config
Expand Down
4 changes: 4 additions & 0 deletions infra/app/app-config/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ output "enable_identity_provider" {
value = local.enable_identity_provider
}

output "enable_notifications" {
value = local.enable_notifications
}

output "shared_network_name" {
value = local.shared_network_name
}
1 change: 1 addition & 0 deletions infra/app/app-config/prod.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module "prod_config" {
has_database = local.has_database
has_incident_management_service = local.has_incident_management_service
enable_identity_provider = local.enable_identity_provider
enable_notifications = local.enable_notifications

# These numbers are a starting point based on this article
# Update the desired instance size and counts based on the project's specific needs
Expand Down
1 change: 1 addition & 0 deletions infra/app/app-config/staging.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module "staging_config" {
has_database = local.has_database
has_incident_management_service = local.has_incident_management_service
enable_identity_provider = local.enable_identity_provider
enable_notifications = local.enable_notifications

# Enables ECS Exec access for debugging or jump access.
# See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
Expand Down
56 changes: 53 additions & 3 deletions infra/app/service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ locals {

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

# Notifications locals.
#
# 1. If notifications are enabled and the verification method is 'email', then we
# extract the domain_name from the sender_email. For example:
# example.com from sender_email: [email protected]
#
# 2. If notifications are enabled and the verification method is 'domain', then we
# construct the domain_name using the format: <terraform workspace><environment.<domain>
# For example: t-123456-dev.bar.com

mail_from_domain = "mail.${local.notifications_sender_email_domain_name}"

notifications_sender_email_domain_name = module.app_config.enable_notifications ? (
local.notifications_config.email_verification_method == "email" ?
regex("@(.*)", local.notifications_config.sender_email)[0] :
"${local.prefix}${var.environment_name}.${local.service_config.domain_name}"
) : null

notifications_sender_email = module.app_config.enable_notifications ? (
local.notifications_config.email_verification_method == "email" ?
local.notifications_config.sender_email :
"${regex("(.*)@", local.notifications_config.sender_email)[0]}@${local.notifications_sender_email_domain_name}"
) : null

# Identity provider locals.
# If this is a temporary environment, re-use an existing Cognito user pool.
# Otherwise, create a new one.
Expand Down Expand Up @@ -233,6 +257,29 @@ module "storage" {
is_temporary = local.is_temporary
}

module "email_identity" {
count = module.app_config.enable_notifications ? 1 : 0
source = "../../modules/email-identity"

email_verification_method = local.notifications_config.email_verification_method
name = local.notifications_config.name
sender_email = local.notifications_sender_email
mail_from_domain = local.mail_from_domain
domain_name = local.notifications_sender_email_domain_name
}

module "notifications" {
count = module.app_config.enable_notifications ? 1 : 0
source = "../../modules/notifications"

email_configuration_set_name = module.email_identity[0].email_configuration_set_name
email_identity_arn = module.email_identity[0].email_identity_arn

name = "${local.prefix}${local.notifications_config.name}"
sender_display_name = local.notifications_config.sender_display_name
sender_email = local.notifications_sender_email
}

# 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" {
Expand All @@ -247,9 +294,12 @@ module "identity_provider" {
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
sender_email = module.app_config.enable_notifications ? local.notifications_sender_email : null
sender_display_name = module.app_config.enable_notifications ? local.notifications_config.sender_display_name : null
reply_to_email = module.app_config.enable_notifications ? local.notifications_config.reply_to_email : null

# This requires an email identity that has been verified for sending.
email_identity_arn = module.app_config.enable_notifications ? module.email_identity[0].verified_email_identity_arn : null
}

# If the app has `enable_identity_provider` set to true AND this *is* a temporary
Expand Down
23 changes: 23 additions & 0 deletions infra/modules/email-identity/logs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Configures AWS SES to send additional logging to AWS Cloudwatch.
# See https://docs.aws.amazon.com/ses/latest/dg/event-destinations-manage.html
resource "aws_ses_event_destination" "logs" {
name = "${var.name}-email-identity-logs"
configuration_set_name = aws_sesv2_configuration_set.email.configuration_set_name
enabled = true
matching_types = [
"bounce",
"click",
"complaint",
"delivery",
"open",
"reject",
"renderingFailure",
"send"
]

cloudwatch_destination {
dimension_name = "email_type"
default_value = "other"
value_source = "messageTag"
}
}
131 changes: 131 additions & 0 deletions infra/modules/email-identity/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# This module manages an SESv2 email identity.
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

locals {
stripped_domain_name = replace(var.domain_name, "/[.]$/", "")
stripped_mail_from_domain = replace(var.mail_from_domain, "/[.]$/", "")
dash_domain = replace(var.domain_name, ".", "-")
}

# Verify email sender identity.
# Docs: https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-email-manage-verify.html
resource "aws_sesv2_email_identity" "sender" {
email_identity = var.email_verification_method == "email" ? var.sender_email : local.stripped_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.name

delivery_options {
tls_policy = "REQUIRE"
}

reputation_options {
reputation_metrics_enabled = true
}

sending_options {
sending_enabled = true
}

suppression_options {
suppressed_reasons = ["BOUNCE", "COMPLAINT"]
}
}

# Allow AWS Pinpoint to send email on behalf of this email identity.
# Docs: https://docs.aws.amazon.com/pinpoint/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_resource-based-policy-examples-access-ses-identities
resource "aws_sesv2_email_identity_policy" "sender" {
email_identity = aws_sesv2_email_identity.sender.email_identity
policy_name = "PinpointEmail"

policy = jsonencode(
{
Version = "2008-10-17",
Statement = [
{
Sid = "PinpointEmail",
Effect = "Allow",
Principal = {
Service = "pinpoint.amazonaws.com"
},
Action = "ses:*",
Resource = "${aws_sesv2_email_identity.sender.arn}",
Condition = {
StringEquals = {
"aws:SourceAccount" = "${data.aws_caller_identity.current.account_id}"
},
StringLike = {
"aws:SourceArn" = "arn:aws:mobiletargeting:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:apps/*"
}
}
}
]
}
)
}

# If email_verification_method is "domain", create a Route53 hosted zone for the sending
# domain.
resource "aws_route53_zone" "zone" {
count = var.email_verification_method == "domain" ? 1 : 0
name = var.domain_name
# checkov:skip=CKV2_AWS_38:TODO(https://github.com/navapbc/template-infra/issues/560) enable DNSSEC
}

resource "aws_sesv2_email_identity_mail_from_attributes" "sender" {
email_identity = aws_sesv2_email_identity.sender.email_identity
mail_from_domain = local.stripped_mail_from_domain

depends_on = [aws_sesv2_email_identity.sender]
}

# DNS records for email identity verification if email_verification_method is "domain"
resource "aws_route53_record" "dkim" {
count = var.email_verification_method == "domain" ? 3 : 0

allow_overwrite = true
ttl = 60
type = "CNAME"
zone_id = aws_route53_zone.zone[0].zone_id
name = "${aws_sesv2_email_identity.sender.dkim_signing_attributes[0].tokens[count.index]}._domainkey"
records = ["${aws_sesv2_email_identity.sender.dkim_signing_attributes[0].tokens[count.index]}.dkim.amazonses.com"]

depends_on = [aws_sesv2_email_identity.sender]
}

resource "aws_route53_record" "spf_mail_from" {
count = var.email_verification_method == "domain" ? 1 : 0

allow_overwrite = true
ttl = "600"
type = "TXT"
zone_id = aws_route53_zone.zone[0].zone_id
name = aws_sesv2_email_identity_mail_from_attributes.sender.mail_from_domain
records = ["v=spf1 include:amazonses.com ~all"]
}

resource "aws_route53_record" "mx_send_mail_from" {
count = var.email_verification_method == "domain" ? 1 : 0

allow_overwrite = true
type = "MX"
ttl = "600"
zone_id = aws_route53_zone.zone[0].zone_id
name = aws_sesv2_email_identity_mail_from_attributes.sender.mail_from_domain
records = ["10 feedback-smtp.${data.aws_region.current.name}.amazonses.com"]
}

resource "aws_route53_record" "mx_receive" {
count = var.email_verification_method == "domain" ? 1 : 0

allow_overwrite = true
type = "MX"
ttl = "600"
name = var.mail_from_domain
zone_id = aws_route53_zone.zone[0].zone_id
records = ["10 inbound-smtp.${data.aws_region.current.name}.amazonaws.com"]
}
11 changes: 11 additions & 0 deletions infra/modules/email-identity/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
output "email_configuration_set_name" {
value = aws_sesv2_configuration_set.email.configuration_set_name
}

output "email_identity_arn" {
value = aws_sesv2_email_identity.sender.arn
}

output "verified_email_identity_arn" {
value = aws_sesv2_email_identity.sender.verified_for_sending_status ? aws_sesv2_email_identity.sender.arn : null
}
29 changes: 29 additions & 0 deletions infra/modules/email-identity/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
variable "email_verification_method" {
type = string
description = "The method to use to verify the sender email address"
default = "email"
validation {
condition = can(regex("^(email|domain)$", var.email_verification_method))
error_message = "email_verification_method must be either 'email' or 'domain'"
}
}

variable "name" {
type = string
description = "Name of the notifications project/application"
}

variable "sender_email" {
type = string
description = "Email address to use to send notification emails"
}

variable "domain_name" {
description = "The domain name to configure SES."
type = string
}

variable "mail_from_domain" {
type = string
description = "Subdomain (of the route53 zone) which is to be used as MAIL FROM address"
}
13 changes: 4 additions & 9 deletions infra/modules/identity-provider/resources/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
## - 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

Expand All @@ -34,12 +29,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.sender_email != null ? data.aws_ses_email_identity.sender[0].arn : null
email_sending_account = var.sender_email != null ? "DEVELOPER" : "COGNITO_DEFAULT"
source_arn = var.email_identity_arn
email_sending_account = var.email_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.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
from_email_address = var.email_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
}

password_policy {
Expand Down
6 changes: 6 additions & 0 deletions infra/modules/identity-provider/resources/variables.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
variable "email_identity_arn" {
type = string
description = "The arn of the SESv2 email identity to use to send emails"
default = null
}

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
Expand Down
Loading
Loading