Skip to content

Commit 5e56b15

Browse files
auth: remove multiple auth prompts (#3887)
* auth: remove yes/no prompt when using multiple auths Problem: If a user has an auth already set up, eg credentials, then I add a new auth, eg builder id, they will be shown a confusing message. The message asks if they want to explicitly switch to the new connection (no), or use both at the same time (yes). Solution: Remove this yes/no prompt and simply choose the (yes) option. Additional: Does some refactoring of secondaryAuth#registerAuthListener() since it was clear what it was intended to be doing. Signed-off-by: nkomonen <[email protected]> * auth: dont prompt to upgrade builder id scopes Problem: If a CodeCatalyst Builder ID was setup and then a CodeCatalyst Builder ID setup was requested, the user would be asked if they want to 'switch' their connection. Solution: Don't prompt the user to switch their connect and just upgrade their CodeWhisperer Builder ID scopes to also include CodeCatalyst scopes. Signed-off-by: nkomonen <[email protected]> * authNode: remove from developer tools menu Removes the auth node from the developer tools menu since the actual services in the developer tools menu may not be using the same connection as the auth node and it is confusing to users. Signed-off-by: nkomonen <[email protected]> * codewhisperer: fix unit tests Now that we have removed the prompts when adding/switching sso connections, the tests need to be updated since they user message that was being expected is not shown anymore. Signed-off-by: nkomonen <[email protected]> --------- Signed-off-by: nkomonen <[email protected]>
1 parent ee00581 commit 5e56b15

File tree

7 files changed

+55
-118
lines changed

7 files changed

+55
-118
lines changed

src/auth/secondaryAuth.ts

Lines changed: 24 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,79 +7,30 @@ import globals from '../shared/extensionGlobals'
77

88
import * as vscode from 'vscode'
99
import { getLogger } from '../shared/logger'
10-
import { showQuickPick } from '../shared/ui/pickerPrompter'
1110
import { cast, Optional } from '../shared/utilities/typeConstructors'
1211
import { Auth } from './auth'
1312
import { once } from '../shared/utilities/functionUtils'
14-
import { telemetry } from '../shared/telemetry/telemetry'
15-
import { createExitButton, createHelpButton } from '../shared/ui/buttons'
1613
import { isNonNullable } from '../shared/utilities/tsUtils'
1714
import { CancellationError } from '../shared/utilities/timeoutUtils'
1815
import { Connection, SsoConnection, StatefulConnection } from './connection'
1916

20-
async function promptUseNewConnection(newConn: Connection, oldConn: Connection, tools: string[], swapNo: boolean) {
21-
// Multi-select picker would be better ?
22-
const saveConnectionItem = {
23-
label: `Yes, keep using ${newConn.label} with ${tools.join(', ')} while using ${
24-
oldConn.label
25-
} with other services.`,
26-
detail: `To remove later, select "Remove Connection from Tool" from the tool's context (right-click) menu.`,
27-
data: 'yes',
28-
} as const
29-
30-
const useConnectionItem = {
31-
label: `No, switch everything to authenticate with ${(swapNo ? newConn : oldConn).label}.`,
32-
detail: 'This will not log you out; you can reconnect at any time by switching connections.',
33-
data: 'no',
34-
} as const
35-
36-
const helpButton = createHelpButton()
37-
const openLink = helpButton.onClick.bind(helpButton)
38-
helpButton.onClick = () => {
39-
telemetry.ui_click.emit({ elementId: 'connection_multiple_auths_help' })
40-
openLink()
41-
}
42-
43-
const resp = await showQuickPick([saveConnectionItem, useConnectionItem], {
44-
title: `Some tools you've been using don't work with ${oldConn.label}. Keep using ${newConn.label} in the background while using ${oldConn.label}?`,
45-
placeholder: 'Confirm choice',
46-
buttons: [helpButton, createExitButton()],
47-
})
48-
49-
switch (resp) {
50-
case 'yes':
51-
telemetry.ui_click.emit({ elementId: 'connection_multiple_auths_yes' })
52-
break
53-
case 'no':
54-
telemetry.ui_click.emit({ elementId: 'connection_multiple_auths_no' })
55-
break
56-
default:
57-
telemetry.ui_click.emit({ elementId: 'connection_multiple_auths_exit' })
58-
}
59-
60-
return resp
61-
}
62-
63-
let oldConn: Auth['activeConnection']
17+
let currentConn: Auth['activeConnection']
6418
const auths = new Map<string, SecondaryAuth>()
6519
const multiConnectionListeners = new WeakMap<Auth, vscode.Disposable>()
6620
const registerAuthListener = (auth: Auth) => {
67-
return auth.onDidChangeActiveConnection(async conn => {
68-
const potentialConn = oldConn
69-
if (conn !== undefined && potentialConn?.state === 'valid') {
21+
return auth.onDidChangeActiveConnection(async newConn => {
22+
// When we change the active connection, there may be
23+
// secondary auths that were dependent on the previous active connection.
24+
// To ensure secondary auths still work, when we change to a new active connection,
25+
// the following will "save" the oldConn with the secondary auths that are using it.
26+
const oldConn = currentConn
27+
if (newConn && oldConn?.state === 'valid') {
7028
const saveableAuths = Array.from(auths.values()).filter(
71-
a => !a.isUsingSavedConnection && a.isUsable(potentialConn) && !a.isUsable(conn)
29+
a => !a.hasSavedConnection && a.isUsable(oldConn) && !a.isUsable(newConn)
7230
)
73-
const toolNames = saveableAuths.map(a => a.toolLabel)
74-
if (
75-
saveableAuths.length > 0 &&
76-
(await promptUseNewConnection(potentialConn, conn, toolNames, false)) === 'yes'
77-
) {
78-
await Promise.all(saveableAuths.map(a => a.saveConnection(potentialConn)))
79-
}
31+
await Promise.all(saveableAuths.map(a => a.saveConnection(oldConn)))
8032
}
81-
82-
oldConn = conn
33+
currentConn = newConn
8334
})
8435
}
8536

@@ -104,15 +55,13 @@ export function getSecondaryAuth<T extends Connection>(
10455
* Gets all {@link SecondaryAuth} instances that have saved the connection
10556
*/
10657
export function getDependentAuths(conn: Connection): SecondaryAuth[] {
107-
return Array.from(auths.values()).filter(
108-
auth => auth.isUsingSavedConnection && auth.activeConnection?.id === conn.id
109-
)
58+
return Array.from(auths.values()).filter(auth => auth.hasSavedConnection && auth.activeConnection?.id === conn.id)
11059
}
11160

11261
export function getAllConnectionsInUse(auth: Auth): StatefulConnection[] {
11362
const connMap = new Map<Connection['id'], StatefulConnection>()
11463
const toolConns = Array.from(auths.values())
115-
.filter(a => a.isUsingSavedConnection)
64+
.filter(a => a.hasSavedConnection)
11665
.map(a => a.activeConnection)
11766

11867
for (const conn of [auth.activeConnection, ...toolConns].filter(isNonNullable)) {
@@ -174,13 +123,18 @@ export class SecondaryAuth<T extends Connection = Connection> {
174123
}
175124

176125
public get activeConnection(): T | undefined {
177-
return (
178-
this.#savedConnection ??
179-
(this.#activeConnection && this.isUsable(this.#activeConnection) ? this.#activeConnection : undefined)
180-
)
126+
if (this.#savedConnection) {
127+
return this.#savedConnection
128+
}
129+
130+
if (this.#activeConnection && this.isUsable(this.#activeConnection)) {
131+
return this.#activeConnection
132+
}
133+
134+
return undefined
181135
}
182136

183-
public get isUsingSavedConnection() {
137+
public get hasSavedConnection() {
184138
return this.#savedConnection !== undefined
185139
}
186140

@@ -202,11 +156,7 @@ export class SecondaryAuth<T extends Connection = Connection> {
202156

203157
public async useNewConnection(conn: T) {
204158
if (this.auth.activeConnection !== undefined && !this.isUsable(this.auth.activeConnection)) {
205-
if ((await promptUseNewConnection(conn, this.auth.activeConnection, [this.toolLabel], true)) === 'yes') {
206-
await this.saveConnection(conn)
207-
} else {
208-
await this.auth.useConnection(conn)
209-
}
159+
await this.saveConnection(conn)
210160
} else {
211161
await this.auth.useConnection(conn)
212162
}

src/awsexplorer/activation.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@ import { telemetry } from '../shared/telemetry/telemetry'
2727
import { cdkNode, CdkRootNode } from '../cdk/explorer/rootNode'
2828
import { CodeWhispererNode, codewhispererNode } from '../codewhisperer/explorer/codewhispererNode'
2929
import { once } from '../shared/utilities/functionUtils'
30-
import { Auth } from '../auth/auth'
3130
import { CodeCatalystRootNode } from '../codecatalyst/explorer'
3231
import { CodeCatalystAuthenticationProvider } from '../codecatalyst/auth'
3332
import { S3FolderNode } from '../s3/explorer/s3FolderNode'
34-
import { AuthNode } from '../auth/utils'
3533
import { TreeNode } from '../shared/treeview/resourceTreeDataProvider'
3634

3735
/**
@@ -78,9 +76,8 @@ export async function activate(args: {
7876
)
7977

8078
const authProvider = CodeCatalystAuthenticationProvider.fromContext(args.context.extensionContext)
81-
const authNode = new AuthNode(Auth.instance)
8279
const codecatalystNode = isCloud9('classic') ? [] : [new CodeCatalystRootNode(authProvider)]
83-
const nodes = [authNode, ...codecatalystNode, cdkNode, codewhispererNode]
80+
const nodes = [...codecatalystNode, cdkNode, codewhispererNode]
8481
const developerTools = createLocalExplorerView(nodes)
8582
args.context.extensionContext.subscriptions.push(developerTools)
8683

@@ -99,7 +96,6 @@ export async function activate(args: {
9996
})
10097

10198
registerDeveloperToolsCommands(args.context.extensionContext, developerTools, {
102-
auth: authNode,
10399
codeCatalyst: codecatalystNode ? codecatalystNode[0] : undefined,
104100
codeWhisperer: codewhispererNode,
105101
})
@@ -175,7 +171,6 @@ async function registerDeveloperToolsCommands(
175171
ctx: vscode.ExtensionContext,
176172
developerTools: vscode.TreeView<TreeNode>,
177173
nodes: {
178-
auth: AuthNode
179174
codeWhisperer: CodeWhispererNode
180175
codeCatalyst: CodeCatalystRootNode | undefined
181176
}
@@ -203,10 +198,9 @@ async function registerDeveloperToolsCommands(
203198
)
204199
}
205200

206-
registerShowDeveloperToolsNode('CodeWhisperer', codewhispererNode)
201+
registerShowDeveloperToolsNode('CodeWhisperer', nodes.codeWhisperer)
207202

208-
const codeCatalystNode = nodes.codeCatalyst
209-
if (codeCatalystNode) {
210-
registerShowDeveloperToolsNode('CodeCatalyst', codeCatalystNode)
203+
if (nodes.codeCatalyst) {
204+
registerShowDeveloperToolsNode('CodeCatalyst', nodes.codeCatalyst)
211205
}
212206
}

src/codecatalyst/auth.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { CodeCatalystClient, createClient } from '../shared/clients/codecatalyst
88
import { Auth } from '../auth/auth'
99
import { getSecondaryAuth } from '../auth/secondaryAuth'
1010
import { getLogger } from '../shared/logger'
11-
import * as localizedText from '../shared/localizedText'
1211
import { ToolkitError, isAwsError } from '../shared/errors'
1312
import { MetricName, MetricShapes, telemetry } from '../shared/telemetry/telemetry'
1413
import { openUrl } from '../shared/utilities/vsCodeUtils'
@@ -66,7 +65,7 @@ export class CodeCatalystAuthenticationProvider {
6665
}
6766

6867
public get isUsingSavedConnection() {
69-
return this.secondaryAuth.isUsingSavedConnection
68+
return this.secondaryAuth.hasSavedConnection
7069
}
7170

7271
public isConnectionValid(): boolean {
@@ -123,14 +122,22 @@ export class CodeCatalystAuthenticationProvider {
123122
throw new ToolkitError('Not onboarded with CodeCatalyst', { code: 'NotOnboarded', cancelled: true })
124123
}
125124

126-
public async promptNotConnected(): Promise<SsoConnection> {
125+
/**
126+
* Return a Builder ID connection that works with CodeCatalyst.
127+
*
128+
* This cannot create a Builder ID, but will return an existing Builder ID,
129+
* upgrading the scopes if necessary.
130+
*/
131+
public async tryGetBuilderIdConnection(): Promise<SsoConnection> {
132+
if (this.activeConnection && isBuilderIdConnection(this.activeConnection)) {
133+
return this.activeConnection
134+
}
135+
127136
type ConnectionFlowEvent = Partial<MetricShapes[MetricName]> & {
128137
readonly codecatalyst_connectionFlow: 'Create' | 'Switch' | 'Upgrade' // eslint-disable-line @typescript-eslint/naming-convention
129138
}
130139

131140
const conn = (await this.auth.listConnections()).find(isBuilderIdConnection)
132-
const continueItem: vscode.MessageItem = { title: localizedText.continueText }
133-
const cancelItem: vscode.MessageItem = { title: localizedText.cancel, isCloseAffordance: true }
134141

135142
if (conn === undefined) {
136143
telemetry.record({
@@ -153,21 +160,11 @@ export class CodeCatalystAuthenticationProvider {
153160
return this.secondaryAuth.addScopes(conn, defaultScopes)
154161
}
155162

156-
if (isBuilderIdConnection(conn) && this.auth.activeConnection?.id !== conn.id) {
163+
if (this.auth.activeConnection?.id !== conn.id) {
157164
telemetry.record({
158165
codecatalyst_connectionFlow: 'Switch',
159166
} satisfies ConnectionFlowEvent as MetricShapes[MetricName])
160167

161-
const resp = await vscode.window.showInformationMessage(
162-
'CodeCatalyst requires an AWS Builder ID connection.\n\n Switch to it now?',
163-
{ modal: true },
164-
continueItem,
165-
cancelItem
166-
)
167-
if (resp !== continueItem) {
168-
throw new ToolkitError('Not connected to CodeCatalyst', { code: 'NoConnection', cancelled: true })
169-
}
170-
171168
if (isUpgradeableConnection(conn)) {
172169
await upgrade()
173170
}

src/codecatalyst/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function createClientInjector(authProvider: CodeCatalystAuthenticationProvider):
136136
telemetry.record({ userId: AccountStatus.NotSet })
137137

138138
await authProvider.restore()
139-
const conn = authProvider.activeConnection ?? (await authProvider.promptNotConnected())
139+
const conn = await authProvider.tryGetBuilderIdConnection()
140140
const validatedConn = await validateConnection(conn, authProvider.auth)
141141
const client = await createClient(validatedConn)
142142
telemetry.record({ userId: client.identity.id })

src/codecatalyst/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function isInDevEnv(): boolean {
6969
export const getStartedCommand = Commands.register('aws.codecatalyst.getStarted', setupCodeCatalystBuilderId)
7070

7171
export async function setupCodeCatalystBuilderId(authProvider: CodeCatalystAuthenticationProvider) {
72-
let conn = authProvider.activeConnection ?? (await authProvider.promptNotConnected())
72+
let conn = await authProvider.tryGetBuilderIdConnection()
7373

7474
if (authProvider.auth.getConnectionState(conn) === 'invalid') {
7575
conn = await authProvider.auth.reauthenticate(conn)

src/codewhisperer/util/authUtil.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export class AuthUtil {
104104
}
105105

106106
public get isUsingSavedConnection() {
107-
return this.conn !== undefined && this.secondaryAuth.isUsingSavedConnection
107+
return this.conn !== undefined && this.secondaryAuth.hasSavedConnection
108108
}
109109

110110
public isConnected(): boolean {

src/test/codewhisperer/util/authUtil.test.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ describe('AuthUtil', async function () {
2222
authUtil = new AuthUtil(auth)
2323
})
2424

25+
afterEach(async function () {
26+
await auth.logout()
27+
})
28+
2529
it('if there is no valid AwsBuilderID conn, it will create one and use it', async function () {
2630
getTestWindow().onDidShowQuickPick(async picker => {
2731
await picker.untilReady()
@@ -83,22 +87,20 @@ describe('AuthUtil', async function () {
8387
)
8488
})
8589

86-
it('prompts to attach connection to CodeWhisperer when switching to an unsupported connection', async function () {
90+
it('CodeWhisperer uses fallback connection when switching to an unsupported connection', async function () {
8791
const supportedConn = await auth.createConnection(createBuilderIdProfile({ scopes: codewhispererScopes }))
8892
const unsupportedConn = await auth.createConnection(createSsoProfile())
8993

90-
getTestWindow().onDidShowQuickPick(picker => {
91-
assert.ok(picker.title?.startsWith(`Some tools you've been using don't work with ${unsupportedConn.label}`))
92-
const keepUsing = picker.findItemOrThrow(new RegExp(`keep using ${supportedConn.label}`))
93-
picker.acceptItem(keepUsing)
94-
})
95-
9694
await auth.useConnection(supportedConn)
9795
assert.ok(authUtil.isConnected())
9896
assert.strictEqual(auth.activeConnection?.id, authUtil.conn?.id)
9997

98+
// Switch to unsupported connection
99+
const cwAuthUpdatedConnection = captureEventOnce(authUtil.secondaryAuth.onDidChangeActiveConnection)
100100
await auth.useConnection(unsupportedConn)
101-
await captureEventOnce(authUtil.secondaryAuth.onDidChangeActiveConnection)
101+
await cwAuthUpdatedConnection
102+
103+
// Is using the fallback connection
102104
assert.ok(authUtil.isConnected())
103105
assert.ok(authUtil.isUsingSavedConnection)
104106
assert.notStrictEqual(auth.activeConnection?.id, authUtil.conn?.id)
@@ -114,13 +116,7 @@ describe('AuthUtil', async function () {
114116
assert.strictEqual((await auth.listConnections()).filter(isSsoConnection).length, 1)
115117
})
116118

117-
it('prompts to upgrade connections if they do not have the required scopes', async function () {
118-
getTestWindow().onDidShowMessage(message => {
119-
assert.ok(message.modal)
120-
message.assertMessage(/CodeWhisperer requires access to your/)
121-
message.selectItem('Proceed')
122-
})
123-
119+
it('automatically upgrades connections if they do not have the required scopes', async function () {
124120
const upgradeableConn = await auth.createConnection(createBuilderIdProfile())
125121
await auth.useConnection(upgradeableConn)
126122
assert.strictEqual(authUtil.isConnected(), false)

0 commit comments

Comments
 (0)