diff --git a/README.md b/README.md index f175e19..7d1c770 100755 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ located within its directory. ## Features -- Deploy a [custom SMS sender Lambda function for AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sms-sender.html) -- Customizable Open Policy Agent (OPA) policy to filter and throttle SMS sending -- Ability to dynamically use SMS sender ID and short code +- [Custom SMS sender Lambda function for AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sms-sender.html) + - Customizable (OPA) policy to filter and throttle SMS sending + - Ability to dynamically use SMS sender ID and short code +- [Custom Email sender Lambda function for AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-email-sender.html) + - Dyanmic decided which SES template to use based on context of request. ## Usage @@ -25,6 +27,9 @@ module "cognito_custom_sms_sender" { source = "cruxstack/cognito-custom-message-sender/aws" version = "x.x.x" + email_sender_enabled = true + email_sender_policy_content = "" + sms_sender_enabled = true sms_sender_policy_content = "" sms_sender_throttle_period_in_minutes = 15 @@ -42,6 +47,8 @@ for more details on these variables. |-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:---------:|:--------:| | `kms_key_alias_prefix` | The prefix for the KMS key alias. It must start with 'alias' and only include alphanumeric characters, dashes, underscores, colons or slashes, but doesn't end with a slash. | `string` | `"alias"` | no | | `service_log_level` | The log level for the service. It must be one of 'debug', 'info', 'warn', 'error', 'panic' or 'fatal'. | `string` | `"info"` | no | +| `email_sender_enabled` | Whether or not the eamil sender is enabled. | `bool` | `false` | no | +| `email_sender_policy_content` | The content of the Open Policy Agent policy for email sender. It must include the string 'package cognito_custom_sender_email_policy'. | `string` | n/a | yes | | `sms_sender_enabled` | Whether or not the SMS sender is enabled. | `bool` | `false` | no | | `sms_sender_policy_content` | The content of the Open Policy Agent policy for SMS sender. It must include the string 'package cognito_custom_sender_sms_policy'. | `string` | n/a | yes | | `sms_sender_throttle_period_in_minutes` | The throttle period for the SMS sender, in minutes. It must be a positive integer. | `number` | `15` | no | diff --git a/assets/custom-email-sender b/assets/custom-email-sender new file mode 160000 index 0000000..09df8bd --- /dev/null +++ b/assets/custom-email-sender @@ -0,0 +1 @@ +Subproject commit 09df8bddc10f1727af96ecc4656cb276b701a2ce diff --git a/main.tf b/main.tf index 18014ad..ed88cc0 100755 --- a/main.tf +++ b/main.tf @@ -4,7 +4,9 @@ locals { aws_account_id = try(coalesce(var.aws_account_id, data.aws_caller_identity.current[0].account_id), "") aws_region_name = try(coalesce(var.aws_region_name, data.aws_region.current[0].name), "") - email_sender_enabled = false + email_sender_enabled = local.enabled && var.email_sender_enabled + email_sender_policy_path = "./policy.rego" + email_sender_policy_content = var.email_sender_policy_content sms_sender_enabled = local.enabled && var.sms_sender_enabled sms_sender_policy_path = "./policy.wasm" @@ -13,11 +15,11 @@ locals { } data "aws_caller_identity" "current" { - count = local.enabled && var.aws_account_id == "" ? 1 : 0 + count = local.enabled ? 1 : 0 } data "aws_region" "current" { - count = local.enabled && var.aws_region_name == "" ? 1 : 0 + count = local.enabled ? 1 : 0 } # ============================================================ message-sender === @@ -30,34 +32,161 @@ module "message_sender_label" { context = module.this.context } -module "message_sender_code" { +# ---------------------------------------------------------------------- kms --- + +module "kms_key" { + source = "cloudposse/kms-key/aws" + version = "0.12.2" + + alias = "${var.kms_key_alias_prefix}/${module.message_sender_label.name}" + deletion_window_in_days = 7 + + context = module.message_sender_label.context +} + +# ---------------------------------------------------------------------- iam --- + +resource "aws_iam_role" "this" { + count = local.enabled ? 1 : 0 + + name = module.message_sender_label.id + description = "" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow" + Principal = { "Service" : "lambda.amazonaws.com" } + Action = ["sts:AssumeRole", "sts:TagSession"] + }] + }) + + managed_policy_arns = [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + + inline_policy { + name = "message-sender-access" + policy = data.aws_iam_policy_document.this[0].json + } + + tags = module.message_sender_label.tags +} + +data "aws_iam_policy_document" "this" { + count = local.enabled ? 1 : 0 + + statement { + effect = "Allow" + actions = [ + "kms:Decrypt" + ] + resources = [ + module.kms_key.key_arn, + ] + } + + dynamic "statement" { + for_each = local.email_sender_enabled ? [true] : [] + content { + effect = "Allow" + actions = [ + "ses:GetTemplate", + "ses:SendEmail", + "ses:SendTemplatedEmail", + ] + resources = [ + "*" + ] + } + } + + dynamic "statement" { + for_each = local.sms_sender_enabled ? [true] : [] + content { + effect = "Allow" + actions = [ + "sns:Publish" + ] + resources = [ + "*" + ] + } + } + + dynamic "statement" { + for_each = local.sms_sender_enabled ? [true] : [] + content { + effect = "Allow" + actions = [ + "dynamodb:Scan", + "dynamodb:Query", + "dynamodb:*Item*", + ] + resources = [ + aws_dynamodb_table.sms_history[0].arn, + "${aws_dynamodb_table.sms_history[0].arn}/*" + ] + } + } + + depends_on = [ + module.kms_key + ] +} + +# =============================================================== sms-sender === + +resource "terraform_data" "sms_msg_sender_policy" { + count = local.enabled && local.sms_sender_enabled ? 1 : 0 + + input = base64encode(local.sms_sender_policy_content) + + lifecycle { + precondition { + condition = startswith(var.sms_sender_policy_content, "package cognito_custom_sender_sms_policy") + error_message = "The email sender policy content must include 'package cognito_custom_sender_sms_policy'." + } + } +} + +module "sms_msg_sender_label" { + source = "cloudposse/label/null" + version = "0.25.0" + + attributes = ["sms"] + context = module.message_sender_label.context +} + +module "sms_msg_sender_code" { source = "cruxstack/artifact-packager/docker" version = "1.3.6" + count = local.sms_sender_enabled ? 1 : 0 artifact_src_path = "/tmp/package.zip" docker_build_context = abspath("${path.module}/assets/custom-message-sender") docker_build_target = "package" docker_build_args = { - SERVICE_OPA_POLICY_ENCODED = base64encode(local.sms_sender_policy_content) + SERVICE_OPA_POLICY_ENCODED = terraform_data.sms_msg_sender_policy[0].output } - context = module.message_sender_label.context + context = module.sms_msg_sender_label.context } -resource "aws_cloudwatch_log_group" "message_sender" { - count = local.enabled ? 1 : 0 +resource "aws_cloudwatch_log_group" "sms_msg_sender" { + count = local.sms_sender_enabled ? 1 : 0 - name = "/aws/lambda/${module.message_sender_label.id}" + name = "/aws/lambda/${module.sms_msg_sender_label.id}" retention_in_days = 90 - tags = module.message_sender_label.tags + tags = module.sms_msg_sender_label.tags } -resource "aws_lambda_function" "message_sender" { - count = module.message_sender_label.enabled ? 1 : 0 +resource "aws_lambda_function" "sms_msg_sender" { + count = local.sms_sender_enabled ? 1 : 0 - function_name = module.message_sender_label.id - filename = module.message_sender_code.artifact_package_path + function_name = module.sms_msg_sender_label.id + filename = module.sms_msg_sender_code[0].artifact_package_path handler = "index.handler" runtime = "nodejs18.x" timeout = 45 @@ -72,7 +201,7 @@ resource "aws_lambda_function" "message_sender" { variables = { LOG_LEVEL = var.service_log_level KMS_KEY_ID = module.kms_key.key_arn - DDB_TABLE_HISTORY_NAME = aws_dynamodb_table.history[0].name + DDB_TABLE_HISTORY_NAME = aws_dynamodb_table.sms_history[0].name DDB_TABLE_HISTORY_TTL = 43200 EMAIL_SENDER_ENABLED = local.email_sender_enabled SMS_SENDER_ENABLED = local.sms_sender_enabled @@ -81,19 +210,19 @@ resource "aws_lambda_function" "message_sender" { } } - tags = module.message_sender_label.tags + tags = module.sms_msg_sender_label.tags depends_on = [ - module.message_sender_code, - aws_cloudwatch_log_group.message_sender, + module.sms_msg_sender_code, + aws_cloudwatch_log_group.sms_msg_sender, ] } -resource "aws_lambda_permission" "message_sender" { - count = module.message_sender_label.enabled ? 1 : 0 +resource "aws_lambda_permission" "sms_msg_sender" { + count = local.sms_sender_enabled ? 1 : 0 statement_id = "allow-cognito-trigger" - function_name = aws_lambda_function.message_sender[0].function_name + function_name = aws_lambda_function.sms_msg_sender[0].function_name action = "lambda:InvokeFunction" principal = "cognito-idp.amazonaws.com" source_arn = "arn:aws:cognito-idp:${local.aws_region_name}:${local.aws_account_id}:userpool/*" @@ -101,18 +230,18 @@ resource "aws_lambda_permission" "message_sender" { # ---------------------------------------------------------------------- ddb --- -module "history_label" { +module "sms_history_label" { source = "cloudposse/label/null" version = "0.25.0" - attributes = ["history"] - context = module.message_sender_label.context + attributes = ["sms-history"] + context = module.sms_msg_sender_label.context } -resource "aws_dynamodb_table" "history" { - count = module.message_sender_label.enabled ? 1 : 0 +resource "aws_dynamodb_table" "sms_history" { + count = local.sms_sender_enabled ? 1 : 0 - name = module.history_label.id + name = module.sms_history_label.id billing_mode = "PAY_PER_REQUEST" hash_key = "historyId" @@ -157,98 +286,94 @@ resource "aws_dynamodb_table" "history" { projection_type = "ALL" } - tags = module.history_label.tags + tags = module.sms_history_label.tags } -# ---------------------------------------------------------------------- kms --- +# ============================================================= email-sender === -module "kms_key" { - source = "cloudposse/kms-key/aws" - version = "0.12.1" +resource "terraform_data" "email_msg_sender_policy" { + count = local.enabled && local.email_sender_enabled ? 1 : 0 - alias = "${var.kms_key_alias_prefix}/${module.message_sender_label.name}" - deletion_window_in_days = 7 + input = base64encode(local.email_sender_policy_content) - context = module.message_sender_label.context + lifecycle { + precondition { + condition = startswith(var.email_sender_policy_content, "package cognito_custom_sender_email_policy") + error_message = "The email sender policy content must include 'package cognito_custom_sender_email_policy'." + } + } } -# ---------------------------------------------------------------------- iam --- - -resource "aws_iam_role" "this" { - count = local.enabled ? 1 : 0 +module "email_msg_sender_label" { + source = "cloudposse/label/null" + version = "0.25.0" - name = module.message_sender_label.id - description = "" + attributes = ["email"] + context = module.message_sender_label.context +} - assume_role_policy = jsonencode({ - Version = "2012-10-17", - Statement = [{ - Effect = "Allow" - Principal = { "Service" : "lambda.amazonaws.com" } - Action = ["sts:AssumeRole", "sts:TagSession"] - }] - }) +module "email_msg_sender_code" { + source = "cruxstack/artifact-packager/docker" + version = "1.3.6" + count = local.email_sender_enabled ? 1 : 0 - managed_policy_arns = [ - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] + artifact_src_path = "/tmp/package.zip" + docker_build_context = abspath("${path.module}/assets/custom-email-sender") + docker_build_target = "package" - inline_policy { - name = "message-sender-access" - policy = data.aws_iam_policy_document.this[0].json + docker_build_args = { + SERVICE_OPA_POLICY_ENCODED = terraform_data.email_msg_sender_policy[0].output } - tags = module.message_sender_label.tags + context = module.email_msg_sender_label.context } -data "aws_iam_policy_document" "this" { - count = local.enabled ? 1 : 0 +resource "aws_cloudwatch_log_group" "email_msg_sender" { + count = local.email_sender_enabled ? 1 : 0 - statement { - effect = "Allow" - actions = [ - "kms:Decrypt" - ] - resources = [ - module.kms_key.key_arn - ] - } + name = "/aws/lambda/${module.email_msg_sender_label.id}" + retention_in_days = 90 + tags = module.email_msg_sender_label.tags +} - statement { - effect = "Allow" - actions = [ - "sns:Publish" - ] - resources = [ - "*" - ] - } +resource "aws_lambda_function" "email_msg_sender" { + count = local.email_sender_enabled ? 1 : 0 - statement { - effect = "Allow" - actions = [ - "ses:GetTemplate", - "ses:SendEmail" - ] - resources = [ - "*" - ] + function_name = module.email_msg_sender_label.id + filename = module.email_msg_sender_code[0].artifact_package_path + handler = "bootstrap" + runtime = "provided.al2023" + timeout = 10 + role = aws_iam_role.this[0].arn + layers = [] + + tracing_config { + mode = "Active" } - statement { - effect = "Allow" - actions = [ - "dynamodb:Scan", - "dynamodb:Query", - "dynamodb:*Item*", - ] - resources = [ - aws_dynamodb_table.history[0].arn, - "${aws_dynamodb_table.history[0].arn}/*" - ] + environment { + variables = { + LOG_LEVEL = var.service_log_level + KMS_KEY_ID = module.kms_key.key_arn + EMAIL_SENDER_ENABLED = local.email_sender_enabled + EMAIL_SENDER_POLICY_PATH = local.email_sender_policy_path + } } + tags = module.email_msg_sender_label.tags + depends_on = [ - module.kms_key + module.email_msg_sender_code, + aws_cloudwatch_log_group.email_msg_sender, ] } + +resource "aws_lambda_permission" "email_msg_sender" { + count = local.email_sender_enabled ? 1 : 0 + + statement_id = "allow-cognito-trigger" + function_name = aws_lambda_function.email_msg_sender[0].function_name + action = "lambda:InvokeFunction" + principal = "cognito-idp.amazonaws.com" + source_arn = "arn:aws:cognito-idp:${local.aws_region_name}:${local.aws_account_id}:userpool/*" +} diff --git a/outputs.tf b/outputs.tf index d2ec0a8..072700a 100755 --- a/outputs.tf +++ b/outputs.tf @@ -2,6 +2,10 @@ output "kms_key_arn" { value = module.kms_key.key_arn } -output "lambda_fn_arn" { - value = local.enabled ? aws_lambda_function.message_sender[0].arn : "" +output "email_lambda_fn_arn" { + value = local.email_sender_enabled ? aws_lambda_function.email_msg_sender[0].arn : "" +} + +output "sms_lambda_fn_arn" { + value = local.sms_sender_enabled ? aws_lambda_function.sms_msg_sender[0].arn : "" } diff --git a/variables.tf b/variables.tf index 33213ca..8987a54 100755 --- a/variables.tf +++ b/variables.tf @@ -22,6 +22,22 @@ variable "service_log_level" { } } +# ------------------------------------------------------------- email-sender --- + +variable "email_sender_enabled" { + description = "Whether or not the Email sender is enabled." + type = bool + default = false +} + +variable "email_sender_policy_content" { + description = "The content of the Open Policy Agent policy for email sender." + type = string + default = "" +} + +# --------------------------------------------------------------- sms-sender --- + variable "sms_sender_enabled" { description = "Whether or not the SMS sender is enabled." type = bool @@ -31,11 +47,7 @@ variable "sms_sender_enabled" { variable "sms_sender_policy_content" { description = "The content of the Open Policy Agent policy for SMS sender." type = string - - validation { - condition = startswith(var.sms_sender_policy_content, "package cognito_custom_sender_sms_policy") - error_message = "The SMS sender policy content must include 'package cognito_custom_sender_sms_policy'." - } + default = "" } variable "sms_sender_throttle_period_in_minutes" {