Skip to content

Commit 72f5168

Browse files
authored
Merge pull request #7707 from rhamilt/ssoQuickPickUpdate
feat(auth): Display second quickpick for SSO linked IAM profiles
2 parents b980406 + 9f082d9 commit 72f5168

File tree

3 files changed

+101
-5
lines changed

3 files changed

+101
-5
lines changed

packages/core/src/auth/utils.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
hasScopes,
4141
scopesSsoAccountAccess,
4242
isSsoConnection,
43+
IamConnection,
4344
} from './connection'
4445
import { Commands, placeholder } from '../shared/vscode/commands2'
4546
import { Auth } from './auth'
@@ -79,6 +80,18 @@ export async function promptForConnection(auth: Auth, type?: 'iam' | 'iam-only'
7980
return globals.awsContextCommands.onCommandEditCredentials()
8081
}
8182

83+
// If selected connection is SSO connection and has linked IAM profiles, show second quick pick with the linked IAM profiles
84+
if (isSsoConnection(resp)) {
85+
const linkedProfiles = await getLinkedIamProfiles(auth, resp)
86+
87+
if (linkedProfiles.length > 0) {
88+
const linkedResp = await showLinkedProfilePicker(linkedProfiles, resp)
89+
if (linkedResp) {
90+
return linkedResp
91+
}
92+
}
93+
}
94+
8295
return resp
8396
}
8497

@@ -340,6 +353,36 @@ export const createDeleteConnectionButton: () => vscode.QuickInputButton = () =>
340353
return { tooltip: deleteConnection, iconPath: getIcon('vscode-trash') }
341354
}
342355

356+
async function getLinkedIamProfiles(auth: Auth, ssoConnection: SsoConnection): Promise<IamConnection[]> {
357+
const allConnections = await auth.listAndTraverseConnections().promise()
358+
359+
return allConnections.filter(
360+
(conn) => isIamConnection(conn) && conn.id.startsWith(`sso:${ssoConnection.id}#`)
361+
) as IamConnection[]
362+
}
363+
364+
/**
365+
* Shows a quick pick with linked IAM profiles for a selected SSO connection
366+
*/
367+
async function showLinkedProfilePicker(
368+
linkedProfiles: IamConnection[],
369+
ssoConnection: SsoConnection
370+
): Promise<IamConnection | undefined> {
371+
const title = `Select an IAM Role for ${ssoConnection.label}`
372+
373+
const items: DataQuickPickItem<IamConnection>[] = linkedProfiles.map((profile) => ({
374+
label: codicon`${getIcon('vscode-key')} ${profile.label}`,
375+
description: 'IAM Credential, sourced from IAM Identity Center',
376+
data: profile,
377+
}))
378+
379+
return await showQuickPick(items, {
380+
title,
381+
placeholder: 'Select an IAM role',
382+
buttons: [createRefreshButton(), createExitButton()],
383+
})
384+
}
385+
343386
export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | 'sso') {
344387
const addNewConnection = {
345388
label: codicon`${getIcon('vscode-plus')} Add New Connection`,
@@ -433,22 +476,22 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' |
433476
for await (const conn of connections) {
434477
if (conn.label.includes('profile:') && !hasShownEdit) {
435478
hasShownEdit = true
436-
yield [toPickerItem(conn), editCredentials]
479+
yield [await toPickerItem(conn), editCredentials]
437480
} else {
438-
yield [toPickerItem(conn)]
481+
yield [await toPickerItem(conn)]
439482
}
440483
}
441484
}
442485

443-
function toPickerItem(conn: Connection): DataQuickPickItem<Connection> {
486+
async function toPickerItem(conn: Connection): Promise<DataQuickPickItem<Connection>> {
444487
const state = auth.getConnectionState(conn)
445488
// Only allow SSO connections to be deleted
446489
const deleteButton: vscode.QuickInputButton[] = conn.type === 'sso' ? [createDeleteConnectionButton()] : []
447490
if (state === 'valid') {
448491
return {
449492
data: conn,
450493
label: codicon`${getConnectionIcon(conn)} ${conn.label}`,
451-
description: getConnectionDescription(conn),
494+
description: await getConnectionDescription(conn),
452495
buttons: [...deleteButton],
453496
}
454497
}
@@ -502,7 +545,7 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' |
502545
}
503546
}
504547

505-
function getConnectionDescription(conn: Connection) {
548+
async function getConnectionDescription(conn: Connection) {
506549
if (conn.type === 'iam') {
507550
// TODO: implement a proper `getConnectionSource` method to discover where a connection came from
508551
const descSuffix = conn.id.startsWith('profile:')
@@ -514,6 +557,14 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' |
514557
return `IAM Credential, ${descSuffix}`
515558
}
516559

560+
// If this is an SSO connection, check if it has linked IAM profiles
561+
if (isSsoConnection(conn)) {
562+
const linkedProfiles = await getLinkedIamProfiles(auth, conn)
563+
if (linkedProfiles.length > 0) {
564+
return `Has ${linkedProfiles.length} IAM role${linkedProfiles.length > 1 ? 's' : ''} (click to select)`
565+
}
566+
}
567+
517568
const toolAuths = getDependentAuths(conn)
518569
if (toolAuths.length === 0) {
519570
return undefined

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,47 @@ describe('Auth', function () {
570570
assert.strictEqual((await promptForConnection(auth))?.id, conn.id)
571571
})
572572

573+
it('shows a second quickPick for linked IAM profiles when selecting an SSO connection', async function () {
574+
let quickPickCount = 0
575+
getTestWindow().onDidShowQuickPick(async (picker) => {
576+
await picker.untilReady()
577+
quickPickCount++
578+
579+
if (quickPickCount === 1) {
580+
// First picker: select the SSO connection
581+
const connItem = picker.findItemOrThrow(/IAM Identity Center/)
582+
picker.acceptItem(connItem)
583+
} else if (quickPickCount === 2) {
584+
// Second picker: select the linked IAM profile
585+
const linkedItem = picker.findItemOrThrow(/TestRole/)
586+
picker.acceptItem(linkedItem)
587+
}
588+
})
589+
590+
const linkedSsoProfile = createSsoProfile({ scopes: scopesSsoAccountAccess })
591+
const conn = await auth.createConnection(linkedSsoProfile)
592+
593+
// Mock the SSOClient to return account roles
594+
auth.ssoClient.listAccounts.returns(
595+
toCollection(async function* () {
596+
yield [{ accountId: '123456789012' }]
597+
})
598+
)
599+
auth.ssoClient.listAccountRoles.callsFake(() =>
600+
toCollection(async function* () {
601+
yield [{ accountId: '123456789012', roleName: 'TestRole' }]
602+
})
603+
)
604+
605+
// Should get a linked IAM profile back, not the SSO connection
606+
const result = await promptForConnection(auth)
607+
assert.ok(isIamConnection(result || undefined), 'Expected an IAM connection to be returned')
608+
assert.ok(
609+
result?.id.startsWith(`sso:${conn.id}#`),
610+
'Expected the IAM connection to be linked to the SSO connection'
611+
)
612+
})
613+
573614
it('refreshes when clicking the refresh button', async function () {
574615
getTestWindow().onDidShowQuickPick(async (picker) => {
575616
await picker.untilReady()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Improved connection actions for SSO"
4+
}

0 commit comments

Comments
 (0)