Skip to content

Commit 05c3577

Browse files
authored
feat(auth): use minimal scopes for CC and CW (#3264)
## Problem The Toolkit requests permissions for CodeWhisperer and CodeCatalyst even if the user doesn't want to use both ## Solution Request the minimum permissions required to use a feature
1 parent 342bff4 commit 05c3577

File tree

13 files changed

+453
-369
lines changed

13 files changed

+453
-369
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "feat(auth): IAM Identity Center connections now request the least permissive set of scopes for features. Using the same connection for multiple features will request additional scopes to be used."
4+
}

src/codecatalyst/auth.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
codecatalystScopes,
1515
hasScopes,
1616
createBuilderIdConnection,
17+
ssoAccountAccessScopes,
1718
} from '../credentials/auth'
1819
import { getSecondaryAuth } from '../credentials/secondaryAuth'
1920
import { getLogger } from '../shared/logger'
@@ -36,9 +37,13 @@ export class CodeCatalystAuthStorage {
3637
}
3738
}
3839

40+
const defaultScopes = [...ssoAccountAccessScopes, ...codecatalystScopes]
3941
export const isValidCodeCatalystConnection = (conn: Connection): conn is SsoConnection =>
4042
isBuilderIdConnection(conn) && hasScopes(conn, codecatalystScopes)
4143

