Skip to content

Commit 35ec8bf

Browse files
authored
fix(codecatalyst): migrate CodeWhisperer builder ID connections
Problem: AWS Builder ID connections created in the public Toolkit release do not have CodeCatalyst scopes Solution: Attach the required scopes when the user creates a new builder ID connection or clicks the 'Start' CodeCatalyst entry point feature/mde currently has logic to wipe old builder ID connections on start. This PR implements a nicer UX for CodeWhisperer users. One edge-case is left unhandled: saved connections attached to the CodeWhisperer node. Not handling it means the migrated connection won't be attached on next reload. Which isn't a big deal IMO.
1 parent dd8e5b0 commit 35ec8bf

File tree

4 files changed

+55
-36
lines changed

4 files changed

+55
-36
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/codecatalyst/auth.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
import * as vscode from 'vscode'
77
import { ConnectedCodeCatalystClient } from '../shared/clients/codecatalystClient'
88
import { isCloud9 } from '../shared/extensionUtilities'
9-
import { Auth, isBuilderIdConnection, Connection, SsoConnection, codecatalystScopes } from '../credentials/auth'
9+
import {
10+
Auth,
11+
isBuilderIdConnection,
12+
Connection,
13+
SsoConnection,
14+
codecatalystScopes,
15+
hasScopes,
16+
} from '../credentials/auth'
1017
import { getSecondaryAuth } from '../credentials/secondaryAuth'
1118
import { getLogger } from '../shared/logger'
1219

@@ -26,7 +33,7 @@ export class CodeCatalystAuthStorage {
2633
}
2734

2835
const isValidCodeCatalystConnection = (conn: Connection): conn is SsoConnection =>
29-
isBuilderIdConnection(conn) && codecatalystScopes.every(s => conn.scopes?.includes(s))
36+
isBuilderIdConnection(conn) && hasScopes(conn, codecatalystScopes)
3037

3138
export class CodeCatalystAuthenticationProvider {
3239
public readonly onDidChangeActiveConnection = this.secondaryAuth.onDidChangeActiveConnection

src/codewhisperer/util/authUtil.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
isBuilderIdConnection,
1515
createSsoProfile,
1616
isSsoConnection,
17+
hasScopes,
1718
} from '../../credentials/auth'
1819
import { Connection, SsoConnection } from '../../credentials/auth'
1920
import { ToolkitError } from '../../shared/errors'
@@ -28,7 +29,7 @@ import { getCodeCatalystDevEnvId } from '../../shared/vscode/env'
2829
export const awsBuilderIdSsoProfile = createBuilderIdProfile()
2930
// No connections are valid within C9
3031
const isValidCodeWhispererConnection = (conn: Connection): conn is SsoConnection =>
31-
!isCloud9() && conn.type === 'sso' && codewhispererScopes.every(s => conn.scopes?.includes(s))
32+
!isCloud9() && isSsoConnection(conn) && hasScopes(conn, codewhispererScopes)
3233

3334
export class AuthUtil {
3435
private readonly isAvailable = getCodeCatalystDevEnvId() === undefined

src/credentials/auth.ts

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export function createSsoProfile(startUrl: string, region = 'us-east-1'): SsoPro
6262
}
6363
}
6464

65+
export function hasScopes(target: SsoConnection | SsoProfile, scopes: string[]): boolean {
66+
return scopes?.every(s => target.scopes?.includes(s))
67+
}
68+
6569
export interface SsoConnection {
6670
readonly type: 'sso'
6771
readonly id: string
@@ -343,6 +347,8 @@ export class Auth implements AuthService, ConnectionManager {
343347
}
344348
}
345349

