mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 14:13:13 +00:00
* feat(api): custom domain procurement-api.seahaven.com for the read API Stacked on feat/shoc-wo-webhook. Gives the SHOC-facing read API a stable, brandable endpoint instead of the opaque execute-api URL. - procurement_api_stack.py: REGIONAL API Gateway DomainName (TLS 1.2) + empty base-path mapping to the prod stage, so callers hit https://procurement-api.seahaven.com/work-orders (no /prod segment). The ACM cert ARN is read from SSM (/procurement-api/custom-domain/certificate-arn) via value_for_string_parameter, because the seahaven.com zone is in the mgmt account (cross-account DNS) and the cert is issued out of band. Outputs expose the regional alias target + hosted-zone id for the mgmt A-record. - scripts/setup_procurement_api_domain.sh: idempotent two-step runbook (cert: request + mgmt-zone validation + wait + SSM; alias: post-deploy A-record from stack outputs). Verifies both account identities. - handler._base_url: omit the /{stage} segment for a custom-domain request (the base-path mapping serves the stage at the root) so the docs never advertise a broken server URL; execute-api hosts keep /{stage}. - openapi.json: custom domain added as servers[0] (recommended), execute-api kept as the direct fallback + the per-request injection target. No IAM/auth/policy change (same API id + resource policy), so the SigV4 surface and the mandatory cross-family gates are unaffected. 751 pytest, ruff, cdk synth, redocly lint all green. * fix(api): use .endswith('.amazonaws.com') instead of substring check for execute-api detection The prior '.execute-api.' in domain substring check is fragile and triggers CodeQL incomplete-sanitization warnings. All API Gateway default domains end with .amazonaws.com, so a suffix check is more precise and also silences the false-positive alert. Refs: https://github.com/Sea-Haven-Industries/procurement-ingest/security/code-scanning/6
128 lines
5.7 KiB
Bash
Executable file
128 lines
5.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
###############################################################################
|
|
# setup_procurement_api_domain.sh
|
|
#
|
|
# One-time (idempotent) wiring for the procurement-api custom domain
|
|
# (procurement-api.seahaven.com). CROSS-ACCOUNT: the API + ACM cert live in
|
|
# seahaven-prod (011934824531), but the seahaven.com public zone lives in the
|
|
# mgmt account (328440206208), so the cert's DNS-validation record and the
|
|
# final A-alias are added to the mgmt zone.
|
|
#
|
|
# Order of operations:
|
|
# 1. ./setup_procurement_api_domain.sh cert
|
|
# - requests (or reuses) the ACM cert in prod, us-east-1
|
|
# - adds its DNS-validation CNAME to the mgmt seahaven.com zone
|
|
# - waits for ISSUED, then writes the cert ARN to prod SSM
|
|
# (/procurement-api/custom-domain/certificate-arn)
|
|
# 2. deploy the procurement-api stack (cdk deploy procurement-api) -- it
|
|
# reads the SSM param and creates the API Gateway DomainName + mapping
|
|
# 3. ./setup_procurement_api_domain.sh alias
|
|
# - reads the stack's regional alias target from the outputs
|
|
# - adds the A-alias (procurement-api.seahaven.com -> API GW) to the
|
|
# mgmt zone
|
|
#
|
|
# Requires SSO sessions for BOTH profiles (prod for ACM/SSM, mgmt for Route53).
|
|
###############################################################################
|
|
set -euo pipefail
|
|
|
|
DOMAIN="procurement-api.seahaven.com"
|
|
REGION="us-east-1"
|
|
PROD_PROFILE="${PROD_PROFILE:-seahaven-prod}"
|
|
MGMT_PROFILE="${MGMT_PROFILE:-seahaven-mgmt}"
|
|
PROD_ACCOUNT="011934824531"
|
|
MGMT_ACCOUNT="328440206208"
|
|
ZONE_ID="Z06652411XKH89KTZD3XA" # seahaven.com public zone, in the mgmt account
|
|
SSM_PARAM="/procurement-api/custom-domain/certificate-arn"
|
|
STACK_NAME="procurement-api"
|
|
|
|
_verify_account() {
|
|
local profile="$1" expected="$2"
|
|
local got
|
|
got="$(aws sts get-caller-identity --profile "${profile}" --query Account --output text)"
|
|
if [[ "${got}" != "${expected}" ]]; then
|
|
echo "ERROR: profile ${profile} resolves to ${got}, expected ${expected}. Aborting." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_cert() {
|
|
_verify_account "${PROD_PROFILE}" "${PROD_ACCOUNT}"
|
|
_verify_account "${MGMT_PROFILE}" "${MGMT_ACCOUNT}"
|
|
|
|
echo "==> Finding or requesting ACM cert for ${DOMAIN} in ${PROD_ACCOUNT}/${REGION}"
|
|
local cert_arn
|
|
cert_arn="$(aws acm list-certificates --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--query "CertificateSummaryList[?DomainName=='${DOMAIN}'].CertificateArn | [0]" --output text)"
|
|
if [[ "${cert_arn}" == "None" || -z "${cert_arn}" ]]; then
|
|
cert_arn="$(aws acm request-certificate --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--domain-name "${DOMAIN}" --validation-method DNS \
|
|
--query CertificateArn --output text)"
|
|
echo " requested ${cert_arn}; waiting for the validation record to populate..."
|
|
sleep 8
|
|
else
|
|
echo " reusing existing ${cert_arn}"
|
|
fi
|
|
|
|
echo "==> Reading the DNS-validation record"
|
|
local rec_name rec_value
|
|
rec_name="$(aws acm describe-certificate --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--certificate-arn "${cert_arn}" \
|
|
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Name" --output text)"
|
|
rec_value="$(aws acm describe-certificate --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--certificate-arn "${cert_arn}" \
|
|
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Value" --output text)"
|
|
|
|
echo "==> Upserting validation CNAME in the mgmt seahaven.com zone"
|
|
aws route53 change-resource-record-sets --profile "${MGMT_PROFILE}" \
|
|
--hosted-zone-id "${ZONE_ID}" --change-batch "$(cat <<JSON
|
|
{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{
|
|
"Name":"${rec_name}","Type":"CNAME","TTL":300,
|
|
"ResourceRecords":[{"Value":"${rec_value}"}]}}]}
|
|
JSON
|
|
)" >/dev/null
|
|
|
|
echo "==> Waiting for cert to reach ISSUED (can take a few minutes)"
|
|
aws acm wait certificate-validated --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--certificate-arn "${cert_arn}"
|
|
|
|
echo "==> Writing cert ARN to prod SSM ${SSM_PARAM}"
|
|
aws ssm put-parameter --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--name "${SSM_PARAM}" --type String --overwrite --value "${cert_arn}" >/dev/null
|
|
|
|
echo "OK: cert ISSUED and SSM param set. Now: cdk deploy ${STACK_NAME}, then '$0 alias'."
|
|
}
|
|
|
|
cmd_alias() {
|
|
_verify_account "${PROD_PROFILE}" "${PROD_ACCOUNT}"
|
|
_verify_account "${MGMT_PROFILE}" "${MGMT_ACCOUNT}"
|
|
|
|
echo "==> Reading regional alias target from the ${STACK_NAME} stack outputs"
|
|
local target zone
|
|
target="$(aws cloudformation describe-stacks --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--stack-name "${STACK_NAME}" \
|
|
--query "Stacks[0].Outputs[?OutputKey=='ProcurementApiAliasTarget'].OutputValue | [0]" --output text)"
|
|
zone="$(aws cloudformation describe-stacks --profile "${PROD_PROFILE}" --region "${REGION}" \
|
|
--stack-name "${STACK_NAME}" \
|
|
--query "Stacks[0].Outputs[?OutputKey=='ProcurementApiAliasHostedZoneId'].OutputValue | [0]" --output text)"
|
|
if [[ -z "${target}" || "${target}" == "None" ]]; then
|
|
echo "ERROR: no alias target output; deploy the stack first." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> Upserting A-alias ${DOMAIN} -> ${target} in the mgmt zone"
|
|
aws route53 change-resource-record-sets --profile "${MGMT_PROFILE}" \
|
|
--hosted-zone-id "${ZONE_ID}" --change-batch "$(cat <<JSON
|
|
{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{
|
|
"Name":"${DOMAIN}","Type":"A",
|
|
"AliasTarget":{"DNSName":"${target}","HostedZoneId":"${zone}","EvaluateTargetHealth":false}}}]}
|
|
JSON
|
|
)" >/dev/null
|
|
|
|
echo "OK: A-alias set. Verify: curl -sS -o /dev/null -w '%{http_code}\\n' https://${DOMAIN}/docs (expect 401 without a token)."
|
|
}
|
|
|
|
case "${1:-}" in
|
|
cert) cmd_cert ;;
|
|
alias) cmd_alias ;;
|
|
*) echo "usage: $0 {cert|alias}" >&2; exit 2 ;;
|
|
esac
|