locals { api_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "ShocBackendDevDataRead" Effect = "Allow" Principal = { AWS = local.shoc_consumer_role_arn } 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 } } 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 }