Skip to content

Commit 4a27d8b

Browse files
fix(amazonq): Use the correct state for Q in remote environment (#5575)
## Problem: As part of our design we intentionally separate the Auth state between a local environment, versus a remote (ssh'd) one. We do this so that if the user has 2 IDE instances, one local and the other remote, if one changes its auth state the other one will not be affected. The reason for separating auth that I can remember is due to how we store the actual tokens on disk. Because the remote does not have access to the disk of the local, we need to ensure the remote operates its auth independently of the local auth. By design globalState is shared by both local and remote instances. So for local we need to ensure we do not use the base globalState, otherwise the same Auth state as the remote will be used. The problem, SecondaryAuth is not doing this for all cases. It is using the globalState when it should be asking Auth which state it should use. The happy path was working though, but there can be potential issues with this bug at some other point. ## Solution: Expose the state that Auth is using so that something like SecondaryAuth can ask Auth for the correct state object depending on the local vs remote. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Signed-off-by: Nikolas Komonen <[email protected]> Signed-off-by: nkomonen-amazon <[email protected]>
1 parent 6074374 commit 4a27d8b

File tree

5 files changed

+41
-13
lines changed

5 files changed

+41
-13
lines changed

packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,11 @@ describe('AuthUtil', async function () {
233233
// Switch to unsupported connection
234234
const cwAuthUpdatedConnection = captureEventOnce(authUtil.secondaryAuth.onDidChangeActiveConnection)
235235
await auth.useConnection(unsupportedConn)
236+
// This is triggered when the main Auth connection is switched
236237
await cwAuthUpdatedConnection
238+
// This is triggered by registerAuthListener() when it saves the previous active connection as a fallback.
239+
// TODO in a refactor see if we can simplify multiple multiple triggers on the same event.
240+
await captureEventOnce(authUtil.secondaryAuth.onDidChangeActiveConnection)
237241

238242
// Is using the fallback connection
239243
assert.ok(authUtil.isConnected())

packages/core/src/auth/auth.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1022,9 +1022,20 @@ export class Auth implements AuthService, ConnectionManager {
10221022
}
10231023
}
10241024

1025+
/**
1026+
* Auth uses its own state, separate from the default {@link globalState}
1027+
* that is normally used throughout the codebase.
1028+
*
1029+
* IMPORTANT: Anything involving auth should ONLY use this state since the state
1030+
* can vary on certain conditions. So if you see something explicitly using
1031+
* globalState verify if it should actually be using that.
1032+
*/
1033+
public getStateMemento: () => vscode.Memento = () => Auth._getStateMemento()
1034+
private static _getStateMemento = once(() => getEnvironmentSpecificMemento())
1035+
10251036
static #instance: Auth | undefined
10261037
public static get instance() {
1027-
return (this.#instance ??= new Auth(new ProfileStore(getEnvironmentSpecificMemento())))
1038+
return (this.#instance ??= new Auth(new ProfileStore(Auth._getStateMemento())))
10281039
}
10291040

10301041
private getSsoProfileLabel(profile: SsoProfile) {

packages/core/src/auth/secondaryAuth.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import globals from '../shared/extensionGlobals'
7-
86
import * as vscode from 'vscode'
97
import { getLogger } from '../shared/logger'
108
import { cast, Optional } from '../shared/utilities/typeConstructors'
@@ -18,6 +16,7 @@ import { AuthStatus, telemetry } from '../shared/telemetry/telemetry'
1816
import { asStringifiedStack } from '../shared/telemetry/spans'
1917
import { withTelemetryContext } from '../shared/telemetry/util'
2018
import { isNetworkError } from '../shared/errors'
19+
import globals from '../shared/extensionGlobals'
2120

2221
export type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId'
2322

@@ -214,10 +213,14 @@ export class SecondaryAuth<T extends Connection = Connection> {
214213
return !!this.activeConnection && this.auth.getConnectionState(this.activeConnection) === 'invalid'
215214
}
216215

216+
public get state() {
217+
return this.auth.getStateMemento()
218+
}
219+
217220
public async saveConnection(conn: T) {
218221
// TODO: fix this
219222
// eslint-disable-next-line aws-toolkits/no-banned-usages
220-
await globals.context.globalState.update(this.key, conn.id)
223+
await this.state.update(this.key, conn.id)
221224
this.#savedConnection = conn
222225
this.#onDidChangeActiveConnection.fire(this.activeConnection)
223226
}
@@ -251,7 +254,7 @@ export class SecondaryAuth<T extends Connection = Connection> {
251254
public async clearSavedConnection() {
252255
// TODO: fix this
253256
// eslint-disable-next-line aws-toolkits/no-banned-usages
254-
await globals.context.globalState.update(this.key, undefined)
257+
await this.state.update(this.key, undefined)
255258
this.#savedConnection = undefined
256259
this.#onDidChangeActiveConnection.fire(this.activeConnection)
257260
}
@@ -326,9 +329,6 @@ export class SecondaryAuth<T extends Connection = Connection> {
326329
* Provides telemetry if called by restoreConnection() (or another auth_modifyConnection context)
327330
*/
328331
private async _loadSavedConnection() {
329-
// TODO: fix this
330-
// eslint-disable-next-line aws-toolkits/no-banned-usages
331-
const globalState = globals.context.globalState
332332
const id = this.getStateConnectionId()
333333
if (id === undefined) {
334334
return
@@ -337,10 +337,10 @@ export class SecondaryAuth<T extends Connection = Connection> {
337337
const conn = await this.auth.getConnection({ id })
338338
if (conn === undefined) {
339339
getLogger().warn(`auth (${this.toolId}): removing saved connection "${this.key}" as it no longer exists`)
340-
await globalState.update(this.key, undefined)
340+
await this.state.update(this.key, undefined)
341341
} else if (!this.isUsable(conn)) {
342342
getLogger().warn(`auth (${this.toolId}): saved connection "${this.key}" is not valid`)
343-
await globalState.update(this.key, undefined)
343+
await this.state.update(this.key, undefined)
344344
} else {
345345
const getAuthStatus = (state: ReturnType<typeof this.auth.getConnectionState>): AuthStatus => {
346346
return state === 'invalid' ? 'expired' : 'connected'
@@ -377,7 +377,7 @@ export class SecondaryAuth<T extends Connection = Connection> {
377377
}
378378

379379
private getStateConnectionId() {
380-
return cast(globals.globalState.get(this.key), Optional(String))
380+
return cast(this.state.get(this.key), Optional(String))
381381
}
382382
}
383383

packages/core/src/shared/utilities/mementos.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ export function partition(memento: vscode.Memento, key: string): vscode.Memento
2929
}
3030
}
3131

32-
/** Avoids sharing globalState with remote vscode instances. */
32+
/**
33+
* Resolves the appropriate memento/state for the current runtime environment.
34+
*
35+
* Why?
36+
* - In remote instances where we ssh in to them, we do not always want to share
37+
* with the local globalState. We want certain functionality to be isolated to
38+
* the remote instance.
39+
*/
3340
export function getEnvironmentSpecificMemento(): vscode.Memento {
3441
if (!vscode.env.remoteName) {
3542
// local compute: no further partitioning

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createBuilderIdProfile, createTestAuth } from './testUtil'
99
import { Connection, createSsoProfile, hasScopes, isSsoConnection } from '../../auth/connection'
1010
import assert from 'assert'
1111
import globals from '../../shared/extensionGlobals'
12+
import { waitUntil } from '../../shared/utilities/timeoutUtils'
1213

1314
describe('SecondaryAuth', function () {
1415
let auth: ReturnType<typeof createTestAuth>
@@ -100,8 +101,13 @@ describe('SecondaryAuth', function () {
100101
// delete SecondaryAuth
101102
await auth.deleteConnection(conn)
102103

104+
// currently both Auth onDidChangeConnectionState and onDidDeleteConnection trigger
105+
// and we need to wait for both of the callbacks defined through them in SecondaryAuth to complete.
106+
// We know they all completed when SecondaryAuth.onDidChangeActiveConnection has been called twice
107+
await waitUntil(async () => onDidChangeActiveConnection.callCount === 2, { interval: 10, timeout: 10000 })
108+
103109
// we fallback to the PrimaryAuth connection
104-
assert.strictEqual(onDidChangeActiveConnection.called, true)
110+
assert.strictEqual(onDidChangeActiveConnection.callCount, 2)
105111
assert.deepStrictEqual(
106112
{
107113
id: secondaryAuth.activeConnection?.id,

0 commit comments

Comments
 (0)