diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt index 96cf8bf2063..997423c0300 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt @@ -18,6 +18,7 @@ enum class CssVariable( TextColorWeak("--mynah-color-text-weak"), TextColorLink("--mynah-color-text-link"), TextColorInput("--mynah-color-text-input"), + TextColorDisabled("--mynah-color-text-disabled"), Background("--mynah-color-bg"), BackgroundAlt("--mynah-color-bg-alt"), diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt index b32b6e3af51..d8b7e4fb4d0 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt @@ -40,6 +40,7 @@ class ThemeBrowserAdapter { append(CssVariable.TextColorInput, theme.textFieldForeground) append(CssVariable.TextColorLink, theme.linkText) append(CssVariable.TextColorWeak, theme.inactiveText) + append(CssVariable.TextColorDisabled, theme.inactiveText) append(CssVariable.Background, theme.background) append(CssVariable.BackgroundAlt, theme.background) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt index 17f32e5bbf4..b837631caa1 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt @@ -27,7 +27,6 @@ import software.aws.toolkits.core.utils.error import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn -import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener @@ -43,7 +42,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenF import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogResult import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata -import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.TelemetryParsingUtil import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService @@ -155,19 +153,7 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC val connection = ToolkitConnectionManager.getInstance(project) .activeConnectionForFeature(QConnection.getInstance()) - when (connection) { - is AwsBearerTokenConnection -> { - ConnectionMetadata( - SsoProfileData(connection.startUrl) - ) - } - else -> { - // If no connection or not a bearer token connection return default builderID start url - ConnectionMetadata( - SsoProfileData(AmazonQLspConstants.AWS_BUILDER_ID_URL) - ) - } - } + connection?.let { ConnectionMetadata.fromConnection(it) } } override fun openTab(params: LSPAny): CompletableFuture { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/AuthCredentialsService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/AuthCredentialsService.kt index a38c8da4bbc..fb40fb75f35 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/AuthCredentialsService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/AuthCredentialsService.kt @@ -4,9 +4,10 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.auth import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage +import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection import java.util.concurrent.CompletableFuture interface AuthCredentialsService { - fun updateTokenCredentials(accessToken: String, encrypted: Boolean): CompletableFuture + fun updateTokenCredentials(connection: ToolkitConnection, encrypted: Boolean): CompletableFuture fun deleteTokenCredentials(): CompletableFuture } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt index 22191c518fa..1c71b8b3184 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt @@ -22,6 +22,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryp import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.UpdateConfigurationParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.BearerCredentials +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayloadData import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile @@ -46,7 +47,7 @@ class DefaultAuthCredentialsService( private val scheduler: ScheduledExecutorService = AppExecutorUtil.getAppScheduledExecutorService() private var tokenSyncTask: ScheduledFuture<*>? = null - private val tokenSyncIntervalSeconds = 900L + private val tokenSyncIntervalMinutes = 5L init { project.messageBus.connect(serverInstance).apply { @@ -98,14 +99,18 @@ class DefaultAuthCredentialsService( LOG.warn(e) { "Failed to sync bearer token to Flare" } } }, - tokenSyncIntervalSeconds, - tokenSyncIntervalSeconds, - TimeUnit.SECONDS + tokenSyncIntervalMinutes, + tokenSyncIntervalMinutes, + TimeUnit.MINUTES ) } - override fun updateTokenCredentials(accessToken: String, encrypted: Boolean): CompletableFuture { - val payload = createUpdateCredentialsPayload(accessToken, encrypted) + override fun updateTokenCredentials(connection: ToolkitConnection, encrypted: Boolean): CompletableFuture { + val payload = try { + createUpdateCredentialsPayload(connection, encrypted) + } catch (e: Exception) { + return CompletableFuture.failedFuture(e) + } return AmazonQLspService.executeIfRunning(project) { server -> server.updateTokenCredentials(payload) @@ -142,35 +147,39 @@ class DefaultAuthCredentialsService( } private fun updateTokenFromConnection(connection: ToolkitConnection): CompletableFuture = - (connection.getConnectionSettings() as? TokenConnectionSettings) + updateTokenCredentials(connection, true) + + override fun invalidate(providerId: String) { + deleteTokenCredentials() + } + + private fun createUpdateCredentialsPayload(connection: ToolkitConnection, encrypted: Boolean): UpdateCredentialsPayload { + val token = (connection.getConnectionSettings() as? TokenConnectionSettings) ?.tokenProvider ?.delegate ?.let { it as? BearerTokenProvider } ?.currentToken() ?.accessToken - ?.let { token -> updateTokenCredentials(token, true) } - ?: CompletableFuture.failedFuture(IllegalStateException("Unable to get token from connection")) + ?: error("Unable to get token from connection") - override fun invalidate(providerId: String) { - deleteTokenCredentials() - } - - private fun createUpdateCredentialsPayload(token: String, encrypted: Boolean): UpdateCredentialsPayload = - if (encrypted) { + return if (encrypted) { UpdateCredentialsPayload( data = encryptionManager.encrypt( UpdateCredentialsPayloadData( BearerCredentials(token) ) ), + metadata = ConnectionMetadata.fromConnection(connection), encrypted = true ) } else { UpdateCredentialsPayload( data = token, + metadata = ConnectionMetadata.fromConnection(connection), encrypted = false ) } + } override fun onProfileSelected(project: Project, profile: QRegionProfile?) { updateConfiguration() diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt index c6216b97cff..3f180085694 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt @@ -3,9 +3,29 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials +import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection +import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspConstants + data class ConnectionMetadata( val sso: SsoProfileData, -) +) { + companion object { + fun fromConnection(connection: ToolkitConnection) = when (connection) { + is AwsBearerTokenConnection -> { + ConnectionMetadata( + SsoProfileData(connection.startUrl) + ) + } + else -> { + // If no connection or not a bearer token connection return default builderID start url + ConnectionMetadata( + SsoProfileData(AmazonQLspConstants.AWS_BUILDER_ID_URL) + ) + } + } + } +} data class SsoProfileData( val startUrl: String, diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt index a427330c055..86f8b6457b3 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt @@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentia data class UpdateCredentialsPayload( val data: String, + val metadata: ConnectionMetadata, val encrypted: Boolean, ) diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt index b77dacb06a1..c390120c111 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt @@ -331,7 +331,7 @@ class AmazonQLanguageClientImplTest { every { mockConnectionManager.activeConnectionForFeature(QConnection.getInstance()) } returns null assertThat(sut.getConnectionMetadata().get()) - .isEqualTo(ConnectionMetadata(SsoProfileData(AmazonQLspConstants.AWS_BUILDER_ID_URL))) + .isEqualTo(null) } @Test diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.kt index d141268d2c3..ad25a019eee 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.kt @@ -30,6 +30,8 @@ import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.InteractiveBe import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLanguageServer import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload import software.aws.toolkits.jetbrains.utils.isQConnected import software.aws.toolkits.jetbrains.utils.isQExpired @@ -112,6 +114,7 @@ class DefaultAuthCredentialsServiceTest { connectionId: String = "test-connection-id", ): AwsBearerTokenConnection = mockk { every { id } returns connectionId + every { startUrl } returns "startUrl" every { getConnectionSettings() } returns createMockTokenSettings(accessToken) } @@ -192,17 +195,18 @@ class DefaultAuthCredentialsServiceTest { @Test fun `test updateTokenCredentials unencrypted success`() { - sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk()) - - val token = "unencryptedToken" val isEncrypted = false + sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk()) - sut.updateTokenCredentials(token, isEncrypted) + sut.updateTokenCredentials(mockConnection, isEncrypted) verify(exactly = 1) { mockLanguageServer.updateTokenCredentials( UpdateCredentialsPayload( - token, + "test-access-token", + ConnectionMetadata( + SsoProfileData("startUrl") + ), isEncrypted ) ) @@ -214,17 +218,19 @@ class DefaultAuthCredentialsServiceTest { sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk()) val encryptedToken = "encryptedToken" - val decryptedToken = "decryptedToken" val isEncrypted = true every { mockEncryptionManager.encrypt(any()) } returns encryptedToken - sut.updateTokenCredentials(decryptedToken, isEncrypted) + sut.updateTokenCredentials(mockConnection, isEncrypted) verify(atLeast = 1) { mockLanguageServer.updateTokenCredentials( UpdateCredentialsPayload( encryptedToken, + ConnectionMetadata( + SsoProfileData("startUrl") + ), isEncrypted ) )