Skip to content

Commit d70e0cb

Browse files
authored
Support testing using Builder ID credentials (#3586)
1 parent 9cfc20b commit d70e0cb

File tree

22 files changed

+447
-131
lines changed

22 files changed

+447
-131
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.core.utils
5+
6+
import kotlin.reflect.full.hasAnnotation
7+
import kotlin.reflect.full.memberProperties
8+
9+
@Target(AnnotationTarget.PROPERTY)
10+
annotation class SensitiveField
11+
12+
fun redactedString(o: Any): String {
13+
val clazz = o::class
14+
if (!clazz.isData) {
15+
error("Only supports redacting data classes")
16+
}
17+
18+
return buildString {
19+
append(clazz.simpleName)
20+
append("(")
21+
22+
val properties = o::class.memberProperties
23+
properties.forEachIndexed { i, prop ->
24+
append(prop.name)
25+
append("=")
26+
if (prop.hasAnnotation<SensitiveField>()) {
27+
if (prop.getter.call(o) == null) {
28+
append("null")
29+
} else {
30+
append("<redacted>")
31+
}
32+
} else {
33+
append(prop.getter.call(o))
34+
}
35+
36+
if (i != properties.size - 1) {
37+
append(", ")
38+
}
39+
}
40+
41+
append(")")
42+
}
43+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.core.credentials.sso.bearer
5+
6+
import com.intellij.openapi.util.Disposer
7+
import com.intellij.testFramework.ApplicationExtension
8+
import org.assertj.core.api.Assertions.assertThat
9+
import org.assertj.core.api.Assumptions.assumeThat
10+
import org.junit.jupiter.api.MethodOrderer
11+
import org.junit.jupiter.api.Order
12+
import org.junit.jupiter.api.Test
13+
import org.junit.jupiter.api.TestMethodOrder
14+
import org.junit.jupiter.api.extension.ExtendWith
15+
import org.junit.jupiter.api.io.TempDir
16+
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_REGION
17+
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
18+
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessTokenCacheKey
19+
import software.aws.toolkits.jetbrains.core.credentials.sso.DiskCache
20+
import software.aws.toolkits.jetbrains.utils.extensions.SsoLogin
21+
import software.aws.toolkits.jetbrains.utils.extensions.SsoLoginExtension
22+
import java.nio.file.Path
23+
import java.time.Instant
24+
25+
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
26+
@ExtendWith(ApplicationExtension::class, SsoLoginExtension::class)
27+
@SsoLogin("codecatalyst-test-account")
28+
class InteractiveBearerTokenProviderIntegrationTest {
29+
companion object {
30+
@JvmStatic
31+
@TempDir
32+
private lateinit var diskCachePath: Path
33+
34+
private val testScopes = listOf("sso:account:access")
35+
private val diskCache by lazy { DiskCache(cacheDir = diskCachePath) }
36+
private val cacheKey = AccessTokenCacheKey(SONO_REGION, SONO_URL, testScopes)
37+
}
38+
39+
@Test
40+
@Order(1)
41+
fun `test Builder ID login`() {
42+
val initialToken = diskCache.loadAccessToken(cacheKey)
43+
assertThat(initialToken).isNull()
44+
45+
val sut = InteractiveBearerTokenProvider(
46+
startUrl = SONO_URL,
47+
region = SONO_REGION,
48+
scopes = testScopes,
49+
cache = diskCache
50+
)
51+
52+
sut.reauthenticate()
53+
assertThat(sut.resolveToken()).isNotNull()
54+
55+
Disposer.dispose(sut)
56+
}
57+
58+
@Test
59+
@Order(2)
60+
fun `test token refresh`() {
61+
val initialToken = diskCache.loadAccessToken(cacheKey)
62+
assumeThat(initialToken).isNotNull
63+
64+
diskCache.saveAccessToken(cacheKey, initialToken!!.copy(accessToken = "invalid", expiresAt = Instant.EPOCH))
65+
val sut = InteractiveBearerTokenProvider(
66+
startUrl = SONO_URL,
67+
region = SONO_REGION,
68+
scopes = testScopes,
69+
cache = diskCache
70+
)
71+
72+
assertThat(sut.resolveToken()).satisfies {
73+
assertThat(it).isNotNull()
74+
assertThat(it).isNotEqualTo(initialToken)
75+
}
76+
77+
Disposer.dispose(sut)
78+
}
79+
}

jetbrains-core/resources/META-INF/plugin.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ with what features/services are supported.
198198
<applicationService serviceInterface="software.aws.toolkits.jetbrains.settings.AwsSettings"
199199
serviceImplementation="software.aws.toolkits.jetbrains.settings.DefaultAwsSettings"
200200
testServiceImplementation="software.aws.toolkits.jetbrains.settings.MockAwsSettings" />
201+
<applicationService serviceInterface="software.aws.toolkits.jetbrains.core.credentials.sso.SsoLoginCallbackProvider"
202+
serviceImplementation="software.aws.toolkits.jetbrains.core.credentials.sso.DefaultSsoLoginCallbackProvider"
203+
testServiceImplementation="software.aws.toolkits.jetbrains.core.credentials.sso.MockSsoLoginCallbackProvider" />
201204

202205
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.settings.EcsExecCommandSettings"/>
203206
<applicationService serviceImplementation="software.aws.toolkits.jetbrains.settings.SamDisplayDevModeWarningSettings"/>

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/SsoSupport.kt

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,16 @@
33

44
package software.aws.toolkits.jetbrains.core.credentials
55

6-
import com.intellij.ide.BrowserUtil
76
import com.intellij.openapi.actionSystem.AnAction
8-
import com.intellij.openapi.progress.ProcessCanceledException
9-
import com.intellij.openapi.project.ProjectManager
10-
import software.aws.toolkits.jetbrains.core.credentials.sso.Authorization
117
import software.aws.toolkits.jetbrains.core.credentials.sso.DiskCache
128
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoCache
13-
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoLoginCallback
14-
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.CopyUserCodeForLoginDialog
15-
import software.aws.toolkits.jetbrains.utils.computeOnEdt
16-
import software.aws.toolkits.jetbrains.utils.notifyError
179
import software.aws.toolkits.resources.message
18-
import software.aws.toolkits.telemetry.AwsTelemetry
19-
import software.aws.toolkits.telemetry.CredentialType
20-
import software.aws.toolkits.telemetry.Result
2110

2211
/**
2312
* Shared disk cache for SSO for the IDE
2413
*/
2514
val diskCache by lazy { DiskCache() }
2615

27-
object SsoPrompt : SsoLoginCallback {
28-
override fun tokenPending(authorization: Authorization) {
29-
computeOnEdt {
30-
val result = CopyUserCodeForLoginDialog(
31-
ProjectManager.getInstance().defaultProject,
32-
authorization.userCode,
33-
message("credentials.sso.login.title"),
34-
CredentialType.SsoProfile
35-
).showAndGet()
36-
if (result) {
37-
AwsTelemetry.loginWithBrowser(project = null, Result.Succeeded, CredentialType.SsoProfile)
38-
BrowserUtil.browse(authorization.verificationUri)
39-
} else {
40-
AwsTelemetry.loginWithBrowser(project = null, Result.Cancelled, CredentialType.SsoProfile)
41-
throw ProcessCanceledException(IllegalStateException(message("credentials.sso.login.cancelled")))
42-
}
43-
}
44-
}
45-
46-
override fun tokenRetrieved() {}
47-
48-
override fun tokenRetrievalFailure(e: Exception) {
49-
e.notifyError(message("credentials.sso.login.failed"))
50-
}
51-
}
52-
5316
interface SsoRequiredInteractiveCredentials : InteractiveCredential {
5417
val ssoCache: SsoCache
5518
val ssoUrl: String

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitConnectionImpls.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import com.intellij.openapi.Disposable
77
import com.intellij.openapi.util.Disposer
88
import software.aws.toolkits.core.TokenConnectionSettings
99
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
10-
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoLoginCallback
11-
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenPrompt
1210
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
1311
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.InteractiveBearerTokenProvider
1412
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.ProfileSdkTokenProviderWrapper
@@ -18,7 +16,6 @@ class ManagedBearerSsoConnection(
1816
val startUrl: String,
1917
val region: String,
2018
override val scopes: List<String>,
21-
private val prompt: SsoLoginCallback = BearerTokenPrompt
2219
) : BearerSsoConnection, Disposable {
2320
override val id: String = ToolkitBearerTokenProvider.ssoIdentifier(startUrl, region)
2421
override val label: String = ToolkitBearerTokenProvider.ssoDisplayName(startUrl)
@@ -28,7 +25,6 @@ class ManagedBearerSsoConnection(
2825
InteractiveBearerTokenProvider(
2926
startUrl,
3027
region,
31-
prompt,
3228
scopes
3329
),
3430
region

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/profiles/ProfileSsoProvider.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import software.amazon.awssdk.services.sso.SsoClient
1313
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
1414
import software.amazon.awssdk.utils.SdkAutoCloseable
1515
import software.aws.toolkits.jetbrains.core.AwsClientManager
16-
import software.aws.toolkits.jetbrains.core.credentials.SsoPrompt
1716
import software.aws.toolkits.jetbrains.core.credentials.diskCache
1817
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoAccessTokenProvider
1918
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoCredentialProvider
@@ -33,7 +32,6 @@ class ProfileSsoProvider(profile: Profile) : AwsCredentialsProvider, SdkAutoClos
3332
val ssoAccessTokenProvider = SsoAccessTokenProvider(
3433
profile.requiredProperty(ProfileProperty.SSO_START_URL),
3534
ssoRegion,
36-
SsoPrompt,
3735
diskCache,
3836
ssoOidcClient
3937
)

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/sono/SonoConstants.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package software.aws.toolkits.jetbrains.core.credentials.sono
66
import software.aws.toolkits.jetbrains.core.credentials.ManagedBearerSsoConnection
77
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
88

9-
internal const val SONO_REGION = "us-east-1"
9+
const val SONO_REGION = "us-east-1"
1010
const val SONO_URL = "https://view.awsapps.com/start"
1111
internal val CODEWHISPERER_SCOPES = listOf(
1212
"codewhisperer:completions",

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/sso/AccessToken.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.fasterxml.jackson.annotation.JsonInclude
77
import software.amazon.awssdk.auth.token.credentials.SdkToken
88
import software.amazon.awssdk.services.sso.SsoClient
99
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
10+
import software.aws.toolkits.core.utils.SensitiveField
11+
import software.aws.toolkits.core.utils.redactedString
1012
import java.time.Instant
1113
import java.util.Optional
1214

@@ -16,7 +18,9 @@ import java.util.Optional
1618
data class AccessToken(
1719
val startUrl: String,
1820
val region: String,
21+
@SensitiveField
1922
val accessToken: String,
23+
@SensitiveField
2024
@JsonInclude(JsonInclude.Include.NON_NULL)
2125
val refreshToken: String? = null,
2226
val expiresAt: Instant,
@@ -25,6 +29,8 @@ data class AccessToken(
2529
override fun token() = accessToken
2630

2731
override fun expirationTime() = Optional.of(expiresAt)
32+
33+
override fun toString() = redactedString(this)
2834
}
2935

3036
// diverging from SDK/CLI impl here since they do: sha1sum(sessionName ?: startUrl)

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/sso/Authorization.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44
package software.aws.toolkits.jetbrains.core.credentials.sso
55

66
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
7+
import software.aws.toolkits.core.utils.SensitiveField
8+
import software.aws.toolkits.core.utils.redactedString
79
import java.time.Instant
810

911
/**
1012
* Returned by [SsoOidcClient.startDeviceAuthorization] that contains the required data to construct the user visible SSO login flow.
1113
*/
1214
data class Authorization(
15+
@SensitiveField
1316
val deviceCode: String,
1417
val userCode: String,
1518
val verificationUri: String,
1619
val verificationUriComplete: String,
1720
val expiresAt: Instant,
1821
val pollInterval: Long,
1922
val createdAt: Instant
20-
)
23+
) {
24+
override fun toString(): String = redactedString(this)
25+
}

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/sso/AwsBuilderIdPrompt.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)