/**
* `chant lifecycle whoami` for AWS (chant #1982).
*
* The transport is injected everywhere here, so nothing reaches a network and
* no real credential is involved. The load-bearing test is the last one: the
* identity query and the declared-entity read must resolve the same target,
* because a whoami that names a binding the read does not use is worse than no
* whoami at all.
*/
import { describe, test, expect } from "vitest";
import { describeIdentity } from "./caller-identity";
import { describeStackResources, type AwsReadHttp } from "./api/read-client";
const CALLER_IDENTITY_XML = `
arn:aws:sts::491500000000:assumed-role/deploy/ci
AROAEXAMPLEID123456:ci
491500000000
`;
const STACK_XML = `
bucket
AWS::S3::Bucket
`;
/** Records every request so a test can assert what went on the wire. */
function recording(text: string, status = 200) {
const calls: Array<{ url: string; headers: Record; body: string }> = [];
const http: AwsReadHttp = (url, init) => {
calls.push({ url, headers: init.headers, body: init.body });
return Promise.resolve({ status, text });
};
return { http, calls };
}
/** Credentials that exist only in this record — never read from the process. */
const CREDS = { AWS_ACCESS_KEY_ID: "AKIAEXAMPLEKEYID0000", AWS_SECRET_ACCESS_KEY: "notarealsecret" };
describe("aws describeIdentity (#1982)", () => {
test("reports the STS principal, the account+region scope, and where the binding came from", async () => {
const { http, calls } = recording(CALLER_IDENTITY_XML);
const result = await describeIdentity({
environment: "prod",
region: "eu-west-1",
client: { http, env: CREDS },
});
expect(result).toEqual({
identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
scope: "491500000000 eu-west-1",
source: "env AWS_ACCESS_KEY_ID; stacks[].region",
endpoint: "https://sts.eu-west-1.amazonaws.com/",
});
expect(calls).toHaveLength(1);
expect(calls[0].body).toContain("Action=GetCallerIdentity");
// The principal id adds nothing an ARN does not already say, and the
// narrower the surface the fewer ways a credential reaches a report.
expect(JSON.stringify(result)).not.toContain("AROAEXAMPLEID123456");
});
test("no environment credentials against real AWS is no-credentials, and the call is never made", async () => {
const { http, calls } = recording(CALLER_IDENTITY_XML);
const result = await describeIdentity({ environment: "prod", client: { http, env: {} } });
expect(result).toEqual({
unresolved: {
reason: "no-credentials",
detail: "no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the environment",
},
});
expect(calls).toHaveLength(0);
});
test("AWS_PROFILE alone is reported as no-credentials, naming the profile that does not sign", async () => {
const { http } = recording(CALLER_IDENTITY_XML);
const result = await describeIdentity({
environment: "prod",
client: { http, env: { AWS_PROFILE: "acme-ci" } },
});
expect(result).toMatchObject({ unresolved: { reason: "no-credentials" } });
const detail = (result as { unresolved: { detail: string } }).unresolved.detail;
expect(detail).toContain("AWS_PROFILE=acme-ci");
expect(detail).toContain("not the profile file");
});
test("an unsigned emulator read still resolves an identity, and says it was unsigned", async () => {
const { http, calls } = recording(CALLER_IDENTITY_XML);
const result = await describeIdentity({
environment: "floci",
client: { http, env: { AWS_ENDPOINT_URL: "http://localhost:4566" } },
});
expect(result).toMatchObject({
identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
endpoint: "http://localhost:4566/",
});
expect((result as { source: string }).source).toContain("unsigned (endpoint override)");
expect((result as { source: string }).source).toContain("endpoint override http://localhost:4566");
expect(calls[0].url).toBe("http://localhost:4566/");
});
test("a refused call is no-credentials; a broken one is read-failed", async () => {
const denied = recording(
"InvalidClientTokenIdbad token",
403,
);
expect(
await describeIdentity({ environment: "prod", client: { http: denied.http, env: CREDS } }),
).toMatchObject({ unresolved: { reason: "no-credentials" } });
const broken = recording(
"ServiceUnavailabletry later",
503,
);
const result = await describeIdentity({ environment: "prod", client: { http: broken.http, env: CREDS } });
expect(result).toMatchObject({ unresolved: { reason: "read-failed" } });
expect((result as { unresolved: { detail: string } }).unresolved.detail).toContain("ServiceUnavailable");
});
test("an answer with no Arn is read-failed, never an empty identity", async () => {
const { http } = recording("");
const result = await describeIdentity({ environment: "prod", client: { http, env: CREDS } });
expect(result).toMatchObject({ unresolved: { reason: "read-failed" } });
});
test("the region an undeclared project actually reads is reported, not the one AWS_REGION suggests", async () => {
// The read transport takes its region from `stacks[].region` and defaults
// to us-east-1; it does not consult AWS_REGION. Reporting the shell's var
// here would describe a target no read uses.
const { http, calls } = recording(CALLER_IDENTITY_XML);
const result = await describeIdentity({
environment: "prod",
client: { http, env: { ...CREDS, AWS_REGION: "ap-south-1" } },
});
expect((result as { scope: string }).scope).toBe("491500000000 us-east-1");
expect((result as { source: string }).source).toContain("AWS_REGION=ap-south-1 is not read by this transport");
expect(calls[0].url).toBe("https://sts.us-east-1.amazonaws.com/");
});
test("no credential material appears anywhere in the reported row", async () => {
const { http } = recording(CALLER_IDENTITY_XML);
const result = await describeIdentity({ environment: "prod", client: { http, env: CREDS } });
const serialized = JSON.stringify(result);
expect(serialized).not.toContain(CREDS.AWS_SECRET_ACCESS_KEY);
expect(serialized).not.toContain(CREDS.AWS_ACCESS_KEY_ID);
});
});
describe("whoami and the live read resolve the same target (#1982 acceptance)", () => {
test("an endpoint override sends both to the same origin", async () => {
const env = { ...CREDS, AWS_ENDPOINT_URL: "http://localhost:4566" };
const identityHttp = recording(CALLER_IDENTITY_XML);
const identity = await describeIdentity({
environment: "floci",
region: "eu-west-1",
client: { http: identityHttp.http, env },
});
const readHttp = recording(STACK_XML);
await describeStackResources("floci", { http: readHttp.http, env, region: "eu-west-1" });
const origin = (url: string): string => new URL(url).origin;
expect(origin(identityHttp.calls[0].url)).toBe(origin(readHttp.calls[0].url));
// And the row names it, so the operator sees the target rather than
// inferring it.
expect((identity as { endpoint: string }).endpoint).toBe(readHttp.calls[0].url);
});
test("against real AWS both resolve the same region, on each service's own host", async () => {
const identityHttp = recording(CALLER_IDENTITY_XML);
await describeIdentity({
environment: "prod",
region: "eu-west-1",
client: { http: identityHttp.http, env: CREDS },
});
const readHttp = recording(STACK_XML);
await describeStackResources("prod", { http: readHttp.http, env: CREDS, region: "eu-west-1" });
const host = (url: string): string => new URL(url).host;
expect(host(identityHttp.calls[0].url)).toBe("sts.eu-west-1.amazonaws.com");
expect(host(readHttp.calls[0].url)).toBe("cloudformation.eu-west-1.amazonaws.com");
// The credential scope both signatures carry is the same region — which is
// the part a wrong answer would get wrong.
for (const calls of [identityHttp.calls, readHttp.calls]) {
expect(calls[0].headers.authorization ?? calls[0].headers.Authorization).toContain("/eu-west-1/");
}
});
});