procurement-ingest/terraform/api.tf
Adam Moussa 0c1dcd7844
fix(terraform): grant shoc-backend-staging HMAC and API access (PLAT-211) (#212)
Terraform still pinned only shoc-backend-dev, so the next ingest apply would drop the live staging HMAC grant.
2026-09-18 18:27:55 +00:00

343 lines
12 KiB
HCL

locals {
api_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ShocBackendDataRead"
Effect = "Allow"
Principal = {
AWS = local.shoc_consumer_role_arns
}
Action = "execute-api:Invoke"
Resource = [
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/work-orders",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/work-orders/*",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/purchase-orders",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/purchase-orders/*",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/verified-sites",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/verified-sites/*",
]
},
{
Sid = "DocsTokenGatedRoutes"
Effect = "Allow"
Principal = { AWS = "*" }
Action = "execute-api:Invoke"
Resource = [
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/docs",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/openapi.json",
]
}
]
})
# Live resource IDs (import). parent_key null => API root.
api_resources = {
work_orders = {
id = "h5iu4f"
parent_key = null
path_part = "work-orders"
}
work_order_id = {
id = "4uk6uh"
parent_key = "work_orders"
path_part = "{workOrderId}"
}
work_order_comments = {
id = "gfx0te"
parent_key = "work_order_id"
path_part = "comments"
}
purchase_orders = {
id = "pfn6qm"
parent_key = null
path_part = "purchase-orders"
}
po_number = {
id = "nclnn3"
parent_key = "purchase_orders"
path_part = "{poNumber}"
}
verified_sites = {
id = "vyst7e"
parent_key = null
path_part = "verified-sites"
}
site_code = {
id = "rmaawy"
parent_key = "verified_sites"
path_part = "{siteCode}"
}
docs = {
id = "k4vl85"
parent_key = null
path_part = "docs"
}
openapi_json = {
id = "xos715"
parent_key = null
path_part = "openapi.json"
}
}
api_iam_methods = {
work_orders_get = { resource = "work_orders", method = "GET" }
work_order_id_get = { resource = "work_order_id", method = "GET" }
work_order_id_patch = { resource = "work_order_id", method = "PATCH" }
work_order_comments_get = { resource = "work_order_comments", method = "GET" }
work_order_comments_post = { resource = "work_order_comments", method = "POST" }
purchase_orders_get = { resource = "purchase_orders", method = "GET" }
po_number_get = { resource = "po_number", method = "GET" }
verified_sites_get = { resource = "verified_sites", method = "GET" }
site_code_get = { resource = "site_code", method = "GET" }
}
api_none_methods = {
docs_get = { resource = "docs", method = "GET" }
openapi_json_get = { resource = "openapi_json", method = "GET" }
}
}
resource "aws_cloudwatch_log_group" "procurement_api" {
name = "/aws/lambda/procurement-api"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "procurement_api" {
function_name = "procurement-api"
role = aws_iam_role.procurement_api.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 30
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["procurement_api"].key
source_code_hash = data.archive_file.lambda["procurement_api"].output_base64sha256
environment {
variables = {
VERIFIED_SITES_TABLE = aws_dynamodb_table.verified_sites.name
WORK_ORDERS_TABLE = aws_dynamodb_table.work_orders_kebab.name
COMMENTS_TABLE = aws_dynamodb_table.work_order_comments_kebab.name
PO_TABLE = aws_dynamodb_table.purchase_orders.name
WEB_UI_AUTH_TOKEN_SECRET_ARN = var.web_ui_auth_token_secret_arn
SENTRY_DSN = var.sentry_dsn
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.procurement_api,
aws_iam_role_policy_attachment.procurement_api_basic,
aws_iam_role_policy.procurement_api_ddb,
aws_iam_role_policy.procurement_api_kms,
aws_iam_role_policy.procurement_api_secrets,
]
}
resource "aws_api_gateway_rest_api" "procurement" {
name = "procurement-api"
description = "Read API over procurement-ingest work orders + purchase orders; token-gated OpenAPI docs at /docs"
policy = local.api_policy
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_resource" "work_orders" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "work-orders"
}
resource "aws_api_gateway_resource" "work_order_id" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.work_orders.id
path_part = "{workOrderId}"
}
resource "aws_api_gateway_resource" "work_order_comments" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.work_order_id.id
path_part = "comments"
}
resource "aws_api_gateway_resource" "purchase_orders" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "purchase-orders"
}
resource "aws_api_gateway_resource" "po_number" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.purchase_orders.id
path_part = "{poNumber}"
}
resource "aws_api_gateway_resource" "verified_sites" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "verified-sites"
}
resource "aws_api_gateway_resource" "site_code" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.verified_sites.id
path_part = "{siteCode}"
}
resource "aws_api_gateway_resource" "docs" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "docs"
}
resource "aws_api_gateway_resource" "openapi_json" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "openapi.json"
}
locals {
api_resource_ids = {
work_orders = aws_api_gateway_resource.work_orders.id
work_order_id = aws_api_gateway_resource.work_order_id.id
work_order_comments = aws_api_gateway_resource.work_order_comments.id
purchase_orders = aws_api_gateway_resource.purchase_orders.id
po_number = aws_api_gateway_resource.po_number.id
verified_sites = aws_api_gateway_resource.verified_sites.id
site_code = aws_api_gateway_resource.site_code.id
docs = aws_api_gateway_resource.docs.id
openapi_json = aws_api_gateway_resource.openapi_json.id
}
}
resource "aws_api_gateway_method" "iam" {
for_each = local.api_iam_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = each.value.method
authorization = "AWS_IAM"
}
resource "aws_api_gateway_method" "none" {
for_each = local.api_none_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = each.value.method
authorization = "NONE"
}
resource "aws_api_gateway_integration" "iam" {
for_each = local.api_iam_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = aws_api_gateway_method.iam[each.key].http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.procurement_api.invoke_arn
}
resource "aws_api_gateway_integration" "none" {
for_each = local.api_none_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = aws_api_gateway_method.none[each.key].http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.procurement_api.invoke_arn
}
resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.procurement_api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.procurement.execution_arn}/*/*"
}
resource "aws_api_gateway_deployment" "procurement" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
# Hash live method/integration/resource attributes (not just static locals)
# so path, auth, or proxy URI edits force a new stage deployment.
triggers = {
redeployment = sha1(jsonencode({
resources = {
work_orders = aws_api_gateway_resource.work_orders
work_order_id = aws_api_gateway_resource.work_order_id
work_order_comments = aws_api_gateway_resource.work_order_comments
purchase_orders = aws_api_gateway_resource.purchase_orders
po_number = aws_api_gateway_resource.po_number
verified_sites = aws_api_gateway_resource.verified_sites
site_code = aws_api_gateway_resource.site_code
docs = aws_api_gateway_resource.docs
openapi_json = aws_api_gateway_resource.openapi_json
}
methods_iam = aws_api_gateway_method.iam
methods_none = aws_api_gateway_method.none
integrations_iam = aws_api_gateway_integration.iam
integrations_none = aws_api_gateway_integration.none
policy = local.api_policy
lambda_hash = data.archive_file.lambda["procurement_api"].output_base64sha256
}))
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_integration.iam,
aws_api_gateway_integration.none,
]
}
resource "aws_api_gateway_stage" "prod" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
deployment_id = aws_api_gateway_deployment.procurement.id
stage_name = local.api_stage_name
xray_tracing_enabled = false
}
resource "aws_api_gateway_method_settings" "prod_all" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
stage_name = aws_api_gateway_stage.prod.stage_name
method_path = "*/*"
settings {
metrics_enabled = false
logging_level = "OFF"
data_trace_enabled = false
throttling_burst_limit = 100
throttling_rate_limit = 50
}
}
resource "aws_api_gateway_domain_name" "procurement" {
domain_name = local.api_domain_name
regional_certificate_arn = data.aws_ssm_parameter.procurement_api_cert_arn.value
security_policy = "TLS_1_2"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_base_path_mapping" "procurement" {
api_id = aws_api_gateway_rest_api.procurement.id
stage_name = aws_api_gateway_stage.prod.stage_name
domain_name = aws_api_gateway_domain_name.procurement.domain_name
}