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