44+
export const isUpgradeableConnection = (conn: Connection): conn is SsoConnection =>
45+
isBuilderIdConnection(conn) && !isValidCodeCatalystConnection(conn)
46+
4247
export class CodeCatalystAuthenticationProvider {
4348
public readonly onDidChangeActiveConnection = this.secondaryAuth.onDidChangeActiveConnection
4449

@@ -105,13 +110,12 @@ export class CodeCatalystAuthenticationProvider {
105110
}
106111

107112
const conn = (await this.auth.listConnections()).find(isBuilderIdConnection)
108-
const isNewUser = conn === undefined
109113
const continueItem: vscode.MessageItem = { title: localizedText.continueText }
110114
const cancelItem: vscode.MessageItem = { title: localizedText.cancel, isCloseAffordance: true }
111115

112-
if (isNewUser || !isValidCodeCatalystConnection(conn)) {
116+
if (conn === undefined) {
113117
// TODO: change to `satisfies` on TS 4.9
114-
telemetry.record({ codecatalyst_connectionFlow: isNewUser ? 'Create' : 'Upgrade' } as ConnectionFlowEvent)
118+
telemetry.record({ codecatalyst_connectionFlow: 'Create' } as ConnectionFlowEvent)
115119

116120
const message = `The ${
117121
getIdeProperties().company
@@ -122,15 +126,22 @@ export class CodeCatalystAuthenticationProvider {
122126
throw new ToolkitError('Not connected to CodeCatalyst', { code: 'NoConnection', cancelled: true })
123127
}
124128

125-
const newConn = await createBuilderIdConnection(this.auth)
129+
const newConn = await createBuilderIdConnection(this.auth, defaultScopes)
126130
if (this.auth.activeConnection?.id !== newConn.id) {
127131
await this.secondaryAuth.useNewConnection(newConn)
128132
}
129133

130134
return newConn
131135
}
132136

133-
if (this.auth.activeConnection?.id !== conn.id) {
137+
const upgrade = async () => {
138+
// TODO: change to `satisfies` on TS 4.9
139+
telemetry.record({ codecatalyst_connectionFlow: 'Upgrade' } as ConnectionFlowEvent)
140+
141+
return this.secondaryAuth.addScopes(conn, defaultScopes)
142+
}
143+
144+
if (isBuilderIdConnection(conn) && this.auth.activeConnection?.id !== conn.id) {
134145
// TODO: change to `satisfies` on TS 4.9
135146
telemetry.record({ codecatalyst_connectionFlow: 'Switch' } as ConnectionFlowEvent)
136147

@@ -144,11 +155,19 @@ export class CodeCatalystAuthenticationProvider {
144155
throw new ToolkitError('Not connected to CodeCatalyst', { code: 'NoConnection', cancelled: true })
145156
}
146157

158+
if (isUpgradeableConnection(conn)) {
159+
await upgrade()
160+
}
161+
147162
await this.secondaryAuth.useNewConnection(conn)
148163

149164
return conn
150165
}
151166

167+
if (isUpgradeableConnection(conn)) {
168+
return upgrade()
169+
}
170+
152171
throw new ToolkitError('Not connected to CodeCatalyst', { code: 'NoConnectionBadState' })
153172
}
154173

src/codecatalyst/explorer.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as vscode from 'vscode'
77
import { RootNode } from '../awsexplorer/localExplorer'
8-
import { Connection, createBuilderIdConnection, isBuilderIdConnection } from '../credentials/auth'
8+
import { Connection, isBuilderIdConnection } from '../credentials/auth'
99
import { DevEnvironment } from '../shared/clients/codecatalystClient'
1010
import { isCloud9 } from '../shared/extensionUtilities'
1111
import { addColor, getIcon } from '../shared/icons'
@@ -19,10 +19,7 @@ import { getLogger } from '../shared/logger'
1919

2020
const getStartedCommand = Commands.register(
2121
'aws.codecatalyst.getStarted',
22-
async (authProvider: CodeCatalystAuthenticationProvider) => {
23-
const conn = await createBuilderIdConnection(authProvider.auth)
24-
await authProvider.secondaryAuth.useNewConnection(conn)
25-
}
22+
(authProvider: CodeCatalystAuthenticationProvider) => authProvider.promptNotConnected()
2623
)
2724

2825
const learnMoreCommand = Commands.register('aws.learnMore', async (docsUrl: vscode.Uri) => {
@@ -126,6 +123,8 @@ export class CodeCatalystRootNode implements RootNode {
126123
}
127124

128125
public async getTreeItem() {
126+
await this.authProvider.restore()
127+
129128
this.devenv = (await getThisDevEnv(this.authProvider))?.unwrapOrElse(err => {
130129
getLogger().warn('codecatalyst: failed to get current Dev Enviroment: %s', err)
131130
return undefined

src/codewhisperer/util/authUtil.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
createSsoProfile,
1616
isSsoConnection,
1717
hasScopes,
18+
ssoAccountAccessScopes,
1819
isIamConnection,
1920
} from '../../credentials/auth'
2021
import { Connection, SsoConnection } from '../../credentials/auth'
@@ -26,7 +27,8 @@ import { isCloud9 } from '../../shared/extensionUtilities'
2627
import { TelemetryHelper } from './telemetryHelper'
2728
import { PromptSettings } from '../../shared/settings'
2829

29-
export const awsBuilderIdSsoProfile = createBuilderIdProfile()
30+
const defaultScopes = [...ssoAccountAccessScopes, ...codewhispererScopes]
31+
export const awsBuilderIdSsoProfile = createBuilderIdProfile(defaultScopes)
3032

3133
export const isValidCodeWhispererConnection = (conn: Connection): conn is Connection => {
3234
if (isCloud9('classic')) {
@@ -48,7 +50,7 @@ export class AuthUtil {
4850
private readonly clearAccessToken = once(() =>
4951
globals.context.globalState.update(CodeWhispererConstants.accessToken, undefined)
5052
)
51-
private readonly secondaryAuth = getSecondaryAuth(
53+
public readonly secondaryAuth = getSecondaryAuth(
5254
this.auth,
5355
'codewhisperer',
5456
'CodeWhisperer',
@@ -80,8 +82,6 @@ export class AuthUtil {
8082
vscode.commands.executeCommand('aws.codeWhisperer.updateReferenceLog'),
8183
])
8284
})
83-
84-
Commands.register('aws.codeWhisperer.removeConnection', () => this.secondaryAuth.removeConnection())
8585
}
8686

8787
// current active cwspr connection
@@ -115,7 +115,7 @@ export class AuthUtil {
115115
return existingConn
116116
}
117117

118-
return this.rescopeConnection(existingConn)
118+
return this.secondaryAuth.addScopes(existingConn, defaultScopes)
119119
}
120120

121121
public async connectToEnterpriseSso(startUrl: string, region: string) {
@@ -125,17 +125,24 @@ export class AuthUtil {
125125
)
126126

127127
if (!existingConn) {
128-
const conn = await this.auth.createConnection(createSsoProfile(startUrl, region))
128+
const conn = await this.auth.createConnection(createSsoProfile(startUrl, region, defaultScopes))
129129
return this.secondaryAuth.useNewConnection(conn)
130130
} else if (isValidCodeWhispererConnection(existingConn)) {
131131
return this.secondaryAuth.useNewConnection(existingConn)
132132
} else if (isSsoConnection(existingConn)) {
133-
return this.rescopeConnection(existingConn)
133+
return this.secondaryAuth.addScopes(existingConn, defaultScopes)
134134
}
135135
}
136136

137137
public static get instance() {
138-
return (this.#instance ??= new this())
138+
if (this.#instance !== undefined) {
139+
return this.#instance
140+
}
141+
142+
const self = (this.#instance = new this())
143+
Commands.register('aws.codeWhisperer.removeConnection', () => self.secondaryAuth.removeConnection())
144+
145+
return self
139146
}
140147

141148
public async getBearerToken(): Promise<string> {
@@ -227,23 +234,7 @@ export class AuthUtil {
227234
this.showReauthenticatePrompt(isAutoTrigger)
228235
}
229236

230-
public async rescopeConnection(existingConn: SsoConnection) {
231-
const upgradedConn = await this.auth.createConnection(createSsoProfile(existingConn.startUrl))
232-
await this.auth.deleteConnection(existingConn)
233-
234-
if (this.auth.activeConnection?.id === (existingConn as SsoConnection).id) {
235-
await this.auth.useConnection(upgradedConn)
236-
} else {
237-
await this.secondaryAuth.useNewConnection(upgradedConn)
238-
}
239-
240-
return upgradedConn
241-
}
242-
243237
public hasAccessToken() {
244238
return !!globals.context.globalState.get(CodeWhispererConstants.accessToken)
245239
}
246240
}
247-
248-
export const isUpgradeableConnection = (conn?: Connection): conn is SsoConnection =>
249-
!!conn && !isValidCodeWhispererConnection(conn) && isSsoConnection(conn)

src/codewhisperer/util/getStartUrl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import { isValidResponse } from '../../shared/wizards/wizard'
99
import { AuthUtil } from './authUtil'
1010
import { CancellationError } from '../../shared/utilities/timeoutUtils'
1111
import { ToolkitError } from '../../shared/errors'
12-
import { createStartUrlPrompter, showRegionPrompter } from '../../credentials/auth'
12+
import { codewhispererScopes, createStartUrlPrompter, showRegionPrompter } from '../../credentials/auth'
1313
import { telemetry } from '../../shared/telemetry/telemetry'
1414

1515
export const getStartUrl = async () => {
16-
const inputBox = await createStartUrlPrompter('IAM Identity Center', false)
16+
const inputBox = await createStartUrlPrompter('IAM Identity Center', codewhispererScopes)
1717
const userInput = await inputBox.prompt()
1818
if (!isValidResponse(userInput)) {
1919
telemetry.ui_click.emit({ elementId: 'connection_optionescapecancel' })

0 commit comments

Comments
 (0)