@intentius/chant-lexicon-aws 0.52.1 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/read-client.d.ts +19 -0
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/caller-identity.d.ts +44 -0
- package/dist/caller-identity.d.ts.map +1 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api/read-client.ts +48 -0
- package/src/caller-identity.test.ts +191 -0
- package/src/caller-identity.ts +149 -0
- package/src/plugin.ts +12 -1
|
@@ -169,6 +169,25 @@ export interface StackDetail {
|
|
|
169
169
|
export declare function describeStackDetail(stackName: string, options?: AwsReadClientOptions): Promise<StackDetail>;
|
|
170
170
|
/** `DescribeStacks` outputs for one stack, as `key → value`. */
|
|
171
171
|
export declare function describeStackOutputs(stackName: string, options?: AwsReadClientOptions): Promise<Record<string, string>>;
|
|
172
|
+
/** What `sts:GetCallerIdentity` answers with. No credential is in the response. */
|
|
173
|
+
export interface CallerIdentity {
|
|
174
|
+
/** The principal's ARN — an IAM user, or an assumed-role session. */
|
|
175
|
+
arn: string;
|
|
176
|
+
/** The 12-digit account the credentials belong to. */
|
|
177
|
+
account: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* `sts:GetCallerIdentity` — who these credentials are, per the service itself
|
|
181
|
+
* (#1982). Query protocol like CloudFormation, so it rides the same transport,
|
|
182
|
+
* the same endpoint-override rule and the same signing decision as every other
|
|
183
|
+
* read, which is what keeps `chant lifecycle whoami` and `describeResources`
|
|
184
|
+
* from resolving different targets.
|
|
185
|
+
*
|
|
186
|
+
* The response carries an ARN, an account and a `UserId`. Only the first two
|
|
187
|
+
* are returned: `UserId` adds nothing a principal ARN does not already say,
|
|
188
|
+
* and the narrower the surface, the fewer ways a credential reaches a report.
|
|
189
|
+
*/
|
|
190
|
+
export declare function getCallerIdentity(options?: AwsReadClientOptions): Promise<CallerIdentity>;
|
|
172
191
|
/** One live resource, as Cloud Control describes it. */
|
|
173
192
|
export interface CloudControlDescription {
|
|
174
193
|
identifier: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who chant would act as in AWS — the lexicon half of `chant lifecycle whoami`
|
|
3
|
+
* (chant #1982).
|
|
4
|
+
*
|
|
5
|
+
* `sts:GetCallerIdentity` on the same transport every read uses, so the answer
|
|
6
|
+
* cannot describe a target the read does not reach: the endpoint override, the
|
|
7
|
+
* region and the signing decision are resolved by `read-client.ts` once, for
|
|
8
|
+
* both. That parity is the point of the command — a whoami that reports real
|
|
9
|
+
* AWS while `describeResources` reads Floci is worse than none — and a test
|
|
10
|
+
* pins it.
|
|
11
|
+
*
|
|
12
|
+
* ## What the region actually is
|
|
13
|
+
*
|
|
14
|
+
* The native read transport takes its region from the stack being observed
|
|
15
|
+
* (`stacks[].region`, #1261) and falls back to `us-east-1` inside `serviceUrl`.
|
|
16
|
+
* It does NOT read `AWS_REGION`. So that is what is reported, including the
|
|
17
|
+
* awkward case: an operator whose shell exports `AWS_REGION=eu-west-1` and
|
|
18
|
+
* whose project declares no stack region is reading us-east-1, and has had no
|
|
19
|
+
* way to find that out short of a wrong answer. `source` names it.
|
|
20
|
+
*
|
|
21
|
+
* ## Credentials
|
|
22
|
+
*
|
|
23
|
+
* `resolveCredentials` (./api/sigv4.ts) reads `AWS_ACCESS_KEY_ID` /
|
|
24
|
+
* `AWS_SECRET_ACCESS_KEY` and nothing else — no profile file, no IMDS, no
|
|
25
|
+
* container endpoint, deliberately. `AWS_PROFILE` alone therefore signs
|
|
26
|
+
* nothing, and against real AWS the call comes back unauthorized. That is
|
|
27
|
+
* reported as `no-credentials` naming the profile, which is a far better
|
|
28
|
+
* answer than an empty stack read an hour later.
|
|
29
|
+
*
|
|
30
|
+
* No credential value is ever returned. The identity is the principal ARN the
|
|
31
|
+
* service itself answers with, and the scope is an account and a region.
|
|
32
|
+
*/
|
|
33
|
+
import type { DescribeIdentityOptions, DescribeIdentityResult } from "@intentius/chant/lexicon";
|
|
34
|
+
import { type AwsReadClientOptions } from "./api/read-client.js";
|
|
35
|
+
/** Options this module needs beyond the contract — the injectable transport, for tests. */
|
|
36
|
+
export interface CallerIdentityOptions extends DescribeIdentityOptions {
|
|
37
|
+
client?: AwsReadClientOptions;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The AWS `describeIdentity`. Returns the STS principal and the account+region
|
|
41
|
+
* scope, or a typed refusal — never a guessed identity and never an empty one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function describeIdentity(options: CallerIdentityOptions): Promise<DescribeIdentityResult>;
|
|
44
|
+
//# sourceMappingURL=caller-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caller-identity.d.ts","sourceRoot":"","sources":["../src/caller-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAChG,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAgC3B,2FAA2F;AAC3F,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAmEtG"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "36bcea9cb5afae1dacc1c5da5c2f8898f43e6f92e3d62fe0e6a2c65255f50f84",
|
|
5
5
|
"meta.json": "49cbb2ca9b454391a7644e14ee9cfd91bcf906c6af683bf46a182aa85047ecc4",
|
|
6
6
|
"types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
|
|
7
7
|
"rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
|
|
73
73
|
"skills/chant-aws-carve-terraform.md": "0ae4376590db47979be144d3ac6cdcfd56c9640d0536421aacf7330459d38e52"
|
|
74
74
|
},
|
|
75
|
-
"composite": "
|
|
75
|
+
"composite": "1a4039050a412b6ef930275495ec9b9100274b952986d9437022ce0f424547c9"
|
|
76
76
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAqQ,MAAM,0BAA0B,CAAC;AAuCjU;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aA8/BvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.
|
|
83
|
+
"@intentius/chant": "^0.53.0",
|
|
84
84
|
"typescript": "^5.9.3"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/src/api/read-client.ts
CHANGED
|
@@ -364,6 +364,54 @@ export async function describeStackOutputs(
|
|
|
364
364
|
return (await describeStackDetail(stackName, options)).outputs;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
/* ── STS ──────────────────────────────────────────────────────────────────── */
|
|
368
|
+
|
|
369
|
+
const STS_API_VERSION = "2011-06-15";
|
|
370
|
+
|
|
371
|
+
/** What `sts:GetCallerIdentity` answers with. No credential is in the response. */
|
|
372
|
+
export interface CallerIdentity {
|
|
373
|
+
/** The principal's ARN — an IAM user, or an assumed-role session. */
|
|
374
|
+
arn: string;
|
|
375
|
+
/** The 12-digit account the credentials belong to. */
|
|
376
|
+
account: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* `sts:GetCallerIdentity` — who these credentials are, per the service itself
|
|
381
|
+
* (#1982). Query protocol like CloudFormation, so it rides the same transport,
|
|
382
|
+
* the same endpoint-override rule and the same signing decision as every other
|
|
383
|
+
* read, which is what keeps `chant lifecycle whoami` and `describeResources`
|
|
384
|
+
* from resolving different targets.
|
|
385
|
+
*
|
|
386
|
+
* The response carries an ARN, an account and a `UserId`. Only the first two
|
|
387
|
+
* are returned: `UserId` adds nothing a principal ARN does not already say,
|
|
388
|
+
* and the narrower the surface, the fewer ways a credential reaches a report.
|
|
389
|
+
*/
|
|
390
|
+
export async function getCallerIdentity(options: AwsReadClientOptions = {}): Promise<CallerIdentity> {
|
|
391
|
+
options = withEndpointOverride("sts", options);
|
|
392
|
+
const http = options.http ?? defaultHttp;
|
|
393
|
+
const url = serviceUrl("sts", options.endpoint, options.region);
|
|
394
|
+
const body = new URLSearchParams({ Action: "GetCallerIdentity", Version: STS_API_VERSION }).toString();
|
|
395
|
+
const res = await http(
|
|
396
|
+
url,
|
|
397
|
+
{
|
|
398
|
+
headers: requestHeaders("sts", url, body, { "content-type": "application/x-www-form-urlencoded" }, options),
|
|
399
|
+
body,
|
|
400
|
+
},
|
|
401
|
+
options.signal,
|
|
402
|
+
);
|
|
403
|
+
const err = xmlError(res.text);
|
|
404
|
+
if (err || res.status >= 400) {
|
|
405
|
+
throw new AwsReadError(
|
|
406
|
+
err?.message ?? `GetCallerIdentity failed with HTTP ${res.status}`,
|
|
407
|
+
res.status,
|
|
408
|
+
err?.code,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const leaves = xmlLeaves(res.text);
|
|
412
|
+
return { arn: leaves.Arn ?? "", account: leaves.Account ?? "" };
|
|
413
|
+
}
|
|
414
|
+
|
|
367
415
|
/* ── Cloud Control ────────────────────────────────────────────────────────── */
|
|
368
416
|
|
|
369
417
|
/** One live resource, as Cloud Control describes it. */
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant lifecycle whoami` for AWS (chant #1982).
|
|
3
|
+
*
|
|
4
|
+
* The transport is injected everywhere here, so nothing reaches a network and
|
|
5
|
+
* no real credential is involved. The load-bearing test is the last one: the
|
|
6
|
+
* identity query and the declared-entity read must resolve the same target,
|
|
7
|
+
* because a whoami that names a binding the read does not use is worse than no
|
|
8
|
+
* whoami at all.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { describeIdentity } from "./caller-identity";
|
|
12
|
+
import { describeStackResources, type AwsReadHttp } from "./api/read-client";
|
|
13
|
+
|
|
14
|
+
const CALLER_IDENTITY_XML = `
|
|
15
|
+
<GetCallerIdentityResponse><GetCallerIdentityResult>
|
|
16
|
+
<Arn>arn:aws:sts::491500000000:assumed-role/deploy/ci</Arn>
|
|
17
|
+
<UserId>AROAEXAMPLEID123456:ci</UserId>
|
|
18
|
+
<Account>491500000000</Account>
|
|
19
|
+
</GetCallerIdentityResult></GetCallerIdentityResponse>`;
|
|
20
|
+
|
|
21
|
+
const STACK_XML = `
|
|
22
|
+
<DescribeStackResourcesResponse><DescribeStackResourcesResult><StackResources>
|
|
23
|
+
<member>
|
|
24
|
+
<LogicalResourceId>bucket</LogicalResourceId>
|
|
25
|
+
<ResourceType>AWS::S3::Bucket</ResourceType>
|
|
26
|
+
</member>
|
|
27
|
+
</StackResources></DescribeStackResourcesResult></DescribeStackResourcesResponse>`;
|
|
28
|
+
|
|
29
|
+
/** Records every request so a test can assert what went on the wire. */
|
|
30
|
+
function recording(text: string, status = 200) {
|
|
31
|
+
const calls: Array<{ url: string; headers: Record<string, string>; body: string }> = [];
|
|
32
|
+
const http: AwsReadHttp = (url, init) => {
|
|
33
|
+
calls.push({ url, headers: init.headers, body: init.body });
|
|
34
|
+
return Promise.resolve({ status, text });
|
|
35
|
+
};
|
|
36
|
+
return { http, calls };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Credentials that exist only in this record — never read from the process. */
|
|
40
|
+
const CREDS = { AWS_ACCESS_KEY_ID: "AKIAEXAMPLEKEYID0000", AWS_SECRET_ACCESS_KEY: "notarealsecret" };
|
|
41
|
+
|
|
42
|
+
describe("aws describeIdentity (#1982)", () => {
|
|
43
|
+
test("reports the STS principal, the account+region scope, and where the binding came from", async () => {
|
|
44
|
+
const { http, calls } = recording(CALLER_IDENTITY_XML);
|
|
45
|
+
const result = await describeIdentity({
|
|
46
|
+
environment: "prod",
|
|
47
|
+
region: "eu-west-1",
|
|
48
|
+
client: { http, env: CREDS },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual({
|
|
52
|
+
identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
|
|
53
|
+
scope: "491500000000 eu-west-1",
|
|
54
|
+
source: "env AWS_ACCESS_KEY_ID; stacks[].region",
|
|
55
|
+
endpoint: "https://sts.eu-west-1.amazonaws.com/",
|
|
56
|
+
});
|
|
57
|
+
expect(calls).toHaveLength(1);
|
|
58
|
+
expect(calls[0].body).toContain("Action=GetCallerIdentity");
|
|
59
|
+
// The principal id adds nothing an ARN does not already say, and the
|
|
60
|
+
// narrower the surface the fewer ways a credential reaches a report.
|
|
61
|
+
expect(JSON.stringify(result)).not.toContain("AROAEXAMPLEID123456");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("no environment credentials against real AWS is no-credentials, and the call is never made", async () => {
|
|
65
|
+
const { http, calls } = recording(CALLER_IDENTITY_XML);
|
|
66
|
+
const result = await describeIdentity({ environment: "prod", client: { http, env: {} } });
|
|
67
|
+
expect(result).toEqual({
|
|
68
|
+
unresolved: {
|
|
69
|
+
reason: "no-credentials",
|
|
70
|
+
detail: "no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the environment",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
expect(calls).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("AWS_PROFILE alone is reported as no-credentials, naming the profile that does not sign", async () => {
|
|
77
|
+
const { http } = recording(CALLER_IDENTITY_XML);
|
|
78
|
+
const result = await describeIdentity({
|
|
79
|
+
environment: "prod",
|
|
80
|
+
client: { http, env: { AWS_PROFILE: "acme-ci" } },
|
|
81
|
+
});
|
|
82
|
+
expect(result).toMatchObject({ unresolved: { reason: "no-credentials" } });
|
|
83
|
+
const detail = (result as { unresolved: { detail: string } }).unresolved.detail;
|
|
84
|
+
expect(detail).toContain("AWS_PROFILE=acme-ci");
|
|
85
|
+
expect(detail).toContain("not the profile file");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("an unsigned emulator read still resolves an identity, and says it was unsigned", async () => {
|
|
89
|
+
const { http, calls } = recording(CALLER_IDENTITY_XML);
|
|
90
|
+
const result = await describeIdentity({
|
|
91
|
+
environment: "floci",
|
|
92
|
+
client: { http, env: { AWS_ENDPOINT_URL: "http://localhost:4566" } },
|
|
93
|
+
});
|
|
94
|
+
expect(result).toMatchObject({
|
|
95
|
+
identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
|
|
96
|
+
endpoint: "http://localhost:4566/",
|
|
97
|
+
});
|
|
98
|
+
expect((result as { source: string }).source).toContain("unsigned (endpoint override)");
|
|
99
|
+
expect((result as { source: string }).source).toContain("endpoint override http://localhost:4566");
|
|
100
|
+
expect(calls[0].url).toBe("http://localhost:4566/");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a refused call is no-credentials; a broken one is read-failed", async () => {
|
|
104
|
+
const denied = recording(
|
|
105
|
+
"<ErrorResponse><Error><Code>InvalidClientTokenId</Code><Message>bad token</Message></Error></ErrorResponse>",
|
|
106
|
+
403,
|
|
107
|
+
);
|
|
108
|
+
expect(
|
|
109
|
+
await describeIdentity({ environment: "prod", client: { http: denied.http, env: CREDS } }),
|
|
110
|
+
).toMatchObject({ unresolved: { reason: "no-credentials" } });
|
|
111
|
+
|
|
112
|
+
const broken = recording(
|
|
113
|
+
"<ErrorResponse><Error><Code>ServiceUnavailable</Code><Message>try later</Message></Error></ErrorResponse>",
|
|
114
|
+
503,
|
|
115
|
+
);
|
|
116
|
+
const result = await describeIdentity({ environment: "prod", client: { http: broken.http, env: CREDS } });
|
|
117
|
+
expect(result).toMatchObject({ unresolved: { reason: "read-failed" } });
|
|
118
|
+
expect((result as { unresolved: { detail: string } }).unresolved.detail).toContain("ServiceUnavailable");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("an answer with no Arn is read-failed, never an empty identity", async () => {
|
|
122
|
+
const { http } = recording("<GetCallerIdentityResponse><GetCallerIdentityResult/></GetCallerIdentityResponse>");
|
|
123
|
+
const result = await describeIdentity({ environment: "prod", client: { http, env: CREDS } });
|
|
124
|
+
expect(result).toMatchObject({ unresolved: { reason: "read-failed" } });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("the region an undeclared project actually reads is reported, not the one AWS_REGION suggests", async () => {
|
|
128
|
+
// The read transport takes its region from `stacks[].region` and defaults
|
|
129
|
+
// to us-east-1; it does not consult AWS_REGION. Reporting the shell's var
|
|
130
|
+
// here would describe a target no read uses.
|
|
131
|
+
const { http, calls } = recording(CALLER_IDENTITY_XML);
|
|
132
|
+
const result = await describeIdentity({
|
|
133
|
+
environment: "prod",
|
|
134
|
+
client: { http, env: { ...CREDS, AWS_REGION: "ap-south-1" } },
|
|
135
|
+
});
|
|
136
|
+
expect((result as { scope: string }).scope).toBe("491500000000 us-east-1");
|
|
137
|
+
expect((result as { source: string }).source).toContain("AWS_REGION=ap-south-1 is not read by this transport");
|
|
138
|
+
expect(calls[0].url).toBe("https://sts.us-east-1.amazonaws.com/");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("no credential material appears anywhere in the reported row", async () => {
|
|
142
|
+
const { http } = recording(CALLER_IDENTITY_XML);
|
|
143
|
+
const result = await describeIdentity({ environment: "prod", client: { http, env: CREDS } });
|
|
144
|
+
const serialized = JSON.stringify(result);
|
|
145
|
+
expect(serialized).not.toContain(CREDS.AWS_SECRET_ACCESS_KEY);
|
|
146
|
+
expect(serialized).not.toContain(CREDS.AWS_ACCESS_KEY_ID);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("whoami and the live read resolve the same target (#1982 acceptance)", () => {
|
|
151
|
+
test("an endpoint override sends both to the same origin", async () => {
|
|
152
|
+
const env = { ...CREDS, AWS_ENDPOINT_URL: "http://localhost:4566" };
|
|
153
|
+
|
|
154
|
+
const identityHttp = recording(CALLER_IDENTITY_XML);
|
|
155
|
+
const identity = await describeIdentity({
|
|
156
|
+
environment: "floci",
|
|
157
|
+
region: "eu-west-1",
|
|
158
|
+
client: { http: identityHttp.http, env },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const readHttp = recording(STACK_XML);
|
|
162
|
+
await describeStackResources("floci", { http: readHttp.http, env, region: "eu-west-1" });
|
|
163
|
+
|
|
164
|
+
const origin = (url: string): string => new URL(url).origin;
|
|
165
|
+
expect(origin(identityHttp.calls[0].url)).toBe(origin(readHttp.calls[0].url));
|
|
166
|
+
// And the row names it, so the operator sees the target rather than
|
|
167
|
+
// inferring it.
|
|
168
|
+
expect((identity as { endpoint: string }).endpoint).toBe(readHttp.calls[0].url);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("against real AWS both resolve the same region, on each service's own host", async () => {
|
|
172
|
+
const identityHttp = recording(CALLER_IDENTITY_XML);
|
|
173
|
+
await describeIdentity({
|
|
174
|
+
environment: "prod",
|
|
175
|
+
region: "eu-west-1",
|
|
176
|
+
client: { http: identityHttp.http, env: CREDS },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const readHttp = recording(STACK_XML);
|
|
180
|
+
await describeStackResources("prod", { http: readHttp.http, env: CREDS, region: "eu-west-1" });
|
|
181
|
+
|
|
182
|
+
const host = (url: string): string => new URL(url).host;
|
|
183
|
+
expect(host(identityHttp.calls[0].url)).toBe("sts.eu-west-1.amazonaws.com");
|
|
184
|
+
expect(host(readHttp.calls[0].url)).toBe("cloudformation.eu-west-1.amazonaws.com");
|
|
185
|
+
// The credential scope both signatures carry is the same region — which is
|
|
186
|
+
// the part a wrong answer would get wrong.
|
|
187
|
+
for (const calls of [identityHttp.calls, readHttp.calls]) {
|
|
188
|
+
expect(calls[0].headers.authorization ?? calls[0].headers.Authorization).toContain("/eu-west-1/");
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who chant would act as in AWS — the lexicon half of `chant lifecycle whoami`
|
|
3
|
+
* (chant #1982).
|
|
4
|
+
*
|
|
5
|
+
* `sts:GetCallerIdentity` on the same transport every read uses, so the answer
|
|
6
|
+
* cannot describe a target the read does not reach: the endpoint override, the
|
|
7
|
+
* region and the signing decision are resolved by `read-client.ts` once, for
|
|
8
|
+
* both. That parity is the point of the command — a whoami that reports real
|
|
9
|
+
* AWS while `describeResources` reads Floci is worse than none — and a test
|
|
10
|
+
* pins it.
|
|
11
|
+
*
|
|
12
|
+
* ## What the region actually is
|
|
13
|
+
*
|
|
14
|
+
* The native read transport takes its region from the stack being observed
|
|
15
|
+
* (`stacks[].region`, #1261) and falls back to `us-east-1` inside `serviceUrl`.
|
|
16
|
+
* It does NOT read `AWS_REGION`. So that is what is reported, including the
|
|
17
|
+
* awkward case: an operator whose shell exports `AWS_REGION=eu-west-1` and
|
|
18
|
+
* whose project declares no stack region is reading us-east-1, and has had no
|
|
19
|
+
* way to find that out short of a wrong answer. `source` names it.
|
|
20
|
+
*
|
|
21
|
+
* ## Credentials
|
|
22
|
+
*
|
|
23
|
+
* `resolveCredentials` (./api/sigv4.ts) reads `AWS_ACCESS_KEY_ID` /
|
|
24
|
+
* `AWS_SECRET_ACCESS_KEY` and nothing else — no profile file, no IMDS, no
|
|
25
|
+
* container endpoint, deliberately. `AWS_PROFILE` alone therefore signs
|
|
26
|
+
* nothing, and against real AWS the call comes back unauthorized. That is
|
|
27
|
+
* reported as `no-credentials` naming the profile, which is a far better
|
|
28
|
+
* answer than an empty stack read an hour later.
|
|
29
|
+
*
|
|
30
|
+
* No credential value is ever returned. The identity is the principal ARN the
|
|
31
|
+
* service itself answers with, and the scope is an account and a region.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { DescribeIdentityOptions, DescribeIdentityResult } from "@intentius/chant/lexicon";
|
|
35
|
+
import {
|
|
36
|
+
AwsReadError,
|
|
37
|
+
getCallerIdentity,
|
|
38
|
+
resolveEndpointOverride,
|
|
39
|
+
serviceUrl,
|
|
40
|
+
type AwsReadClientOptions,
|
|
41
|
+
} from "./api/read-client";
|
|
42
|
+
import { resolveCredentials } from "./api/sigv4";
|
|
43
|
+
|
|
44
|
+
/** The region the read transport falls back to when no stack declares one. */
|
|
45
|
+
const DEFAULT_REGION = "us-east-1";
|
|
46
|
+
|
|
47
|
+
/** True when the failure says the call was refused rather than broken. */
|
|
48
|
+
function isAuthFailure(err: AwsReadError): boolean {
|
|
49
|
+
if (err.status === 401 || err.status === 403) return true;
|
|
50
|
+
return /credential|token|expired|AccessDenied|not authorized|Unauthorized|InvalidClientTokenId|SignatureDoesNotMatch/i.test(
|
|
51
|
+
`${err.code ?? ""} ${err.message}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Where the region came from, said plainly enough to act on. */
|
|
56
|
+
function regionSource(declared: string | undefined, env: Record<string, string | undefined>): string {
|
|
57
|
+
if (declared) return `stacks[].region`;
|
|
58
|
+
const ambient = env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
59
|
+
if (ambient && ambient !== DEFAULT_REGION) {
|
|
60
|
+
return `chant default (no stacks[].region; AWS_REGION=${ambient} is not read by this transport)`;
|
|
61
|
+
}
|
|
62
|
+
return "chant default (no stacks[].region)";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Where the signature came from, without any part of the credential itself. */
|
|
66
|
+
function credentialSource(env: Record<string, string | undefined>): string | undefined {
|
|
67
|
+
if (!resolveCredentials(undefined, env)) return undefined;
|
|
68
|
+
return env.AWS_SESSION_TOKEN
|
|
69
|
+
? "env AWS_ACCESS_KEY_ID + AWS_SESSION_TOKEN"
|
|
70
|
+
: "env AWS_ACCESS_KEY_ID";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Options this module needs beyond the contract — the injectable transport, for tests. */
|
|
74
|
+
export interface CallerIdentityOptions extends DescribeIdentityOptions {
|
|
75
|
+
client?: AwsReadClientOptions;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The AWS `describeIdentity`. Returns the STS principal and the account+region
|
|
80
|
+
* scope, or a typed refusal — never a guessed identity and never an empty one.
|
|
81
|
+
*/
|
|
82
|
+
export async function describeIdentity(options: CallerIdentityOptions): Promise<DescribeIdentityResult> {
|
|
83
|
+
const client: AwsReadClientOptions = {
|
|
84
|
+
...options.client,
|
|
85
|
+
...(options.region ? { region: options.region } : {}),
|
|
86
|
+
};
|
|
87
|
+
const env = client.env ?? process.env;
|
|
88
|
+
const override = resolveEndpointOverride("sts", client.endpoint, env);
|
|
89
|
+
// The region the transport resolves, not the one an operator might assume:
|
|
90
|
+
// `serviceUrl` defaults it, so this is the host the read is actually sent to.
|
|
91
|
+
const region = client.region ?? DEFAULT_REGION;
|
|
92
|
+
const endpoint = serviceUrl("sts", override, client.region);
|
|
93
|
+
|
|
94
|
+
const credential = credentialSource(env);
|
|
95
|
+
// Against an emulator the call is unsigned by design (Floci verifies no
|
|
96
|
+
// signature), so an absent credential is not yet a refusal — the emulator
|
|
97
|
+
// answers with the identity it models. Against real AWS it is.
|
|
98
|
+
if (!credential && !override) {
|
|
99
|
+
return {
|
|
100
|
+
unresolved: {
|
|
101
|
+
reason: "no-credentials",
|
|
102
|
+
detail: env.AWS_PROFILE
|
|
103
|
+
? `no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the environment; AWS_PROFILE=${env.AWS_PROFILE} is set, but chant's native transport reads only the environment credentials, not the profile file`
|
|
104
|
+
: "no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the environment",
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let identity;
|
|
110
|
+
try {
|
|
111
|
+
identity = await getCallerIdentity(client);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof AwsReadError) {
|
|
114
|
+
return {
|
|
115
|
+
unresolved: {
|
|
116
|
+
reason: isAuthFailure(err) ? "no-credentials" : "read-failed",
|
|
117
|
+
detail: `sts:GetCallerIdentity against ${endpoint} failed: ${err.code ? `${err.code}: ` : ""}${err.message}`,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
unresolved: {
|
|
123
|
+
reason: "read-failed",
|
|
124
|
+
detail: `sts:GetCallerIdentity against ${endpoint} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!identity.arn) {
|
|
130
|
+
return {
|
|
131
|
+
unresolved: {
|
|
132
|
+
reason: "read-failed",
|
|
133
|
+
detail: `sts:GetCallerIdentity against ${endpoint} answered without an Arn`,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const sources = [
|
|
139
|
+
credential ?? "unsigned (endpoint override)",
|
|
140
|
+
regionSource(client.region, env),
|
|
141
|
+
...(override ? [`endpoint override ${override}`] : []),
|
|
142
|
+
];
|
|
143
|
+
return {
|
|
144
|
+
identity: identity.arn,
|
|
145
|
+
scope: identity.account ? `${identity.account} ${region}` : region,
|
|
146
|
+
source: sources.join("; "),
|
|
147
|
+
endpoint,
|
|
148
|
+
};
|
|
149
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
2
|
import { detectTemplate } from "./detect";
|
|
3
|
-
import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, DisruptionQuery, DisruptionVerdict, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
|
|
3
|
+
import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, DescribeIdentityOptions, DescribeIdentityResult, DisruptionQuery, DisruptionVerdict, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
6
6
|
import type { TemplateParser } from "@intentius/chant/import/parser";
|
|
@@ -800,6 +800,17 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
800
800
|
return awsDisruption(options);
|
|
801
801
|
},
|
|
802
802
|
|
|
803
|
+
/**
|
|
804
|
+
* Who chant would act as here, before it acts (#1982) —
|
|
805
|
+
* `sts:GetCallerIdentity` on the same transport, endpoint override and
|
|
806
|
+
* region resolution `describeResources` uses, so the principal reported and
|
|
807
|
+
* the account read are the same one. Implementation in ./caller-identity.ts.
|
|
808
|
+
*/
|
|
809
|
+
async describeIdentity(options: DescribeIdentityOptions): Promise<DescribeIdentityResult> {
|
|
810
|
+
const { describeIdentity } = await import("./caller-identity");
|
|
811
|
+
return describeIdentity(options);
|
|
812
|
+
},
|
|
813
|
+
|
|
803
814
|
/**
|
|
804
815
|
* Resources of a managed kind that exist without being declared or
|
|
805
816
|
* referenced (#1278) — the account's default security groups, an unattached
|