Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" \
Comment thread
royosherove marked this conversation as resolved.
--action-names "cloudformation:CreateStack" "iam:CreateRole" "ec2:CreateVpc" \
"ec2:CreateVpcBlockPublicAccessExclusion" "ec2:DescribeVpcBlockPublicAccessExclusions" \
"ec2:ModifyVpcBlockPublicAccessExclusion" "ec2:DeleteVpcBlockPublicAccessExclusion" \
Expand All @@ -1692,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
Expand Down
201 changes: 201 additions & 0 deletions tests/test-permission-preflight.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#!/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"
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 '%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'
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_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"
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"