Skip to content

Commit 55b575d

Browse files
committed
feat: add mfa serial input, telemetry for iam and sync with small fix in flare-iam
1 parent ea6a170 commit 55b575d

File tree

11 files changed

+131
-13
lines changed

11 files changed

+131
-13
lines changed

packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ChatTriggerType,
1717
EditorContextExtractor,
1818
PromptMessage,
19+
TriggerEvent,
1920
TriggerEventsStorage,
2021
TriggerPayload,
2122
triggerPayloadToChatRequest,
@@ -30,6 +31,7 @@ import { extractAuthFollowUp } from 'aws-core-vscode/amazonq'
3031
import { InlineChatParams, InlineChatResult } from '@aws/language-server-runtimes-types'
3132
import { decryptResponse, encryptRequest } from '../../lsp/encryption'
3233
import { getCursorState } from '../../lsp/utils'
34+
import { CwsprChatTriggerInteraction, telemetry } from 'aws-core-vscode/telemetry'
3335

3436
export class InlineChatProvider {
3537
private readonly editorContextExtractor: EditorContextExtractor
@@ -68,7 +70,34 @@ export class InlineChatProvider {
6870
}
6971
}
7072

73+
private getTriggerInteractionFromTriggerEvent(triggerEvent: TriggerEvent | undefined): CwsprChatTriggerInteraction {
74+
switch (triggerEvent?.type) {
75+
case 'editor_context_command':
76+
return triggerEvent.command?.triggerType === 'keybinding' ? 'hotkeys' : 'contextMenu'
77+
case 'follow_up':
78+
case 'chat_message':
79+
default:
80+
return 'click'
81+
}
82+
}
83+
7184
public async processPromptMessageLSP(message: PromptMessage): Promise<InlineChatResult> {
85+
const triggerInteraction = this.getTriggerInteractionFromTriggerEvent(
86+
this.triggerEventsStorage.getLastTriggerEventByTabID(message.tabID)
87+
)
88+
if (!AuthUtil.instance.isSsoSession()) {
89+
telemetry.amazonq_messageResponseError.emit({
90+
result: 'Failed',
91+
cwsprChatConversationType: 'Chat',
92+
cwsprChatRequestLength: message.message?.length ?? 0,
93+
cwsprChatResponseCode: 401,
94+
cwsprChatTriggerInteraction: triggerInteraction,
95+
reason: 'AuthenticationError',
96+
reasonDesc: 'Inline chat requires SSO authentication, but current session is not',
97+
})
98+
throw new ToolkitError('Inline chat is only available with SSO authentication')
99+
}
100+
72101
// TODO: handle partial responses.
73102
getLogger().info('Making inline chat request with message %O', message)
74103
const params = this.getCurrentEditorParams(message.message ?? '')
@@ -83,6 +112,23 @@ export class InlineChatProvider {
83112

84113
// TODO: remove in favor of LSP implementation.
85114
public async processPromptMessage(message: PromptMessage) {
115+
const triggerInteraction = this.getTriggerInteractionFromTriggerEvent(
116+
this.triggerEventsStorage.getLastTriggerEventByTabID(message.tabID)
117+
)
118+
if (!AuthUtil.instance.isSsoSession()) {
119+
telemetry.amazonq_messageResponseError.emit({
120+
result: 'Failed',
121+
cwsprChatConversationType: 'Chat',
122+
cwsprChatRequestLength: message.message?.length ?? 0,
123+
cwsprChatResponseCode: 401,
124+
cwsprChatTriggerInteraction: triggerInteraction,
125+
reason: 'AuthenticationError',
126+
reasonDesc: 'Inline chat requires SSO authentication, but current session is not',
127+
credentialStartUrl: AuthUtil.instance.connection?.startUrl,
128+
})
129+
throw new ToolkitError('Inline chat is only available with SSO authentication')
130+
}
131+
86132
return this.editorContextExtractor
87133
.extractContextForTrigger('ChatMessage')
88134
.then((context) => {
@@ -143,7 +189,7 @@ export class InlineChatProvider {
143189
private async generateResponse(
144190
triggerPayload: TriggerPayload & { projectContextQueryLatencyMs?: number },
145191
triggerID: string
146-
) {
192+
): Promise<GenerateAssistantResponseCommandOutput | undefined> {
147193
const triggerEvent = this.triggerEventsStorage.getTriggerEvent(triggerID)
148194
if (triggerEvent === undefined) {
149195
return
@@ -182,7 +228,12 @@ export class InlineChatProvider {
182228
let response: GenerateAssistantResponseCommandOutput | undefined = undefined
183229
session.createNewTokenSource()
184230
try {
185-
response = await session.chatSso(request)
231+
if (AuthUtil.instance.isSsoSession()) {
232+
response = await session.chatSso(request)
233+
} else {
234+
// Call sendMessage because Q Developer Streaming Client does not have generateAssistantResponse
235+
throw new ToolkitError('Inline chat is only available with SSO authentication')
236+
}
186237
getLogger().info(
187238
`response to tab: ${tabID} conversationID: ${session.sessionIdentifier} requestID: ${response.$metadata.requestId} metadata: %O`,
188239
response.$metadata

packages/amazonq/src/lsp/client.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import { processUtils } from 'aws-core-vscode/shared'
5858
import { activate as activateChat } from './chat/activation'
5959
import { activate as activeInlineChat } from '../inlineChat/activation'
6060
import { AmazonQResourcePaths } from './lspInstaller'
61-
import { auth2, getMfaTokenFromUser } from 'aws-core-vscode/auth'
61+
import { auth2, getMfaTokenFromUser, getMfaSerialFromUser } from 'aws-core-vscode/auth'
6262
import { ConfigSection, isValidConfigSection, pushConfigUpdate, toAmazonQLSPLogLevel } from './config'
6363
import { telemetry } from 'aws-core-vscode/telemetry'
6464
import { SessionManager } from '../app/inline/sessionManager'
@@ -343,8 +343,17 @@ async function postStartLanguageServer(
343343
client.onRequest(
344344
auth2.notificationTypes.getMfaCode.method,
345345
async (params: GetMfaCodeParams): Promise<GetMfaCodeResult> => {
346-
const mfaCode = await getMfaTokenFromUser(params.mfaSerial, params.profileName)
347-
return { code: mfaCode ?? '' }
346+
if (params.mfaSerial) {
347+
globals.globalState.update('recentMfaSerial', { mfaSerial: params.mfaSerial })
348+
}
349+
const defaultMfaSerial = globals.globalState.tryGet('recentMfaSerial', Object, {
350+
mfaSerial: '',
351+
}).mfaSerial
352+
let mfaSerial = await getMfaSerialFromUser(defaultMfaSerial, params.profileName)
353+
mfaSerial = mfaSerial.trim()
354+
globals.globalState.update('recentMfaSerial', { mfaSerial: mfaSerial })
355+
const mfaCode = await getMfaTokenFromUser(mfaSerial, params.profileName)
356+
return { code: mfaCode ?? '', mfaSerial: mfaSerial ?? '' }
348357
}
349358
)
350359

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,34 @@ export class CredentialsSettings extends fromExtensionManifest('aws', { profile:
102102

103103
const errorMessageUserCancelled = localize('AWS.error.mfa.userCancelled', 'User cancelled entering authentication code')
104104

105+
/**
106+
* @description Prompts user for MFA serial number
107+
*
108+
* Entered token is passed to the callback.
109+
* If user cancels out, the callback is passed an error with a fixed message string.
110+
*
111+
* @param profileName Name of Credentials profile we are asking an MFA Token for
112+
* @param callback tokens/errors are passed through here
113+
*/
114+
export async function getMfaSerialFromUser(defaultSerial: string, profileName: string): Promise<string> {
115+
const inputBox = createInputBox({
116+
ignoreFocusOut: true,
117+
placeholder: localize('AWS.prompt.mfa.enterCode.placeholder', 'Enter mfaSerial Number Here'),
118+
title: localize('AWS.prompt.mfa.enterCode.title', 'MFA Challenge for {0}', profileName),
119+
prompt: localize('AWS.prompt.mfa.enterCode.prompt', 'Enter Serial Number for MFA device', defaultSerial),
120+
value: defaultSerial, // Pre-fill with default value
121+
})
122+
123+
const token = await inputBox.prompt()
124+
125+
// Distinguish user cancel vs code entry issues with the error message
126+
if (!isValidResponse(token)) {
127+
throw new Error(errorMessageUserCancelled)
128+
}
129+
130+
return token
131+
}
132+
105133
/**
106134
* @description Prompts user for MFA token
107135
*

packages/core/src/auth/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export {
2222
} from './connection'
2323
export { Auth } from './auth'
2424
export { CredentialsStore } from './credentials/store'
25-
export { getMfaTokenFromUser } from './credentials/utils'
25+
export { getMfaTokenFromUser, getMfaSerialFromUser } from './credentials/utils'
2626
export { LoginManager } from './deprecated/loginManager'
2727
export * as constants from './sso/constants'
2828
export * as cache from './sso/cache'

packages/core/src/codewhisperer/ui/codeWhispererNodes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,11 @@ export function createManageSubscription(): DataQuickPickItem<'manageSubscriptio
192192
export function createSignout(): DataQuickPickItem<'signout'> {
193193
const label = localize('AWS.codewhisperer.signoutNode.label', 'Sign Out')
194194
const icon = getIcon('vscode-export')
195-
const connection = AuthUtil.instance.isBuilderIdConnection() ? 'AWS Builder ID' : 'IAM Identity Center'
195+
const connection = AuthUtil.instance.isIamConnection()
196+
? 'IAM Credentials'
197+
: AuthUtil.instance.isBuilderIdConnection()
198+
? 'AWS Builder ID'
199+
: 'IAM Identity Center'
196200

197201
return {
198202
data: 'signout',

packages/core/src/codewhisperer/util/authUtil.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ export class AuthUtil implements IAuthProvider {
277277
return Boolean(this.connection?.startUrl && this.connection?.startUrl !== builderIdStartUrl)
278278
}
279279

280+
isIamConnection() {
281+
return Boolean(this.connection?.accessKey && this.connection?.secretKey)
282+
}
283+
280284
isInternalAmazonUser(): boolean {
281285
return this.isConnected() && this.connection?.startUrl === internalStartUrl
282286
}

packages/core/src/codewhispererChat/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ export { ChatTriggerType, PromptMessage, TriggerPayload } from './controllers/ch
1212
export { UserIntentRecognizer } from './controllers/chat/userIntent/userIntentRecognizer'
1313
export { EditorContextExtractor } from './editor/context/extractor'
1414
export { ChatSessionStorage } from './storages/chatSession'
15-
export { TriggerEventsStorage } from './storages/triggerEvents'
15+
export { TriggerEventsStorage, TriggerEvent } from './storages/triggerEvents'
1616
export { ReferenceLogController } from './view/messages/referenceLogController'
1717
export { extractLanguageNameFromFile } from './editor/context/file/languages'

packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { builderIdStartUrl } from '../../../../auth/sso/constants'
2020
import { RegionProfile, vsCodeState } from '../../../../codewhisperer/models/model'
2121
import { randomUUID } from '../../../../shared/crypto'
2222
import globals from '../../../../shared/extensionGlobals'
23-
import { telemetry } from '../../../../shared/telemetry/telemetry'
23+
import { CredentialType, telemetry } from '../../../../shared/telemetry/telemetry'
2424
import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager'
2525

2626
const className = 'AmazonQLoginWebview'
@@ -204,12 +204,21 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
204204
// Defining separate auth function to emit telemetry before returning from this method
205205
await globals.globalState.update('recentIamKeys', { accessKey: accessKey })
206206
await globals.globalState.update('recentRoleArn', { roleArn: roleArn })
207+
let credentialsType: CredentialType | undefined
208+
if (!sessionToken && !roleArn) {
209+
credentialsType = 'staticProfile'
210+
} else if (roleArn) {
211+
credentialsType = 'assumeRoleProfile'
212+
} else {
213+
credentialsType = 'staticSessionProfile'
214+
}
215+
207216
const runAuth = async (): Promise<AuthError | undefined> => {
208217
try {
209218
await AuthUtil.instance.loginIam(accessKey, secretKey, sessionToken, roleArn)
210219
} catch (e) {
211220
getLogger().error('Failed submitting credentials %O', e)
212-
const message = e instanceof Error ? e.message : e as string
221+
const message = e instanceof Error ? e.message : (e as string)
213222
return { id: this.id, text: message }
214223
}
215224
// Enable code suggestions
@@ -224,7 +233,10 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
224233
const result = await runAuth()
225234
this.storeMetricMetadata({
226235
credentialSourceId: 'sharedCredentials',
227-
authEnabledFeatures: 'codewhisperer',
236+
featureId: 'codewhisperer',
237+
credentialType: credentialsType,
238+
isReAuth: false,
239+
isAggregated: false,
228240
...this.getResultForMetrics(result),
229241
})
230242
this.emitAuthMetric()

packages/core/src/login/webview/vue/backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export abstract class CommonAuthWebview extends VueWebview {
177177
accessKey: string,
178178
secretKey: string,
179179
sessionToken?: string,
180-
role_arn?: string,
180+
roleArn?: string
181181
): Promise<AuthError | undefined>
182182

183183
async showResourceExplorer(): Promise<void> {

packages/core/src/login/webview/vue/login.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,9 @@ export default defineComponent({
512512
}
513513
}
514514
} else if (this.selectedLoginOption === LoginOption.IAM_CREDENTIAL) {
515+
// Emit telemetry when IAM Credentials option is selected and Continue is clicked
516+
void client.emitUiClick('auth_credentialsOption')
517+
515518
this.stage = 'AWS_PROFILE'
516519
this.$nextTick(() => document.getElementById('profileName')!.focus())
517520
}
@@ -533,7 +536,13 @@ export default defineComponent({
533536
return
534537
}
535538
this.stage = 'AUTHENTICATING'
536-
const error = await client.startIamCredentialSetup(this.profileName, this.accessKey, this.secretKey, this.sessionToken, this.roleArn)
539+
const error = await client.startIamCredentialSetup(
540+
this.profileName,
541+
this.accessKey,
542+
this.secretKey,
543+
this.sessionToken,
544+
this.roleArn
545+
)
537546
if (error) {
538547
this.stage = 'START'
539548
void client.errorNotification(error)

0 commit comments

Comments
 (0)