2026-08-07 19:19:51 -04:00
|
|
|
# Execution roles for the seven Lambda functions plus the API Gateway role that
|
|
|
|
|
# invokes the admin authorizer.
|
|
|
|
|
#
|
2026-08-20 16:02:46 -04:00
|
|
|
# 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.
|
2026-08-07 19:19:51 -04:00
|
|
|
#
|
|
|
|
|
# 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]
|
|
|
|
|
}
|
2026-09-03 22:04:02 +00:00
|
|
|
|
|
|
|
|
statement {
|
|
|
|
|
sid = "CheckcomponentsSend"
|
|
|
|
|
effect = "Allow"
|
|
|
|
|
actions = ["sqs:SendMessage"]
|
|
|
|
|
resources = [var.checkcomponents_queue_arn]
|
|
|
|
|
}
|
2026-08-07 19:19:51 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 19:21:45 -04:00
|
|
|
# Scope SendRawEmail to the verified sender domain/identity rather than "*".
|
|
|
|
|
# SES still enforces verification; IAM pins the From identity ARNs.
|
2026-08-07 19:19:51 -04:00
|
|
|
statement {
|
2026-08-07 19:33:49 -04:00
|
|
|
sid = "SendPayrollReport"
|
|
|
|
|
effect = "Allow"
|
2026-08-07 19:21:45 -04:00
|
|
|
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",
|
|
|
|
|
]
|
2026-08-07 19:19:51 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|