Skip to content

fix: clean errors for expired tokens/bad accounts, guardrail bypass, audit timestamps - #20

Merged
DustyStudy merged 4 commits into
mainfrom
fix/clean-errors-guardrails-audit-doctor
Sep 22, 2026
Merged

DustyStudy merged 4 commits into
mainfrom
fix/clean-errors-guardrails-audit-doctor

Conversation

@DustyStudy

Copy link
Copy Markdown
Owner

Summary

Follow-up to #18, fixing four more correctness bugs found in review. One commit per fix, each with regression tests.

  1. Revoked/expired SSO token wasn't handled. list_accounts, list_account_roles, and get_role_credentials let a rejected access token raise a raw ClientError, and never cleared the now-useless cached token — every subsequent command failed the same way until you knew to run orgctl logout. On UnauthorizedException/ForbiddenException they now clear the cached token and raise SsoTokenExpiredError pointing at orgctl login; any other ClientError (e.g. a real permission denial) still passes through unaltered.
  2. whoami/check-policy crashed with a raw traceback on an unknown account/role — unlike every other command, they had no error handling around account/role resolution at all. All commands that use the token after login (exec, shell, export-env, list-remote, creds-process, whoami, check-policy) now also catch the new SsoTokenExpiredError. doctor's "cache dir writable" check used to print OK unconditionally without testing anything; it now does a real probe write+delete.
  3. The other built-in guardrail deny patterns still required aws immediately followed by the service name (only the recursive-s3 rm one was widened in fix: token cache key, policy simulator creds, s3 rm guardrail, sync-aws-config #18), so aws --profile x organizations close-account ... and similar slipped past. All five patterns now tolerate a global flag in between.
  4. audit-log --push-cloudwatch stamped every event with push time, not when the command actually ran, so CloudWatch showed everything bunched at push time — defeating the point of an audit trail. Each event now uses its own recorded ts, falling back to push time only if that's missing/unparseable.

Verification

  • ruff check, ruff format --check, mypy src: clean.
  • pytest: 108 passed (was 80); coverage 51% → 70%. cli.py went from 0% to 53% and sso.py from 27% to 60% — this is a new tests/test_cli.py exercising the CLI directly via click.testing.CliRunner, which is exactly the gap that let bugs Bump actions/setup-python from 5 to 7 #1/Bump googleapis/release-please-action from 4 to 5 #2 through unnoticed.
  • Every regression test was checked against the pre-fix code (via git stash per file) and confirmed to fail there and pass with the fix; a few new tests intentionally cover already-correct paths (e.g. creds-process's existing ConfigError handling) and pass either way.

Not in this PR

Still open from the original review: cache files are briefly world-readable before chmod locks them down (no-op on Windows anyway), export-env doesn't shlex.quote its output, README/threat-model claims about audit coverage on creds-process/whoami/check-policy and about credentials never leaving the child process (contradicted by export-env/creds-process printing them by design), and protected_account_ids only blocking exec not shell.

…S rejects it

list_accounts, list_account_roles, and get_role_credentials let a revoked or
expired SSO access token raise a raw botocore ClientError straight out of
the library, and never cleared the now-useless cached token — every
subsequent command failed the same way until the caller knew to run
`orgctl logout` themselves.

On UnauthorizedException/ForbiddenException (AWS's codes for "this access
token is no good", as opposed to a permission denial on the specific
action), these now clear the cached token and raise SsoTokenExpiredError
with a message pointing at `orgctl login`. Any other ClientError (e.g. a
real AccessDeniedException) still passes through unaltered.
- whoami and check-policy called resolve_account/resolve_role/
  get_role_credentials with no error handling at all, unlike every other
  command — an unknown account/role, or an SSO token AWS has since
  rejected, dumped a raw traceback instead of a clean message.
- exec, shell, export-env, list-remote, and creds-process now also catch
  SsoTokenExpiredError (see the sso.py fix) around their AWS calls, instead
  of only around the initial login() call — a token can be rejected on any
  later call just as easily as on login.
- doctor printed "OK cache dir writable" unconditionally, without ever
  testing that a write actually succeeds; it now does a real probe
  write+delete and reports FAIL if that fails.
- also updated the now-adjacent creds-process timestamp formatting off the
  deprecated datetime.utcfromtimestamp().

Introduces a small _guard() helper so each command's account/role
resolution and AWS calls funnel through the same clean-error handling
instead of repeating a try/except per command.
…in deny patterns

The previous fix only widened the recursive-s3-rm pattern to allow a global
flag between "aws" and the service name. The other four built-in patterns
(delete-account-alias, leave-organization, close-account, s3 rb --force)
still required "aws" to be immediately followed by the service, so
`aws --profile x organizations close-account ...` (or any other flag
inserted before the service name, which is normal `aws` CLI usage) slipped
past them undetected.
… push time

push_to_cloudwatch stamped every event with time.time() at push time instead
of the time the command actually ran, so CloudWatch showed everything
bunched at push time rather than when it happened — defeating the point of
an audit trail. Each event's timestamp is now parsed from its own "ts"
field, falling back to push time only for an entry with no/unparseable ts
(e.g. a hand-edited log line).
@DustyStudy
DustyStudy merged commit ed28272 into main Sep 22, 2026
9 checks passed
@DustyStudy
DustyStudy deleted the fix/clean-errors-guardrails-audit-doctor branch September 22, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant