mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 05:23:13 +00:00
Constrain the email-report role to the sender and domain identity ARNs so the policy is not unconstrained write.
408 lines
13 KiB
HCL
408 lines
13 KiB
HCL
# Execution roles for the seven Lambda functions plus the API Gateway role that
|
|
# invokes the admin authorizer.
|
|
#
|
|
# Every role is created under the /tf-managed/ path and carries the account's
|
|
# seahaven-lambda-execution-boundary permissions boundary. The path is what
|
|
# distinguishes Terraform-owned roles from the /cfn-managed/ roles the retired
|
|
# SAM stack created.
|
|
#
|
|
# 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
|
|
}
|
|
|
|
# Role API Gateway assumes to invoke the authorizer Lambda. The authorizer has
|
|
# no resource policy of its own; this identity-based grant is the only path.
|
|
data "aws_iam_policy_document" "apigateway_assume" {
|
|
statement {
|
|
effect = "Allow"
|
|
actions = ["sts:AssumeRole"]
|
|
|
|
principals {
|
|
type = "Service"
|
|
identifiers = ["apigateway.amazonaws.com"]
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role" "admin_authorizer_invoke" {
|
|
name = "${local.project}-admin-authorizer-invoke"
|
|
path = "/tf-managed/"
|
|
assume_role_policy = data.aws_iam_policy_document.apigateway_assume.json
|
|
permissions_boundary = local.boundary_arn
|
|
}
|
|
|
|
data "aws_iam_policy_document" "admin_authorizer_invoke" {
|
|
statement {
|
|
sid = "InvokeAdminAuthorizer"
|
|
effect = "Allow"
|
|
actions = ["lambda:InvokeFunction"]
|
|
resources = [aws_lambda_function.admin_authorizer.arn]
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role_policy" "admin_authorizer_invoke" {
|
|
name = "invoke-admin-authorizer"
|
|
role = aws_iam_role.admin_authorizer_invoke.id
|
|
policy = data.aws_iam_policy_document.admin_authorizer_invoke.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]
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|