Skip to content

Commit 397b95c

Browse files
committed
fix(amazonq): add missing auth checks / invalidate token if server claims token is invalid
we have several existing checks to guard against invocation, but do not handle the case where token is no longer valid
1 parent 736ae74 commit 397b95c

File tree

1 file changed

+69
-24
lines changed
  • plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup

1 file changed

+69
-24
lines changed

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/QInlineCompletionProvider.kt

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,21 @@ import kotlinx.coroutines.channels.Channel
4242
import kotlinx.coroutines.flow.receiveAsFlow
4343
import kotlinx.coroutines.future.await
4444
import kotlinx.coroutines.launch
45+
import kotlinx.coroutines.withContext
4546
import migration.software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
47+
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
4648
import org.eclipse.lsp4j.jsonrpc.messages.Either
49+
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
4750
import software.aws.toolkits.core.utils.debug
4851
import software.aws.toolkits.core.utils.getLogger
52+
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
53+
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
54+
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
55+
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
56+
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
4957
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
5058
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
59+
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
5160
import software.aws.toolkits.jetbrains.services.codewhisperer.importadder.CodeWhispererImportAdder
5261
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InlineCompletionItemContext
5362
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InlineCompletionSessionContext
@@ -59,7 +68,9 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispe
5968
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
6069
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
6170
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
71+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil
6272
import software.aws.toolkits.jetbrains.utils.isQConnected
73+
import software.aws.toolkits.jetbrains.utils.isQExpired
6374
import software.aws.toolkits.resources.message
6475
import software.aws.toolkits.telemetry.CodewhispererTriggerType
6576
import java.awt.Dimension
@@ -391,8 +402,23 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
391402

392403
override suspend fun getSuggestion(request: InlineCompletionRequest): InlineCompletionSuggestion {
393404
val editor = request.editor
394-
val document = editor.document
395405
val project = editor.project ?: return InlineCompletionSuggestion.Empty
406+
if (!isCodeWhispererEnabled(project)) return InlineCompletionSuggestion.Empty
407+
408+
// try to refresh automatically if possible, otherwise ask user to login again
409+
if (isQExpired(project)) {
410+
// consider changing to only running once a ~minute since this is relatively expensive
411+
// say the connection is un-refreshable if refresh fails for 3 times
412+
val shouldReauth = withContext(getCoroutineBgContext()) {
413+
CodeWhispererUtil.promptReAuth(project)
414+
}
415+
416+
if (shouldReauth) {
417+
return InlineCompletionSuggestion.Empty
418+
}
419+
}
420+
421+
val document = editor.document
396422
val handler = InlineCompletion.getHandlerOrNull(editor) ?: return InlineCompletionSuggestion.Empty
397423
val session = InlineCompletionSession.getOrNull(editor) ?: return InlineCompletionSuggestion.Empty
398424
val triggerSessionId = triggerSessionId++
@@ -443,31 +469,28 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
443469
}
444470

445471
try {
446-
// Launch coroutine for background pagination progress
447-
cs.launch {
448-
var nextToken: Either<String, Int>? = null
449-
do {
450-
nextToken = startPaginationInBackground(
451-
project,
452-
editor,
453-
triggerTypeInfo,
454-
triggerSessionId,
455-
nextToken,
456-
sessionContext,
457-
)
458-
} while (nextToken != null && !nextToken.left.isNullOrEmpty())
472+
var nextToken: Either<String, Int>? = null
473+
do {
474+
nextToken = startPaginationInBackground(
475+
project,
476+
editor,
477+
triggerTypeInfo,
478+
triggerSessionId,
479+
nextToken,
480+
sessionContext,
481+
)
482+
} while (nextToken != null && !nextToken.left.isNullOrEmpty())
459483

460-
// closing all channels since pagination for this session has finished
484+
// closing all channels since pagination for this session has finished
485+
logInline(triggerSessionId) {
486+
"Pagination finished, closing all channels"
487+
}
488+
sessionContext.itemContexts.forEach {
489+
it.channel.close()
490+
}
491+
if (session.context.isDisposed) {
461492
logInline(triggerSessionId) {
462-
"Pagination finished, closing all channels"
463-
}
464-
sessionContext.itemContexts.forEach {
465-
it.channel.close()
466-
}
467-
if (session.context.isDisposed) {
468-
logInline(triggerSessionId) {
469-
"Current display session already disposed by a new trigger before pagination finishes, exiting"
470-
}
493+
"Current display session already disposed by a new trigger before pagination finishes, exiting"
471494
}
472495
}
473496

@@ -572,6 +595,27 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
572595
logInline(triggerSessionId, e) {
573596
"Error during pagination"
574597
}
598+
if (e is ResponseErrorException) {
599+
// convoluted but lines up with "The bearer token included in the request is invalid"
600+
// https://github.com/aws/language-servers/blob/1f3e93024eeb22186a34f0bd560f8d552f517300/server/aws-lsp-codewhisperer/src/language-server/chat/utils.ts#L22-L23
601+
// error data is nullable
602+
if (e.responseError.data?.toString()?.contains("E_AMAZON_Q_CONNECTION_EXPIRED") == true) {
603+
// kill the session if the connection is expired
604+
val connection = ToolkitConnectionManager
605+
.getInstance(project)
606+
.activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
607+
val tokenProvider = connection?.let { it.getConnectionSettings().tokenProvider.delegate as? BearerTokenProvider }
608+
tokenProvider?.let {
609+
// TODO: fragile
610+
try {
611+
it.refresh()
612+
} catch (_: InvalidGrantException){
613+
it.invalidate()
614+
CodeWhispererUtil.reconnectCodeWhisperer(project)
615+
}
616+
}
617+
}
618+
}
575619
return null
576620
}
577621
}
@@ -591,6 +635,7 @@ class QInlineCompletionProvider(private val cs: CoroutineScope) : InlineCompleti
591635
val editor = request.editor
592636
val project = editor.project ?: return false
593637

638+
// qExpired case handled in completion handler
594639
if (!isQConnected(project)) return false
595640
if (!CodeWhispererExplorerActionManager.getInstance().isAutoEnabled() && event.isManualCall()) return false
596641
if (QRegionProfileManager.getInstance().hasValidConnectionButNoActiveProfile(project)) return false

0 commit comments

Comments
 (0)