Skip to content

Commit d957d2c

Browse files
JLargent42Jacob Largent
andauthored
feat(appcomposer): prompt for IAM credentials #4247
Problem: SAM Sync feature shows an ambiguous error message when the user is signed in with SSO or Builder ID (no active IAM credentials). Solution: Prompt the user with a list of IAM credentials to choose from, if an IAM connection is not already active. Co-authored-by: Jacob Largent <[email protected]>
1 parent b730f71 commit d957d2c

File tree

5 files changed

+68
-20
lines changed

5 files changed

+68
-20
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Add invalid auth handling to Application Composer's Sync button"
4+
}

src/applicationcomposer/webviewManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class ApplicationComposerManager {
106106
protected handleErr(err: Error): void {
107107
void vscode.window.showInformationMessage(
108108
localize(
109-
'AWS.applicationcomposer.visualisation.errors.rendering',
109+
'AWS.applicationComposer.visualisation.errors.rendering',
110110
'There was an error rendering Application Composer, check logs for details.'
111111
)
112112
)

src/auth/utils.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ import { isValidCodeWhispererCoreConnection } from '../codewhisperer/util/authUt
5353
// TODO: Look to do some refactoring to handle circular dependency later and move this to ./commands.ts
5454
export const showConnectionsPageCommand = 'aws.auth.manageConnections'
5555

56-
export async function promptForConnection(auth: Auth, type?: 'iam' | 'sso'): Promise<Connection | void> {
56+
// iam-only excludes Builder ID and IAM Identity Center from the list of valid connections
57+
// TODO: Understand if "iam" should include these from the list at all
58+
export async function promptForConnection(auth: Auth, type?: 'iam' | 'iam-only' | 'sso'): Promise<Connection | void> {
5759
const resp = await createConnectionPrompter(auth, type).prompt()
5860
if (!isValidResponse(resp)) {
5961
throw new CancellationError('user')
@@ -331,7 +333,7 @@ export const createDeleteConnectionButton: () => vscode.QuickInputButton = () =>
331333
return { tooltip: deleteConnection, iconPath: getIcon('vscode-trash') }
332334
}
333335

334-
export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'sso') {
336+
export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | 'sso') {
335337
const addNewConnection = {
336338
label: codicon`${getIcon('vscode-plus')} Add New Connection`,
337339
data: 'addNewConnection' as const,
@@ -341,7 +343,7 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'sso') {
341343
data: 'editCredentials' as const,
342344
}
343345
const placeholder =
344-
type === 'iam'
346+
type === 'iam' || type === 'iam-only'
345347
? localize('aws.auth.promptConnection.iam.placeholder', 'Select an IAM credential')
346348
: localize('aws.auth.promptConnection.all.placeholder', 'Select a connection')
347349

@@ -375,7 +377,8 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'sso') {
375377
return 2
376378
}
377379

378-
const prompter = createQuickPick(loadItems(), {
380+
const excludeSso = type === 'iam-only'
381+
const prompter = createQuickPick(loadItems(excludeSso), {
379382
placeholder,
380383
title: localize('aws.auth.promptConnection.title', 'Switch Connection'),
381384
buttons: [refreshButton, createExitButton()],
@@ -404,10 +407,13 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'sso') {
404407

405408
return prompter
406409

407-
async function* loadItems(): AsyncGenerator<
408-
DataQuickPickItem<Connection | 'addNewConnection' | 'editCredentials'>[]
409-
> {
410-
const connections = auth.listAndTraverseConnections()
410+
async function* loadItems(
411+
excludeSso?: boolean
412+
): AsyncGenerator<DataQuickPickItem<Connection | 'addNewConnection' | 'editCredentials'>[]> {
413+
let connections = auth.listAndTraverseConnections()
414+
if (excludeSso) {
415+
connections = connections.filter(item => item.type !== 'sso')
416+
}
411417

412418
let hasShownEdit = false
413419

src/shared/sam/sync.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ import { parse } from 'semver'
4141
import { isAutomation } from '../vscode/env'
4242
import { getOverriddenParameters } from '../../lambda/config/parameterUtils'
4343
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
44-
import { samSyncUrl, samInitDocUrl, samUpgradeUrl } from '../constants'
44+
import { samSyncUrl, samInitDocUrl, samUpgradeUrl, credentialHelpUrl } from '../constants'
4545
import { getAwsConsoleUrl } from '../awsConsole'
4646
import { openUrl } from '../utilities/vsCodeUtils'
47-
import { showOnce } from '../utilities/messages'
47+
import { showMessageWithUrl, showOnce } from '../utilities/messages'
4848
import { IamConnection } from '../../auth/connection'
4949
import { CloudFormationTemplateRegistry } from '../fs/templateRegistry'
50+
import { promptAndUseConnection } from '../../auth/utils'
5051

5152
const localize = nls.loadMessageBundle()
5253

@@ -535,6 +536,38 @@ export type SamSyncResult = {
535536
isSuccess: boolean
536537
}
537538

539+
async function getAuthOrPrompt() {
540+
const connection = Auth.instance.activeConnection
541+
if (connection?.type === 'iam' && connection.state === 'valid') {
542+
return connection
543+
}
544+
let errorMessage = localize(
545+
'aws.sam.sync.authModal.message',
546+
'Syncing requires authentication with IAM credentials.'
547+
)
548+
if (connection?.state === 'valid') {
549+
errorMessage =
550+
localize(
551+
'aws.sam.sync.authModal.invalidAuth',
552+
'Authentication through Builder ID or IAM Identity Center detected. '
553+
) + errorMessage
554+
}
555+
const acceptMessage = localize('aws.sam.sync.authModal.accept', 'Authenticate with IAM credentials')
556+
const modalResponse = await showMessageWithUrl(
557+
errorMessage,
558+
credentialHelpUrl,
559+
localizedText.viewDocs,
560+
'info',
561+
[acceptMessage],
562+
true
563+
)
564+
if (modalResponse !== acceptMessage) {
565+
return
566+
}
567+
await promptAndUseConnection(Auth.instance, 'iam-only')
568+
return Auth.instance.activeConnection
569+
}
570+
538571
export function registerSync() {
539572
async function runSync(
540573
deployType: SyncParams['deployType'],
@@ -543,9 +576,11 @@ export function registerSync() {
543576
): Promise<SamSyncResult> {
544577
telemetry.record({ syncedResources: deployType === 'infra' ? 'AllResources' : 'CodeOnly' })
545578

546-
const connection = Auth.instance.activeConnection
547-
if (connection?.type !== 'iam') {
548-
throw new ToolkitError('Syncing SAM applications requires IAM credentials', { code: 'NoIAMCredentials' })
579+
const connection = await getAuthOrPrompt()
580+
if (connection?.type !== 'iam' || connection?.state !== 'valid') {
581+
throw new ToolkitError('Syncing SAM applications requires IAM credentials', {
582+
code: 'NoIAMCredentials',
583+
})
549584
}
550585

551586
// Constructor of `vscode.Uri` is marked private but that shouldn't matter when checking the instance type

src/shared/utilities/messages.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,17 @@ export function makeFailedWriteMessage(filename: string): string {
3535
function showMessageWithItems(
3636
message: string,
3737
kind: 'info' | 'warn' | 'error' = 'error',
38-
items: string[] = []
38+
items: string[] = [],
39+
useModal: boolean = false
3940
): Thenable<string | undefined> {
4041
switch (kind) {
4142
case 'info':
42-
return vscode.window.showInformationMessage(message, ...items)
43+
return vscode.window.showInformationMessage(message, { modal: useModal }, ...items)
4344
case 'warn':
44-
return vscode.window.showWarningMessage(message, ...items)
45+
return vscode.window.showWarningMessage(message, { modal: useModal }, ...items)
4546
case 'error':
4647
default:
47-
return vscode.window.showErrorMessage(message, ...items)
48+
return vscode.window.showErrorMessage(message, { modal: useModal }, ...items)
4849
}
4950
}
5051

@@ -56,6 +57,7 @@ function showMessageWithItems(
5657
* @param urlItem URL button text (default: "View Documentation")
5758
* @param kind Kind of message to show
5859
* @param extraItems Extra buttons shown _before_ the "View Documentation" button
60+
* @param useModal Flag to use a modal instead of a toast notification
5961
* @returns Promise that resolves when a button is clicked or the message is
6062
* dismissed, and returns the selected button text.
6163
*/
@@ -64,12 +66,13 @@ export async function showMessageWithUrl(
6466
url: string | vscode.Uri,
6567
urlItem: string = localizedText.viewDocs,
6668
kind: 'info' | 'warn' | 'error' = 'error',
67-
extraItems: string[] = []
69+
extraItems: string[] = [],
70+
useModal: boolean = false
6871
): Promise<string | undefined> {
6972
const uri = typeof url === 'string' ? vscode.Uri.parse(url) : url
7073
const items = [...extraItems, urlItem]
7174

72-
const p = showMessageWithItems(message, kind, items)
75+
const p = showMessageWithItems(message, kind, items, useModal)
7376
return p.then<string | undefined>(selection => {
7477
if (selection === urlItem) {
7578
void openUrl(uri)

0 commit comments

Comments
 (0)