350+
public async useConnection({ id }: Pick<SsoConnection, 'id'>): Promise<SsoConnection>
351+
public async useConnection({ id }: Pick<Connection, 'id'>): Promise<Connection>
346352
public async useConnection({ id }: Pick<Connection, 'id'>): Promise<Connection> {
347353
const profile = this.store.getProfile(id)
348354
if (profile === undefined) {
@@ -522,14 +528,21 @@ export class Auth implements AuthService, ConnectionManager {
522528

523529
// XXX: always read from the same location in a dev environment
524530
private getSsoSessionName = once(() => {
525-
const configFile = getConfigFilename()
526-
const contents: string = require('fs').readFileSync(configFile, 'utf-8')
527-
const identifier = contents.match(/\[sso\-session (.*)\]/)?.[1]
528-
if (!identifier) {
529-
throw new ToolkitError('No sso-session name found in ~/.aws/config', { code: 'NoSsoSessionName' })
530-
}
531+
try {
532+
const configFile = getConfigFilename()
533+
const contents: string = require('fs').readFileSync(configFile, 'utf-8')
534+
const identifier = contents.match(/\[sso\-session (.*)\]/)?.[1]
535+
if (!identifier) {
536+
throw new ToolkitError('No sso-session name found in ~/.aws/config', { code: 'NoSsoSessionName' })
537+
}
531538

532-
return identifier
539+
return identifier
540+
} catch (err) {
541+
const defaultName = 'codecatalyst'
542+
getLogger().warn(`auth: unable to get an sso session name, defaulting to "${defaultName}": %s`, err)
543+
544+
return defaultName
545+
}
533546
})
534547

535548
private getTokenProvider(id: Connection['id'], profile: StoredProfile<SsoProfile>) {
@@ -659,20 +672,6 @@ export class Auth implements AuthService, ConnectionManager {
659672
return
660673
}
661674

662-
// Remove connections that do not have the required scopes
663-
const requiredScopes = createBuilderIdProfile().scopes
664-
for (const [id, profile] of this.store.listProfiles()) {
665-
if (
666-
profile.type === 'sso' &&
667-
profile.startUrl === builderIdStartUrl &&
668-
!requiredScopes?.every(s => profile.scopes?.includes(s))
669-
) {
670-
await this.store.deleteProfile(id).catch(err => {
671-
getLogger().warn(`auth: failed to remove profile ${id}: %s`, err)
672-
})
673-
}
674-
}
675-
676675
// Use the environment token if available
677676
if (getCodeCatalystDevEnvId() !== undefined) {
678677
const profile = createBuilderIdProfile()
@@ -880,7 +879,7 @@ export async function createStartUrlPrompter(title: string, ignoreScopes = true)
880879
a.authority.toLowerCase() === b.authority.toLowerCase()
881880
const oldConn = existingConnections.find(conn => isSameAuthority(vscode.Uri.parse(conn.startUrl), uri))
882881

883-
if (oldConn && (ignoreScopes || requiredScopes?.every(s => oldConn.scopes?.includes(s)))) {
882+
if (oldConn && (ignoreScopes || hasScopes(oldConn, requiredScopes))) {
884883
return 'A connection for this start URL already exists. Sign out before creating a new one.'
885884
}
886885
} catch (err) {
@@ -897,14 +896,13 @@ export async function createStartUrlPrompter(title: string, ignoreScopes = true)
897896
}
898897

899898
export async function createBuilderIdConnection(auth: Auth) {
899+
const newProfile = createBuilderIdProfile()
900900
const existingConn = (await auth.listConnections()).find(isBuilderIdConnection)
901-
902-
// Right now users can only have 1 builder id connection
903-
if (existingConn !== undefined) {
904-
await auth.deleteConnection(existingConn)
901+
if (existingConn && !hasScopes(existingConn, newProfile.scopes)) {
902+
return migrateBuilderId(auth, existingConn, newProfile)
905903
}
906904

907-
return Auth.instance.createConnection(createBuilderIdProfile())
905+
return existingConn ?? (await auth.createConnection(newProfile))
908906
}
909907

910908
Commands.register('aws.auth.help', async () => {
@@ -916,6 +914,23 @@ Commands.register('aws.auth.signout', () => {
916914

917915
return signout(Auth.instance)
918916
})
917+
918+
// XXX: right now users can only have 1 builder id connection, so de-dupe
919+
// This logic can be removed or re-purposed once we have access to identities
920+
async function migrateBuilderId(auth: Auth, existingConn: SsoConnection, newProfile: SsoProfile) {
921+
const newConn = await auth.createConnection(newProfile)
922+
const shouldUseConnection = auth.activeConnection?.id === existingConn.id
923+
await auth.deleteConnection(existingConn).catch(err => {
924+
getLogger().warn(`auth: failed to remove old connection "${existingConn.id}": %s`, err)
925+
})
926+
927+
if (shouldUseConnection) {
928+
return auth.useConnection(newConn)
929+
}
930+
931+
return newConn
932+
}
933+
919934
const addConnection = Commands.register('aws.auth.addConnection', async () => {
920935
const c9IamItem = createIamItem()
921936
c9IamItem.detail =
@@ -948,11 +963,7 @@ const addConnection = Commands.register('aws.auth.addConnection', async () => {
948963
return Auth.instance.useConnection(conn)
949964
}
950965
case 'builderId': {
951-
const existingConn = (await Auth.instance.listConnections()).find(isBuilderIdConnection)
952-
// Right now users can only have 1 builder id connection
953-
const conn = existingConn ?? (await Auth.instance.createConnection(createBuilderIdProfile()))
954-
955-
return Auth.instance.useConnection(conn)
966+
return createBuilderIdConnection(Auth.instance)
956967
}
957968
}
958969
})

0 commit comments

Comments
 (0)