Skip to content

Commit 63d1fa1

Browse files
ashishrp-awsaws-toolkit-automationkeenwilsonaseemxs
authored
fix(amazonq): merge console-session-profile into main (#8424)
## Problem ## Solution --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: aws-toolkit-automation <[email protected]> Co-authored-by: Keen Wilson <[email protected]> Co-authored-by: Aseem sharma <[email protected]>
1 parent 08e23a1 commit 63d1fa1

21 files changed

+3601
-1937
lines changed

package-lock.json

Lines changed: 2894 additions & 1924 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"scan-licenses": "ts-node ./scripts/scan-licenses.ts"
4343
},
4444
"devDependencies": {
45-
"@aws-toolkits/telemetry": "^1.0.338",
45+
"@aws-toolkits/telemetry": "^1.0.341",
4646
"@playwright/browser-chromium": "^1.43.1",
4747
"@stylistic/eslint-plugin": "^2.11.0",
4848
"@types/he": "^1.2.3",

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@
617617
"@aws-sdk/credential-provider-env": "<3.731.0",
618618
"@aws-sdk/credential-provider-process": "<3.731.0",
619619
"@aws-sdk/credential-provider-sso": "<3.731.0",
620-
"@aws-sdk/credential-providers": "<3.731.0",
620+
"@aws-sdk/credential-providers": "^3.936.0",
621621
"@aws-sdk/lib-storage": "<3.731.0",
622622
"@aws-sdk/property-provider": "<3.731.0",
623623
"@aws-sdk/protocol-http": "<3.731.0",

packages/core/package.nls.json

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

packages/core/src/auth/credentials/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const SharedCredentialsKeys = {
1010
AWS_ACCESS_KEY_ID: 'aws_access_key_id',
1111
AWS_SECRET_ACCESS_KEY: 'aws_secret_access_key',
1212
AWS_SESSION_TOKEN: 'aws_session_token',
13+
CONSOLE_SESSION: 'login_session',
1314
CREDENTIAL_PROCESS: 'credential_process',
1415
CREDENTIAL_SOURCE: 'credential_source',
1516
ENDPOINT_URL: 'endpoint_url',

0 commit comments

Comments
 (0)