switch-profile 0.0.9 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.1.2](https://github.com/nicolasdao/switch-profile/compare/v0.1.1...v0.1.2) (2022-05-14)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * Multiple SSO profiles are not supported. It only uses one. ([b9d9d03](https://github.com/nicolasdao/switch-profile/commit/b9d9d03b4edfa048cc157376222378513138e257))
11
+
12
+ ### [0.1.1](https://github.com/nicolasdao/switch-profile/compare/v0.1.0...v0.1.1) (2021-08-03)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * Edge cases where the SSO session expires and a forde login is required ([67f3667](https://github.com/nicolasdao/switch-profile/commit/67f36678f29f2ab5f02c4d4630eb1c7c41ccebbf))
18
+
19
+ ## [0.1.0](https://github.com/nicolasdao/switch-profile/compare/v0.0.9...v0.1.0) (2021-08-03)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * Switching to an SSO profile adds an incorrect 'sso_' prefix to the default region in ~/.aws/config file ([384c17e](https://github.com/nicolasdao/switch-profile/commit/384c17e26b368754a5bfdb5ca47fd9996181f3b7))
25
+
5
26
  ### [0.0.9](https://github.com/nicolasdao/switch-profile/compare/v0.0.8...v0.0.9) (2021-07-19)
6
27
 
7
28
 
package/README.md CHANGED
@@ -15,7 +15,26 @@ This utility also supports creating new AWS profiles, including SSO profiles as
15
15
  > - `AWS_SECRET_ACCESS_KEY`
16
16
  > - `AWS_SESSION_TOKEN`
17
17
 
18
- # Why using this utility?
18
+ # Table of contents
19
+
20
+ > * [Extra features](#extra-features)
21
+ > * [FAQ](#faq)
22
+ > - [Why using this utility?](#why-using-this-utility)
23
+ > - [How to create a new SSO profile?](#how-to-create-a-new-sso-profile)
24
+ > * [Troubleshooting](#troubleshooting)
25
+ > - [`invalid_grant: Invalid grant provided`](#invalid_grant-invalid-grant-provided)
26
+ > - [`Error: Fail to get credentials for profile ... Error loading SSO Token...`](#error-fail-to-get-credentials-for-profile--error-loading-sso-token)
27
+ > * [License](#license)
28
+
29
+ # Extra features
30
+
31
+ The `More options` supports the following:
32
+ - `Create profile`: Helps create both a standard or AWS SSO profile.
33
+ - `Delete profiles`: Helps cleaning one or many profiles.
34
+ - `Refresh default profile`: Helps refreshing the current default profile if it has expired (e.g., SSO profile). This option only appears when the default profile has expired.
35
+
36
+ # FAQ
37
+ ## Why using this utility?
19
38
 
20
39
  `switch-profile` was originally created to refresh short-lived AWS SSO profiles in the terminal and set them as default. Indeed, as of 2021, the only two ways to set up a short-lived SSO profile as default in the terminal are:
21
40
  - Manually paste the short-lived credentials in the terminal:
@@ -31,12 +50,22 @@ The issue with the first method is that you have to repeat it every hour, which
31
50
 
32
51
  `switch-profile` aims to solve those issues by making setting any profile as `default` trivial.
33
52
 
34
- # Extra features
53
+ ## How to create a new SSO profile?
35
54
 
36
- The `More options` supports the following:
37
- - `Create profile`: Helps create both a standard or AWS SSO profile.
38
- - `Delete profiles`: Helps cleaning one or many profiles.
39
- - `Refresh default profile`: Helps refreshing the current default profile if it has expired (e.g., SSO profile). This option only appears when the default profile has expired.
55
+ 1. `npx switch-profile`
56
+ 2. Select `More options`, then `Create profile`.
57
+ 3. Answer all the questions.
58
+
59
+ # Troubleshooting
60
+ ## `invalid_grant: Invalid grant provided`
61
+
62
+ If this error occurs during a [profile creation step](#how-to-create-a-new-sso-profile), this is most likely due to an incorrect SSO region. AWS SSO is region specific. This error occurs if an incorrect region is specified during the [profile creation step](#how-to-create-a-new-sso-profile).
63
+
64
+ ## `Error: Fail to get credentials for profile ... Error loading SSO Token...`
65
+
66
+ This is probably due to a recent deletion of the `~/.aws/sso/cache` folder. This folder keeps track of the SSO profiles created on the local machine. Unfortunately in this case, there is no other option but to recreate all the SSO profiles manually:
67
+ 1. Execute `npx switch-profile`
68
+ 2. Select `More options`, then `Delete profiles`.
40
69
 
41
70
  # License
42
71
 
package/index.js CHANGED
@@ -310,6 +310,7 @@ const setProfileToDefault = async (profileName, profileList, successMsg) => {
310
310
  if (credsErrors)
311
311
  return printAWSerrors([new Error(`Fail to get credentials for profile ${profile.name}`), ...credsErrors])
312
312
 
313
+ // console.log(profile)
313
314
  // console.log(creds)
314
315
 
315
316
  const [errors] = await updateDefaultProfile({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switch-profile",
3
- "version": "0.0.9",
3
+ "version": "0.1.2",
4
4
  "main": "index.js",
5
5
  "bin": "index.js",
6
6
  "description": "NPX script that exposes terminal shortcut to easily switch between cloud configuration (aws, gcp).",
package/src/aws/index.js CHANGED
@@ -7,10 +7,10 @@ const fileHelper = require('../fileHelper')
7
7
  const regions = require('./regions')
8
8
 
9
9
  const SSO_GET_CREDS_TIMEOUT = 5*60*1000 // 5 minutes to complete the SSO login
10
- const AWS_CONFIG_FILE = join(homedir(), '.aws/config')
11
- const AWS_CREDS_FILE = join(homedir(), '.aws/credentials')
12
- const AWS_SSO_FOLDER = join(homedir(), '.aws/sso/cache/')
13
- const AWS_CLI_FOLDER = join(homedir(), '.aws/cli/cache/')
10
+ const AWS_CONFIG_FILE = join(homedir(), '.aws', 'config')
11
+ const AWS_CREDS_FILE = join(homedir(), '.aws', 'credentials')
12
+ const AWS_SSO_FOLDER = join(homedir(), '.aws', 'sso', 'cache')
13
+ const AWS_CLI_FOLDER = join(homedir(), '.aws', 'cli', 'cache')
14
14
  const DEFAULT_CONFIG =
15
15
  `[default]
16
16
  region = ap-southeast-1
@@ -33,15 +33,19 @@ const awsCliV2Exists = noFailIfMissing => catchErrors((async () => {
33
33
  throw wrapErrors('AWS CLI error', awsCliErrors)
34
34
  }
35
35
 
36
- const data = await exec('aws --version') || ''
37
- const [majorVersion] = ((data.match(/aws-cli\/(.*?)\s/g)||[])[0]||'').replace('aws-cli/','').split('.')
38
- const version = majorVersion*1
39
- if (isNaN(version))
40
- throw new Error('Fail to test the AWS CLI version. Please try to run "aws --version" manually to try to debug this issue.')
41
- if (version <= 1)
42
- throw new Error(`AWS CLI version ${version} is not supported. Please upgrade to AWS CLI v2 or greater.`)
43
-
44
- return true
36
+ try {
37
+ const data = await exec('aws --version') || ''
38
+ const [majorVersion] = ((data.match(/aws-cli\/(.*?)\s/g)||[])[0]||'').replace('aws-cli/','').split('.')
39
+ const version = majorVersion*1
40
+ if (isNaN(version))
41
+ throw new Error('Fail to test the AWS CLI version. Please try to run "aws --version" manually to try to debug this issue.')
42
+ if (version <= 1)
43
+ throw new Error(`AWS CLI version ${version} is not supported. Please upgrade to AWS CLI v2 or greater.`)
44
+
45
+ return true
46
+ } catch(err) {
47
+ throw wrapErrors('Fail to check AWS CLI\'s version', [err])
48
+ }
45
49
  })())
46
50
 
47
51
  const getParam = (params, paramName) => {
@@ -107,6 +111,9 @@ const listProfiles = () => catchErrors((async () => {
107
111
  output: getParam(params, 'output')
108
112
  }
109
113
 
114
+ if (p.sso_region)
115
+ p.region = p.sso_region
116
+
110
117
  p.friendlyName = `${p.name}${p.sso_start_url ? ` (SSO [role:${p.sso_role_name||'unknown'} - account:${p.sso_account_id||'unknown'}])` : ''}`
111
118
 
112
119
  return p
@@ -182,14 +189,15 @@ const getSsoCredsFromCacheFile = (access_key_end, secret_key_end) => catchErrors
182
189
 
183
190
  for (let i=0;i<files.length;i++) {
184
191
  const _creds = await fileHelper.json.get(files[i])
192
+
185
193
  // Creds must be SSO
186
194
  if (!_creds || _creds.ProviderType != 'sso' || !_creds.Credentials)
187
195
  continue
188
196
 
189
197
  // Creds must still be valid for the next 2 minutes
190
198
  if (_creds.Credentials.Expiration && (Date.now() - 2*60*1000) < new Date(_creds.Credentials.Expiration).getTime()) {
191
- const credsMatch = _creds.Credentials.AccessKeyId||''.slice(-4) == access_key_end &&
192
- _creds.Credentials.SecretAccessKey||''.slice(-4) == secret_key_end
199
+ const credsMatch = (_creds.Credentials.AccessKeyId||'').slice(-4) == access_key_end &&
200
+ (_creds.Credentials.SecretAccessKey||'').slice(-4) == secret_key_end
193
201
 
194
202
  if (credsMatch)
195
203
  return {
@@ -220,27 +228,40 @@ const getSsoCredsFromCacheFile = (access_key_end, secret_key_end) => catchErrors
220
228
  const getSsoCredentials = profile => catchErrors((async () => {
221
229
  await awsCliV2Exists()
222
230
  const errMsg = `Fail to get AWS SSO credentials for profile ${profile}`
223
- if (!profile)
224
- throw wrapErrors(errMsg, [new Error('Missing required \'profile\' argument')])
225
- const data = await exec(`aws configure list --profile ${profile}`) || ''
226
- const access_key_end = ((data.match(/access_key\s*\*+.{4}/g)||[])[0]||'').slice(-4)
227
- const secret_key_end = ((data.match(/secret_key\s*\*+.{4}/g)||[])[0]||'').slice(-4)
228
-
229
- if (!access_key_end || !secret_key_end)
230
- return null
231
-
232
- const [ssoCredsErrors, creds] = await getSsoCredsFromCacheFile(access_key_end, secret_key_end)
233
- if (ssoCredsErrors)
234
- throw wrapErrors(errMsg, ssoCredsErrors)
235
- return creds
231
+ try {
232
+ if (!profile)
233
+ throw wrapErrors(errMsg, [new Error('Missing required \'profile\' argument')])
234
+ const data = await exec(`aws configure list --profile ${profile}`) || ''
235
+ const access_key_end = ((data.match(/access_key\s*\*+.{4}/g)||[])[0]||'').slice(-4)
236
+ const secret_key_end = ((data.match(/secret_key\s*\*+.{4}/g)||[])[0]||'').slice(-4)
237
+
238
+ if (!access_key_end || !secret_key_end)
239
+ return null
240
+
241
+ const [ssoCredsErrors, creds] = await getSsoCredsFromCacheFile(access_key_end, secret_key_end)
242
+
243
+ if (ssoCredsErrors)
244
+ throw wrapErrors(errMsg, ssoCredsErrors)
245
+ return creds
246
+ } catch(err) {
247
+ throw wrapErrors(errMsg, [err])
248
+ }
236
249
  })())
237
250
 
251
+ const isSSOexpired = ssoSession => {
252
+ if (!ssoSession)
253
+ return true
254
+
255
+ return (new Date(ssoSession.expiresAt).getTime() - Date.now()) < (2*60*1000)
256
+ }
257
+
238
258
  /**
239
259
  * Makes sure the profile is using a valid (i.e., exists and not expired) local SSO session. If not, try
240
260
  * to create one manually by redirecting the user to the SSO page.
241
261
  *
242
- * @param {String} profile e.g., 'sso-dev-cloudless'
243
- * @param {String} ssoUrl e.g., 'https://cloudless.awsapps.com/start'
262
+ * @param {String} profile e.g., 'sso-dev-cloudless'
263
+ * @param {String} ssoUrl e.g., 'https://cloudless.awsapps.com/start'
264
+ * @param {Boolean} options.force
244
265
  *
245
266
  * @return {[Error]}
246
267
  * @return {String} session.aws_access_key_id e.g., '********1234'
@@ -248,32 +269,38 @@ const getSsoCredentials = profile => catchErrors((async () => {
248
269
  * @return {String} session.aws_session_token e.g., 'dwqdwdwqd...dwqdqw'
249
270
  * @return {Date} session.expiry_date e.g., 2021-07-17T11:33:12Z
250
271
  */
251
- const refreshSsoSession = (profile, ssoUrl) => catchErrors((async () => {
272
+ const refreshSsoSession = (profile, ssoUrl, options) => catchErrors((async () => {
252
273
  const errMsg = `Fail to refresh the SSO session for AWS profile ${profile}`
253
274
  let [ssoSessionErrors, ssoSession] = await getSsoSession(ssoUrl)
254
275
  if (ssoSessionErrors)
255
276
  throw wrapErrors(errMsg, ssoSessionErrors)
256
277
 
257
- // No valid SSO session found. Manually get a new one via the SSO portal
258
- const startTime = Date.now()
259
- if (!ssoSession) {
260
- await exec(`aws sso login --profile ${profile}`)
261
- while (!ssoSession && Date.now() - startTime < SSO_GET_CREDS_TIMEOUT) {
262
- const resp = await getSsoSession(ssoUrl)
263
- if (resp[0])
264
- throw wrapErrors(errMsg, resp[0])
265
- ssoSession = resp[1]
266
- if (!ssoSession)
267
- await delay(2000)
278
+ const { force } = options || {}
279
+
280
+ try {
281
+ // No valid SSO session found. Manually get a new one via the SSO portal
282
+ const startTime = Date.now()
283
+ if (force || isSSOexpired(ssoSession)) {
284
+ await exec(`aws sso login --profile ${profile}`)
285
+ while (!ssoSession && Date.now() - startTime < SSO_GET_CREDS_TIMEOUT) {
286
+ const resp = await getSsoSession(ssoUrl)
287
+ if (resp[0])
288
+ throw wrapErrors(errMsg, resp[0])
289
+ ssoSession = resp[1]
290
+ if (!ssoSession)
291
+ await delay(2000)
292
+ }
268
293
  }
269
- }
270
294
 
271
- if (ssoSession)
272
- return ssoSession
273
- else if (Date.now() - startTime > SSO_GET_CREDS_TIMEOUT)
274
- throw wrapErrors(errMsg, [new Error(`Timeout - Time to wait for refreshing the SSO session for profile ${profile} exceeded ${SSO_GET_CREDS_TIMEOUT}ms.`)])
275
- else
276
- return null
295
+ if (ssoSession)
296
+ return ssoSession
297
+ else if (Date.now() - startTime > SSO_GET_CREDS_TIMEOUT)
298
+ throw wrapErrors(errMsg, [new Error(`Timeout - Time to wait for refreshing the SSO session for profile ${profile} exceeded ${SSO_GET_CREDS_TIMEOUT}ms.`)])
299
+ else
300
+ return null
301
+ } catch(err) {
302
+ throw wrapErrors(errMsg, [err])
303
+ }
277
304
  })())
278
305
 
279
306
  /**
@@ -296,9 +323,19 @@ const getCredentials = (profile, ssoUrl) => catchErrors((async () => {
296
323
  const errMsg = `Fail to get AWS credentials for profile ${profile}`
297
324
  if (ssoUrl) {
298
325
  await refreshSsoSession(profile, ssoUrl)
299
- const [ssoCredsErrors, ssoCreds] = await getSsoCredentials(profile)
300
- if (ssoCredsErrors)
301
- throw wrapErrors(errMsg, ssoCredsErrors)
326
+ let [ssoCredsErrors, ssoCreds] = await getSsoCredentials(profile)
327
+ if (ssoCredsErrors) {
328
+ const ssoSessionIsInvalid = ssoCredsErrors.some(e => e.message && e.message.toLowerCase().indexOf('session associated with this profile has expired') >= 0)
329
+ if (ssoSessionIsInvalid) {
330
+ await refreshSsoSession(profile, ssoUrl, { force:true })
331
+ const resp = await getSsoCredentials(profile)
332
+ if (resp[0])
333
+ throw wrapErrors(errMsg, resp[0])
334
+
335
+ ssoCreds = resp[1]
336
+ } else
337
+ throw wrapErrors(errMsg, ssoCredsErrors)
338
+ }
302
339
 
303
340
  return ssoCreds
304
341
  } else {