carlos / platform Public

region-host: validate console_mail_* inputs and add tests

- Add validation block to console_mail_from: must be lowercase email or empty
- Add precondition to host_console_mail policy: region required when From is set
- Add plan tests (console_mail.tftest.hcl): defaults, valid config, missing region, uppercase From
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 1402a82bf356b2096a8967059842e7669aaeee99 parent b82c428
3 files changed, +115 −0
  • infra/modules/region-host/main.tf +6 −0
  • infra/modules/region-host/tests/console_mail.tftest.hcl +105 −0
  • infra/modules/region-host/variables.tf +4 −0
diff --git a/infra/modules/region-host/main.tf b/infra/modules/region-host/main.tf
index f2d159c83..b35ecc196 100644
--- a/infra/modules/region-host/main.tf
+++ b/infra/modules/region-host/main.tf
@@ -718,6 +718,12 @@ resource "aws_iam_role_policy" "host_console_mail" {
}
}]
})
+ lifecycle {
+ precondition {
+ condition = var.console_mail_region != ""
+ error_message = "console_mail_region must be set when console_mail_from is (it must equal the console's CARLOS_MAIL_REGION)."
+ }
+ }
}
# ---- object storage scanning (opt-in) --------------------------------------
diff --git a/infra/modules/region-host/tests/console_mail.tftest.hcl b/infra/modules/region-host/tests/console_mail.tftest.hcl
new file mode 100644
index 000000000..ca7f257ba
--- /dev/null
+++ b/infra/modules/region-host/tests/console_mail.tftest.hcl
@@ -0,0 +1,105 @@
+# Plan-time tests for the console mail grant. mock_provider means no AWS account
+# and no credentials: these assert what the module WOULD create.
+mock_provider "aws" {}
+
+override_data {
+ target = data.aws_vpcs.default
+ values = { ids = ["vpc-0aaaaaaaaaaaaaaaa"] }
+}
+
+override_data {
+ target = data.aws_caller_identity.current
+ values = { account_id = "123456789012" }
+}
+
+# The termination queue's ARN feeds an EventBridge target, which the provider
+# validates as an ARN; the mock's random string is not one.
+override_resource {
+ target = aws_sqs_queue.terminations
+ values = { arn = "arn:aws:sqs:eu-west-1:123456789012:carlos-test-host-terminations" }
+}
+
+variables {
+ name = "carlos-test-ie"
+ sg_description = "carlos test host"
+ iam_name = "carlos-test-host"
+ instance_type = "t4g.small"
+ root_volume_size = 20
+ user_data = "#!/bin/sh\n"
+ deployment_bucket_arn = "arn:aws:s3:::carlos-test-platform"
+ data_bucket_arn = "arn:aws:s3:::carlos-test-ie"
+ ssm_parameter_arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/carlos/*"
+}
+
+run "off_by_default" {
+ command = plan
+ assert {
+ condition = length(aws_iam_role_policy.host_console_mail) == 0
+ error_message = "console_mail_from empty must create no policy"
+ }
+}
+
+run "from_and_region_set_creates_policy" {
+ command = plan
+ variables {
+ console_mail_from = "noreply@console.bab.example.com"
+ console_mail_region = "eu-west-1"
+ }
+ assert {
+ condition = length(aws_iam_role_policy.host_console_mail) == 1
+ error_message = "From + region set must create one policy"
+ }
+ assert {
+ condition = aws_iam_role_policy.host_console_mail[0].name == "carlos-test-host-console-mail"
+ error_message = "the policy is named <iam_name>-console-mail"
+ }
+ assert {
+ condition = toset(jsondecode(aws_iam_role_policy.host_console_mail[0].policy).Statement[0].Action) == toset([
+ "ses:SendEmail",
+ ])
+ error_message = "the only action is ses:SendEmail"
+ }
+ assert {
+ condition = contains(
+ jsondecode(aws_iam_role_policy.host_console_mail[0].policy).Statement[0].Resource,
+ "arn:aws:ses:eu-west-1:123456789012:identity/console.bab.example.com"
+ )
+ error_message = "must contain identity ARN with domain extracted from From address"
+ }
+ assert {
+ condition = contains(
+ jsondecode(aws_iam_role_policy.host_console_mail[0].policy).Statement[0].Resource,
+ "arn:aws:ses:eu-west-1:123456789012:configuration-set/carlos-bab"
+ )
+ error_message = "must contain configuration-set/carlos-bab ARN"
+ }
+ assert {
+ condition = contains(
+ jsondecode(aws_iam_role_policy.host_console_mail[0].policy).Statement[0].Resource,
+ "arn:aws:ses:eu-west-1:123456789012:tenant/carlos-bab/*"
+ )
+ error_message = "must contain tenant/carlos-bab/* ARN"
+ }
+ assert {
+ condition = jsondecode(aws_iam_role_policy.host_console_mail[0].policy).Statement[0].Condition.StringEquals["ses:FromAddress"] == "noreply@console.bab.example.com"
+ error_message = "must have ses:FromAddress condition matching the From address"
+ }
+}
+
+run "from_set_without_region_fails_precondition" {
+ command = plan
+ variables {
+ console_mail_from = "noreply@console.bab.example.com"
+ console_mail_region = ""
+ }
+ expect_failures = [aws_iam_role_policy.host_console_mail]
+}
+
+run "uppercase_from_fails_validation" {
+ command = plan
+ variables {
+ console_mail_from = "noreply@console.bab.EXAMPLE.COM"
+ console_mail_region = "eu-west-1"
+ }
+ expect_failures = [var.console_mail_from]
+}
diff --git a/infra/modules/region-host/variables.tf b/infra/modules/region-host/variables.tf
index ae1e4258e..2af1f1651 100644
--- a/infra/modules/region-host/variables.tf
+++ b/infra/modules/region-host/variables.tf
@@ -222,6 +222,10 @@ variable "console_mail_from" {
description = "The console's own From address (CARLOS_CONSOLE_MAIL_FROM, or noreply@console.bab.<platform suffix>). Empty: the host role gets no ses:SendEmail and the console sends no email of its own."
type = string
default = ""
+ validation {
+ condition = var.console_mail_from == "" || can(regex("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$", var.console_mail_from))
+ error_message = "console_mail_from must be one lowercase email address, the exact From the console sends (CARLOS_CONSOLE_MAIL_FROM, or noreply@console.bab.<platform suffix>)."
+ }
}
variable "console_mail_region" {