#!/usr/bin/env bash ############################################################################### # create-assessment-reader.sh # # Creates / updates the TEMPORARY cross-account read role # `shoc-assessment-dynamo-reader` in AWS account 011934824531 (seahaven-prod), # us-east-1, for the Luby team's initial data assessment from # seahaven-external-dev (396287094661). # # TEMPORARY BY DESIGN: # - Hard expiry 2026-08-23T00:00:00Z enforced in BOTH layers: the trust # policy (new AssumeRole calls fail) and every permissions statement # (in-flight sessions lose data access at the same instant), so access # dies at the deadline even if teardown never runs. # - Steady-state SHOC access is the procurement-api SigV4 read API plus the # shoc-webhook emitter, NOT this role. Tear this down (or let it expire) # once the assessment is done; extend only by a deliberate edit to # trust-policy.json re-run through the review gates. # # GATE - DO NOT EXECUTE until BOTH of the following have passed on these # exact artifact files: # 1. GPT-4.1 cross-family review (IAM trust/permissions change) via # cross_review.py in the security-review repo. # 2. /sh-security-review (deep agentic pass - IaC/IAM is a gated surface). # # Design notes (deliberate, do not "fix" without re-review): # - Account-root trust (396287094661:root), NOT a pinned role: the assessors # are the Luby humans on SSO Admin sessions in external-dev, and every # principal in that account is Luby-controlled + SCP/boundary-contained. # - NO sts:ExternalId condition: console Switch-Role cannot pass one, and # there is no third-party deputy in this human-driven path. (The old # mgmt-account `shoc-dynamo-reader` used ExternalId because it served an # EC2 role, i.e. an actual service deputy.) # - dynamodb:DescribeTable is included beyond the requested # GetItem/Query/Scan because the DynamoDB console item explorer and most # SDK table bindings require it (metadata only). # - kms:Decrypt is ViaService-pinned to DynamoDB: the three po-ingest tables # are CMK-encrypted (seahaven-dynamodb-cmk); the WO tables are # AWS-owned-key encrypted and need no KMS grant. # - Max session 3600s: sessions from external-dev are role-chained (SSO -> # Admin role -> this role), and chained sessions cap at 1h regardless. ############################################################################### set -euo pipefail PROFILE="seahaven-prod" ROLE_NAME="shoc-assessment-dynamo-reader" POLICY_NAME="shoc-assessment-dynamo-read" ACCOUNT_ID="011934824531" SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" 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 CALLER_ARN="$(aws --profile "${PROFILE}" sts get-caller-identity --query Arn --output text)" echo " Audit: run by ${CALLER_ARN} at $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "==> Validating the KMS key ARN in permissions-policy.json against SSM /seahaven/dynamodb/cmk-arn..." LIVE_CMK_ARN="$(aws --profile "${PROFILE}" ssm get-parameter \ --name /seahaven/dynamodb/cmk-arn --query Parameter.Value --output text)" if ! grep -qF "\"${LIVE_CMK_ARN}\"" "${SCRIPT_DIR}/permissions-policy.json"; then echo "ERROR: permissions-policy.json does not reference the live DynamoDB CMK (${LIVE_CMK_ARN})." >&2 echo " Copy/paste drift would make CMK-table reads fail silently at runtime. Aborting." >&2 exit 1 fi echo " OK - policy references the live CMK." 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 + re-asserting session cap." aws --profile "${PROFILE}" iam update-assume-role-policy \ --role-name "${ROLE_NAME}" \ --policy-document "${TRUST_POLICY}" aws --profile "${PROFILE}" iam update-role \ --role-name "${ROLE_NAME}" \ --max-session-duration 3600 else echo " Role absent - creating." aws --profile "${PROFILE}" iam create-role \ --role-name "${ROLE_NAME}" \ --assume-role-policy-document "${TRUST_POLICY}" \ --description "TEMPORARY read-only DynamoDB access for Luby initial assessment from seahaven-external-dev; trust self-expires 2026-08-23" \ --max-session-duration 3600 \ --tags Key=project,Value=procurement-ingest Key=managed-by,Value=create-assessment-reader.sh Key=temporary,Value=expires-2026-08-23 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}" 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. Assessment reader ready (expires 2026-08-23T00:00:00Z):" echo " ${ROLE_ARN}"