mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 08:23:14 +00:00
feat(api): custom domain procurement-api.seahaven.com (stacked on PR-2) (#140)
* 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
This commit is contained in:
parent
c040050373
commit
cf8ca2eeb6
6 changed files with 247 additions and 1 deletions
|
|
@ -111,6 +111,7 @@ A read-only REST API (API Gateway + the `procurement-api` Lambda, `lambdas/api/`
|
||||||
|
|
||||||
- **Spec is source of truth:** `lambdas/api/openapi.json` (OpenAPI 3.1). Its top-level `webhooks` section documents the outbound SHOC work-order feed, so one page describes both directions (call + be-called). `tests/test_api_spec_drift.py` pins the spec's paths to the router table, so spec and implementation cannot drift.
|
- **Spec is source of truth:** `lambdas/api/openapi.json` (OpenAPI 3.1). Its top-level `webhooks` section documents the outbound SHOC work-order feed, so one page describes both directions (call + be-called). `tests/test_api_spec_drift.py` pins the spec's paths to the router table, so spec and implementation cannot drift.
|
||||||
- **Auth:** data routes use API Gateway `AWS_IAM` (SigV4) plus a resource policy allowing exactly `arn:aws:iam::396287094661:role/shoc-backend-dev` on `GET/*`; same-account admin callers authorize via identity policy (Postman signs SigV4 natively). Docs routes are auth `NONE` at the gateway (resource-policy carve-out for exactly those two GETs) but the handler fails closed on the shared token (`lambdas/shared/web_ui_auth.py`, secret `procurement-ingest/web-ui-auth-token`) — not an unauthenticated data path (INFRA-74 posture).
|
- **Auth:** data routes use API Gateway `AWS_IAM` (SigV4) plus a resource policy allowing exactly `arn:aws:iam::396287094661:role/shoc-backend-dev` on `GET/*`; same-account admin callers authorize via identity policy (Postman signs SigV4 natively). Docs routes are auth `NONE` at the gateway (resource-policy carve-out for exactly those two GETs) but the handler fails closed on the shared token (`lambdas/shared/web_ui_auth.py`, secret `procurement-ingest/web-ui-auth-token`) — not an unauthenticated data path (INFRA-74 posture).
|
||||||
|
- **Custom domain:** `https://procurement-api.seahaven.com` (REGIONAL API Gateway domain, TLS 1.2, empty base-path mapping to the `prod` stage, so callers hit `/work-orders` with no `/prod` segment). The stable SHOC-facing endpoint; the raw `*.execute-api.us-east-1.amazonaws.com/prod` URL still works. **Cross-account DNS:** the `seahaven.com` public zone is in the mgmt account (`328440206208`), so the ACM cert's validation record and the A-alias are added there out of band — `scripts/setup_procurement_api_domain.sh cert` issues the cert (DNS-validated against the mgmt zone) and writes its ARN to prod SSM `/procurement-api/custom-domain/certificate-arn`, which the stack reads (`value_for_string_parameter`, CFN-resolved at deploy); after `cdk deploy procurement-api`, `scripts/setup_procurement_api_domain.sh alias` adds the A-alias from the stack's `ProcurementApiAliasTarget`/`ProcurementApiAliasHostedZoneId` outputs. SigV4 is unaffected (same underlying API id + resource policy); the docs page injects whichever host served the request into `servers[0].url`.
|
||||||
- **KMS:** `purchase-orders` is CMK-encrypted; the imported-by-name table doesn't carry the key association, so the stack grants `kms:Decrypt`/`DescribeKey` on the CMK from SSM `/seahaven/dynamodb/cmk-arn` explicitly (the INFRA-104 failure class).
|
- **KMS:** `purchase-orders` is CMK-encrypted; the imported-by-name table doesn't carry the key association, so the stack grants `kms:Decrypt`/`DescribeKey` on the CMK from SSM `/seahaven/dynamodb/cmk-arn` explicitly (the INFRA-104 failure class).
|
||||||
- **No access logging in v1** (keeps the `?token=` shim out of any log and avoids the account-level API Gateway CloudWatch role); rotate the docs token before ever enabling it. No CORS (server-to-server + Postman callers).
|
- **No access logging in v1** (keeps the `?token=` shim out of any log and avoids the account-level API Gateway CloudWatch role); rotate the docs token before ever enabling it. No CORS (server-to-server + Postman callers).
|
||||||
- **Alarms:** `procurement-api-errors`/`-throttles`/`-duration` (p99 ≥ 22.5 s) + gateway `procurement-api-5xx`, all ALARM-only → `site-alerts`. No 4XX alarm (401/403/404 are expected traffic).
|
- **Alarms:** `procurement-api-errors`/`-throttles`/`-duration` (p99 ≥ 22.5 s) + gateway `procurement-api-5xx`, all ALARM-only → `site-alerts`. No 4XX alarm (401/403/404 are expected traffic).
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from aws_cdk import (
|
||||||
Duration,
|
Duration,
|
||||||
Stack,
|
Stack,
|
||||||
aws_apigateway as apigateway,
|
aws_apigateway as apigateway,
|
||||||
|
aws_certificatemanager as acm,
|
||||||
aws_cloudwatch as cloudwatch,
|
aws_cloudwatch as cloudwatch,
|
||||||
aws_cloudwatch_actions as cw_actions,
|
aws_cloudwatch_actions as cw_actions,
|
||||||
aws_dynamodb as dynamodb,
|
aws_dynamodb as dynamodb,
|
||||||
|
|
@ -39,6 +40,12 @@ SHOC_BACKEND_DEV_ROLE_ARN = "arn:aws:iam::396287094661:role/shoc-backend-dev"
|
||||||
|
|
||||||
STAGE_NAME = "prod"
|
STAGE_NAME = "prod"
|
||||||
|
|
||||||
|
# Custom domain for the SHOC-facing read API. The seahaven.com zone is in the
|
||||||
|
# mgmt account, so the cert is issued out of band and its ARN handed in via
|
||||||
|
# this SSM parameter (see the custom-domain block below).
|
||||||
|
CUSTOM_DOMAIN_NAME = "procurement-api.seahaven.com"
|
||||||
|
CUSTOM_DOMAIN_CERT_ARN_SSM_PARAM = "/procurement-api/custom-domain/certificate-arn"
|
||||||
|
|
||||||
|
|
||||||
class ProcurementApiStack(Stack):
|
class ProcurementApiStack(Stack):
|
||||||
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
|
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
|
||||||
|
|
@ -280,6 +287,41 @@ class ProcurementApiStack(Stack):
|
||||||
authorization_type=apigateway.AuthorizationType.NONE,
|
authorization_type=apigateway.AuthorizationType.NONE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Custom domain: procurement-api.seahaven.com ---
|
||||||
|
# Stable, brandable endpoint for the SHOC backend to sign SigV4 against
|
||||||
|
# (replaces the opaque execute-api URL). REGIONAL to match the API, so
|
||||||
|
# the ACM cert lives in this account+region (us-east-1).
|
||||||
|
#
|
||||||
|
# CROSS-ACCOUNT DNS: the seahaven.com public zone
|
||||||
|
# (Z06652411XKH89KTZD3XA) is in the mgmt account (328440206208), not
|
||||||
|
# here. So the cert's DNS-validation CNAME and the final A-alias record
|
||||||
|
# are added to that zone OUT OF BAND (see the README runbook /
|
||||||
|
# scripts/setup_procurement_api_domain.sh), and the issued cert ARN is
|
||||||
|
# handed to this stack via SSM. Reading it with value_for_string_
|
||||||
|
# parameter keeps the reference CFN-resolved at deploy -- no synth-time
|
||||||
|
# AWS creds, unlike a cross-account HostedZone.from_lookup. The
|
||||||
|
# A-alias record itself is created out of band too (the zone is not in
|
||||||
|
# this account, so CDK cannot own it); the outputs below give the exact
|
||||||
|
# alias target.
|
||||||
|
cert_arn = ssm.StringParameter.value_for_string_parameter(
|
||||||
|
self, CUSTOM_DOMAIN_CERT_ARN_SSM_PARAM
|
||||||
|
)
|
||||||
|
certificate = acm.Certificate.from_certificate_arn(
|
||||||
|
self, "ProcurementApiCertificate", cert_arn
|
||||||
|
)
|
||||||
|
custom_domain = apigateway.DomainName(
|
||||||
|
self,
|
||||||
|
"ProcurementApiDomain",
|
||||||
|
domain_name=CUSTOM_DOMAIN_NAME,
|
||||||
|
certificate=certificate,
|
||||||
|
endpoint_type=apigateway.EndpointType.REGIONAL,
|
||||||
|
security_policy=apigateway.SecurityPolicy.TLS_1_2,
|
||||||
|
)
|
||||||
|
# Empty base path: the custom domain root maps to the prod stage, so
|
||||||
|
# callers hit https://procurement-api.seahaven.com/work-orders (no
|
||||||
|
# /prod segment -- the mapping strips it).
|
||||||
|
custom_domain.add_base_path_mapping(api, stage=api.deployment_stage)
|
||||||
|
|
||||||
# --- Alarms (ALARM-only -> site-alerts, NOT_BREACHING) ---
|
# --- Alarms (ALARM-only -> site-alerts, NOT_BREACHING) ---
|
||||||
# common.add_standard_lambda_alarms is NOT used here: its duration
|
# common.add_standard_lambda_alarms is NOT used here: its duration
|
||||||
# threshold is a fixed 45000 ms (75% of the processors' 60 s timeout),
|
# threshold is a fixed 45000 ms (75% of the processors' 60 s timeout),
|
||||||
|
|
@ -362,3 +404,24 @@ class ProcurementApiStack(Stack):
|
||||||
value=api_fn.function_arn,
|
value=api_fn.function_arn,
|
||||||
description="ARN of the procurement-api Lambda",
|
description="ARN of the procurement-api Lambda",
|
||||||
)
|
)
|
||||||
|
cdk.CfnOutput(
|
||||||
|
self,
|
||||||
|
"ProcurementApiCustomDomainUrl",
|
||||||
|
value=f"https://{CUSTOM_DOMAIN_NAME}/",
|
||||||
|
description="procurement-api custom domain (SHOC-facing endpoint)",
|
||||||
|
)
|
||||||
|
# A-alias target for the mgmt-account Route53 record. Create an ALIAS A
|
||||||
|
# record: procurement-api.seahaven.com -> this regional domain name,
|
||||||
|
# with this hosted-zone id, EvaluateTargetHealth=false.
|
||||||
|
cdk.CfnOutput(
|
||||||
|
self,
|
||||||
|
"ProcurementApiAliasTarget",
|
||||||
|
value=custom_domain.domain_name_alias_domain_name,
|
||||||
|
description="Route53 A-alias target (add in the mgmt seahaven.com zone)",
|
||||||
|
)
|
||||||
|
cdk.CfnOutput(
|
||||||
|
self,
|
||||||
|
"ProcurementApiAliasHostedZoneId",
|
||||||
|
value=custom_domain.domain_name_alias_hosted_zone_id,
|
||||||
|
description="Route53 A-alias target hosted-zone id (mgmt zone record)",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,16 @@ def _load_spec() -> str:
|
||||||
def _base_url(event: dict) -> str | None:
|
def _base_url(event: dict) -> str | None:
|
||||||
rc = event.get("requestContext") or {}
|
rc = event.get("requestContext") or {}
|
||||||
domain, stage = rc.get("domainName"), rc.get("stage")
|
domain, stage = rc.get("domainName"), rc.get("stage")
|
||||||
return f"https://{domain}/{stage}" if domain and stage else None
|
if not domain:
|
||||||
|
return None
|
||||||
|
# A custom domain (base-path mapping) serves the stage at the root, so the
|
||||||
|
# public URL has NO /{stage} segment; only the raw execute-api host carries
|
||||||
|
# it. Emitting /{stage} for a custom-domain request would advertise a
|
||||||
|
# broken server URL in the docs. All API Gateway default domains end with
|
||||||
|
# .amazonaws.com, so check the suffix rather than a substring.
|
||||||
|
if domain.endswith(".amazonaws.com"):
|
||||||
|
return f"https://{domain}/{stage}" if stage else None
|
||||||
|
return f"https://{domain}"
|
||||||
|
|
||||||
|
|
||||||
def _spec_for_request(event: dict) -> str:
|
def _spec_for_request(event: dict) -> str:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
"description": "Read API over the procurement-ingest pipelines (work orders and purchase orders). The outbound SHOC work-order webhook feed is documented under **webhooks** below.\n\nData endpoints use AWS IAM SigV4; the `/docs` and `/openapi.json` routes use a shared token. Listings are unordered, cursor-paginated scans. Endpoints tagged **x-planned** are phase 2 and currently answer `501`.\n\nThis API replaces SHOC's retired SyncController DynamoDB scan as the reconciliation and backfill path. Full detail: the repo README and `docs/shoc-webhook-contract.md`."
|
"description": "Read API over the procurement-ingest pipelines (work orders and purchase orders). The outbound SHOC work-order webhook feed is documented under **webhooks** below.\n\nData endpoints use AWS IAM SigV4; the `/docs` and `/openapi.json` routes use a shared token. Listings are unordered, cursor-paginated scans. Endpoints tagged **x-planned** are phase 2 and currently answer `501`.\n\nThis API replaces SHOC's retired SyncController DynamoDB scan as the reconciliation and backfill path. Full detail: the repo README and `docs/shoc-webhook-contract.md`."
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "https://procurement-api.seahaven.com",
|
||||||
|
"description": "seahaven-prod (011934824531) custom domain — recommended."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://mvul1efda2.execute-api.us-east-1.amazonaws.com/prod",
|
"url": "https://mvul1efda2.execute-api.us-east-1.amazonaws.com/prod",
|
||||||
"description": "seahaven-prod (011934824531)."
|
"description": "seahaven-prod (011934824531)."
|
||||||
|
|
|
||||||
128
scripts/setup_procurement_api_domain.sh
Executable file
128
scripts/setup_procurement_api_domain.sh
Executable file
|
|
@ -0,0 +1,128 @@
|
||||||
|
#!/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
|
||||||
|
|
@ -130,6 +130,47 @@ def test_docs_served_when_authenticated(api, monkeypatch):
|
||||||
assert json.loads(spec["body"])["openapi"] == "3.1.0"
|
assert json.loads(spec["body"])["openapi"] == "3.1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_url_custom_domain_omits_stage(api):
|
||||||
|
mod, _ = api
|
||||||
|
# execute-api host keeps the /{stage} segment...
|
||||||
|
execapi = {
|
||||||
|
"requestContext": {
|
||||||
|
"domainName": "mvul1efda2.execute-api.us-east-1.amazonaws.com",
|
||||||
|
"stage": "prod",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert mod._base_url(execapi) == (
|
||||||
|
"https://mvul1efda2.execute-api.us-east-1.amazonaws.com/prod"
|
||||||
|
)
|
||||||
|
# ...but a custom domain (base-path mapping) serves the stage at the root,
|
||||||
|
# so no /{stage} is appended (it would advertise a broken server URL).
|
||||||
|
custom = {
|
||||||
|
"requestContext": {
|
||||||
|
"domainName": "procurement-api.seahaven.com",
|
||||||
|
"stage": "prod",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert mod._base_url(custom) == "https://procurement-api.seahaven.com"
|
||||||
|
assert mod._base_url({}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_spec_injects_custom_domain_server(api):
|
||||||
|
mod, _ = api
|
||||||
|
event = {
|
||||||
|
"requestContext": {
|
||||||
|
"domainName": "procurement-api.seahaven.com",
|
||||||
|
"stage": "prod",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec = json.loads(mod._spec_for_request(event))
|
||||||
|
urls = [s["url"] for s in spec["servers"]]
|
||||||
|
# Accessed via the custom domain, every server URL resolves to the custom
|
||||||
|
# domain (the static entry plus the injected one), and none carry a /prod
|
||||||
|
# suffix. Exact-equality checks (no substring-in-URL) so this reads
|
||||||
|
# cleanly and doesn't trip CodeQL's URL-sanitization heuristic.
|
||||||
|
assert set(urls) == {"https://procurement-api.seahaven.com"}
|
||||||
|
|
||||||
|
|
||||||
def test_token_query_shim_synthesizes_header(api, monkeypatch):
|
def test_token_query_shim_synthesizes_header(api, monkeypatch):
|
||||||
mod, _ = api
|
mod, _ = api
|
||||||
seen = {}
|
seen = {}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue