Repository navigation
fix(idle-shutdown): give DescribeInstanceStatus Resource * in wake policy #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| #!/usr/bin/env bash | ||
| # tests/test-idle-wake-policy.sh — Contract test for the wake Lambda IAM policy | ||
| # in bootstraps/optional/idle-shutdown/install-idle.sh. | ||
| # | ||
| # EC2 Describe* actions do not support resource-level permissions. They match | ||
| # only "Resource": "*". This test reads the WAKE_INLINE policy from the script | ||
| # and makes sure that: | ||
| # - the policy is valid JSON after variable substitution | ||
| # - each statement with an ec2:Describe* action has "Resource": "*" | ||
| # - ec2:StartInstances stays limited to the one instance ARN | ||
| # | ||
| # No AWS required. Run standalone or via CI (discovered by pack-tests.yml). | ||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| INSTALL="$REPO_ROOT/bootstraps/optional/idle-shutdown/install-idle.sh" | ||
|
|
||
| if [[ ! -f "$INSTALL" ]]; then | ||
| echo "FAIL: install-idle.sh not found at $INSTALL" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| RED='\033[0;31m'; GREEN='\033[0;32m'; BOLD='\033[1m'; NC='\033[0m' | ||
| PASS=0; FAIL=0 | ||
|
|
||
| pass() { printf "${GREEN} \xE2\x9C\x93${NC} %s\n" "$1"; PASS=$((PASS + 1)); } | ||
| fail() { printf "${RED} \xE2\x9C\x97${NC} %s\n" "$1"; FAIL=$((FAIL + 1)); } | ||
|
|
||
| printf "${BOLD}idle-shutdown wake Lambda IAM policy${NC}\n" | ||
|
|
||
| # Get the heredoc body between 'WAKE_INLINE=$(cat <<JSON' and the closing 'JSON'. | ||
| POLICY="$(awk '/^WAKE_INLINE=\$\(cat <<JSON$/{f=1; next} f && /^JSON$/{exit} f' "$INSTALL")" | ||
|
|
||
| if [[ -z "$POLICY" ]]; then | ||
| fail "WAKE_INLINE heredoc found in install-idle.sh" | ||
| printf "\n%d passed, %d failed\n" "$PASS" "$FAIL" | ||
| exit 1 | ||
| fi | ||
| pass "WAKE_INLINE heredoc found in install-idle.sh" | ||
|
|
||
| # Check the policy with Python. Each output line is "PASS <msg>" or "FAIL <msg>". | ||
| RESULTS="$(POLICY="$POLICY" python3 - <<'PY' | ||
| import json, os | ||
|
|
||
| region, acct, inst = "us-east-1", "123456789012", "i-0123456789abcdef0" | ||
| text = (os.environ["POLICY"] | ||
| .replace("${REGION}", region) | ||
| .replace("${ACCT_ID}", acct) | ||
| .replace("${INSTANCE_ID}", inst)) | ||
| instance_arn = f"arn:aws:ec2:{region}:{acct}:instance/{inst}" | ||
|
|
||
| def out(ok, msg): | ||
| print(("PASS " if ok else "FAIL ") + msg) | ||
|
|
||
| try: | ||
| doc = json.loads(text) | ||
| except ValueError as e: | ||
| out(False, f"policy is valid JSON ({e})") | ||
| raise SystemExit(0) | ||
| out(True, "policy is valid JSON") | ||
|
|
||
| def as_list(v): | ||
| return v if isinstance(v, list) else [v] | ||
|
|
||
| stmts = as_list(doc.get("Statement", [])) | ||
| describe_seen = False | ||
| start_seen = False | ||
| for s in stmts: | ||
| actions = as_list(s.get("Action", [])) | ||
| resources = as_list(s.get("Resource", [])) | ||
| describes = [a for a in actions if a.startswith("ec2:Describe")] | ||
| if describes: | ||
| describe_seen = True | ||
| out(resources == ["*"], | ||
| f"{', '.join(describes)} uses Resource \"*\" (got {resources})") | ||
| if "ec2:StartInstances" in actions: | ||
| start_seen = True | ||
| out(resources == [instance_arn], | ||
| f"ec2:StartInstances is limited to the instance ARN (got {resources})") | ||
|
|
||
| out(describe_seen, "policy allows ec2:DescribeInstanceStatus") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This assertion labels any Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in d767a4b. The test now sets the "allows ec2:DescribeInstanceStatus" result only when that exact action is in a statement. I checked it with a mutation: when I change the action to |
||
| out(start_seen, "policy allows ec2:StartInstances") | ||
| PY | ||
| )" | ||
|
|
||
| while IFS= read -r line; do | ||
| [[ -z "$line" ]] && continue | ||
| case "$line" in | ||
| PASS\ *) pass "${line#PASS }" ;; | ||
| *) fail "${line#FAIL }" ;; | ||
| esac | ||
| done <<< "$RESULTS" | ||
|
|
||
| printf "\n%d passed, %d failed\n" "$PASS" "$FAIL" | ||
| [[ "$FAIL" -eq 0 ]] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although the new test is discovered when the workflow runs,
.github/workflows/pack-tests.ymllines 6-20 do not includebootstraps/**in either path filter. A future PR that changes onlybootstraps/optional/idle-shutdown/install-idle.shtherefore will not start this workflow, so the contract test cannot catch the policy regression it was added to prevent; add the relevant bootstrap path to both workflow filters.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct:
pack-tests.ymldoes not havebootstraps/**in its path filters, so a change toinstall-idle.shalone does not start this test. I did not change the workflow, because changes to CI configuration are a maintainer decision and not part of a fix for #129. In d767a4b I added a note to the test header about this. Recommendation for a maintainer: addbootstraps/**to both thepushandpull_requestpath filters ofpack-tests.yml(tests/test-idle-check.sh has the same gap foridle-check.py).