# Execution roles for the seven Lambda functions plus the API Gateway role that # invokes the admin authorizer. # # Every Lambda execution role is created under the /tf-managed/ path and # carries seahaven-lambda-execution-boundary-meal-order-manager (PLAT-52). # The path distinguishes Terraform-owned roles from the retired SAM # /cfn-managed/ roles. # # The inline policies below are hand-expanded from the SAM policy templates in # template.yaml (DynamoDBCrudPolicy, DynamoDBReadPolicy, S3CrudPolicy, # S3ReadPolicy). Two deliberate narrowings from the SAM expansions: # - s3:PutObjectAcl is omitted. The reports bucket enforces # BucketOwnerEnforced ownership, so ACL writes fail regardless. # - s3:GetLifecycleConfiguration / s3:PutLifecycleConfiguration are omitted. # Bucket lifecycle is owned by this configuration, not by function code. data "aws_iam_policy_document" "lambda_assume" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } } # --------------------------------------------------------------------------- # Reusable policy documents # --------------------------------------------------------------------------- data "aws_iam_policy_document" "dynamodb_crud" { statement { sid = "OrdersTableCrud" effect = "Allow" actions = [ "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:ConditionCheckItem", "dynamodb:DeleteItem", "dynamodb:DescribeTable", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:UpdateItem", ] resources = [ aws_dynamodb_table.orders.arn, "${aws_dynamodb_table.orders.arn}/index/*", ] } } data "aws_iam_policy_document" "dynamodb_read" { statement { sid = "OrdersTableRead" effect = "Allow" actions = [ "dynamodb:BatchGetItem", "dynamodb:ConditionCheckItem", "dynamodb:DescribeTable", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", ] resources = [ aws_dynamodb_table.orders.arn, "${aws_dynamodb_table.orders.arn}/index/*", ] } } data "aws_iam_policy_document" "ssm_read" { statement { sid = "ReadProjectParameters" effect = "Allow" actions = ["ssm:GetParameter"] resources = [local.ssm_parameter_arn_wildcard] } } # The SAM template granted secretsmanager:GetSecretValue on # `secret:meal-order-manager/*`. Scoped here to the one secret the code actually # reads, supplied as an ARN so the grant cannot drift onto a future secret that # happens to share the prefix. data "aws_iam_policy_document" "slack_bot_secret_read" { statement { sid = "ReadSlackBotToken" effect = "Allow" actions = ["secretsmanager:GetSecretValue"] resources = [var.slack_bot_secret_arn] } } # --------------------------------------------------------------------------- # submit-order # --------------------------------------------------------------------------- resource "aws_iam_role" "submit_order" { name = "${local.project}-submit-order" path = "/tf-managed/" assume_role_policy = data.aws_iam_policy_document.lambda_assume.json permissions_boundary = local.boundary_arn } resource "aws_iam_role_policy_attachment" "submit_order_basic" { role = aws_iam_role.submit_order.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } data "aws_iam_policy_document" "submit_order" { source_policy_documents = [ data.aws_iam_policy_document.dynamodb_crud.json, data.aws_iam_policy_document.ssm_read.json, ] statement { sid = "InvokeSlackNotifier" effect = "Allow" actions = ["lambda:InvokeFunction"] resources = [aws_lambda_function.slack_notifier.arn] } # Read-only access to the weekly summary PDFs only — not the payroll or order # CSVs — for the admin summary-pdf presigned-URL endpoint. statement { sid = "ReadWeeklySummaryPdfs" effect = "Allow" actions = ["s3:GetObject"] resources = ["${aws_s3_bucket.reports.arn}/reports/*/weekly-summary-*.pdf"] } } resource "aws_iam_role_policy" "submit_order" { name = "submit-order" role = aws_iam_role.submit_order.id policy = data.aws_iam_policy_document.submit_order.json } # --------------------------------------------------------------------------- # admin-authorizer # --------------------------------------------------------------------------- resource "aws_iam_role" "admin_authorizer" { name = "${local.project}-admin-authorizer" path = "/tf-managed/" assume_role_policy = data.aws_iam_policy_document.lambda_assume.json permissions_boundary = local.boundary_arn } resource "aws_iam_role_policy_attachment" "admin_authorizer_basic" { role = aws_iam_role.admin_authorizer.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } data "aws_iam_policy_document" "admin_authorizer" { source_policy_documents = [ data.aws_iam_policy_document.dynamodb_read.json, data.aws_iam_policy_document.ssm_read.json, ] } resource "aws_iam_role_policy" "admin_authorizer" { name = "admin-authorizer" role = aws_iam_role.admin_authorizer.id policy = data.aws_iam_policy_document.admin_authorizer.json } # --------------------------------------------------------------------------- # close-form # --------------------------------------------------------------------------- resource "aws_iam_role" "close_form" { name = "${local.project}-close-form" path = "/tf-managed/" assume_role_policy = data.aws_iam_policy_document.lambda_assume.json permissions_boundary = local.boundary_arn } resource "aws_iam_role_policy_attachment" "close_form_basic" { role = aws_iam_role.close_form.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } data "aws_iam_policy_document" "close_form" { source_policy_documents = [data.aws_iam_policy_document.dynamodb_crud.json] statement { sid = "InvokeAggregateOrders" effect = "Allow" actions = ["lambda:InvokeFunction"] resources = [aws_lambda_function.aggregate_orders.arn] } } resource "aws_iam_role_policy" "close_form" { name = "close-form" role = aws_iam_role.close_form.id policy = data.aws_iam_policy_document.close_form.json } # --------------------------------------------------------------------------- # aggregate-orders # --------------------------------------------------------------------------- resource "aws_iam_role" "aggregate_orders" { name = "${local.project}-aggregate-orders" path = "/tf-managed/" assume_role_policy = data.aws_iam_policy_document.lambda_assume.json permissions_boundary = local.boundary_arn } resource "aws_iam_role_policy_attachment" "aggregate_orders_basic" { role = aws_iam_role.aggregate_orders.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } data "aws_iam_policy_document" "aggregate_orders" { source_policy_documents = [data.aws_iam_policy_document.dynamodb_crud.json] statement { sid = "ReportsBucketCrud" effect = "Allow" actions = [ "s3:DeleteObject", "s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", ] resources = ["${aws_s3_bucket.reports.arn}/*"] } statement { sid = "ReportsBucketList" effect = "Allow" actions = ["s3:GetBucketLocation", "s3:ListBucket"] resources = [aws_s3_bucket.reports.arn] } statement { sid = "InvokeSlackNotifier" effect = "Allow" actions = ["lambda:InvokeFunction"] resources = [aws_lambda_function.slack_notifier.arn] } statement { sid = "CheckcomponentsSend" effect = "Allow" actions = ["sqs:SendMessage"] resources = [var.checkcomponents_queue_arn] } } resource "aws_iam_role_policy" "aggregate_orders" { name = "aggregate-orders" role = aws_iam_role.aggregate_orders.id policy = data.aws_iam_policy_document.aggregate_orders.json } # --------------------------------------------------------------------------- # slack-notifier # --------------------------------------------------------------------------- resource "aws_iam_role" "slack_notifier" { name = "${local.project}-slack-notifier" path = "/tf-managed/" assume_role_policy = data.aws_iam_policy_document.lambda_assume.json permissions_boundary = local.boundary_arn } resource "aws_iam_role_policy_attachment" "slack_notifier_basic" { role = aws_iam_role.slack_notifier.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } data "aws_iam_policy_document" "slack_notifier" { source_policy_documents = [ data.aws_iam_policy_document.dynamodb_read.json, data.aws_iam_policy_document.ssm_read.json, data.aws_iam_policy_document.slack_bot_secret_read.json, ] } resource "aws_iam_role_policy" "slack_notifier" { name = "slack-notifier" role = aws_iam_role.slack_notifier.id policy = data.aws_iam_policy_document.slack_notifier.json } # --------------------------------------------------------------------------- # sync-roster # --------------------------------------------------------------------------- resource "aws_iam_role" "sync_roster" { name = "${local.project}-sync-roster" path = "/tf-managed/" assume_role_policy = data.aws_iam_policy_document.lambda_assume.json permissions_boundary = local.boundary_arn } resource "aws_iam_role_policy_attachment" "sync_roster_basic" { role = aws_iam_role.sync_roster.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } data "aws_iam_policy_document" "sync_roster" { source_policy_documents = [ data.aws_iam_policy_document.dynamodb_crud.json, data.aws_iam_policy_document.ssm_read.json, data.aws_iam_policy_document.slack_bot_secret_read.json, ] } resource "aws_iam_role_policy" "sync_roster" { name = "sync-roster" role = aws_iam_role.sync_roster.id policy = data.aws_iam_policy_document.sync_roster.json } # --------------------------------------------------------------------------- # email-report # --------------------------------------------------------------------------- resource "aws_iam_role" "email_report" { name = "${local.project}-email-report" path = "/tf-managed/" assume_role_policy = data.aws_iam_policy_document.lambda_assume.json permissions_boundary = local.boundary_arn } resource "aws_iam_role_policy_attachment" "email_report_basic" { role = aws_iam_role.email_report.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } data "aws_iam_policy_document" "email_report" { source_policy_documents = [data.aws_iam_policy_document.dynamodb_read.json] statement { sid = "ReportsBucketRead" effect = "Allow" actions = ["s3:GetObject", "s3:GetObjectVersion"] resources = ["${aws_s3_bucket.reports.arn}/*"] } statement { sid = "ReportsBucketList" effect = "Allow" actions = ["s3:GetBucketLocation", "s3:ListBucket"] resources = [aws_s3_bucket.reports.arn] } # Scope SendRawEmail to the verified sender domain/identity rather than "*". # SES still enforces verification; IAM pins the From identity ARNs. statement { sid = "SendPayrollReport" effect = "Allow" actions = ["ses:SendRawEmail"] resources = [ "arn:aws:ses:${var.aws_region}:${local.account_id}:identity/${var.sender_email}", "arn:aws:ses:${var.aws_region}:${local.account_id}:identity/seahavenind.com", ] } } resource "aws_iam_role_policy" "email_report" { name = "email-report" role = aws_iam_role.email_report.id policy = data.aws_iam_policy_document.email_report.json }