runline 0.36.0 → 0.37.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/credentials/aws-plugin.d.ts +3 -0
- package/dist/credentials/aws-plugin.js +7 -2
- package/dist/credentials/aws.d.ts +11 -0
- package/dist/credentials/aws.js +110 -11
- package/dist/credentials/broker.js +8 -7
- package/dist/credentials/transport.js +5 -1
- package/dist/credentials/types.d.ts +7 -0
- package/package.json +1 -1
|
@@ -35,6 +35,9 @@ export interface AwsCredentialSpec {
|
|
|
35
35
|
* `roleDurationSeconds` shape each session request.
|
|
36
36
|
* - `stsRegion`, else `region`, else `us-east-1`, is where sessions are
|
|
37
37
|
* requested.
|
|
38
|
+
* - `awsProfile` names an AWS CLI profile (an SSO login) whose key this
|
|
39
|
+
* process signs with in place of a stored one; a host that stores the
|
|
40
|
+
* key ignores it.
|
|
38
41
|
*/
|
|
39
42
|
export declare function awsCredential(spec: AwsCredentialSpec): CredentialDeclaration;
|
|
40
43
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as t from "typebox";
|
|
7
7
|
import { AuthError } from "../auth/errors.js";
|
|
8
|
-
import { awsEndpoint, awsErrorCode, awsRole, parseAwsXml } from "./aws.js";
|
|
8
|
+
import { awsEndpoint, awsErrorCode, awsProfileName, awsRole, parseAwsXml, } from "./aws.js";
|
|
9
9
|
import { credentialRequest, errorIdentifier, failureMessage, jsonAnswer, } from "./plugin.js";
|
|
10
10
|
import { AwsAccessKeySchema, AwsSessionSchema } from "./registry.js";
|
|
11
11
|
const STRICT = { additionalProperties: false };
|
|
@@ -57,6 +57,9 @@ function configString(value) {
|
|
|
57
57
|
* `roleDurationSeconds` shape each session request.
|
|
58
58
|
* - `stsRegion`, else `region`, else `us-east-1`, is where sessions are
|
|
59
59
|
* requested.
|
|
60
|
+
* - `awsProfile` names an AWS CLI profile (an SSO login) whose key this
|
|
61
|
+
* process signs with in place of a stored one; a host that stores the
|
|
62
|
+
* key ignores it.
|
|
60
63
|
*/
|
|
61
64
|
export function awsCredential(spec) {
|
|
62
65
|
const key = { credential: AwsAccessKeySchema };
|
|
@@ -68,6 +71,8 @@ export function awsCredential(spec) {
|
|
|
68
71
|
return (config) => {
|
|
69
72
|
const targets = spec.targets(config);
|
|
70
73
|
const probe = spec.probe ? { probe: spec.probe } : {};
|
|
74
|
+
const profile = configString(config.awsProfile);
|
|
75
|
+
const awsProfile = profile === undefined ? undefined : awsProfileName(profile);
|
|
71
76
|
const roleArn = configString(config.roleArn);
|
|
72
77
|
const externalId = configString(config.roleExternalId);
|
|
73
78
|
const durationSeconds = configInteger(config.roleDurationSeconds);
|
|
@@ -93,7 +98,7 @@ export function awsCredential(spec) {
|
|
|
93
98
|
},
|
|
94
99
|
},
|
|
95
100
|
},
|
|
96
|
-
localSecret,
|
|
101
|
+
...(awsProfile === undefined ? { localSecret } : { awsProfile }),
|
|
97
102
|
...(roleArn === undefined
|
|
98
103
|
? { method: "accessKey" }
|
|
99
104
|
: {
|
|
@@ -14,6 +14,17 @@ export declare function awsEndpoint(service: string, region: string): string;
|
|
|
14
14
|
* signature. Anything else is refused before any request.
|
|
15
15
|
*/
|
|
16
16
|
export declare function awsAccessKey(value: unknown): AwsAccessKey;
|
|
17
|
+
/** A profile name from config; anything else is refused before the CLI runs. */
|
|
18
|
+
export declare function awsProfileName(value: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* The access key of an AWS CLI profile, as the CLI resolves it: an SSO
|
|
21
|
+
* login, an assumed role, or a key in `~/.aws/credentials`, through
|
|
22
|
+
* `aws configure export-credentials` (the `credential_process` contract).
|
|
23
|
+
* Held until five minutes before it expires; however many callers find
|
|
24
|
+
* it missing or stale, one CLI run serves them all, and a failed run is
|
|
25
|
+
* not held.
|
|
26
|
+
*/
|
|
27
|
+
export declare function awsProfileKey(name: string): Promise<AwsAccessKey>;
|
|
17
28
|
/** A role as STS accepts it; anything else is refused before any request. */
|
|
18
29
|
export declare function awsRole(role: AwsRole): AwsRole;
|
|
19
30
|
/**
|
package/dist/credentials/aws.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { XMLParser } from "fast-xml-parser";
|
|
2
3
|
import { AuthError } from "../auth/errors.js";
|
|
3
4
|
import { sendResource } from "./http.js";
|
|
@@ -55,6 +56,110 @@ export function awsAccessKey(value) {
|
|
|
55
56
|
: { sessionToken: secret(key.sessionToken) }),
|
|
56
57
|
};
|
|
57
58
|
}
|
|
59
|
+
/** An AWS CLI profile name, as `~/.aws/config` section names allow it. */
|
|
60
|
+
const PROFILE = /^[\w+=,.@-]{1,64}$/;
|
|
61
|
+
/** A profile name from config; anything else is refused before the CLI runs. */
|
|
62
|
+
export function awsProfileName(value) {
|
|
63
|
+
if (typeof value !== "string" || !PROFILE.test(value))
|
|
64
|
+
throw new AuthError("invalid_credentials");
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/** A profile's key is used until this long before it expires. */
|
|
68
|
+
const PROFILE_KEY_MARGIN_MS = 5 * 60_000;
|
|
69
|
+
/**
|
|
70
|
+
* Each profile's key as this process holds it: settled while it lasts,
|
|
71
|
+
* or still in flight, so concurrent callers share one CLI run.
|
|
72
|
+
*/
|
|
73
|
+
const profileKeys = new Map();
|
|
74
|
+
/** A key a provider issued: one that cannot sign is a malformed answer. */
|
|
75
|
+
function issuedKey(value) {
|
|
76
|
+
try {
|
|
77
|
+
return awsAccessKey(value);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new AuthError("invalid_response");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Why the CLI produced no key: it is not installed, it ran out of time,
|
|
85
|
+
* it answered more than a key, or it refused (no login, or an expired
|
|
86
|
+
* SSO session, which `aws sso login` mends).
|
|
87
|
+
*/
|
|
88
|
+
function cliFailure(error) {
|
|
89
|
+
if (error.code === "ENOENT")
|
|
90
|
+
return new AuthError("invalid_credentials");
|
|
91
|
+
if (error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
92
|
+
return new AuthError("invalid_response");
|
|
93
|
+
if (error.killed)
|
|
94
|
+
return new AuthError("request_failed");
|
|
95
|
+
return new AuthError("reconnect_required");
|
|
96
|
+
}
|
|
97
|
+
/** One CLI run: the profile's key, as `credential_process` defines it. */
|
|
98
|
+
async function exportProfileKey(profile) {
|
|
99
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
100
|
+
execFile("aws", [
|
|
101
|
+
"configure",
|
|
102
|
+
"export-credentials",
|
|
103
|
+
"--profile",
|
|
104
|
+
profile,
|
|
105
|
+
"--format",
|
|
106
|
+
"process",
|
|
107
|
+
], { timeout: 30_000, maxBuffer: 64 * 1024 }, (error, out) => (error ? reject(cliFailure(error)) : resolve(out)));
|
|
108
|
+
});
|
|
109
|
+
let exported;
|
|
110
|
+
try {
|
|
111
|
+
exported = JSON.parse(stdout);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new AuthError("invalid_response");
|
|
115
|
+
}
|
|
116
|
+
const expiresAt = exported.Expiration === undefined
|
|
117
|
+
? undefined
|
|
118
|
+
: Date.parse(String(exported.Expiration));
|
|
119
|
+
if (exported.Version !== 1 ||
|
|
120
|
+
(expiresAt !== undefined && !Number.isSafeInteger(expiresAt)))
|
|
121
|
+
throw new AuthError("invalid_response");
|
|
122
|
+
return {
|
|
123
|
+
key: issuedKey({
|
|
124
|
+
accessKeyId: exported.AccessKeyId,
|
|
125
|
+
secretAccessKey: exported.SecretAccessKey,
|
|
126
|
+
...(exported.SessionToken === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: { sessionToken: exported.SessionToken }),
|
|
129
|
+
}),
|
|
130
|
+
expiresAt,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The access key of an AWS CLI profile, as the CLI resolves it: an SSO
|
|
135
|
+
* login, an assumed role, or a key in `~/.aws/credentials`, through
|
|
136
|
+
* `aws configure export-credentials` (the `credential_process` contract).
|
|
137
|
+
* Held until five minutes before it expires; however many callers find
|
|
138
|
+
* it missing or stale, one CLI run serves them all, and a failed run is
|
|
139
|
+
* not held.
|
|
140
|
+
*/
|
|
141
|
+
export async function awsProfileKey(name) {
|
|
142
|
+
const profile = awsProfileName(name);
|
|
143
|
+
for (;;) {
|
|
144
|
+
const held = profileKeys.get(profile);
|
|
145
|
+
if (!held)
|
|
146
|
+
break;
|
|
147
|
+
const { key, expiresAt } = await held;
|
|
148
|
+
if (expiresAt === undefined ||
|
|
149
|
+
expiresAt > Date.now() + PROFILE_KEY_MARGIN_MS)
|
|
150
|
+
return key;
|
|
151
|
+
// Stale: refresh it, unless another caller already started to.
|
|
152
|
+
if (profileKeys.get(profile) === held)
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const exporting = exportProfileKey(profile);
|
|
156
|
+
profileKeys.set(profile, exporting);
|
|
157
|
+
exporting.catch(() => {
|
|
158
|
+
if (profileKeys.get(profile) === exporting)
|
|
159
|
+
profileKeys.delete(profile);
|
|
160
|
+
});
|
|
161
|
+
return (await exporting).key;
|
|
162
|
+
}
|
|
58
163
|
/** `arn:<partition>:iam::<account>:role/<path/name>`, as IAM allows it. */
|
|
59
164
|
const ROLE_ARN = /^arn:aws(?:-[a-z]+)*:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/;
|
|
60
165
|
const SESSION_NAME = /^[\w+=,.@-]{2,64}$/;
|
|
@@ -193,17 +298,11 @@ export async function assumeAwsRole(key, role, options) {
|
|
|
193
298
|
!Number.isSafeInteger(expiresAt) ||
|
|
194
299
|
expiresAt <= now)
|
|
195
300
|
throw new AuthError("invalid_response");
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
sessionToken: result.SessionToken,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
throw new AuthError("invalid_response");
|
|
206
|
-
}
|
|
301
|
+
const session = issuedKey({
|
|
302
|
+
accessKeyId: result.AccessKeyId,
|
|
303
|
+
secretAccessKey: result.SecretAccessKey,
|
|
304
|
+
sessionToken: result.SessionToken,
|
|
305
|
+
});
|
|
207
306
|
return {
|
|
208
307
|
accessKeyId: session.accessKeyId,
|
|
209
308
|
secretAccessKey: session.secretAccessKey,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { AuthError } from "../auth/errors.js";
|
|
3
|
+
import { awsProfileKey } from "./aws.js";
|
|
3
4
|
import { CredentialRegistry } from "./registry.js";
|
|
4
5
|
import { CredentialTransport } from "./transport.js";
|
|
5
6
|
/**
|
|
@@ -122,7 +123,7 @@ function localSigner(ctx, declaration) {
|
|
|
122
123
|
const auth = definition.methods[method].authentication;
|
|
123
124
|
const identity = { name: ctx.connection.name, plugin: ctx.connection.plugin };
|
|
124
125
|
const expected = authorityOf(selection);
|
|
125
|
-
const project = (current) => {
|
|
126
|
+
const project = async (current) => {
|
|
126
127
|
if (authorityOf(declaration(current)) !== expected)
|
|
127
128
|
throw new AuthError("binding_changed");
|
|
128
129
|
if (auth.kind === "none")
|
|
@@ -132,9 +133,9 @@ function localSigner(ctx, declaration) {
|
|
|
132
133
|
return secret ? { [auth.field]: secret } : {};
|
|
133
134
|
}
|
|
134
135
|
if (auth.kind === "awsSigV4") {
|
|
135
|
-
const key =
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
const key = selection.awsProfile
|
|
137
|
+
? await awsProfileKey(selection.awsProfile)
|
|
138
|
+
: staticSecret(current, selection.localSecret, ["sessionToken"]);
|
|
138
139
|
const cached = current.awsSession;
|
|
139
140
|
const sessionField = auth.assumeRole?.sessionField;
|
|
140
141
|
return {
|
|
@@ -178,9 +179,9 @@ function localSigner(ctx, declaration) {
|
|
|
178
179
|
: createHash("sha256").update(JSON.stringify(tokens)).digest("hex");
|
|
179
180
|
return { [auth.field]: { tokens, revision } };
|
|
180
181
|
};
|
|
181
|
-
const snapshot = () => ({
|
|
182
|
+
const snapshot = async () => ({
|
|
182
183
|
...identity,
|
|
183
|
-
config: project(ctx.connection.config),
|
|
184
|
+
config: await project(ctx.connection.config),
|
|
184
185
|
});
|
|
185
186
|
const binding = {
|
|
186
187
|
type: definition.id,
|
|
@@ -199,7 +200,7 @@ function localSigner(ctx, declaration) {
|
|
|
199
200
|
async update(change) {
|
|
200
201
|
const flatten = writeBack(auth, expected);
|
|
201
202
|
await ctx.updateConnection(async (current) => {
|
|
202
|
-
const projected = project(current);
|
|
203
|
+
const projected = await project(current);
|
|
203
204
|
const patch = typeof change === "function" ? await change(projected) : change;
|
|
204
205
|
return patch ? flatten(patch, current) : undefined;
|
|
205
206
|
});
|
|
@@ -244,7 +244,11 @@ export class CredentialTransport {
|
|
|
244
244
|
try {
|
|
245
245
|
snapshot = await binding.connection.read();
|
|
246
246
|
}
|
|
247
|
-
catch {
|
|
247
|
+
catch (error) {
|
|
248
|
+
// A store's own error can carry anything; an AuthError is already
|
|
249
|
+
// sanitized and says what is wrong with the credential itself.
|
|
250
|
+
if (error instanceof AuthError)
|
|
251
|
+
throw error;
|
|
248
252
|
throw new AuthError("credential_store_failed");
|
|
249
253
|
}
|
|
250
254
|
return boundSnapshot(binding, method, snapshot);
|
|
@@ -225,6 +225,13 @@ export interface CredentialSelection {
|
|
|
225
225
|
resourceOwner?: OAuthResourceOwner;
|
|
226
226
|
/** The role an `assumeRole` method's sessions are issued for. */
|
|
227
227
|
awsRole?: AwsRole;
|
|
228
|
+
/**
|
|
229
|
+
* The AWS CLI profile whose credentials an `awsSigV4` method's key
|
|
230
|
+
* comes from, read through the CLI when this process signs with its
|
|
231
|
+
* own connection: an SSO login, with no key stored anywhere. Hosts that
|
|
232
|
+
* store the key ignore it, as they ignore `localSecret`.
|
|
233
|
+
*/
|
|
234
|
+
awsProfile?: string;
|
|
228
235
|
/**
|
|
229
236
|
* How a process signing with its own flat connection config (the CLI)
|
|
230
237
|
* assembles a static secret: each of its parts, from a named config
|