feat(sts): GetCallerIdentity + real configure-aws-credentials integration test - #126
feat(sts): GetCallerIdentity + real configure-aws-credentials integration test#126alukach wants to merge 3 commits into
Conversation
PR #112 made /.sts a drop-in AssumeRoleWithWebIdentity target for AWS SDK STS clients, which send parameters in a form-encoded POST body rather than the query string. aws-actions/configure-aws-credentials relies on that path. The integration suite only exercised the query-string GET, so a regression in form-body parsing would go unnoticed. - Extract XML->creds parsing into `_parse_sts_credentials`, shared by both the existing query GET helper and the new form-POST helper. - Add `assume_role_form_post`, reproducing the exact wire request the action emits (application/x-www-form-urlencoded body via requests `data=`). - Add `test_configure_aws_credentials_action_grants_write_access`: assume the github-actions role that way and prove a PUT/GET round-trip on private-uploads succeeds. Runs in the OIDC-gated class (id-token: write CI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @alukach's task in 1m 37s —— View job ✅ No blocking issues — safe to merge. Reviewed Notes (non-blocking)
Simplify (ponytail)Nothing to flag — 💰 Estimated review cost: $0.77 · 1m36s · 22 turns |
`aws-actions/configure-aws-credentials` (and other AWS tooling) validates freshly assumed credentials by issuing an unconditional, SigV4-signed `GetCallerIdentity` call before exporting them. multistore-sts only parsed `AssumeRoleWithWebIdentity`, so the action could never succeed against the proxy — it would retry GetCallerIdentity 12 times and then fail the step. Closes #127. - **`caller_identity`** (new module) — `handle_get_caller_identity` authenticates the call against the sealed session token: it recovers the minted credentials from `x-amz-security-token`, checks the auth-header access key matches, and verifies the SigV4 signature with the proxy's own `verify_sigv4_signature` over the recovered secret. AWS SDKs sign STS POSTs over the SHA-256 of the form body, usually without an `x-amz-content-sha256` header, so the payload hash is taken from that header when present and recomputed from the collected body otherwise. Verification runs over the raw signing path so the trailing slash AWS SDK JS v3 appends (`/.sts` -> `/.sts/`) is honored, not normalized. - **`request::is_get_caller_identity`** — detects the action in a query string or form body (the two places AWS SDKs put STS parameters). - **`responses`** — `build_caller_identity_response` emits STS-shaped `GetCallerIdentityResponse` XML. `Account` is the fabricated `SYNTHETIC_ACCOUNT_ID` (`000000000000`) — the proxy has no real AWS account — used consistently in the assumed-role ARN so the identity is coherent. `build_sts_error_response` now maps `SignatureDoesNotMatch`/`ExpiredCredentials` to STS-shaped 403s instead of a generic 500. - **`route_handler`** — `StsHandler` dispatches GetCallerIdentity (authenticated, needs the full request) ahead of the unauthenticated assume-role exchange. `with_sts` now registers both `/.sts` and `/.sts/` (adding a `Clone` bound on the config) so SDK-JS callers, which hit the trailing-slash path, reach the handler. - **docs** — `auth/proxy-auth.md` documents GetCallerIdentity, the synthetic account, the trailing-slash rule, and a `configure-aws-credentials` workflow example; `reference/operations.md` and `architecture/crate-layout.md` updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Python suite only reproduced the action's wire requests. Now that the proxy serves GetCallerIdentity (#127), run the actual action end to end and prove the credentials it exports can write. - **ci.yml** — after pytest, the `integration` job runs `aws-actions/configure-aws-credentials@v6` (SHA-pinned) with `sts-endpoint` pointed at the proxy, then `aws s3 cp` uploads/downloads/deletes an object in `private-uploads` using the exported credentials. The action's own mandatory GetCallerIdentity validation must pass for the step to succeed, so this covers the full assume-role -> validate -> use flow. - **wrangler.integration.toml** — adds a role keyed by the full ARN `arn:aws:iam::000000000000:role/github-actions` (write scope on private-uploads). The action sends `role-to-assume` verbatim as the STS RoleArn and `get_role` matches `role_id` exactly, so the config key must be the full ARN; account `000000000000` matches the synthetic GetCallerIdentity account. - **test_integration.py** — adds `test_get_caller_identity` (botocore signs a real GetCallerIdentity over the assumed creds; asserts the synthetic account and ARN) and `test_get_caller_identity_rejects_static_credentials` (no session token -> 403). Rescopes the earlier form-POST test to `test_sdk_form_post_assume_role_grants_write_access`, since the headline "action works" claim is now proven by the real action in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
📖 Docs preview deployed to https://multistore-docs-pr-126.development-seed.workers.dev
|
|
🚀 Latest commit deployed to https://multistore-proxy-pr-126.development-seed.workers.dev
|
What I'm changing
aws-actions/configure-aws-credentialsis the flagship consumer of the STS endpoint, but it could never work against the proxy: afterAssumeRoleWithWebIdentityit issues an unconditional, SigV4-signedGetCallerIdentitycall to validate the credentials before exporting them (no opt-out; 12 retries then the step fails). multistore-sts only parsedAssumeRoleWithWebIdentity, so that call fell through unhandled.This PR implements
GetCallerIdentity(closes #127) and adds an integration test that runs the real action end to end and proves the credentials it exports can write.How I did it
STS crate —
GetCallerIdentity(#127)caller_identity(new module): authenticates the call against the sealed session token — recovers the minted credentials fromx-amz-security-token, checks the auth-header access key matches, and verifies the SigV4 signature with the proxy's ownverify_sigv4_signature. Payload hash comes fromx-amz-content-sha256when present, else recomputed from the collected form body (AWS SDKs sign STS POSTs over the body hash, often without the header). Verifies over the raw signing path so the trailing slash AWS SDK JS v3 appends (/.sts->/.sts/) is honored, not normalized.request::is_get_caller_identitydetects the action in a query string or form body.responses::build_caller_identity_responseemits STS-shaped XML.Accountis the fabricatedSYNTHETIC_ACCOUNT_ID(000000000000) — the proxy has no real AWS account — used consistently in the assumed-role ARN.build_sts_error_responsenow mapsSignatureDoesNotMatch/ExpiredCredentialsto STS-shaped 403s.route_handler: dispatches GetCallerIdentity ahead of the assume-role exchange;with_stsregisters both/.stsand/.sts/(adds aClonebound) so SDK-JS callers reach the handler.Integration test — the real action
ci.yml: theintegrationjob now runsaws-actions/configure-aws-credentials@v6(SHA-pinned) withsts-endpointat the proxy, thenaws s3 cpround-trips an object inprivate-uploadswith the exported credentials. The action's own GetCallerIdentity validation must pass for the step to succeed.wrangler.integration.toml: adds a role keyed by the full ARNarn:aws:iam::000000000000:role/github-actions(write scope), since the action sendsrole-to-assumeverbatim as the RoleArn andget_rolematchesrole_idexactly.test_integration.py: addstest_get_caller_identity(botocore signs a real GetCallerIdentity over the assumed creds) andtest_get_caller_identity_rejects_static_credentials; rescopes the earlier form-POST test totest_sdk_form_post_assume_role_grants_write_access.Docs:
auth/proxy-auth.md(GetCallerIdentity, synthetic account, trailing-slash rule,configure-aws-credentialsworkflow example),reference/operations.md,architecture/crate-layout.md.Test plan
cargo test -p multistore-sts(35 pass),cargo fmt,cargo clippy --fixcargo check --all-targets,cargo check -p multistore-cf-workers --target wasm32-unknown-unknownintegrationCI job: real action assume-role -> GetCallerIdentity validation ->aws s3 cpwrite, plus the two new OIDC-gated pytest cases. The signed GetCallerIdentity happy-path is verified in CI (botocore + the action), not locally — locally I audited the exact inputs handed toverify_sigv4_signature.🤖 Generated with Claude Code