From 33ccd3fd9344745b86e3c698f01f465ef046e902 Mon Sep 17 00:00:00 2001 From: Chris Mendoza Date: Mon, 20 Apr 2026 08:43:43 -0400 Subject: [PATCH 1/2] fix(cloudformation): handle credentials with empty session token --- .../services/cfnlsp/CfnCredentialsService.kt | 7 +- .../cfnlsp/CfnCredentialsServiceTest.kt | 139 ++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt diff --git a/plugins/toolkit/jetbrains-core/src-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsService.kt b/plugins/toolkit/jetbrains-core/src-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsService.kt index 25a5fb81ba9..08a7226caff 100644 --- a/plugins/toolkit/jetbrains-core/src-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsService.kt +++ b/plugins/toolkit/jetbrains-core/src-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsService.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.cfnlsp +import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager @@ -161,7 +162,7 @@ internal class CfnCredentialsService(private val project: Project) : Disposable } private fun encrypt(credentials: IamCredentials): String { - val payload = """{"data":${jacksonObjectMapper().writeValueAsString(credentials)}}""" + val payload = """{"data":${MAPPER.writeValueAsString(credentials)}}""" val jwe = JWEObject( JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM), Payload(payload) @@ -175,6 +176,10 @@ internal class CfnCredentialsService(private val project: Project) : Disposable companion object { private val LOG = getLogger() + private val MAPPER = jacksonObjectMapper().apply { + setSerializationInclusion(JsonInclude.Include.NON_NULL) + } + fun getInstance(project: Project): CfnCredentialsService = project.service() private fun generateKey(): SecretKey { diff --git a/plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt b/plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt new file mode 100644 index 00000000000..ee55141a40f --- /dev/null +++ b/plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt @@ -0,0 +1,139 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cfnlsp + +import com.intellij.testFramework.ProjectRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkObject +import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials +import software.aws.toolkit.core.credentials.ToolkitCredentialsProvider +import software.aws.toolkit.core.region.AwsRegion +import software.aws.toolkit.jetbrains.core.credentials.AwsConnectionManager +import software.aws.toolkits.jetbrains.services.cfnlsp.protocol.UpdateCredentialsParams +import java.util.concurrent.CompletableFuture + +class CfnCredentialsServiceTest { + + @get:Rule + val projectRule = ProjectRule() + + private lateinit var mockConnectionManager: AwsConnectionManager + private lateinit var mockCredentialProvider: ToolkitCredentialsProvider + private lateinit var mockCfnClient: CfnClientService + private lateinit var credentialsService: CfnCredentialsService + + @Before + fun setUp() { + mockConnectionManager = mockk() + mockCredentialProvider = mockk() + mockCfnClient = mockk() + + mockkObject(AwsConnectionManager) + mockkObject(CfnClientService) + + every { AwsConnectionManager.getInstance(projectRule.project) } returns mockConnectionManager + every { CfnClientService.getInstance(projectRule.project) } returns mockCfnClient + every { mockCfnClient.updateIamCredentials(any()) } returns CompletableFuture.completedFuture(mockk()) + + // Mock the selectedRegion call that happens in constructor + every { mockConnectionManager.selectedRegion } returns null + + credentialsService = CfnCredentialsService(projectRule.project) + } + + @After + fun tearDown() { + unmockkObject(AwsConnectionManager) + unmockkObject(CfnClientService) + } + + @Test + fun `sendCredentials excludes null sessionToken from payload`() { + val testRegion = AwsRegion("us-east-1", "US East 1", "aws") + val basicCredentials = AwsBasicCredentials.create("testAccessKey", "testSecretKey") + var capturedParams: UpdateCredentialsParams? = null + + every { mockConnectionManager.selectedRegion } returns testRegion + every { mockConnectionManager.activeCredentialProvider } returns mockCredentialProvider + every { mockCredentialProvider.shortName } returns "testProfile" + every { mockCredentialProvider.resolveCredentials() } returns basicCredentials + every { mockCfnClient.updateIamCredentials(capture(slot())) } answers { + capturedParams = firstArg() + CompletableFuture.completedFuture(mockk()) + } + + credentialsService.sendCredentials() + + verify { mockCfnClient.updateIamCredentials(any()) } + + // Verify the encrypted payload is sent + assertThat(capturedParams).isNotNull() + assertThat(capturedParams!!.encrypted).isTrue() + assertThat(capturedParams!!.data).isNotEmpty() + + // Use reflection to verify the mapper excludes null sessionToken + val mapperField = CfnCredentialsService::class.java.getDeclaredField("MAPPER") + mapperField.isAccessible = true + val mapper = mapperField.get(null) as com.fasterxml.jackson.databind.ObjectMapper + + val testCredentials = IamCredentials("testProfile", "us-east-1", "testAccessKey", "testSecretKey", null) + val json = mapper.writeValueAsString(testCredentials) + + assertThat(json).doesNotContain("sessionToken") + } + + @Test + fun `sendCredentials includes sessionToken when present`() { + val testRegion = AwsRegion("us-east-1", "US East 1", "aws") + val sessionCredentials = AwsSessionCredentials.create("testAccessKey", "testSecretKey", "testSessionToken") + var capturedParams: UpdateCredentialsParams? = null + + every { mockConnectionManager.selectedRegion } returns testRegion + every { mockConnectionManager.activeCredentialProvider } returns mockCredentialProvider + every { mockCredentialProvider.shortName } returns "testProfile" + every { mockCredentialProvider.resolveCredentials() } returns sessionCredentials + every { mockCfnClient.updateIamCredentials(capture(slot())) } answers { + capturedParams = firstArg() + CompletableFuture.completedFuture(mockk()) + } + + credentialsService.sendCredentials() + + verify { mockCfnClient.updateIamCredentials(any()) } + + // Verify the encrypted payload is sent + assertThat(capturedParams).isNotNull() + assertThat(capturedParams!!.encrypted).isTrue() + assertThat(capturedParams!!.data).isNotEmpty() + + // Use reflection to verify the mapper includes non-null sessionToken + val mapperField = CfnCredentialsService::class.java.getDeclaredField("MAPPER") + mapperField.isAccessible = true + val mapper = mapperField.get(null) as com.fasterxml.jackson.databind.ObjectMapper + + val testCredentials = IamCredentials("testProfile", "us-east-1", "testAccessKey", "testSecretKey", "testSessionToken") + val json = mapper.writeValueAsString(testCredentials) + + assertThat(json).contains("\"sessionToken\":\"testSessionToken\"") + } + + @Test + fun `sendCredentials returns early when no region selected`() { + every { mockConnectionManager.selectedRegion } returns null + + credentialsService.sendCredentials() + + verify(exactly = 0) { mockCfnClient.updateIamCredentials(any()) } + } +} From 30a94da73afb849de7f23f4939502f8e748ce021 Mon Sep 17 00:00:00 2001 From: Chris Mendoza Date: Mon, 20 Apr 2026 09:23:39 -0400 Subject: [PATCH 2/2] linting --- .../services/cfnlsp/CfnCredentialsServiceTest.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt b/plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt index ee55141a40f..b1ff5d280b5 100644 --- a/plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt +++ b/plugins/toolkit/jetbrains-core/tst-253+/software/aws/toolkits/jetbrains/services/cfnlsp/CfnCredentialsServiceTest.kt @@ -79,8 +79,10 @@ class CfnCredentialsServiceTest { // Verify the encrypted payload is sent assertThat(capturedParams).isNotNull() - assertThat(capturedParams!!.encrypted).isTrue() - assertThat(capturedParams!!.data).isNotEmpty() + capturedParams?.let { + assertThat(it.encrypted).isTrue() + assertThat(it.data).isNotEmpty() + } // Use reflection to verify the mapper excludes null sessionToken val mapperField = CfnCredentialsService::class.java.getDeclaredField("MAPPER") @@ -114,8 +116,10 @@ class CfnCredentialsServiceTest { // Verify the encrypted payload is sent assertThat(capturedParams).isNotNull() - assertThat(capturedParams!!.encrypted).isTrue() - assertThat(capturedParams!!.data).isNotEmpty() + capturedParams?.let { + assertThat(it.encrypted).isTrue() + assertThat(it.data).isNotEmpty() + } // Use reflection to verify the mapper includes non-null sessionToken val mapperField = CfnCredentialsService::class.java.getDeclaredField("MAPPER")