mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 13:03:14 +00:00
* feat(migration): prepare stacks and tooling for the seahaven-prod account move
Phase 1 of the mgmt (328440206208) -> seahaven-prod (011934824531)
migration. No behavior change in-account; everything here is additive or
account-portability hygiene:
- infra/deploy-role/: reviewed OIDC deploy-role artifacts for prod
(trust main-only, cdk-hnb659fds-* AssumeRole, smoke-invoke-lambda scoped
to exactly the two email-processor fn ARNs). Codifies the previously
out-of-band smoke-invoke grant.
- Table resource policies: make_slack_bot_read_policy in cdk/common.py,
applied to purchase-orders, verified-sites, WorkOrders,
WorkOrderComments (NOT pending-site-review; no bot consumer). Grants the
mgmt-resident seahaven-slack-bot roles read-only cross-account access
post-move (bot-side identity grants land in the slack-bot repo).
- scripts/migrate_tables.py: dry-run-default backfill tool implementing
the plan's per-table semantics (superset overwrite, ingested_at cutoff
for WorkOrderComments, backup-gated truncate-and-load for the two
site tables) plus a verify subcommand (count parity, spot checks,
sticky-Cancelled drift check).
- tests/test_resource_policy_helper.py: statement-shape unit tests +
static pins that exactly the four bot-read tables carry the policy.
- Account-literal fixes: account-agnostic fixture bucket in
test_reprocess_contract; runbook/README/po-template-parser account
references updated to prod with historical mgmt notes; README gains the
account-prerequisites list (imported-by-name dependencies).
deploy.yaml is deliberately unchanged (push-to-main auto-deploy kept).
Merge is held until migration Phase 0 completes; flipping the
AWS_DEPLOY_ROLE_ARN repo secret and merging this PR IS the first prod
deploy.
* fix(migration): verify backup AVAILABLE pre-truncate; document wildcard risk acceptance (cross-review FIX/NIT)
* refactor(migration): drop cross-account read grants (slack-bot decommissioned); harden backfill + deploy role
seahaven-slack-bot was decommissioned 2026-07-23 (stack DELETE_IN_PROGRESS,
consumer Lambdas gone); its successor sh-mcp is undeployed and uses
same-account DynamoDB access. So no live consumer reads these tables
cross-account. Per Adam's call, drop the cross-account grants entirely and
re-add correctly-scoped ones if/when sh-mcp deploys to a different account.
- Remove the four table resource policies + make_slack_bot_read_policy helper
+ its constants (cdk/common.py, po_stack.py, wo_stack.py) and the helper's
unit test. Both stacks synth with zero table ResourcePolicy.
- scripts/migrate_tables.py hardening (fixes from the sh-security-review
fan-out on the destructive backfill tool):
* validate --cutoff strictly (parse ISO-8601, require aware UTC, re-emit
canonical second-precision form) so a malformed cutoff can't silently
copy dual-window rows or drop history;
* reject `copy --all` up front (must run tables individually, in order,
with the stream-drain wait) instead of writing three tables then erroring;
* truncate backup gate now also checks recency (<1h) and TableId, not just
status+name;
* verify requires --cutoff whenever a cutoff table is in scope (else it
false-flags dual-window rows as MISSING);
* sticky-cancel is now PREVENTED copy-side (a non-Cancelled source item
never overwrites a dest-Cancelled PO), and the verify comment no longer
overstates what its source-side scan covers;
* spot-check all modes (truncate_load keys are verbatim, so key-existence
is sound there too).
- Deploy role: scope cloudformation:DescribeStacks to this repo's stacks +
CDKToolkit (was Resource:*, disclosed all tenant stacks in the shared prod
account); add a drift check warning on unexpected role policies and drop the
dead SMOKE_POLICY_NAME var; document the shared-account bootstrap-role
accepted risk in the deploy-role README.
* docs(deploy-role): fold in cross-review NITs (DescribeStacks maintenance note, warn-only drift rationale)
* ci: update workflow to use new workflow tag (ruff versioning fix)
* fix(migration): address Open SWE review findings on migrate_tables.py
- Validate the truncate backup on dry-run as well as --execute so a
missing/stale/wrong-incarnation --backup-arn surfaces on the rehearsal
run (finding f_24a48b8900).
- Assert configured keys match the live key schema of both tables before
any key projection, turning config/schema drift into a descriptive
abort instead of a mid-backfill KeyError (finding f_cb6b5a6c59).
- Clarify why key-existence spot-checks are sound for WorkOrderComments:
the copy Puts source items verbatim and the sample uses the same
cutoff filter, so per-account comment_id divergence never enters the
check (finding f_390b7d6c3b is a false positive; comment hardened).
96 lines
4.8 KiB
Bash
Executable file
96 lines
4.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
###############################################################################
|
|
# create-deploy-role.sh
|
|
#
|
|
# Creates / updates the GitHub Actions OIDC deploy role
|
|
# `githubdeploy-procurement-ingest` in AWS account 011934824531 (seahaven-prod),
|
|
# us-east-1, for the Sea-Haven-Industries/procurement-ingest repo (main branch).
|
|
#
|
|
# Part of the mgmt -> seahaven-prod migration. The mgmt-account role of the
|
|
# same name is left untouched until decommission (emergency mgmt deploy path).
|
|
#
|
|
# GATE - DO NOT EXECUTE until BOTH of the following have passed on these
|
|
# exact artifact files:
|
|
# 1. GPT-4.1 cross-family review (IAM policy / trust-policy change) via
|
|
# cross_review.py in the security-review repo.
|
|
# 2. /sh-security-review (deep agentic pass - IaC/IAM is a gated surface).
|
|
#
|
|
# The SmokeInvokeLambda statement is REQUIRED, not optional: deploy.yaml runs
|
|
# scripts/post-deploy-smoke.sh under this role's session (not the assumed
|
|
# cdk-* roles), and the smoke gate fails closed without lambda:InvokeFunction
|
|
# on exactly the two email-processor function ARNs. The mgmt-era equivalent
|
|
# of this policy was applied out-of-band and documented nowhere; these
|
|
# artifacts are the fix for that debt.
|
|
#
|
|
# Resolved facts (verified 2026-07-23, read-only):
|
|
# Account : 011934824531 (seahaven-prod)
|
|
# Region : us-east-1
|
|
# Qualifier : hnb659fds (AWS CDK DEFAULT - matches cdk/cdk.json)
|
|
# OIDC prov : arn:aws:iam::011934824531:oidc-provider/token.actions.githubusercontent.com
|
|
###############################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
PROFILE="seahaven-prod"
|
|
ROLE_NAME="githubdeploy-procurement-ingest"
|
|
POLICY_NAME="procurement-ingest-deploy"
|
|
ACCOUNT_ID="011934824531"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
TRUST_POLICY="file://${SCRIPT_DIR}/trust-policy.json"
|
|
PERMS_POLICY="file://${SCRIPT_DIR}/permissions-policy.json"
|
|
|
|
echo "==> Verifying active account for profile '${PROFILE}'..."
|
|
CALLER_ACCOUNT="$(aws --profile "${PROFILE}" sts get-caller-identity --query Account --output text)"
|
|
if [[ "${CALLER_ACCOUNT}" != "${ACCOUNT_ID}" ]]; then
|
|
echo "ERROR: profile '${PROFILE}' resolves to account ${CALLER_ACCOUNT}, expected ${ACCOUNT_ID}. Aborting." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> Ensuring role '${ROLE_NAME}' exists with the correct trust policy..."
|
|
if aws --profile "${PROFILE}" iam get-role --role-name "${ROLE_NAME}" >/dev/null 2>&1; then
|
|
echo " Role exists - updating assume-role (trust) policy."
|
|
aws --profile "${PROFILE}" iam update-assume-role-policy \
|
|
--role-name "${ROLE_NAME}" \
|
|
--policy-document "${TRUST_POLICY}"
|
|
else
|
|
echo " Role absent - creating."
|
|
aws --profile "${PROFILE}" iam create-role \
|
|
--role-name "${ROLE_NAME}" \
|
|
--assume-role-policy-document "${TRUST_POLICY}" \
|
|
--description "GitHub Actions OIDC deploy role for Sea-Haven-Industries/procurement-ingest (main)" \
|
|
--max-session-duration 3600 \
|
|
--tags Key=project,Value=procurement-ingest Key=managed-by,Value=create-deploy-role.sh
|
|
fi
|
|
|
|
echo "==> Putting inline permissions policy '${POLICY_NAME}' (create-or-replace)..."
|
|
aws --profile "${PROFILE}" iam put-role-policy \
|
|
--role-name "${ROLE_NAME}" \
|
|
--policy-name "${POLICY_NAME}" \
|
|
--policy-document "${PERMS_POLICY}"
|
|
|
|
# Drift check: this script only reconciles the single inline policy above, so
|
|
# any OTHER inline or attached policy on the role is an unreviewed grant that
|
|
# diverges the live role from these artifacts. Warn-only (never auto-delete) is
|
|
# deliberate: this is a shared prod-tenant account and an extra policy might be
|
|
# a legitimate out-of-band grant an operator must adjudicate, not blindly strip.
|
|
echo "==> Checking for unexpected policies on the role..."
|
|
EXTRA_INLINE="$(aws --profile "${PROFILE}" iam list-role-policies \
|
|
--role-name "${ROLE_NAME}" --query "PolicyNames[?@!='${POLICY_NAME}']" --output text)"
|
|
EXTRA_ATTACHED="$(aws --profile "${PROFILE}" iam list-attached-role-policies \
|
|
--role-name "${ROLE_NAME}" --query 'AttachedPolicies[].PolicyName' --output text)"
|
|
if [[ -n "${EXTRA_INLINE}" || -n "${EXTRA_ATTACHED}" ]]; then
|
|
echo " WARNING: unreviewed policies present on ${ROLE_NAME} (not in these artifacts):" >&2
|
|
[[ -n "${EXTRA_INLINE}" ]] && echo " inline: ${EXTRA_INLINE}" >&2
|
|
[[ -n "${EXTRA_ATTACHED}" ]] && echo " attached: ${EXTRA_ATTACHED}" >&2
|
|
echo " Review and remove them (delete-role-policy / detach-role-policy) if unexpected." >&2
|
|
else
|
|
echo " OK - only ${POLICY_NAME} present."
|
|
fi
|
|
|
|
ROLE_ARN="$(aws --profile "${PROFILE}" iam get-role --role-name "${ROLE_NAME}" \
|
|
--query Role.Arn --output text)"
|
|
|
|
echo "==> Done. Deploy role ready:"
|
|
echo " ${ROLE_ARN}"
|
|
echo " Set the GitHub repo secret AWS_DEPLOY_ROLE_ARN to this ARN (Phase 1 merge sequencing:"
|
|
echo " flip the secret only when Phase 0 is complete and the migration PR is about to merge)."
|