az2aws 1.6.1 → 1.7.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/.gitattributes +1 -0
- package/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +20 -0
- package/README.md +53 -2
- package/lib/awsConfig.js +104 -11
- package/lib/configureProfileAsync.js +10 -8
- package/lib/index.js +16 -3
- package/lib/login.js +166 -112
- package/lib/loginStates.js +72 -89
- package/lib/sensitiveOutput.js +85 -0
- package/lib/sessionDuration.js +27 -0
- package/lib/updateNotifier.js +25 -11
- package/lib/validateCliOptions.js +12 -0
- package/package.json +9 -17
package/lib/login.js
CHANGED
|
@@ -4,23 +4,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.login = void 0;
|
|
7
|
-
const
|
|
7
|
+
const promises_1 = require("node:timers/promises");
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
9
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
9
10
|
const zlib_1 = __importDefault(require("zlib"));
|
|
10
11
|
const client_sts_1 = require("@aws-sdk/client-sts");
|
|
11
12
|
const cheerio_1 = require("cheerio");
|
|
12
|
-
const uuid_1 = require("uuid");
|
|
13
13
|
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
14
14
|
const querystring_1 = __importDefault(require("querystring"));
|
|
15
15
|
const debug_1 = __importDefault(require("debug"));
|
|
16
16
|
const CLIError_1 = require("./CLIError");
|
|
17
17
|
const awsConfig_1 = require("./awsConfig");
|
|
18
18
|
const paths_1 = require("./paths");
|
|
19
|
-
const
|
|
20
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
19
|
+
const promises_2 = __importDefault(require("fs/promises"));
|
|
21
20
|
const https_1 = require("https");
|
|
22
21
|
const node_http_handler_1 = require("@smithy/node-http-handler");
|
|
23
22
|
const loginStates_1 = require("./loginStates");
|
|
23
|
+
const sessionDuration_1 = require("./sessionDuration");
|
|
24
|
+
const sensitiveOutput_1 = require("./sensitiveOutput");
|
|
24
25
|
const debug = (0, debug_1.default)("az2aws");
|
|
25
26
|
const WIDTH = 425;
|
|
26
27
|
const HEIGHT = 550;
|
|
@@ -31,6 +32,7 @@ const AZURE_AD_SSO = "autologon.microsoftazuread-sso.com";
|
|
|
31
32
|
const AWS_SAML_ENDPOINT = "https://signin.aws.amazon.com/saml";
|
|
32
33
|
const AWS_CN_SAML_ENDPOINT = "https://signin.amazonaws.cn/saml";
|
|
33
34
|
const AWS_GOV_SAML_ENDPOINT = "https://signin.amazonaws-us-gov.com/saml";
|
|
35
|
+
const REDACTED = "[redacted]";
|
|
34
36
|
// Keep the runtime import as native `import()` so CommonJS output can load
|
|
35
37
|
// the ESM-only https-proxy-agent package.
|
|
36
38
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
@@ -39,48 +41,77 @@ const getProxyUrl = () => process.env.https_proxy ||
|
|
|
39
41
|
process.env.HTTPS_PROXY ||
|
|
40
42
|
process.env.http_proxy ||
|
|
41
43
|
process.env.HTTP_PROXY;
|
|
44
|
+
function handleBackgroundPromise(promise, description) {
|
|
45
|
+
void promise.catch((error) => {
|
|
46
|
+
const message = (0, sensitiveOutput_1.formatDebugErrorMessage)(error);
|
|
47
|
+
debug(`${description}: ${message}`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
42
50
|
exports.login = {
|
|
43
51
|
async _createHttpsProxyAgentAsync(proxyUrl, proxyOptions) {
|
|
44
52
|
const { HttpsProxyAgent } = await importHttpsProxyAgent();
|
|
45
53
|
return new HttpsProxyAgent(proxyUrl, proxyOptions);
|
|
46
54
|
},
|
|
47
|
-
async loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito = false) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
headless
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
async loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito = false, credentialProcess = false) {
|
|
56
|
+
const originalConsoleLog = console.log;
|
|
57
|
+
const effectiveNoPrompt = credentialProcess ? true : noPrompt;
|
|
58
|
+
try {
|
|
59
|
+
if (credentialProcess) {
|
|
60
|
+
console.log = (...args) => console.error(...args);
|
|
61
|
+
}
|
|
62
|
+
let headless, cliProxy;
|
|
63
|
+
if (mode === "cli") {
|
|
64
|
+
headless = true;
|
|
65
|
+
cliProxy = true;
|
|
66
|
+
}
|
|
67
|
+
else if (mode === "gui") {
|
|
68
|
+
headless = false;
|
|
69
|
+
cliProxy = false;
|
|
70
|
+
}
|
|
71
|
+
else if (mode === "debug") {
|
|
72
|
+
headless = false;
|
|
73
|
+
cliProxy = true;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw new CLIError_1.CLIError("Invalid mode");
|
|
77
|
+
}
|
|
78
|
+
const profile = await this._loadProfileAsync(profileName);
|
|
79
|
+
console.log(`Using AWS region ${profile.region || "(from AWS SDK defaults)"}`);
|
|
80
|
+
if (profile.region && profile.region.startsWith("us-gov")) {
|
|
81
|
+
console.warn("GovCloud region detected in profile. Note: Other AWS CLI operations " +
|
|
82
|
+
"will use your AWS CLI default region. If needed, set it to match " +
|
|
83
|
+
"this GovCloud region (us-gov-west-1 or us-gov-east-1).");
|
|
84
|
+
}
|
|
85
|
+
let assertionConsumerServiceURL = AWS_SAML_ENDPOINT;
|
|
86
|
+
if (profile.region && profile.region.startsWith("us-gov")) {
|
|
87
|
+
assertionConsumerServiceURL = AWS_GOV_SAML_ENDPOINT;
|
|
88
|
+
}
|
|
89
|
+
if (profile.region && profile.region.startsWith("cn-")) {
|
|
90
|
+
assertionConsumerServiceURL = AWS_CN_SAML_ENDPOINT;
|
|
91
|
+
}
|
|
92
|
+
console.log("Using AWS SAML endpoint", assertionConsumerServiceURL);
|
|
93
|
+
const loginUrl = await this._createLoginUrlAsync(profile.azure_app_id_uri, profile.azure_tenant_id, assertionConsumerServiceURL);
|
|
94
|
+
const allowSensitiveOutput = (0, sensitiveOutput_1.shouldAllowSensitiveOutput)();
|
|
95
|
+
const samlResponse = await this._performLoginAsync(loginUrl, headless, disableSandbox, cliProxy, effectiveNoPrompt, enableChromeNetworkService, profile.azure_default_username, profile.azure_default_password, enableChromeSeamlessSso, profile.azure_default_remember_me, noDisableExtensions, disableGpu, incognito, allowSensitiveOutput);
|
|
96
|
+
const roles = this._parseRolesFromSamlResponse(samlResponse);
|
|
97
|
+
const { role, durationHours } = await this._askUserForRoleAndDurationAsync(roles, effectiveNoPrompt, profile.azure_default_role_arn, profile.azure_default_duration_hours, credentialProcess ? "--credential-process" : "--no-prompt");
|
|
98
|
+
const credentials = await this._assumeRoleAsync(profileName, samlResponse, role, durationHours, awsNoVerifySsl, profile.region, !credentialProcess);
|
|
99
|
+
if (credentialProcess) {
|
|
100
|
+
if (!credentials) {
|
|
101
|
+
throw new CLIError_1.CLIError("Unable to retrieve credentials.");
|
|
102
|
+
}
|
|
103
|
+
originalConsoleLog(JSON.stringify({
|
|
104
|
+
Version: 1,
|
|
105
|
+
AccessKeyId: credentials.aws_access_key_id,
|
|
106
|
+
SecretAccessKey: credentials.aws_secret_access_key,
|
|
107
|
+
SessionToken: credentials.aws_session_token,
|
|
108
|
+
Expiration: credentials.aws_expiration,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
74
111
|
}
|
|
75
|
-
|
|
76
|
-
|
|
112
|
+
finally {
|
|
113
|
+
console.log = originalConsoleLog;
|
|
77
114
|
}
|
|
78
|
-
console.log("Using AWS SAML endpoint", assertionConsumerServiceURL);
|
|
79
|
-
const loginUrl = await this._createLoginUrlAsync(profile.azure_app_id_uri, profile.azure_tenant_id, assertionConsumerServiceURL);
|
|
80
|
-
const samlResponse = await this._performLoginAsync(loginUrl, headless, disableSandbox, cliProxy, noPrompt, enableChromeNetworkService, profile.azure_default_username, profile.azure_default_password, enableChromeSeamlessSso, profile.azure_default_remember_me, noDisableExtensions, disableGpu, incognito);
|
|
81
|
-
const roles = this._parseRolesFromSamlResponse(samlResponse);
|
|
82
|
-
const { role, durationHours } = await this._askUserForRoleAndDurationAsync(roles, noPrompt, profile.azure_default_role_arn, profile.azure_default_duration_hours);
|
|
83
|
-
await this._assumeRoleAsync(profileName, samlResponse, role, durationHours, awsNoVerifySsl, profile.region);
|
|
84
115
|
},
|
|
85
116
|
async loginAll(mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, forceRefresh, noDisableExtensions, disableGpu, incognito = false) {
|
|
86
117
|
const profiles = await awsConfig_1.awsConfig.getAllProfileNames();
|
|
@@ -118,12 +149,22 @@ exports.login = {
|
|
|
118
149
|
}
|
|
119
150
|
}
|
|
120
151
|
debug("Environment");
|
|
121
|
-
debug(
|
|
122
|
-
...env,
|
|
123
|
-
azure_default_password: "xxxxxxxxxx",
|
|
124
|
-
});
|
|
152
|
+
debug(this._redactProfileForDebug(env));
|
|
125
153
|
return env;
|
|
126
154
|
},
|
|
155
|
+
_redactProfileForDebug(env) {
|
|
156
|
+
return Object.fromEntries(Object.entries(env).map(([key, value]) => [
|
|
157
|
+
key,
|
|
158
|
+
key === "azure_default_duration_hours" ? value : REDACTED,
|
|
159
|
+
]));
|
|
160
|
+
},
|
|
161
|
+
_redactArnForDebug(arn) {
|
|
162
|
+
const match = arn.match(/^(arn:[^:]+:iam::)[^:]+:(.+?\/).+$/);
|
|
163
|
+
if (!match) {
|
|
164
|
+
return arn;
|
|
165
|
+
}
|
|
166
|
+
return `${match[1]}${REDACTED}:${match[2]}${REDACTED}`;
|
|
167
|
+
},
|
|
127
168
|
// Load the profile
|
|
128
169
|
async _loadProfileAsync(profileName) {
|
|
129
170
|
const profile = await awsConfig_1.awsConfig.getProfileConfigAsync(profileName);
|
|
@@ -150,14 +191,14 @@ exports.login = {
|
|
|
150
191
|
*/
|
|
151
192
|
_createLoginUrlAsync(appIdUri, tenantId, assertionConsumerServiceURL) {
|
|
152
193
|
debug("Generating UUID for SAML request");
|
|
153
|
-
const id =
|
|
194
|
+
const id = node_crypto_1.default.randomUUID();
|
|
154
195
|
const samlRequest = `
|
|
155
196
|
<samlp:AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="id${id}" Version="2.0" IssueInstant="${new Date().toISOString()}" IsPassive="false" AssertionConsumerServiceURL="${assertionConsumerServiceURL}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
156
197
|
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${appIdUri}</Issuer>
|
|
157
198
|
<samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"></samlp:NameIDPolicy>
|
|
158
199
|
</samlp:AuthnRequest>
|
|
159
200
|
`;
|
|
160
|
-
debug("Generated SAML request"
|
|
201
|
+
debug("Generated SAML request");
|
|
161
202
|
debug("Deflating SAML");
|
|
162
203
|
return new Promise((resolve, reject) => {
|
|
163
204
|
zlib_1.default.deflateRaw(samlRequest, (err, samlBuffer) => {
|
|
@@ -167,7 +208,7 @@ exports.login = {
|
|
|
167
208
|
debug("Encoding SAML in base64");
|
|
168
209
|
const samlBase64 = samlBuffer.toString("base64");
|
|
169
210
|
const url = `https://login.microsoftonline.com/${tenantId}/saml2?SAMLRequest=${encodeURIComponent(samlBase64)}`;
|
|
170
|
-
debug("Created login URL", url);
|
|
211
|
+
debug("Created login URL", (0, sensitiveOutput_1.redactUrlForLogs)(url));
|
|
171
212
|
return resolve(url);
|
|
172
213
|
});
|
|
173
214
|
});
|
|
@@ -190,7 +231,7 @@ exports.login = {
|
|
|
190
231
|
* @returns {Promise.<string>} The SAML response.
|
|
191
232
|
* @private
|
|
192
233
|
*/
|
|
193
|
-
async _performLoginAsync(url, headless, disableSandbox, cliProxy, noPrompt, enableChromeNetworkService, defaultUsername, defaultPassword, enableChromeSeamlessSso, rememberMe, noDisableExtensions, disableGpu, incognito = false) {
|
|
234
|
+
async _performLoginAsync(url, headless, disableSandbox, cliProxy, noPrompt, enableChromeNetworkService, defaultUsername, defaultPassword, enableChromeSeamlessSso, rememberMe, noDisableExtensions, disableGpu, incognito = false, allowSensitiveStateOutput = true) {
|
|
194
235
|
debug("Loading login page in Chrome");
|
|
195
236
|
let browser;
|
|
196
237
|
const useRememberMe = rememberMe && !incognito;
|
|
@@ -212,7 +253,7 @@ exports.login = {
|
|
|
212
253
|
args.push(`--user-data-dir=${paths_1.paths.userDataDir}`);
|
|
213
254
|
}
|
|
214
255
|
else {
|
|
215
|
-
await
|
|
256
|
+
await promises_2.default.mkdir(paths_1.paths.chromium, { recursive: true });
|
|
216
257
|
args.push(`--user-data-dir=${paths_1.paths.chromium}`);
|
|
217
258
|
}
|
|
218
259
|
// --profile-directory requires --user-data-dir to work properly
|
|
@@ -249,10 +290,10 @@ exports.login = {
|
|
|
249
290
|
e.name === "TargetCloseError" &&
|
|
250
291
|
useRememberMe &&
|
|
251
292
|
!paths_1.paths.userDataDir) {
|
|
252
|
-
debug(
|
|
293
|
+
debug("Browser launch failed with TargetCloseError. Resetting managed browser profile.");
|
|
253
294
|
console.warn("Browser profile appears incompatible. Resetting profile data and retrying...");
|
|
254
|
-
await
|
|
255
|
-
await
|
|
295
|
+
await promises_2.default.rm(paths_1.paths.chromium, { recursive: true, force: true });
|
|
296
|
+
await promises_2.default.mkdir(paths_1.paths.chromium, { recursive: true });
|
|
256
297
|
browser = await puppeteer_1.default.launch(launchParams);
|
|
257
298
|
}
|
|
258
299
|
else {
|
|
@@ -260,7 +301,7 @@ exports.login = {
|
|
|
260
301
|
}
|
|
261
302
|
}
|
|
262
303
|
// Wait for a bit as sometimes the browser isn't ready.
|
|
263
|
-
await
|
|
304
|
+
await (0, promises_1.setTimeout)(200);
|
|
264
305
|
let page;
|
|
265
306
|
if (incognito) {
|
|
266
307
|
const existingPages = await browser.pages();
|
|
@@ -284,29 +325,27 @@ exports.login = {
|
|
|
284
325
|
const samlResponsePromise = new Promise((resolve) => {
|
|
285
326
|
page.on("request", (req) => {
|
|
286
327
|
const reqURL = req.url();
|
|
287
|
-
|
|
328
|
+
const redactedURL = (0, sensitiveOutput_1.redactUrlForLogs)(reqURL);
|
|
329
|
+
debug(`Request: ${redactedURL}`);
|
|
288
330
|
if (reqURL === AWS_SAML_ENDPOINT ||
|
|
289
331
|
reqURL === AWS_GOV_SAML_ENDPOINT ||
|
|
290
332
|
reqURL === AWS_CN_SAML_ENDPOINT) {
|
|
291
333
|
resolve(undefined);
|
|
292
334
|
samlResponseData = req.postData();
|
|
293
|
-
|
|
294
|
-
req.respond({
|
|
335
|
+
handleBackgroundPromise(req.respond({
|
|
295
336
|
status: 200,
|
|
296
337
|
contentType: "text/plain",
|
|
297
338
|
headers: {},
|
|
298
339
|
body: "",
|
|
299
|
-
});
|
|
340
|
+
}), `Failed to respond to intercepted request ${redactedURL}`);
|
|
300
341
|
if (browser) {
|
|
301
|
-
|
|
302
|
-
browser.close();
|
|
342
|
+
handleBackgroundPromise(browser.close(), "Failed to close browser after receiving SAML response");
|
|
303
343
|
}
|
|
304
344
|
browser = undefined;
|
|
305
345
|
debug(`Received SAML response, browser closed`);
|
|
306
346
|
}
|
|
307
347
|
else {
|
|
308
|
-
|
|
309
|
-
req.continue();
|
|
348
|
+
handleBackgroundPromise(req.continue(), `Failed to continue intercepted request ${redactedURL}`);
|
|
310
349
|
}
|
|
311
350
|
});
|
|
312
351
|
});
|
|
@@ -326,13 +365,12 @@ exports.login = {
|
|
|
326
365
|
if (err instanceof Error) {
|
|
327
366
|
// An error will be thrown if you're still logged in cause the page.goto ot waitForNavigation
|
|
328
367
|
// will be a redirect to AWS. That's usually OK
|
|
329
|
-
debug(`Error occurred during loading the first page: ${err
|
|
368
|
+
debug(`Error occurred during loading the first page: ${(0, sensitiveOutput_1.formatDebugErrorMessage)(err)}`);
|
|
330
369
|
}
|
|
331
370
|
}
|
|
332
371
|
if (cliProxy) {
|
|
333
372
|
let totalUnrecognizedDelay = 0;
|
|
334
|
-
|
|
335
|
-
while (true) {
|
|
373
|
+
for (;;) {
|
|
336
374
|
if (samlResponseData)
|
|
337
375
|
break;
|
|
338
376
|
let foundState = false;
|
|
@@ -346,7 +384,7 @@ exports.login = {
|
|
|
346
384
|
if (err instanceof Error) {
|
|
347
385
|
// An error can be thrown if the page isn't in a good state.
|
|
348
386
|
// If one occurs, try again after another loop.
|
|
349
|
-
debug(`Error when running state "${state.name}". ${
|
|
387
|
+
debug(`Error when running state "${state.name}". ${(0, sensitiveOutput_1.formatDebugErrorMessage)(err)}. Retrying...`);
|
|
350
388
|
}
|
|
351
389
|
break;
|
|
352
390
|
}
|
|
@@ -355,7 +393,7 @@ exports.login = {
|
|
|
355
393
|
debug(`Found state: ${state.name}`);
|
|
356
394
|
await Promise.race([
|
|
357
395
|
samlResponsePromise,
|
|
358
|
-
state.handler(page, selected, noPrompt, defaultUsername, defaultPassword, useRememberMe),
|
|
396
|
+
state.handler(page, selected, noPrompt, defaultUsername, defaultPassword, useRememberMe, allowSensitiveStateOutput),
|
|
359
397
|
]);
|
|
360
398
|
debug(`Finished state: ${state.name}`);
|
|
361
399
|
break;
|
|
@@ -367,12 +405,15 @@ exports.login = {
|
|
|
367
405
|
else {
|
|
368
406
|
debug("State not recognized!");
|
|
369
407
|
if (totalUnrecognizedDelay > MAX_UNRECOGNIZED_PAGE_DELAY) {
|
|
408
|
+
if (!allowSensitiveStateOutput) {
|
|
409
|
+
throw new CLIError_1.CLIError("Unable to recognize page state in a shared environment. Re-run locally with --mode=debug to capture a screenshot.");
|
|
410
|
+
}
|
|
370
411
|
const path = "az2aws-unrecognized-state.png";
|
|
371
412
|
await page.screenshot({ path });
|
|
372
413
|
throw new CLIError_1.CLIError(`Unable to recognize page state! A screenshot has been dumped to ${path}. If this problem persists, try running with --mode=gui or --mode=debug`);
|
|
373
414
|
}
|
|
374
415
|
totalUnrecognizedDelay += DELAY_ON_UNRECOGNIZED_PAGE;
|
|
375
|
-
await
|
|
416
|
+
await (0, promises_1.setTimeout)(DELAY_ON_UNRECOGNIZED_PAGE);
|
|
376
417
|
}
|
|
377
418
|
}
|
|
378
419
|
}
|
|
@@ -384,13 +425,15 @@ exports.login = {
|
|
|
384
425
|
throw new Error("SAML response not found");
|
|
385
426
|
}
|
|
386
427
|
const samlResponse = querystring_1.default.parse(samlResponseData).SAMLResponse;
|
|
387
|
-
debug("Found SAML response", samlResponse);
|
|
388
428
|
if (!samlResponse) {
|
|
389
429
|
throw new Error("SAML response not found");
|
|
390
430
|
}
|
|
391
431
|
else if (Array.isArray(samlResponse)) {
|
|
392
432
|
throw new Error("SAML can't be an array");
|
|
393
433
|
}
|
|
434
|
+
debug("Found SAML response", {
|
|
435
|
+
base64Length: samlResponse.length,
|
|
436
|
+
});
|
|
394
437
|
return samlResponse;
|
|
395
438
|
}
|
|
396
439
|
finally {
|
|
@@ -408,16 +451,16 @@ exports.login = {
|
|
|
408
451
|
_parseRolesFromSamlResponse(assertion) {
|
|
409
452
|
debug("Converting assertion from base64 to UTF-8");
|
|
410
453
|
const samlText = Buffer.from(assertion, "base64").toString("utf8");
|
|
411
|
-
debug("Converted",
|
|
454
|
+
debug("Converted assertion from base64 to UTF-8", {
|
|
455
|
+
xmlLength: samlText.length,
|
|
456
|
+
});
|
|
412
457
|
debug("Parsing SAML XML");
|
|
413
458
|
const saml = (0, cheerio_1.load)(samlText, { xmlMode: true });
|
|
414
459
|
debug("Looking for role SAML attribute");
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
// @ts-ignore
|
|
420
|
-
const roleAndPrincipal = saml(this).text();
|
|
460
|
+
const roleSelection = saml("Attribute[Name='https://aws.amazon.com/SAML/Attributes/Role']>AttributeValue");
|
|
461
|
+
const roleNodes = roleSelection.toArray();
|
|
462
|
+
const roles = roleNodes.map((roleNode) => {
|
|
463
|
+
const roleAndPrincipal = saml(roleNode).text();
|
|
421
464
|
const parts = roleAndPrincipal.split(",");
|
|
422
465
|
// Role / Principal claims may be in either order
|
|
423
466
|
const [roleIdx, principalIdx] = parts[0].includes(":role/")
|
|
@@ -426,9 +469,11 @@ exports.login = {
|
|
|
426
469
|
const roleArn = parts[roleIdx].trim();
|
|
427
470
|
const principalArn = parts[principalIdx].trim();
|
|
428
471
|
return { roleArn, principalArn };
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
|
|
472
|
+
});
|
|
473
|
+
debug("Found roles", roles.map((role) => ({
|
|
474
|
+
roleArn: this._redactArnForDebug(role.roleArn),
|
|
475
|
+
principalArn: this._redactArnForDebug(role.principalArn),
|
|
476
|
+
})));
|
|
432
477
|
return roles;
|
|
433
478
|
},
|
|
434
479
|
/**
|
|
@@ -437,21 +482,15 @@ exports.login = {
|
|
|
437
482
|
* @param {bool} [noPrompt] - Enable skipping of user prompting
|
|
438
483
|
* @param {string} [defaultRoleArn] - The default role ARN
|
|
439
484
|
* @param {number} [defaultDurationHours] - The default session duration in hours
|
|
485
|
+
* @param {string} [nonInteractiveModeLabel] - CLI flag label to reference in
|
|
486
|
+
* non-interactive errors
|
|
440
487
|
* @returns {Promise.<{role: string, durationHours: number}>} The selected role and duration
|
|
441
488
|
* @private
|
|
442
489
|
*/
|
|
443
|
-
async _askUserForRoleAndDurationAsync(roles, noPrompt, defaultRoleArn, defaultDurationHours) {
|
|
490
|
+
async _askUserForRoleAndDurationAsync(roles, noPrompt, defaultRoleArn, defaultDurationHours, nonInteractiveModeLabel = "--no-prompt") {
|
|
491
|
+
var _a;
|
|
444
492
|
let role;
|
|
445
|
-
let durationHours = 1;
|
|
446
|
-
if (defaultDurationHours) {
|
|
447
|
-
const parsedDuration = parseInt(defaultDurationHours, 10);
|
|
448
|
-
if (!Number.isNaN(parsedDuration) &&
|
|
449
|
-
parsedDuration > 0 &&
|
|
450
|
-
parsedDuration <= 12) {
|
|
451
|
-
durationHours = parsedDuration;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
493
|
+
let durationHours = (_a = (0, sessionDuration_1.parseSessionDurationHours)(defaultDurationHours)) !== null && _a !== void 0 ? _a : 1;
|
|
455
494
|
const questions = [];
|
|
456
495
|
if (roles.length === 0) {
|
|
457
496
|
throw new CLIError_1.CLIError("No roles found in SAML response.");
|
|
@@ -463,11 +502,11 @@ exports.login = {
|
|
|
463
502
|
else {
|
|
464
503
|
if (noPrompt) {
|
|
465
504
|
if (!defaultRoleArn) {
|
|
466
|
-
throw new CLIError_1.CLIError(
|
|
505
|
+
throw new CLIError_1.CLIError(`${nonInteractiveModeLabel} requires azure_default_role_arn when multiple roles are available.`);
|
|
467
506
|
}
|
|
468
507
|
role = roles.find((r) => r.roleArn === defaultRoleArn);
|
|
469
508
|
if (!role) {
|
|
470
|
-
throw new CLIError_1.CLIError(
|
|
509
|
+
throw new CLIError_1.CLIError("Configured default role ARN was not found in the SAML response.");
|
|
471
510
|
}
|
|
472
511
|
debug("Valid role found. No need to ask.");
|
|
473
512
|
}
|
|
@@ -495,23 +534,23 @@ exports.login = {
|
|
|
495
534
|
name: "durationHours",
|
|
496
535
|
message: "Session Duration Hours (up to 12):",
|
|
497
536
|
type: "input",
|
|
498
|
-
default:
|
|
499
|
-
validate:
|
|
500
|
-
const num = Number(input);
|
|
501
|
-
if (num > 0 && num <= 12)
|
|
502
|
-
return true;
|
|
503
|
-
return "Duration hours must be between 1 and 12";
|
|
504
|
-
},
|
|
537
|
+
default: String(durationHours),
|
|
538
|
+
validate: sessionDuration_1.validateSessionDurationHours,
|
|
505
539
|
});
|
|
506
540
|
}
|
|
507
541
|
// Don't prompt for questions if not needed, an unneeded TTYWRAP prevents node from exiting when
|
|
508
542
|
// user is logged in and using multiple profiles --all-profiles and --no-prompt
|
|
509
543
|
if (questions.length > 0) {
|
|
510
544
|
const answers = await inquirer_1.default.prompt(questions);
|
|
511
|
-
if (!role)
|
|
545
|
+
if (!role && answers.role) {
|
|
512
546
|
role = roles.find((r) => r.roleArn === answers.role);
|
|
547
|
+
}
|
|
513
548
|
if (answers.durationHours) {
|
|
514
|
-
|
|
549
|
+
const parsedDurationHours = (0, sessionDuration_1.parseSessionDurationHours)(answers.durationHours);
|
|
550
|
+
if (parsedDurationHours === null) {
|
|
551
|
+
throw new CLIError_1.CLIError(sessionDuration_1.sessionDurationHoursValidationMessage);
|
|
552
|
+
}
|
|
553
|
+
durationHours = parsedDurationHours;
|
|
515
554
|
}
|
|
516
555
|
}
|
|
517
556
|
if (!role) {
|
|
@@ -523,16 +562,19 @@ exports.login = {
|
|
|
523
562
|
* Assume the role.
|
|
524
563
|
* @param {string} profileName - The profile name
|
|
525
564
|
* @param {string} assertion - The SAML assertion
|
|
526
|
-
* @param {
|
|
565
|
+
* @param {Role} role - The role to assume
|
|
527
566
|
* @param {number} durationHours - The session duration in hours
|
|
528
|
-
* @param {
|
|
567
|
+
* @param {boolean} awsNoVerifySsl - Whether the AWS SDK STS client should
|
|
568
|
+
* disable TLS certificate verification
|
|
529
569
|
* @param {string} region - AWS region, if specified
|
|
530
|
-
* @
|
|
570
|
+
* @param {boolean} writeProfile - Whether to persist the credentials to the
|
|
571
|
+
* AWS shared credentials file
|
|
572
|
+
* @returns {Promise<AwsCredentials | undefined>} Retrieved credentials, or
|
|
573
|
+
* undefined when STS does not return them
|
|
531
574
|
* @private
|
|
532
575
|
*/
|
|
533
|
-
async _assumeRoleAsync(profileName, assertion, role, durationHours, awsNoVerifySsl, region) {
|
|
534
|
-
|
|
535
|
-
console.log(`Assuming role ${role.roleArn} in region ${region}...`);
|
|
576
|
+
async _assumeRoleAsync(profileName, assertion, role, durationHours, awsNoVerifySsl, region, writeProfile = true) {
|
|
577
|
+
console.log(`Assuming selected role in region ${region}...`);
|
|
536
578
|
let stsOptions = {};
|
|
537
579
|
if (awsNoVerifySsl) {
|
|
538
580
|
console.warn("WARNING: SSL certificate verification is disabled. " +
|
|
@@ -574,13 +616,25 @@ exports.login = {
|
|
|
574
616
|
});
|
|
575
617
|
if (!res.Credentials) {
|
|
576
618
|
debug("Unable to get security credentials from AWS");
|
|
577
|
-
return;
|
|
619
|
+
return undefined;
|
|
578
620
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
621
|
+
if (!res.Credentials.AccessKeyId ||
|
|
622
|
+
!res.Credentials.SecretAccessKey ||
|
|
623
|
+
!res.Credentials.SessionToken ||
|
|
624
|
+
!res.Credentials.Expiration) {
|
|
625
|
+
debug("Received incomplete credentials from AWS");
|
|
626
|
+
throw new CLIError_1.CLIError("AWS returned incomplete credentials. One or more required fields " +
|
|
627
|
+
"(AccessKeyId, SecretAccessKey, SessionToken, Expiration) are missing.");
|
|
628
|
+
}
|
|
629
|
+
const credentials = {
|
|
630
|
+
aws_access_key_id: res.Credentials.AccessKeyId,
|
|
631
|
+
aws_secret_access_key: res.Credentials.SecretAccessKey,
|
|
632
|
+
aws_session_token: res.Credentials.SessionToken,
|
|
633
|
+
aws_expiration: res.Credentials.Expiration.toISOString(),
|
|
634
|
+
};
|
|
635
|
+
if (writeProfile) {
|
|
636
|
+
await awsConfig_1.awsConfig.setProfileCredentialsAsync(profileName, credentials);
|
|
637
|
+
}
|
|
638
|
+
return credentials;
|
|
585
639
|
},
|
|
586
640
|
};
|