forked from aws/aws-toolkit-vscode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconsoleSessionUtils.ts
More file actions
318 lines (291 loc) · 14.6 KB
/
consoleSessionUtils.ts
File metadata and controls
318 lines (291 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'
import * as nls from 'vscode-nls'
const localize = nls.loadMessageBundle()
import { parseKnownFiles } from '@smithy/shared-ini-file-loader'
import { globals } from 'aws-core-vscode/shared'
import { getLogger } from '../shared/logger/logger'
import { ChildProcess } from '../shared/utilities/processUtils'
import { getOrInstallCli, updateAwsCli } from '../shared/utilities/cliUtils'
import { CancellationError } from '../shared/utilities/timeoutUtils'
import { ToolkitError } from '../shared/errors'
import { telemetry } from '../shared/telemetry/telemetry'
import { Auth } from './auth'
import { CredentialsId, asString } from './providers/credentials'
import { createRegionPrompter } from '../shared/ui/common/region'
/**
* @description Authenticates with AWS using browser-based login via AWS CLI.
* Creates a session profile and automatically activates it.
*
* @param profileName Optional profile name. If not provided, user will be prompted.
* @param region Optional AWS region. If not provided, user will be prompted.
*/
export async function authenticateWithConsoleLogin(profileName?: string, region?: string): Promise<void> {
const logger = getLogger()
// Prompt for profile name if not provided
if (!profileName) {
const profileNameInput = await vscode.window.showInputBox({
prompt: localize('AWS.message.prompt.consoleLogin.profileName', 'Enter a name for this profile'),
placeHolder: localize('AWS.message.placeholder.consoleLogin.profileName', 'profile-name'),
validateInput: (value) => {
if (!value || value.trim().length === 0) {
return localize('AWS.message.error.consoleLogin.emptyProfileName', 'Profile name cannot be empty')
}
if (/\s/.test(value)) {
return localize(
'AWS.message.error.consoleLogin.spacesInProfileName',
'Profile name cannot contain spaces'
)
}
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
return localize(
'AWS.message.error.consoleLogin.invalidCharacters',
'Profile name can only contain letters, numbers, underscores, and hyphens'
)
}
return undefined
},
})
if (!profileNameInput) {
throw new ToolkitError('User cancelled entering profile', {
cancelled: true,
})
}
profileName = profileNameInput.trim()
}
// After user interaction has occurred, we can safely emit telemetry
await telemetry.auth_consoleLoginCommand.run(async (span) => {
span.record({ authConsoleLoginStarted: true }) // Track entry into flow (raw count)
// Prompt for region if not provided
if (!region) {
const regionPrompter = createRegionPrompter(undefined, {
title: localize('AWS.message.prompt.consoleLogin.region', 'Select an AWS region for console login'),
})
const selectedRegion = await regionPrompter.prompt()
if (!selectedRegion || typeof selectedRegion === 'symbol') {
throw new ToolkitError('User cancelled selecting region', {
cancelled: true,
})
}
// TypeScript narrowing: at this point selectedRegion is Region
const regionResult = selectedRegion as { id: string }
region = regionResult.id
}
// Verify AWS CLI availability and install if needed
let awsCliPath: string
try {
logger.info('Verifying AWS CLI availability...')
awsCliPath = await getOrInstallCli('aws-cli', true)
logger.info('AWS CLI found at: %s', awsCliPath)
} catch (error) {
logger.error('Failed to verify or install AWS CLI: %O', error)
void vscode.window.showErrorMessage(
localize(
'AWS.message.error.consoleLogin.cliInstallFailed',
'Failed to install AWS CLI. Please install it manually.'
)
)
throw new ToolkitError('Failed to verify or install AWS CLI', {
code: 'CliInstallFailed',
cause: error as Error,
})
}
// Execute login with console credentials command
// At this point, profileName and region are guaranteed to be defined
if (!profileName || !region) {
throw new ToolkitError('Profile name and region are required')
}
logger.info(`Executing login with console credentials command for profile: ${profileName}, region: ${region}`)
const commandArgs = ['login', '--profile', profileName, '--region', region]
// Track if we've shown the URL dialog and if user cancelled
let urlShown = false
let loginUrl: string | undefined
let userCancelled = false
let loginProcess: ChildProcess | undefined
// Start the process and handle output with cancellation support
const result = await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: localize('AWS.message.progress.consoleLogin', 'Login with console credentials'),
cancellable: true,
},
async (progress, token) => {
progress.report({
message: localize(
'AWS.message.progress.waitingForBrowser',
'Waiting for browser authentication...'
),
})
loginProcess = new ChildProcess(awsCliPath, commandArgs, {
collect: true,
rejectOnErrorCode: false,
onStdout: (text: string) => {
// Enhance the UX by showing AWS Sign-in service (signin.aws.amazon.com) URL in VS Code when we detect it.
const urlMatch = text.match(/(https:\/\/[^\s]+signin\.aws\.amazon\.com[^\s]+)/i)
if (urlMatch && !urlShown) {
loginUrl = urlMatch[1]
urlShown = true
// Show URL with Copy button (non-blocking)
const copyUrl = localize('AWS.button.copyUrl', 'Copy URL')
void vscode.window
.showInformationMessage(
localize(
'AWS.message.info.consoleLogin.browserAuth',
'Attempting to open your default browser.\nIf the browser does not open, copy the URL:\n\n{0}',
loginUrl
),
copyUrl
)
.then(async (selection) => {
if (selection === copyUrl && loginUrl) {
await vscode.env.clipboard.writeText(loginUrl)
void vscode.window.showInformationMessage(
localize(
'AWS.message.info.urlCopied',
'AWS Sign-in URL copied to clipboard.'
)
)
}
})
}
// Check if profile is already configured with a session
const overwriteMatch = text.match(
/Profile .+ is already configured to use session .+\. Do you want to overwrite it to use .+ instead\?/s
)
if (overwriteMatch) {
const cliMessage = overwriteMatch[0].trim() // Extract the matched string
const overwriteBtn = localize('AWS.generic.overwrite', 'Overwrite')
const cancelBtn = localize('AWS.generic.cancel', 'Cancel')
void vscode.window
.showInformationMessage(cliMessage, overwriteBtn, cancelBtn)
.then(async (selection) => {
if (selection === overwriteBtn && loginProcess) {
// Send "y" to stdin to proceed with overwrite
await loginProcess.send('y\n')
} else if (loginProcess) {
// User cancelled, stop the process
await loginProcess.send('n\n')
userCancelled = true
}
})
}
},
})
// Handle cancellation
token.onCancellationRequested(() => {
userCancelled = true
loginProcess?.stop()
})
return await loginProcess.run()
}
)
// Check if user cancelled
if (userCancelled) {
void vscode.window.showInformationMessage(
localize('AWS.message.info.consoleLogin.cancelled', 'Login with console credentials was cancelled.')
)
throw new ToolkitError('User cancelled login with console credentials', {
cancelled: true,
})
}
if (result.exitCode === 0) {
telemetry.aws_consoleLoginCLISuccess.emit({ result: 'Succeeded' })
logger.info('Login with console credentials command completed. Exit code: %d', result.exitCode)
} else if (result.exitCode === 254) {
logger.error(
'AWS Sign-in service returned an error. Exit code %d: %s',
result.exitCode,
result.stdout || result.stderr
)
void vscode.window.showErrorMessage(
localize(
'AWS.message.error.consoleLogin.signinServiceError',
'Unable to sign in with console credentials in "{0}". Please try another region.',
region
)
)
throw new ToolkitError('AWS Sign-in service returned an error', {
code: 'SigninServiceError',
details: {
exitCode: result.exitCode,
},
})
} else if (result.exitCode === 252) {
// AWS CLI is outdated, attempt to update
try {
await updateAwsCli()
} catch (err) {
if (CancellationError.isUserCancelled(err)) {
throw new ToolkitError('User cancelled updating AWS CLI', {
cancelled: true,
})
}
logger.error('Failed to update AWS CLI: %O', err)
throw ToolkitError.chain(err, 'AWS CLI update failed')
}
// If we reach here, update attempt completed
const message = 'AWS CLI installer has started. After installation completes, try logging in again.'
void vscode.window.showWarningMessage(message)
throw new ToolkitError(message, { cancelled: true })
} else {
// Show generic error message
void vscode.window.showErrorMessage(
localize(
'AWS.message.error.consoleLogin.commandFailed',
`Login using console credentials with 'aws login' command failed with exit code ${result.exitCode}`
)
)
logger.error(
'Login with console credentials command failed with exit code %d: %s',
result.exitCode,
result.stdout || result.stderr
)
throw new ToolkitError(`Login with console credentials command failed with exit code ${result.exitCode}`, {
code: 'CommandFailed',
})
}
// Load and verify profile with ignoreCache to get newly written config from disk to catch CLI's async writes
logger.info(`Verifying profile configuration for ${profileName}`)
const profiles = await parseKnownFiles({ ignoreCache: true })
const profile = profiles[profileName]
logger.info('Profile found: %O', profile)
logger.info('Login session value: %s, type: %s', profile?.login_session, typeof profile?.login_session)
if (!profiles[profileName]?.login_session) {
throw new ToolkitError(`Console login succeeded but profile ${profileName} not properly configured`, {
code: 'ConsoleLoginConfigError',
})
}
// Show success message after seeing newly created session in the disk
void vscode.window.showInformationMessage(
`Profile "${profileName}" ready with credentials from ${profile.login_session} console session`
)
logger.info(`Activating profile: ${profileName}`)
const credentialsId: CredentialsId = {
credentialSource: 'profile',
credentialTypeId: profileName,
}
const connectionId = asString(credentialsId)
// Invalidate cached credentials to force fresh fetch
getLogger().info(`Invalidated cached credentials for ${connectionId}`)
globals.loginManager.store.invalidateCredentials(credentialsId)
logger.info(`Looking for connection with ID: ${connectionId}`)
// Make sure that connection exists before letting other part use connection
const connection = await Auth.instance.getConnection({ id: connectionId })
if (connection === undefined) {
// Log available connections for debugging
const availableConnections = await Auth.instance.listConnections()
logger.error(
'Connection not found. Available connections: %O',
availableConnections.map((c) => c.id)
)
throw new ToolkitError(`Failed to get connection from profile: ${connectionId}`, {
code: 'MissingConnection',
})
}
// Don't call useConnection() - let credentials be fetched naturally when needed
await Auth.instance.updateConnectionState(connectionId, 'valid')
})
}