data "aws_iam_policy_document" "shoc_webhook_kms" { statement { sid = "EnableRootAccountPermissions" effect = "Allow" actions = ["kms:*"] resources = ["*"] principals { type = "AWS" identifiers = ["arn:aws:iam::${local.account_id}:root"] } } statement { sid = "ShocBackendDecrypt" effect = "Allow" actions = ["kms:Decrypt"] resources = ["*"] principals { type = "AWS" identifiers = local.shoc_consumer_role_arns } condition { test = "StringEquals" variable = "kms:ViaService" values = ["secretsmanager.${var.aws_region}.amazonaws.com"] } } statement { sid = "AllowSecretsManagerViaServiceAccount" effect = "Allow" actions = [ "kms:Decrypt", "kms:Encrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", ] resources = ["*"] principals { type = "AWS" identifiers = ["arn:aws:iam::${local.account_id}:root"] } condition { test = "StringEquals" variable = "kms:ViaService" values = ["secretsmanager.${var.aws_region}.amazonaws.com"] } } statement { sid = "AllowSecretsManagerGrants" effect = "Allow" actions = [ "kms:CreateGrant", "kms:DescribeKey", ] resources = ["*"] principals { type = "AWS" identifiers = ["arn:aws:iam::${local.account_id}:root"] } condition { test = "StringEquals" variable = "kms:ViaService" values = ["secretsmanager.${var.aws_region}.amazonaws.com"] } } statement { sid = "ShocHmacRotator" effect = "Allow" actions = [ "kms:Decrypt", "kms:Encrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", ] resources = ["*"] principals { type = "AWS" identifiers = [aws_iam_role.wo_shoc_hmac_rotator.arn] } condition { test = "StringEquals" variable = "kms:ViaService" values = ["secretsmanager.${var.aws_region}.amazonaws.com"] } } statement { sid = "ShocEmitterDecrypt" effect = "Allow" actions = ["kms:Decrypt"] resources = ["*"] principals { type = "AWS" identifiers = [aws_iam_role.wo_shoc_emitter.arn] } condition { test = "StringEquals" variable = "kms:ViaService" values = ["secretsmanager.${var.aws_region}.amazonaws.com"] } } } resource "aws_kms_key" "shoc_webhook" { description = "Dedicated CMK for the workorder-ingest/shoc-webhook-hmac secret (cross-account readable by the SHOC backend)" enable_key_rotation = true deletion_window_in_days = 7 policy = data.aws_iam_policy_document.shoc_webhook_kms.json } resource "aws_kms_alias" "shoc_webhook" { name = "alias/workorder-ingest-shoc-webhook-kms" target_key_id = aws_kms_key.shoc_webhook.key_id } resource "aws_secretsmanager_secret" "shoc_webhook_hmac" { name = "workorder-ingest/shoc-webhook-hmac" description = "HMAC signing keys for the SHOC work-order webhook (docs/shoc-webhook-contract.md section 6)" kms_key_id = aws_kms_key.shoc_webhook.arn tags = { Purpose = "shoc-webhook-hmac" ManagedBy = "terraform" } } resource "aws_secretsmanager_secret_policy" "shoc_webhook_hmac" { secret_arn = aws_secretsmanager_secret.shoc_webhook_hmac.arn policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { AWS = local.shoc_consumer_role_arns } Action = [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", ] Resource = "*" }, { Effect = "Deny" Principal = { AWS = "arn:aws:iam::${local.account_id}:root" } Action = "secretsmanager:DeleteSecret" Resource = "*" } ] }) } resource "aws_cloudwatch_log_group" "wo_shoc_hmac_rotator" { name = "/aws/lambda/workorder-shoc-hmac-rotator" retention_in_days = 60 lifecycle { prevent_destroy = true } } resource "aws_lambda_function" "wo_shoc_hmac_rotator" { function_name = "workorder-shoc-hmac-rotator" role = aws_iam_role.wo_shoc_hmac_rotator.arn handler = "handler.handler" runtime = "python3.12" architectures = ["arm64"] memory_size = 128 timeout = 60 s3_bucket = aws_s3_bucket.artifacts.id s3_key = aws_s3_object.lambda["wo_shoc_hmac_rotator"].key source_code_hash = data.archive_file.lambda["wo_shoc_hmac_rotator"].output_base64sha256 environment { variables = { SENTRY_DSN = var.sentry_dsn } } depends_on = [ aws_s3_object.lambda, aws_cloudwatch_log_group.wo_shoc_hmac_rotator, aws_iam_role_policy_attachment.wo_shoc_hmac_rotator_basic, aws_iam_role_policy.wo_shoc_hmac_rotator_secrets, aws_iam_role_policy.wo_shoc_hmac_rotator_kms, ] } resource "aws_lambda_permission" "wo_shoc_hmac_rotator" { statement_id = "AllowSecretsManagerRotate" action = "lambda:InvokeFunction" function_name = aws_lambda_function.wo_shoc_hmac_rotator.function_name principal = "secretsmanager.amazonaws.com" source_account = local.account_id source_arn = aws_secretsmanager_secret.shoc_webhook_hmac.arn } resource "aws_secretsmanager_secret_rotation" "shoc_webhook_hmac" { secret_id = aws_secretsmanager_secret.shoc_webhook_hmac.id rotation_lambda_arn = aws_lambda_function.wo_shoc_hmac_rotator.arn rotation_rules { automatically_after_days = 30 } depends_on = [aws_lambda_permission.wo_shoc_hmac_rotator] } resource "aws_sqs_queue" "shoc_emitter_failures" { name = "workorder-shoc-emitter-failures" message_retention_seconds = 1209600 sqs_managed_sse_enabled = true } resource "aws_sqs_queue_policy" "shoc_emitter_failures" { queue_url = aws_sqs_queue.shoc_emitter_failures.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Deny" Principal = { AWS = "*" } Action = "sqs:*" Resource = aws_sqs_queue.shoc_emitter_failures.arn Condition = { Bool = { "aws:SecureTransport" = "false" } } } ] }) } resource "aws_sqs_queue" "shoc_emitter_rejected" { name = "workorder-shoc-emitter-rejected" message_retention_seconds = 1209600 sqs_managed_sse_enabled = true } resource "aws_sqs_queue_policy" "shoc_emitter_rejected" { queue_url = aws_sqs_queue.shoc_emitter_rejected.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Deny" Principal = { AWS = "*" } Action = "sqs:*" Resource = aws_sqs_queue.shoc_emitter_rejected.arn Condition = { Bool = { "aws:SecureTransport" = "false" } } } ] }) } resource "aws_cloudwatch_log_group" "wo_shoc_emitter" { name = "/aws/lambda/workorder-shoc-emitter" retention_in_days = 60 lifecycle { prevent_destroy = true } } resource "aws_lambda_function" "wo_shoc_emitter" { function_name = "workorder-shoc-emitter" role = aws_iam_role.wo_shoc_emitter.arn handler = "handler.handler" runtime = "python3.12" architectures = ["arm64"] memory_size = 256 timeout = 60 s3_bucket = aws_s3_bucket.artifacts.id s3_key = aws_s3_object.lambda["wo_shoc_emitter"].key source_code_hash = data.archive_file.lambda["wo_shoc_emitter"].output_base64sha256 environment { variables = { HMAC_SECRET_ARN = var.shoc_hmac_secret_arn REJECTED_QUEUE_URL = aws_sqs_queue.shoc_emitter_rejected.url SHOC_WEBHOOK_URL = var.shoc_webhook_url # Stream ARN classification (PLAT-11); must match ESM source table names. WORK_ORDERS_TABLE = aws_dynamodb_table.work_orders_kebab.name COMMENTS_TABLE = aws_dynamodb_table.work_order_comments_kebab.name SENTRY_DSN = var.sentry_dsn } } depends_on = [ aws_s3_object.lambda, aws_cloudwatch_log_group.wo_shoc_emitter, aws_iam_role_policy_attachment.wo_shoc_emitter_basic, aws_iam_role_policy.wo_shoc_emitter_streams, aws_iam_role_policy.wo_shoc_emitter_secrets, aws_iam_role_policy.wo_shoc_emitter_kms, aws_iam_role_policy.wo_shoc_emitter_sqs, ] } resource "aws_lambda_event_source_mapping" "wo_shoc_emitter_work_orders" { event_source_arn = aws_dynamodb_table.work_orders_kebab.stream_arn function_name = aws_lambda_function.wo_shoc_emitter.arn starting_position = "LATEST" batch_size = 10 parallelization_factor = 1 maximum_record_age_in_seconds = 86400 maximum_retry_attempts = -1 bisect_batch_on_function_error = false function_response_types = ["ReportBatchItemFailures"] enabled = true destination_config { on_failure { destination_arn = aws_sqs_queue.shoc_emitter_failures.arn } } } resource "aws_lambda_event_source_mapping" "wo_shoc_emitter_comments" { event_source_arn = aws_dynamodb_table.work_order_comments_kebab.stream_arn function_name = aws_lambda_function.wo_shoc_emitter.arn starting_position = "LATEST" batch_size = 10 parallelization_factor = 1 maximum_record_age_in_seconds = 86400 maximum_retry_attempts = -1 bisect_batch_on_function_error = false function_response_types = ["ReportBatchItemFailures"] enabled = true destination_config { on_failure { destination_arn = aws_sqs_queue.shoc_emitter_failures.arn } } }