mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 08:53:13 +00:00
Apply destroyed the inline policy then failed deleting the role on iam:ListInstanceProfilesForRole; the role was removed out of band.
135 lines
5.4 KiB
HCL
135 lines
5.4 KiB
HCL
# HTTP API fronting the order form.
|
|
#
|
|
# Three authorization modes coexist, matching template.yaml:
|
|
# NONE — the public form routes (submit-order, form-status, roster)
|
|
# AWS_IAM — the weekly-menu publication routes, called with SigV4 by
|
|
# scripts/upload_menu.py from GitHub Actions
|
|
# CUSTOM — every /api/admin route, behind the Google ID token authorizer
|
|
#
|
|
# All nine routes integrate with submit-order, which dispatches internally on
|
|
# the route key.
|
|
|
|
resource "aws_apigatewayv2_api" "order_api" {
|
|
name = local.project
|
|
protocol_type = "HTTP"
|
|
description = "meal-order-manager order form and admin API"
|
|
|
|
# Only the production origin. Local development uses the Flask dev server in
|
|
# app.py, which proxies API calls and does not enforce CORS.
|
|
cors_configuration {
|
|
allow_origins = [local.form_url]
|
|
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
|
allow_headers = ["Content-Type", "Authorization"]
|
|
max_age = 3600
|
|
}
|
|
}
|
|
|
|
# Result caching is off. With caching, an expired Google token or an admin
|
|
# removed from the allow-list would stay authorized for the cache TTL, and the
|
|
# tokeninfo call dominates latency anyway.
|
|
#
|
|
# Invoke permission is a Lambda resource policy (below), not AuthorizerCredentialsArn.
|
|
# The credentials-role path returned 500 without invoking the authorizer in prod
|
|
# (PLAT-102); resource policy matches the submit-order route grants and the live hotfix.
|
|
resource "aws_apigatewayv2_authorizer" "admin_google" {
|
|
api_id = aws_apigatewayv2_api.order_api.id
|
|
name = "AdminGoogleAuthorizer"
|
|
authorizer_type = "REQUEST"
|
|
authorizer_uri = aws_lambda_function.admin_authorizer.invoke_arn
|
|
authorizer_payload_format_version = "2.0"
|
|
authorizer_result_ttl_in_seconds = 0
|
|
enable_simple_responses = true
|
|
identity_sources = ["$request.header.Authorization"]
|
|
}
|
|
|
|
resource "aws_apigatewayv2_integration" "submit_order" {
|
|
api_id = aws_apigatewayv2_api.order_api.id
|
|
integration_type = "AWS_PROXY"
|
|
integration_method = "POST"
|
|
integration_uri = aws_lambda_function.submit_order.invoke_arn
|
|
payload_format_version = "2.0"
|
|
timeout_milliseconds = 30000
|
|
}
|
|
|
|
resource "aws_apigatewayv2_route" "this" {
|
|
for_each = local.api_routes
|
|
|
|
api_id = aws_apigatewayv2_api.order_api.id
|
|
route_key = each.value.route_key
|
|
target = "integrations/${aws_apigatewayv2_integration.submit_order.id}"
|
|
|
|
authorization_type = each.value.authorizer
|
|
authorizer_id = each.value.authorizer == "CUSTOM" ? aws_apigatewayv2_authorizer.admin_google.id : null
|
|
}
|
|
|
|
resource "aws_apigatewayv2_stage" "default" {
|
|
api_id = aws_apigatewayv2_api.order_api.id
|
|
name = "$default"
|
|
auto_deploy = true
|
|
|
|
access_log_settings {
|
|
destination_arn = aws_cloudwatch_log_group.api_access.arn
|
|
format = "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"requestTime\":\"$context.requestTime\",\"method\":\"$context.httpMethod\",\"routeKey\":\"$context.routeKey\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\",\"integrationError\":\"$context.integrationErrorMessage\"}"
|
|
}
|
|
|
|
default_route_settings {
|
|
throttling_burst_limit = 50
|
|
throttling_rate_limit = 100
|
|
}
|
|
|
|
# Order submission is human-paced; menu publication runs once a week. Both are
|
|
# throttled well below the account default so a loop in either client cannot
|
|
# exhaust the API's burst budget for the public form.
|
|
route_settings {
|
|
route_key = "POST /api/submit-order"
|
|
throttling_burst_limit = 10
|
|
throttling_rate_limit = 5
|
|
}
|
|
|
|
route_settings {
|
|
route_key = "POST /api/publish/menu"
|
|
throttling_burst_limit = 2
|
|
throttling_rate_limit = 1
|
|
}
|
|
|
|
depends_on = [aws_apigatewayv2_route.this]
|
|
}
|
|
|
|
# One grant per route rather than a single wildcard, so adding a route to the
|
|
# API does not silently make the function invocable through it.
|
|
resource "aws_lambda_permission" "api_route" {
|
|
for_each = local.api_routes
|
|
|
|
statement_id = "AllowApiGatewayInvoke-${each.key}"
|
|
action = "lambda:InvokeFunction"
|
|
function_name = aws_lambda_function.submit_order.function_name
|
|
principal = "apigateway.amazonaws.com"
|
|
source_arn = "${aws_apigatewayv2_api.order_api.execution_arn}/*/${each.value.permission_source}"
|
|
}
|
|
|
|
# Grant API Gateway permission to invoke the admin authorizer Lambda. Source ARN
|
|
# is the authorizer itself (not a route), matching the AWS HTTP API docs.
|
|
# Import adopts the PLAT-102 live hotfix statement so the first apply does not
|
|
# attempt a duplicate AddPermission.
|
|
import {
|
|
to = aws_lambda_permission.admin_authorizer
|
|
id = "meal-order-manager-admin-authorizer/AllowApiGatewayInvokeAuthorizer"
|
|
}
|
|
|
|
resource "aws_lambda_permission" "admin_authorizer" {
|
|
statement_id = "AllowApiGatewayInvokeAuthorizer"
|
|
action = "lambda:InvokeFunction"
|
|
function_name = aws_lambda_function.admin_authorizer.function_name
|
|
principal = "apigateway.amazonaws.com"
|
|
source_arn = "${aws_apigatewayv2_api.order_api.execution_arn}/authorizers/${aws_apigatewayv2_authorizer.admin_google.id}"
|
|
}
|
|
|
|
# PLAT-102 follow-up: role already deleted in AWS after apply failed on
|
|
# iam:ListInstanceProfilesForRole. Drop from state without a destroy call.
|
|
removed {
|
|
from = aws_iam_role.admin_authorizer_invoke
|
|
|
|
lifecycle {
|
|
destroy = false
|
|
}
|
|
}
|