From 2da78850b9e0101f7df47a46a0bf8254a28c29cf Mon Sep 17 00:00:00 2001 From: "Bode (Kiro Crew Issue Radar)" Date: Tue, 25 Aug 2026 19:08:10 +0000 Subject: [PATCH 1/2] fix: resolve assumed roles for IAM simulation --- install.sh | 45 +++++++- tests/test-permission-preflight.sh | 175 +++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 tests/test-permission-preflight.sh diff --git a/install.sh b/install.sh index adbd8aa..854566a 100755 --- a/install.sh +++ b/install.sh @@ -1120,6 +1120,41 @@ verify_aws_credentials() { # Reusable AWS helpers # ============================================================================ +# Resolve the caller identity to an ARN accepted by IAM policy simulation. +# STS assumed-role session ARNs are not valid policy sources, so look up the +# underlying IAM role. get-role also preserves any path in the role ARN. +resolve_policy_source_arn() { + local caller_arn="$1" + case "$caller_arn" in + arn:*:iam::*:user/*|arn:*:iam::*:role/*) + printf '%s\n' "$caller_arn" + ;; + arn:*:sts::*:assumed-role/*/*) + local assumed_role_suffix role_name role_arn + assumed_role_suffix="${caller_arn#*:assumed-role/}" + role_name="${assumed_role_suffix%%/*}" + if ! role_arn=$(aws iam get-role \ + --role-name "$role_name" \ + --query 'Role.Arn' \ + --output text 2>&1); then + printf 'Could not resolve assumed role %s: %s\n' "$role_name" "$role_arn" + return 1 + fi + case "$role_arn" in + arn:*:iam::*:role/*) printf '%s\n' "$role_arn" ;; + *) + printf 'IAM returned an invalid role ARN for %s: %s\n' "$role_name" "$role_arn" + return 1 + ;; + esac + ;; + *) + printf 'Unsupported caller ARN for IAM policy simulation: %s\n' "$caller_arn" + return 1 + ;; + esac +} + # Create a private S3 bucket with versioning + KMS encryption create_s3_bucket() { local bucket="$1" region="$2" @@ -1676,9 +1711,15 @@ check_vpc_quota() { check_permissions() { echo "" info "Checking permissions..." - local denied_actions + local denied_actions policy_source_arn + if ! policy_source_arn=$(resolve_policy_source_arn "$CALLER_ARN"); then + warn "Could not resolve caller identity for deployment permission verification: ${policy_source_arn}" + confirm_or_abort "Continue without verified permissions?" + return 0 + fi + if ! denied_actions=$(aws iam simulate-principal-policy \ - --policy-source-arn "$CALLER_ARN" \ + --policy-source-arn "$policy_source_arn" \ --action-names "cloudformation:CreateStack" "iam:CreateRole" "ec2:CreateVpc" \ "ec2:CreateVpcBlockPublicAccessExclusion" "ec2:DescribeVpcBlockPublicAccessExclusions" \ "ec2:ModifyVpcBlockPublicAccessExclusion" "ec2:DeleteVpcBlockPublicAccessExclusion" \ diff --git a/tests/test-permission-preflight.sh b/tests/test-permission-preflight.sh new file mode 100644 index 0000000..6ef4e44 --- /dev/null +++ b/tests/test-permission-preflight.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# tests/test-permission-preflight.sh — IAM policy simulation identity handling +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INSTALL_SH="${REPO_ROOT}/install.sh" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +PASS=0 +FAIL=0 +pass() { printf ' ✓ %s\n' "$1"; PASS=$((PASS + 1)); } +fail_test() { printf ' ✗ %s\n' "$1"; FAIL=$((FAIL + 1)); } +assert_eq() { + local description="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + pass "$description" + else + fail_test "$description (expected: $expected, actual: $actual)" + fi +} +assert_contains() { + local description="$1" needle="$2" haystack="$3" + [[ "$haystack" == *"$needle"* ]] \ + && pass "$description" \ + || fail_test "$description (missing: $needle)" +} + +FUNCTIONS="${TMPDIR}/functions.sh" +sed -n '/^resolve_policy_source_arn() {/,/^}/p' "$INSTALL_SH" > "$FUNCTIONS" +sed -n '/^check_permissions() {/,/^}/p' "$INSTALL_SH" >> "$FUNCTIONS" +# shellcheck source=/dev/null +source "$FUNCTIONS" + +printf '── Permission preflight identity resolution ──\n' + +test_iam_user_is_passed_through() { + aws() { printf 'unexpected AWS call\n' >&2; return 1; } + local actual + actual="$(resolve_policy_source_arn 'arn:aws:iam::123456789012:user/deployer')" + assert_eq "IAM user ARN is passed through" \ + "arn:aws:iam::123456789012:user/deployer" "$actual" +} +test_iam_user_is_passed_through + +test_iam_role_is_passed_through() { + aws() { printf 'unexpected AWS call\n' >&2; return 1; } + local actual + actual="$(resolve_policy_source_arn 'arn:aws:iam::123456789012:role/platform/deployer')" + assert_eq "IAM role ARN is passed through" \ + "arn:aws:iam::123456789012:role/platform/deployer" "$actual" +} +test_iam_role_is_passed_through + +test_assumed_role_resolves_iam_role_with_path() { + local call_log="${TMPDIR}/get-role-call" + aws() { + printf '%s\n' "$*" > "$call_log" + printf 'arn:aws:iam::123456789012:role/platform/Admin\n' + } + local actual + actual="$(resolve_policy_source_arn \ + 'arn:aws:sts::123456789012:assumed-role/Admin/deploy-session')" + assert_eq "STS assumed role resolves to IAM role ARN with path" \ + "arn:aws:iam::123456789012:role/platform/Admin" "$actual" + assert_eq "role resolution uses the parsed role name" \ + "iam get-role --role-name Admin --query Role.Arn --output text" \ + "$(cat "$call_log")" +} +test_assumed_role_resolves_iam_role_with_path + +test_assumed_role_resolution_failure_is_reported() { + aws() { printf 'AccessDenied from get-role\n' >&2; return 254; } + local output + if output="$(resolve_policy_source_arn \ + 'arn:aws:sts::123456789012:assumed-role/Admin/deploy-session' 2>&1)"; then + fail_test "assumed-role resolution failure returns nonzero" + else + pass "assumed-role resolution failure returns nonzero" + fi + assert_contains "assumed-role resolution failure keeps AWS error" \ + "AccessDenied from get-role" "$output" +} +test_assumed_role_resolution_failure_is_reported + +test_check_permissions_simulates_resolved_role() { + local source_log="${TMPDIR}/simulation-source" + CALLER_ARN='arn:aws:sts::123456789012:assumed-role/Admin/deploy-session' + info() { :; } + warn() { printf 'unexpected warning: %s\n' "$*" >&2; return 1; } + confirm_or_abort() { printf 'unexpected confirmation: %s\n' "$*" >&2; return 1; } + ok() { printf '%s\n' "$*" > "${TMPDIR}/ok-message"; } + aws() { + if [[ "$1 $2" == "iam get-role" ]]; then + printf 'arn:aws:iam::123456789012:role/platform/Admin\n' + return 0 + fi + if [[ "$1 $2" == "iam simulate-principal-policy" ]]; then + while (($#)); do + if [[ "$1" == "--policy-source-arn" ]]; then + printf '%s\n' "$2" > "$source_log" + break + fi + shift + done + return 0 + fi + return 1 + } + + check_permissions >/dev/null + assert_eq "permission simulation uses resolved IAM role ARN" \ + "arn:aws:iam::123456789012:role/platform/Admin" "$(cat "$source_log")" + assert_eq "successful simulation reports verified permissions" \ + "Permissions verified" "$(cat "${TMPDIR}/ok-message")" +} +test_check_permissions_simulates_resolved_role + +test_check_permissions_handles_resolution_failure() { + local warning_log="${TMPDIR}/resolution-warning" + local confirm_log="${TMPDIR}/resolution-confirm" + CALLER_ARN='arn:aws:sts::123456789012:assumed-role/Admin/deploy-session' + info() { :; } + ok() { printf 'must not claim verification\n' > "${TMPDIR}/unexpected-ok"; } + warn() { printf '%s\n' "$*" > "$warning_log"; } + confirm_or_abort() { printf '%s\n' "$*" > "$confirm_log"; } + aws() { + [[ "$1 $2" == "iam get-role" ]] || return 99 + printf 'AccessDenied from get-role\n' >&2 + return 254 + } + + check_permissions >/dev/null + assert_contains "resolution failure warns accurately" \ + "Could not resolve caller identity for deployment permission verification" \ + "$(cat "$warning_log")" + assert_eq "resolution failure asks before continuing" \ + "Continue without verified permissions?" "$(cat "$confirm_log")" + [[ ! -e "${TMPDIR}/unexpected-ok" ]] \ + && pass "resolution failure does not claim verification" \ + || fail_test "resolution failure does not claim verification" +} +test_check_permissions_handles_resolution_failure + +test_check_permissions_handles_simulator_failure() { + local warning_log="${TMPDIR}/simulator-warning" + local confirm_log="${TMPDIR}/simulator-confirm" + CALLER_ARN='arn:aws:iam::123456789012:role/Admin' + info() { :; } + ok() { printf 'must not claim verification\n' > "${TMPDIR}/unexpected-ok"; } + warn() { printf '%s\n' "$*" > "$warning_log"; } + confirm_or_abort() { printf '%s\n' "$*" > "$confirm_log"; } + aws() { + [[ "$1 $2" == "iam simulate-principal-policy" ]] || return 99 + printf 'ServiceFailure from simulator\n' >&2 + return 254 + } + + check_permissions >/dev/null + assert_contains "simulator failure keeps AWS error" \ + "ServiceFailure from simulator" "$(cat "$warning_log")" + assert_eq "simulator failure asks before continuing" \ + "Continue without verified permissions?" "$(cat "$confirm_log")" + [[ ! -e "${TMPDIR}/unexpected-ok" ]] \ + && pass "simulator failure does not claim verification" \ + || fail_test "simulator failure does not claim verification" +} +test_check_permissions_handles_simulator_failure + +printf '\n' +if ((FAIL > 0)); then + printf '%d passed, %d failed\n' "$PASS" "$FAIL" + exit 1 +fi +printf '%d passed, 0 failed\n' "$PASS" From 4796c227c58d4dacb083a5e0535f3e1069dfeeab Mon Sep 17 00:00:00 2001 From: "Bode (Kiro Crew Issue Radar)" Date: Tue, 25 Aug 2026 19:52:37 +0000 Subject: [PATCH 2/2] fix: qualify assumed-role permission checks --- install.sh | 3 +++ tests/test-permission-preflight.sh | 36 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index 854566a..f19b5fc 100755 --- a/install.sh +++ b/install.sh @@ -1733,6 +1733,9 @@ check_permissions() { if [[ -n "$denied_actions" ]]; then warn "Some permissions may be missing: ${denied_actions}" confirm_or_abort "Continue anyway?" + elif [[ "$CALLER_ARN" == arn:*:sts::*:assumed-role/*/* ]]; then + warn "The underlying IAM role allows the requested deployment actions, but this simulation cannot evaluate restrictions from the active STS session policies." + confirm_or_abort "Continue with partially verified permissions?" "default_yes" else ok "Permissions verified" fi diff --git a/tests/test-permission-preflight.sh b/tests/test-permission-preflight.sh index 6ef4e44..6b55c48 100644 --- a/tests/test-permission-preflight.sh +++ b/tests/test-permission-preflight.sh @@ -85,11 +85,13 @@ test_assumed_role_resolution_failure_is_reported test_check_permissions_simulates_resolved_role() { local source_log="${TMPDIR}/simulation-source" + local warning_log="${TMPDIR}/assumed-role-warning" + local confirm_log="${TMPDIR}/assumed-role-confirm" CALLER_ARN='arn:aws:sts::123456789012:assumed-role/Admin/deploy-session' info() { :; } - warn() { printf 'unexpected warning: %s\n' "$*" >&2; return 1; } - confirm_or_abort() { printf 'unexpected confirmation: %s\n' "$*" >&2; return 1; } - ok() { printf '%s\n' "$*" > "${TMPDIR}/ok-message"; } + warn() { printf '%s\n' "$*" > "$warning_log"; } + confirm_or_abort() { printf '%s\n' "$*" > "$confirm_log"; } + ok() { printf 'must not claim full verification\n' > "${TMPDIR}/unexpected-ok"; } aws() { if [[ "$1 $2" == "iam get-role" ]]; then printf 'arn:aws:iam::123456789012:role/platform/Admin\n' @@ -111,11 +113,35 @@ test_check_permissions_simulates_resolved_role() { check_permissions >/dev/null assert_eq "permission simulation uses resolved IAM role ARN" \ "arn:aws:iam::123456789012:role/platform/Admin" "$(cat "$source_log")" - assert_eq "successful simulation reports verified permissions" \ - "Permissions verified" "$(cat "${TMPDIR}/ok-message")" + assert_contains "assumed-role success warns about session-policy limits" \ + "cannot evaluate restrictions from the active STS session policies" \ + "$(cat "$warning_log")" + assert_eq "assumed-role success asks before partially verified continuation" \ + "Continue with partially verified permissions? default_yes" \ + "$(cat "$confirm_log")" + [[ ! -e "${TMPDIR}/unexpected-ok" ]] \ + && pass "assumed-role success does not claim full verification" \ + || fail_test "assumed-role success does not claim full verification" } test_check_permissions_simulates_resolved_role +test_check_permissions_verifies_direct_iam_role() { + CALLER_ARN='arn:aws:iam::123456789012:role/platform/Admin' + info() { :; } + warn() { printf 'unexpected warning: %s\n' "$*" >&2; return 1; } + confirm_or_abort() { printf 'unexpected confirmation: %s\n' "$*" >&2; return 1; } + ok() { printf '%s\n' "$*" > "${TMPDIR}/direct-role-ok"; } + aws() { + [[ "$1 $2" == "iam simulate-principal-policy" ]] || return 99 + return 0 + } + + check_permissions >/dev/null + assert_eq "direct IAM role success reports verified permissions" \ + "Permissions verified" "$(cat "${TMPDIR}/direct-role-ok")" +} +test_check_permissions_verifies_direct_iam_role + test_check_permissions_handles_resolution_failure() { local warning_log="${TMPDIR}/resolution-warning" local confirm_log="${TMPDIR}/resolution-confirm"