@intentius/chant-lexicon-cedar 0.44.10 → 0.44.12
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/avp/client.d.ts +85 -8
- package/dist/avp/client.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/avp/OWNERSHIP.md +38 -0
- package/src/avp/client.test.ts +271 -0
- package/src/avp/client.ts +150 -16
package/dist/avp/client.d.ts
CHANGED
|
@@ -14,14 +14,37 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## Signing
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* SigV4 lives once, in `lexicons/aws/src/api/sigv4.ts` (#1686). This module
|
|
18
|
+
* does not import it. A cedar → aws dependency edge would make the vendor-
|
|
19
|
+
* neutral lexicon unbuildable without the AWS one, which is the same rule that
|
|
20
|
+
* kept `src/avp/embed.ts` free of the aws lexicon: the seam is the data shape.
|
|
21
|
+
* So the signer arrives as a function on {@link AvpClientOptions}, typed here
|
|
22
|
+
* structurally against what the aws lexicon exports, and a project holding both
|
|
23
|
+
* lexicons wires them in one line:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { signRequest } from "@intentius/chant-lexicon-aws";
|
|
27
|
+
* import { describeAvpResources } from "@intentius/chant-lexicon-cedar";
|
|
28
|
+
*
|
|
29
|
+
* await describeAvpResources({
|
|
30
|
+
* environment,
|
|
31
|
+
* entityNames,
|
|
32
|
+
* entities,
|
|
33
|
+
* client: { region: "us-west-2", signer: signRequest },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Three cases go out unsigned, byte for byte as they did before signing
|
|
38
|
+
* existed — the credential scope of {@link regionScope} and nothing more:
|
|
39
|
+
*
|
|
40
|
+
* - **No signer.** The default, and the reason `credentialsAvailable` still
|
|
41
|
+
* gates the readers: a caller that wires nothing in is still an
|
|
42
|
+
* emulator-and-test transport, and real AWS still rejects it.
|
|
43
|
+
* - **No credentials.** A signer with nothing to sign with does not run.
|
|
44
|
+
* - **An endpoint override.** An emulator does not verify signatures, and
|
|
45
|
+
* signing against one would make every local lane need credentials to read
|
|
46
|
+
* what it just deployed. `signEndpointOverride` opts back in for an
|
|
47
|
+
* override that *is* real AWS — a VPC endpoint, a signing proxy.
|
|
25
48
|
*/
|
|
26
49
|
/** Injectable HTTP, mirroring `AwsReadHttp` in the aws lexicon so tests avoid the network. */
|
|
27
50
|
export type AvpHttp = (url: string, init: {
|
|
@@ -40,6 +63,38 @@ export declare class AvpReadError extends Error {
|
|
|
40
63
|
/** The service's own error code (`ResourceNotFoundException`, `AccessDeniedException`, …). */
|
|
41
64
|
code?: string | undefined);
|
|
42
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* A resolved credential set — the aws lexicon's `AwsCredentials`, restated so
|
|
68
|
+
* that this file compiles without it. `sessionToken` is present for STS/role
|
|
69
|
+
* credentials.
|
|
70
|
+
*/
|
|
71
|
+
export interface AvpCredentials {
|
|
72
|
+
accessKeyId: string;
|
|
73
|
+
secretAccessKey: string;
|
|
74
|
+
sessionToken?: string;
|
|
75
|
+
}
|
|
76
|
+
/** A function that decides what to sign with; `undefined` means "nothing to". */
|
|
77
|
+
export type AvpCredentialResolver = () => AvpCredentials | undefined;
|
|
78
|
+
/** Either literal credentials or a resolver for them. */
|
|
79
|
+
export type AvpCredentialSource = AvpCredentials | AvpCredentialResolver;
|
|
80
|
+
/** One request to sign — the aws lexicon's `SigV4Request`, restated. */
|
|
81
|
+
export interface AvpSignableRequest {
|
|
82
|
+
method: string;
|
|
83
|
+
url: string;
|
|
84
|
+
headers: Record<string, string>;
|
|
85
|
+
body: string;
|
|
86
|
+
service: string;
|
|
87
|
+
region: string;
|
|
88
|
+
credentials: AvpCredentials;
|
|
89
|
+
/** Signing clock. Injected by tests; otherwise now. */
|
|
90
|
+
now?: Date;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The signing seam. `signRequest` from `@intentius/chant-lexicon-aws` satisfies
|
|
94
|
+
* this as it stands — the shapes above are its own, restated rather than
|
|
95
|
+
* imported, so cedar keeps no edge to the aws lexicon.
|
|
96
|
+
*/
|
|
97
|
+
export type AvpSigner = (request: AvpSignableRequest) => Record<string, string>;
|
|
43
98
|
export interface AvpClientOptions {
|
|
44
99
|
/** Endpoint override (an emulator, or a VPC endpoint). Omit for real AWS hosts. */
|
|
45
100
|
endpoint?: string;
|
|
@@ -49,7 +104,29 @@ export interface AvpClientOptions {
|
|
|
49
104
|
signal?: AbortSignal;
|
|
50
105
|
/** Environment to read credentials from. Defaults to `process.env`; injectable for tests. */
|
|
51
106
|
env?: Record<string, string | undefined>;
|
|
107
|
+
/** SigV4, injected. Omitted, every request goes out unsigned as it always did. */
|
|
108
|
+
signer?: AvpSigner;
|
|
109
|
+
/**
|
|
110
|
+
* What to sign with: literal credentials, or a resolver that decides. Omitted,
|
|
111
|
+
* the environment answers; when it has nothing, no signature is produced.
|
|
112
|
+
*/
|
|
113
|
+
credentials?: AvpCredentialSource;
|
|
114
|
+
/** Sign even against an endpoint override — for an override that is real AWS. */
|
|
115
|
+
signEndpointOverride?: boolean;
|
|
116
|
+
/** Signing clock. Injected by tests so a signature is reproducible. */
|
|
117
|
+
now?: Date;
|
|
52
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Credentials for a request: explicit → environment → absent.
|
|
121
|
+
*
|
|
122
|
+
* The same rule the aws lexicon's `resolveCredentials` applies, and for the same
|
|
123
|
+
* reasons: an injected resolver is authoritative and its refusal is not
|
|
124
|
+
* second-guessed against the environment, and a half-set environment is absent
|
|
125
|
+
* rather than a signature that cannot verify. A caller with a profile file, IMDS
|
|
126
|
+
* or a container credential endpoint resolves those itself and passes the result
|
|
127
|
+
* in — that is what the resolver form is for.
|
|
128
|
+
*/
|
|
129
|
+
export declare function resolveAvpCredentials(source?: AvpCredentialSource, env?: Record<string, string | undefined>): AvpCredentials | undefined;
|
|
53
130
|
/** Service host for AVP, honouring an endpoint override. */
|
|
54
131
|
export declare function avpUrl(endpoint?: string, region?: string): string;
|
|
55
132
|
/**
|
package/dist/avp/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/avp/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/avp/client.ts"],"names":[],"mappings":"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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "5627f37594343736b5fadb695bfa9a897382050d7ad80438a9bce9f5ac17e3e9",
|
|
5
5
|
"meta.json": "b218779260169a193882aeee76e5ac559588f1682657a001fb0982cffb5843fc",
|
|
6
6
|
"types/index.d.ts": "9e53a1cb3c9ee38f9de2f3c0db9cc28a0b67c3649be410c341e2451b9b0076cd",
|
|
7
7
|
"rules/policy-shape.ts": "1654730d188b7163dbad3051c75a562198a96eda6d36ce35701f2b729bf45194",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"skills/chant-cedar-meta-policy.md": "050b310a87196a48c827f1129698e4c6bbcd96dc963782e22bf1a640bbc20a1a",
|
|
31
31
|
"skills/chant-cedar-dogwood.md": "fc8c9a2ea9cd7bf87037c156b0b419397d46fbf5cd201c09be721249ac91b8da"
|
|
32
32
|
},
|
|
33
|
-
"composite": "
|
|
33
|
+
"composite": "adff77c510732a1fdaea5bee081322121028216083d44b6ea5da1b91637c59f3"
|
|
34
34
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-cedar",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.12",
|
|
4
4
|
"description": "Cedar lexicon for chant — typed authoring for Cedar authorization policies",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"zod": "^4.3.6",
|
|
77
|
-
"@intentius/chant": "^0.44.
|
|
77
|
+
"@intentius/chant": "^0.44.12",
|
|
78
78
|
"typescript": "^5.9.3"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/avp/OWNERSHIP.md
CHANGED
|
@@ -94,6 +94,44 @@ for a lexicon that reported nothing. The obligation is still real for anyone ext
|
|
|
94
94
|
this file: a new read path that cannot see the description must not be added to
|
|
95
95
|
`reads`.
|
|
96
96
|
|
|
97
|
+
## Reading the store: the signing seam
|
|
98
|
+
|
|
99
|
+
The reader was an emulator-and-test transport when #1652 landed, because its requests
|
|
100
|
+
were unsigned and real AWS rejects those. SigV4 now exists in the aws lexicon
|
|
101
|
+
(`lexicons/aws/src/api/sigv4.ts`, chant #1686) and `src/avp/client.ts` uses it — through
|
|
102
|
+
a function on `AvpClientOptions`, not through an import.
|
|
103
|
+
|
|
104
|
+
The reason is the same one that kept `src/avp/embed.ts` free of the aws lexicon: a
|
|
105
|
+
cedar → aws dependency edge would make the vendor-neutral lexicon unbuildable without
|
|
106
|
+
the AWS one, for the sake of a transport most Cedar deployments never use. So
|
|
107
|
+
`AvpSigner`, `AvpCredentials` and `AvpSignableRequest` restate the aws lexicon's own
|
|
108
|
+
shapes, and `signRequest` satisfies `AvpSigner` as it stands. A project that has both
|
|
109
|
+
lexicons installed wires them where it already builds the client options:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { signRequest } from "@intentius/chant-lexicon-aws";
|
|
113
|
+
import { describeAvpResources } from "@intentius/chant-lexicon-cedar";
|
|
114
|
+
|
|
115
|
+
await describeAvpResources({
|
|
116
|
+
environment,
|
|
117
|
+
entityNames,
|
|
118
|
+
entities,
|
|
119
|
+
client: { region: "us-west-2", signer: signRequest },
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The cost of restating rather than importing is that a shape change in the aws lexicon
|
|
124
|
+
shows up as a type error in the consumer that wires the two, not here. That is the
|
|
125
|
+
trade the decoupling buys, and it is why the two files name each other in prose.
|
|
126
|
+
|
|
127
|
+
Unsigned stays the default, and three cases stay unsigned regardless: no signer wired
|
|
128
|
+
in, no credentials resolved, and an endpoint override without `signEndpointOverride`
|
|
129
|
+
(an emulator does not verify signatures, and signing against one would make every local
|
|
130
|
+
lane need credentials to read what it just deployed). `credentialsAvailable` therefore
|
|
131
|
+
still gates the readers exactly as before — a caller that wires nothing in gets every
|
|
132
|
+
entity NOT-OBSERVED with `no-credentials` rather than a request that was never going to
|
|
133
|
+
work.
|
|
134
|
+
|
|
97
135
|
## Alternatives rejected
|
|
98
136
|
|
|
99
137
|
**Store-level tags as the only channel.** The epic's provisional position. Rejected
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AVP transport's signing decision (#1686 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* `lexicons/aws/src/api/sigv4.test.ts` proves a SigV4 signature is correct
|
|
5
|
+
* against AWS's own vectors. Nothing here re-proves that — cedar does not hold
|
|
6
|
+
* the implementation and deliberately does not import it. What these cover is
|
|
7
|
+
* the decision the client makes: whether a signer runs at all, what request it
|
|
8
|
+
* is handed, and that the unsigned path is unchanged when one is not wired in.
|
|
9
|
+
*/
|
|
10
|
+
import { createHmac } from "node:crypto";
|
|
11
|
+
import { describe, expect, test } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
avpCall,
|
|
14
|
+
resolveAvpCredentials,
|
|
15
|
+
type AvpClientOptions,
|
|
16
|
+
type AvpHttp,
|
|
17
|
+
type AvpSignableRequest,
|
|
18
|
+
type AvpSigner,
|
|
19
|
+
} from "./client";
|
|
20
|
+
|
|
21
|
+
const credentials = { accessKeyId: "AKIDEXAMPLE", secretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" };
|
|
22
|
+
const now = new Date("2015-08-30T12:36:00Z");
|
|
23
|
+
|
|
24
|
+
interface Call {
|
|
25
|
+
url: string;
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
body: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** An {@link AvpHttp} that records what it was given and answers an empty object. */
|
|
31
|
+
function recording(): { http: AvpHttp; calls: Call[] } {
|
|
32
|
+
const calls: Call[] = [];
|
|
33
|
+
const http: AvpHttp = async (url, init) => {
|
|
34
|
+
calls.push({ url, headers: init.headers, body: init.body });
|
|
35
|
+
return { status: 200, text: "{}" };
|
|
36
|
+
};
|
|
37
|
+
return { http, calls };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A stand-in for the aws lexicon's `signRequest`, with its shape and its
|
|
42
|
+
* observable habits: it emits `x-amz-date`, a session token when there is one,
|
|
43
|
+
* and an `Authorization` header of the real form, and it never emits `host`.
|
|
44
|
+
* The signature is an HMAC over the body — deterministic, and enough to tell a
|
|
45
|
+
* signed request from the placeholder.
|
|
46
|
+
*/
|
|
47
|
+
function stubSigner(): { signer: AvpSigner; signed: AvpSignableRequest[] } {
|
|
48
|
+
const signed: AvpSignableRequest[] = [];
|
|
49
|
+
const signer: AvpSigner = (request) => {
|
|
50
|
+
signed.push(request);
|
|
51
|
+
const timestamp = (request.now ?? new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
52
|
+
const day = timestamp.slice(0, 8);
|
|
53
|
+
const scope = `${day}/${request.region}/${request.service}/aws4_request`;
|
|
54
|
+
const signature = createHmac("sha256", request.credentials.secretAccessKey).update(request.body).digest("hex");
|
|
55
|
+
return {
|
|
56
|
+
...request.headers,
|
|
57
|
+
"x-amz-date": timestamp,
|
|
58
|
+
...(request.credentials.sessionToken ? { "x-amz-security-token": request.credentials.sessionToken } : {}),
|
|
59
|
+
authorization:
|
|
60
|
+
`AWS4-HMAC-SHA256 Credential=${request.credentials.accessKeyId}/${scope}, ` +
|
|
61
|
+
`SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=${signature}`,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
return { signer, signed };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("SigV4 on the AVP read path", () => {
|
|
68
|
+
test("a signer, credentials and a real host produce a signed request", async () => {
|
|
69
|
+
const { http, calls } = recording();
|
|
70
|
+
const { signer, signed } = stubSigner();
|
|
71
|
+
await avpCall("GetPolicyStore", { policyStoreId: "PS-abc" }, {
|
|
72
|
+
region: "us-west-2",
|
|
73
|
+
credentials,
|
|
74
|
+
signer,
|
|
75
|
+
now,
|
|
76
|
+
http,
|
|
77
|
+
env: {},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const auth = calls[0]!.headers.authorization!;
|
|
81
|
+
expect(auth).toContain("Credential=AKIDEXAMPLE/20150830/us-west-2/verifiedpermissions/aws4_request");
|
|
82
|
+
expect(auth).not.toContain("Signature=unsigned");
|
|
83
|
+
expect(auth).toMatch(/Signature=[0-9a-f]{64}$/);
|
|
84
|
+
expect(calls[0]!.headers["x-amz-date"]).toBe("20150830T123600Z");
|
|
85
|
+
// `host` is signed but never emitted: fetch computes it and forbids the override.
|
|
86
|
+
expect(calls[0]!.headers.host).toBeUndefined();
|
|
87
|
+
|
|
88
|
+
// What the signer was handed is a complete SigV4 request for this call.
|
|
89
|
+
expect(signed).toHaveLength(1);
|
|
90
|
+
expect(signed[0]).toMatchObject({
|
|
91
|
+
method: "POST",
|
|
92
|
+
url: "https://verifiedpermissions.us-west-2.amazonaws.com/",
|
|
93
|
+
service: "verifiedpermissions",
|
|
94
|
+
region: "us-west-2",
|
|
95
|
+
body: JSON.stringify({ policyStoreId: "PS-abc" }),
|
|
96
|
+
credentials,
|
|
97
|
+
now,
|
|
98
|
+
});
|
|
99
|
+
expect(signed[0]!.headers).toEqual({
|
|
100
|
+
"content-type": "application/x-amz-json-1.0",
|
|
101
|
+
"x-amz-target": "VerifiedPermissions.GetPolicyStore",
|
|
102
|
+
});
|
|
103
|
+
// The body the signer hashed is the body that went out.
|
|
104
|
+
expect(calls[0]!.body).toBe(signed[0]!.body);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("the signer's headers are the wire headers — no placeholder scope survives", async () => {
|
|
108
|
+
const { http, calls } = recording();
|
|
109
|
+
const { signer } = stubSigner();
|
|
110
|
+
await avpCall("ListPolicies", { policyStoreId: "PS-abc" }, {
|
|
111
|
+
region: "us-west-2",
|
|
112
|
+
credentials,
|
|
113
|
+
signer,
|
|
114
|
+
now,
|
|
115
|
+
http,
|
|
116
|
+
env: { AWS_ACCESS_KEY_ID: "AKIDENV" },
|
|
117
|
+
});
|
|
118
|
+
expect(Object.keys(calls[0]!.headers).sort()).toEqual([
|
|
119
|
+
"authorization",
|
|
120
|
+
"content-type",
|
|
121
|
+
"x-amz-date",
|
|
122
|
+
"x-amz-target",
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a session token rides along when the credentials are temporary", async () => {
|
|
127
|
+
const { http, calls } = recording();
|
|
128
|
+
const { signer } = stubSigner();
|
|
129
|
+
await avpCall("ListPolicies", { policyStoreId: "PS-abc" }, {
|
|
130
|
+
region: "eu-west-1",
|
|
131
|
+
signer,
|
|
132
|
+
now,
|
|
133
|
+
http,
|
|
134
|
+
env: { AWS_ACCESS_KEY_ID: "AKIDENV", AWS_SECRET_ACCESS_KEY: "s", AWS_SESSION_TOKEN: "tok" },
|
|
135
|
+
});
|
|
136
|
+
expect(calls[0]!.headers.authorization).toContain("Credential=AKIDENV/20150830/eu-west-1/verifiedpermissions/");
|
|
137
|
+
expect(calls[0]!.headers["x-amz-security-token"]).toBe("tok");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("signing without a named region falls back to the same default the host does", async () => {
|
|
141
|
+
const { http, calls } = recording();
|
|
142
|
+
const { signer, signed } = stubSigner();
|
|
143
|
+
await avpCall("ListPolicies", { policyStoreId: "PS-abc" }, { credentials, signer, now, http, env: {} });
|
|
144
|
+
expect(calls[0]!.url).toBe("https://verifiedpermissions.us-east-1.amazonaws.com/");
|
|
145
|
+
expect(signed[0]!.region).toBe("us-east-1");
|
|
146
|
+
expect(calls[0]!.headers.authorization).toContain("/20150830/us-east-1/verifiedpermissions/aws4_request");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("the unsigned path, unchanged", () => {
|
|
151
|
+
/** The headers one `ListPolicies` goes out with under `options`. */
|
|
152
|
+
async function headersFor(options: AvpClientOptions): Promise<Record<string, string>> {
|
|
153
|
+
const { http, calls } = recording();
|
|
154
|
+
await avpCall("ListPolicies", { policyStoreId: "PS-abc" }, { ...options, http });
|
|
155
|
+
return calls[0]!.headers;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test("no signer leaves the transport exactly as it was", async () => {
|
|
159
|
+
const headers = await headersFor({ region: "us-west-2", env: {} });
|
|
160
|
+
expect(Object.keys(headers).sort()).toEqual(["authorization", "content-type", "x-amz-target"]);
|
|
161
|
+
expect(headers["content-type"]).toBe("application/x-amz-json-1.0");
|
|
162
|
+
expect(headers["x-amz-target"]).toBe("VerifiedPermissions.ListPolicies");
|
|
163
|
+
expect(headers.authorization).toContain("Credential=chant/");
|
|
164
|
+
expect(headers.authorization).toContain("/us-west-2/verifiedpermissions/aws4_request");
|
|
165
|
+
expect(headers.authorization).toContain("SignedHeaders=host, Signature=unsigned");
|
|
166
|
+
expect(headers["x-amz-date"]).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a signer with nothing to sign with is byte-identical to no signer", async () => {
|
|
170
|
+
const { signer, signed } = stubSigner();
|
|
171
|
+
const unsigned = await headersFor({ region: "us-west-2", env: {} });
|
|
172
|
+
const withSigner = await headersFor({ region: "us-west-2", signer, env: {} });
|
|
173
|
+
expect(withSigner).toEqual(unsigned);
|
|
174
|
+
expect(signed).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a resolver that declines is respected, and the environment is not consulted behind it", async () => {
|
|
178
|
+
const { signer, signed } = stubSigner();
|
|
179
|
+
const headers = await headersFor({
|
|
180
|
+
region: "us-west-2",
|
|
181
|
+
signer,
|
|
182
|
+
credentials: () => undefined,
|
|
183
|
+
env: { AWS_ACCESS_KEY_ID: "AKIDENV", AWS_SECRET_ACCESS_KEY: "s" },
|
|
184
|
+
});
|
|
185
|
+
expect(headers.authorization).toContain("Signature=unsigned");
|
|
186
|
+
expect(signed).toHaveLength(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("no region still means no authorization header at all", async () => {
|
|
190
|
+
const headers = await headersFor({ env: {} });
|
|
191
|
+
expect(Object.keys(headers).sort()).toEqual(["content-type", "x-amz-target"]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("endpoint overrides", () => {
|
|
196
|
+
test("an override is not signed, so the emulator lanes need no credentials", async () => {
|
|
197
|
+
const { http, calls } = recording();
|
|
198
|
+
const { signer, signed } = stubSigner();
|
|
199
|
+
await avpCall("ListPolicies", { policyStoreId: "PS-abc" }, {
|
|
200
|
+
endpoint: "http://localhost:4566",
|
|
201
|
+
region: "us-west-2",
|
|
202
|
+
credentials,
|
|
203
|
+
signer,
|
|
204
|
+
http,
|
|
205
|
+
env: {},
|
|
206
|
+
});
|
|
207
|
+
expect(calls[0]!.url).toBe("http://localhost:4566/");
|
|
208
|
+
expect(calls[0]!.headers.authorization).toContain("Signature=unsigned");
|
|
209
|
+
expect(calls[0]!.headers["x-amz-date"]).toBeUndefined();
|
|
210
|
+
expect(signed).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("AWS_ENDPOINT_URL counts as an override, the same as the option does", async () => {
|
|
214
|
+
const { http, calls } = recording();
|
|
215
|
+
const { signer, signed } = stubSigner();
|
|
216
|
+
await avpCall("ListPolicies", { policyStoreId: "PS-abc" }, {
|
|
217
|
+
region: "us-west-2",
|
|
218
|
+
credentials,
|
|
219
|
+
signer,
|
|
220
|
+
http,
|
|
221
|
+
env: { AWS_ENDPOINT_URL: "http://localhost:4566" },
|
|
222
|
+
});
|
|
223
|
+
expect(calls[0]!.url).toBe("http://localhost:4566/");
|
|
224
|
+
expect(calls[0]!.headers.authorization).toContain("Signature=unsigned");
|
|
225
|
+
expect(signed).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("signEndpointOverride signs one anyway — for an override that is real AWS", async () => {
|
|
229
|
+
const { http, calls } = recording();
|
|
230
|
+
const { signer, signed } = stubSigner();
|
|
231
|
+
await avpCall("ListPolicies", { policyStoreId: "PS-abc" }, {
|
|
232
|
+
endpoint: "https://vpce-1234.verifiedpermissions.us-west-2.vpce.amazonaws.com",
|
|
233
|
+
region: "us-west-2",
|
|
234
|
+
credentials,
|
|
235
|
+
signer,
|
|
236
|
+
signEndpointOverride: true,
|
|
237
|
+
now,
|
|
238
|
+
http,
|
|
239
|
+
env: {},
|
|
240
|
+
});
|
|
241
|
+
expect(calls[0]!.headers.authorization).toMatch(/Signature=[0-9a-f]{64}$/);
|
|
242
|
+
expect(signed[0]!.url).toBe("https://vpce-1234.verifiedpermissions.us-west-2.vpce.amazonaws.com/");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("resolveAvpCredentials", () => {
|
|
247
|
+
test("literal credentials are final", () => {
|
|
248
|
+
expect(resolveAvpCredentials(credentials, { AWS_ACCESS_KEY_ID: "other" })).toEqual(credentials);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("a resolver is authoritative in both directions", () => {
|
|
252
|
+
expect(resolveAvpCredentials(() => credentials, {})).toEqual(credentials);
|
|
253
|
+
expect(resolveAvpCredentials(() => undefined, { AWS_ACCESS_KEY_ID: "a", AWS_SECRET_ACCESS_KEY: "b" })).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("the environment answers when nothing was passed", () => {
|
|
257
|
+
expect(
|
|
258
|
+
resolveAvpCredentials(undefined, {
|
|
259
|
+
AWS_ACCESS_KEY_ID: "a",
|
|
260
|
+
AWS_SECRET_ACCESS_KEY: "b",
|
|
261
|
+
AWS_SESSION_TOKEN: "t",
|
|
262
|
+
}),
|
|
263
|
+
).toEqual({ accessKeyId: "a", secretAccessKey: "b", sessionToken: "t" });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("a half-set environment is absent, not a signature that cannot verify", () => {
|
|
267
|
+
expect(resolveAvpCredentials(undefined, { AWS_ACCESS_KEY_ID: "a" })).toBeUndefined();
|
|
268
|
+
expect(resolveAvpCredentials(undefined, { AWS_SECRET_ACCESS_KEY: "b" })).toBeUndefined();
|
|
269
|
+
expect(resolveAvpCredentials(undefined, {})).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
});
|
package/src/avp/client.ts
CHANGED
|
@@ -14,14 +14,37 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## Signing
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* SigV4 lives once, in `lexicons/aws/src/api/sigv4.ts` (#1686). This module
|
|
18
|
+
* does not import it. A cedar → aws dependency edge would make the vendor-
|
|
19
|
+
* neutral lexicon unbuildable without the AWS one, which is the same rule that
|
|
20
|
+
* kept `src/avp/embed.ts` free of the aws lexicon: the seam is the data shape.
|
|
21
|
+
* So the signer arrives as a function on {@link AvpClientOptions}, typed here
|
|
22
|
+
* structurally against what the aws lexicon exports, and a project holding both
|
|
23
|
+
* lexicons wires them in one line:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { signRequest } from "@intentius/chant-lexicon-aws";
|
|
27
|
+
* import { describeAvpResources } from "@intentius/chant-lexicon-cedar";
|
|
28
|
+
*
|
|
29
|
+
* await describeAvpResources({
|
|
30
|
+
* environment,
|
|
31
|
+
* entityNames,
|
|
32
|
+
* entities,
|
|
33
|
+
* client: { region: "us-west-2", signer: signRequest },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Three cases go out unsigned, byte for byte as they did before signing
|
|
38
|
+
* existed — the credential scope of {@link regionScope} and nothing more:
|
|
39
|
+
*
|
|
40
|
+
* - **No signer.** The default, and the reason `credentialsAvailable` still
|
|
41
|
+
* gates the readers: a caller that wires nothing in is still an
|
|
42
|
+
* emulator-and-test transport, and real AWS still rejects it.
|
|
43
|
+
* - **No credentials.** A signer with nothing to sign with does not run.
|
|
44
|
+
* - **An endpoint override.** An emulator does not verify signatures, and
|
|
45
|
+
* signing against one would make every local lane need credentials to read
|
|
46
|
+
* what it just deployed. `signEndpointOverride` opts back in for an
|
|
47
|
+
* override that *is* real AWS — a VPC endpoint, a signing proxy.
|
|
25
48
|
*/
|
|
26
49
|
|
|
27
50
|
const DEFAULT_REGION = "us-east-1";
|
|
@@ -53,6 +76,43 @@ export class AvpReadError extends Error {
|
|
|
53
76
|
}
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
/**
|
|
80
|
+
* A resolved credential set — the aws lexicon's `AwsCredentials`, restated so
|
|
81
|
+
* that this file compiles without it. `sessionToken` is present for STS/role
|
|
82
|
+
* credentials.
|
|
83
|
+
*/
|
|
84
|
+
export interface AvpCredentials {
|
|
85
|
+
accessKeyId: string;
|
|
86
|
+
secretAccessKey: string;
|
|
87
|
+
sessionToken?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A function that decides what to sign with; `undefined` means "nothing to". */
|
|
91
|
+
export type AvpCredentialResolver = () => AvpCredentials | undefined;
|
|
92
|
+
|
|
93
|
+
/** Either literal credentials or a resolver for them. */
|
|
94
|
+
export type AvpCredentialSource = AvpCredentials | AvpCredentialResolver;
|
|
95
|
+
|
|
96
|
+
/** One request to sign — the aws lexicon's `SigV4Request`, restated. */
|
|
97
|
+
export interface AvpSignableRequest {
|
|
98
|
+
method: string;
|
|
99
|
+
url: string;
|
|
100
|
+
headers: Record<string, string>;
|
|
101
|
+
body: string;
|
|
102
|
+
service: string;
|
|
103
|
+
region: string;
|
|
104
|
+
credentials: AvpCredentials;
|
|
105
|
+
/** Signing clock. Injected by tests; otherwise now. */
|
|
106
|
+
now?: Date;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The signing seam. `signRequest` from `@intentius/chant-lexicon-aws` satisfies
|
|
111
|
+
* this as it stands — the shapes above are its own, restated rather than
|
|
112
|
+
* imported, so cedar keeps no edge to the aws lexicon.
|
|
113
|
+
*/
|
|
114
|
+
export type AvpSigner = (request: AvpSignableRequest) => Record<string, string>;
|
|
115
|
+
|
|
56
116
|
export interface AvpClientOptions {
|
|
57
117
|
/** Endpoint override (an emulator, or a VPC endpoint). Omit for real AWS hosts. */
|
|
58
118
|
endpoint?: string;
|
|
@@ -62,6 +122,43 @@ export interface AvpClientOptions {
|
|
|
62
122
|
signal?: AbortSignal;
|
|
63
123
|
/** Environment to read credentials from. Defaults to `process.env`; injectable for tests. */
|
|
64
124
|
env?: Record<string, string | undefined>;
|
|
125
|
+
/** SigV4, injected. Omitted, every request goes out unsigned as it always did. */
|
|
126
|
+
signer?: AvpSigner;
|
|
127
|
+
/**
|
|
128
|
+
* What to sign with: literal credentials, or a resolver that decides. Omitted,
|
|
129
|
+
* the environment answers; when it has nothing, no signature is produced.
|
|
130
|
+
*/
|
|
131
|
+
credentials?: AvpCredentialSource;
|
|
132
|
+
/** Sign even against an endpoint override — for an override that is real AWS. */
|
|
133
|
+
signEndpointOverride?: boolean;
|
|
134
|
+
/** Signing clock. Injected by tests so a signature is reproducible. */
|
|
135
|
+
now?: Date;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Credentials for a request: explicit → environment → absent.
|
|
140
|
+
*
|
|
141
|
+
* The same rule the aws lexicon's `resolveCredentials` applies, and for the same
|
|
142
|
+
* reasons: an injected resolver is authoritative and its refusal is not
|
|
143
|
+
* second-guessed against the environment, and a half-set environment is absent
|
|
144
|
+
* rather than a signature that cannot verify. A caller with a profile file, IMDS
|
|
145
|
+
* or a container credential endpoint resolves those itself and passes the result
|
|
146
|
+
* in — that is what the resolver form is for.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveAvpCredentials(
|
|
149
|
+
source?: AvpCredentialSource,
|
|
150
|
+
env: Record<string, string | undefined> = process.env,
|
|
151
|
+
): AvpCredentials | undefined {
|
|
152
|
+
if (typeof source === "function") return source();
|
|
153
|
+
if (source) return source;
|
|
154
|
+
const accessKeyId = env.AWS_ACCESS_KEY_ID;
|
|
155
|
+
const secretAccessKey = env.AWS_SECRET_ACCESS_KEY;
|
|
156
|
+
if (!accessKeyId || !secretAccessKey) return undefined;
|
|
157
|
+
return {
|
|
158
|
+
accessKeyId,
|
|
159
|
+
secretAccessKey,
|
|
160
|
+
...(env.AWS_SESSION_TOKEN ? { sessionToken: env.AWS_SESSION_TOKEN } : {}),
|
|
161
|
+
};
|
|
65
162
|
}
|
|
66
163
|
|
|
67
164
|
/** Service host for AVP, honouring an endpoint override. */
|
|
@@ -95,7 +192,8 @@ export function credentialsAvailable(env: Record<string, string | undefined> = p
|
|
|
95
192
|
* This is NOT SigV4 — the signature is a placeholder, exactly as in the aws
|
|
96
193
|
* lexicon's read client, and it must not be mistaken for a signed read path. It
|
|
97
194
|
* carries the region so that an endpoint override (one host for every region)
|
|
98
|
-
* still reaches the right one
|
|
195
|
+
* still reaches the right one, and it is only ever sent when no signature was
|
|
196
|
+
* produced; the moment one is, {@link requestHeaders} sends that instead.
|
|
99
197
|
*/
|
|
100
198
|
function regionScope(region: string | undefined, env: Record<string, string | undefined>): Record<string, string> {
|
|
101
199
|
if (!region) return {};
|
|
@@ -112,6 +210,44 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
112
210
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
211
|
}
|
|
114
212
|
|
|
213
|
+
/**
|
|
214
|
+
* The headers one request goes out with — signed when a signer was injected,
|
|
215
|
+
* something resolved to sign with, and the target is real AWS; scope-only in
|
|
216
|
+
* every other case, which is the path this transport had before signing.
|
|
217
|
+
*
|
|
218
|
+
* Signing needs a region even when the caller named none, because the scope
|
|
219
|
+
* string has a slot for one; it borrows the same default {@link avpUrl} used to
|
|
220
|
+
* build the host, so the signature agrees with the endpoint it is sent to.
|
|
221
|
+
*/
|
|
222
|
+
function requestHeaders(
|
|
223
|
+
operation: string,
|
|
224
|
+
url: string,
|
|
225
|
+
body: string,
|
|
226
|
+
endpoint: string | undefined,
|
|
227
|
+
options: AvpClientOptions,
|
|
228
|
+
env: Record<string, string | undefined>,
|
|
229
|
+
): Record<string, string> {
|
|
230
|
+
const base: Record<string, string> = {
|
|
231
|
+
"content-type": "application/x-amz-json-1.0",
|
|
232
|
+
"x-amz-target": `${TARGET_PREFIX}.${operation}`,
|
|
233
|
+
};
|
|
234
|
+
const signer = options.signer;
|
|
235
|
+
const credentials = signer ? resolveAvpCredentials(options.credentials, env) : undefined;
|
|
236
|
+
if (!signer || !credentials || (endpoint && options.signEndpointOverride !== true)) {
|
|
237
|
+
return { ...base, ...regionScope(options.region, env) };
|
|
238
|
+
}
|
|
239
|
+
return signer({
|
|
240
|
+
method: "POST",
|
|
241
|
+
url,
|
|
242
|
+
headers: base,
|
|
243
|
+
body,
|
|
244
|
+
service: SERVICE,
|
|
245
|
+
region: options.region ?? DEFAULT_REGION,
|
|
246
|
+
credentials,
|
|
247
|
+
...(options.now ? { now: options.now } : {}),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
115
251
|
/** One AVP call. Throws {@link AvpReadError} carrying the service's `__type`. */
|
|
116
252
|
export async function avpCall(
|
|
117
253
|
operation: string,
|
|
@@ -120,16 +256,14 @@ export async function avpCall(
|
|
|
120
256
|
): Promise<Record<string, unknown>> {
|
|
121
257
|
const env = options.env ?? process.env;
|
|
122
258
|
const http = options.http ?? defaultHttp;
|
|
123
|
-
const
|
|
259
|
+
const endpoint = options.endpoint ?? env.AWS_ENDPOINT_URL;
|
|
260
|
+
const url = avpUrl(endpoint, options.region);
|
|
261
|
+
const payloadJson = JSON.stringify(payload);
|
|
124
262
|
const res = await http(
|
|
125
263
|
url,
|
|
126
264
|
{
|
|
127
|
-
headers:
|
|
128
|
-
|
|
129
|
-
"x-amz-target": `${TARGET_PREFIX}.${operation}`,
|
|
130
|
-
...regionScope(options.region, env),
|
|
131
|
-
},
|
|
132
|
-
body: JSON.stringify(payload),
|
|
265
|
+
headers: requestHeaders(operation, url, payloadJson, endpoint, options, env),
|
|
266
|
+
body: payloadJson,
|
|
133
267
|
},
|
|
134
268
|
options.signal,
|
|
135
269
|
);
|