Skip to content

Commit 5f2549a

Browse files
authored
Add support for the profile key credential_source (#2968)
1 parent ff7a505 commit 5f2549a

File tree

13 files changed

+460
-33
lines changed

13 files changed

+460
-33
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "Added support for AWS profiles that use the `credential_source` key"
4+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2021 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.profiles
5+
6+
enum class CredentialSourceType {
7+
EC2_INSTANCE_METADATA, ECS_CONTAINER, ENVIRONMENT;
8+
9+
companion object {
10+
fun parse(value: String): CredentialSourceType {
11+
if (value.equals("Ec2InstanceMetadata", ignoreCase = true)) {
12+
return EC2_INSTANCE_METADATA
13+
} else if (value.equals("EcsContainer", ignoreCase = true)) {
14+
return ECS_CONTAINER
15+
} else if (value.equals("Environment", ignoreCase = true)) {
16+
return ENVIRONMENT
17+
}
18+
throw IllegalArgumentException("'$value' is not a valid credential_source")
19+
}
20+
}
21+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2021 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.profiles
5+
6+
import software.amazon.awssdk.core.SdkSystemSetting
7+
import software.amazon.awssdk.profiles.Profile
8+
import software.amazon.awssdk.profiles.ProfileProperty
9+
10+
/**
11+
* Retrieves the EC2 metadata endpoint based on profile file, env var, and Java system properties
12+
*
13+
* https://github.com/aws/aws-sdk-java-v2/blob/5fb447594313ab1ab9b9c0ead0ed7cb906b06e93/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataConfigProviderEndpointResolutionTest.java
14+
*/
15+
object Ec2MetadataConfigProvider {
16+
/**
17+
* Default IPv4 endpoint for the Amazon EC2 Instance Metadata Service.
18+
*/
19+
private const val EC2_METADATA_SERVICE_URL_IPV4 = "http://169.254.169.254"
20+
21+
/**
22+
* Default IPv6 endpoint for the Amazon EC2 Instance Metadata Service.
23+
*/
24+
private const val EC2_METADATA_SERVICE_URL_IPV6 = "http://[fd00:ec2::254]"
25+
26+
private enum class EndpointMode {
27+
IPV4, IPV6;
28+
29+
companion object {
30+
fun fromValue(s: String?): EndpointMode = s?.let { _ ->
31+
values().find { it.name.equals(s, ignoreCase = true) }
32+
} ?: throw IllegalArgumentException("Unrecognized value for endpoint mode: '$s'")
33+
}
34+
}
35+
36+
fun Profile.getEc2MedataEndpoint(): String = this.getEndpointOverride() ?: when (this.getEndpointMode()) {
37+
EndpointMode.IPV4 -> EC2_METADATA_SERVICE_URL_IPV4
38+
EndpointMode.IPV6 -> EC2_METADATA_SERVICE_URL_IPV6
39+
}
40+
41+
private fun Profile.getEndpointMode(): EndpointMode {
42+
val endpointMode = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.nonDefaultStringValue
43+
val endpointModeString = if (endpointMode.isPresent) {
44+
endpointMode.get()
45+
} else {
46+
configFileEndpointMode(this) ?: SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.defaultValue()
47+
}
48+
return EndpointMode.fromValue(endpointModeString)
49+
}
50+
51+
private fun Profile.getEndpointOverride(): String? {
52+
val endpointOverride = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.nonDefaultStringValue
53+
return if (endpointOverride.isPresent) {
54+
endpointOverride.get()
55+
} else {
56+
configFileEndpointOverride(this)
57+
}
58+
}
59+
60+
private fun configFileEndpointMode(profile: Profile): String? = profile.property(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT_MODE).orElse(null)
61+
62+
private fun configFileEndpointOverride(profile: Profile): String? = profile.property(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT).orElse(null)
63+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

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

6+
import org.jetbrains.annotations.TestOnly
67
import software.amazon.awssdk.auth.credentials.AwsCredentials
78
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
89
import software.amazon.awssdk.profiles.Profile
@@ -17,7 +18,7 @@ import software.aws.toolkits.jetbrains.core.AwsClientManager
1718
import software.aws.toolkits.jetbrains.core.credentials.promptForMfaToken
1819
import java.util.function.Supplier
1920

20-
class ProfileAssumeRoleProvider(private val parentProvider: AwsCredentialsProvider, region: AwsRegion, profile: Profile) :
21+
class ProfileAssumeRoleProvider(@get:TestOnly internal val parentProvider: AwsCredentialsProvider, region: AwsRegion, profile: Profile) :
2122
AwsCredentialsProvider, SdkAutoCloseable {
2223
private val stsClient: StsClient
2324
private val credentialsProvider: StsAssumeRoleCredentialsProvider
@@ -31,7 +32,7 @@ class ProfileAssumeRoleProvider(private val parentProvider: AwsCredentialsProvid
3132
// https://docs.aws.amazon.com/sdkref/latest/guide/setting-global-duration_seconds.html
3233
val durationSecs = profile.property(ProfileProperty.DURATION_SECONDS).map { it.toIntOrNull() }.orElse(null) ?: 3600
3334

34-
stsClient = AwsClientManager.getInstance().createUnmanagedClient(parentProvider, Region.of(region.id),)
35+
stsClient = AwsClientManager.getInstance().createUnmanagedClient(parentProvider, Region.of(region.id))
3536

3637
credentialsProvider = StsAssumeRoleCredentialsProvider.builder()
3738
.stsClient(stsClient)

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import com.intellij.openapi.actionSystem.AnActionEvent
88
import com.intellij.openapi.project.DumbAwareAction
99
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
1010
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
11+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain
1112
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
13+
import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider
14+
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider
15+
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider
1216
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
17+
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider
1318
import software.amazon.awssdk.profiles.Profile
1419
import software.amazon.awssdk.profiles.ProfileProperty
1520
import software.aws.toolkits.core.credentials.CredentialIdentifier
@@ -24,6 +29,7 @@ import software.aws.toolkits.jetbrains.core.credentials.MfaRequiredInteractiveCr
2429
import software.aws.toolkits.jetbrains.core.credentials.SsoRequiredInteractiveCredentials
2530
import software.aws.toolkits.jetbrains.core.credentials.ToolkitCredentialProcessProvider
2631
import software.aws.toolkits.jetbrains.core.credentials.diskCache
32+
import software.aws.toolkits.jetbrains.core.credentials.profiles.Ec2MetadataConfigProvider.getEc2MedataEndpoint
2733
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoCache
2834
import software.aws.toolkits.jetbrains.settings.AwsSettings
2935
import software.aws.toolkits.jetbrains.settings.ProfilesNotification
@@ -172,7 +178,7 @@ class ProfileCredentialProviderFactory(private val ssoCache: SsoCache = diskCach
172178

173179
// Some profiles failed to load
174180
if (newProfiles.invalidProfiles.isNotEmpty()) {
175-
val message = newProfiles.invalidProfiles.values.joinToString("\n") { it.message ?: it::class.java.name }
181+
val message = newProfiles.invalidProfiles.values.joinToString("\n")
176182

177183
val errorDialogTitle = message("credentials.profile.failed_load")
178184
val numErrorMessage = message("credentials.profile.refresh_errors", newProfiles.invalidProfiles.size)
@@ -215,14 +221,42 @@ class ProfileCredentialProviderFactory(private val ssoCache: SsoCache = diskCach
215221
private fun createSsoProvider(profile: Profile): AwsCredentialsProvider = ProfileSsoProvider(profile)
216222

217223
private fun createAssumeRoleProvider(profile: Profile, region: AwsRegion): AwsCredentialsProvider {
218-
val sourceProfileName = profile.requiredProperty(ProfileProperty.SOURCE_PROFILE)
219-
val sourceProfile = profileHolder.getProfile(sourceProfileName)
220-
?: throw IllegalStateException("Profile $sourceProfileName looks to have been removed")
221-
val parentCredentialProvider = createAwsCredentialProvider(sourceProfile, region)
224+
val sourceProfileName = profile.property(ProfileProperty.SOURCE_PROFILE)
225+
val credentialSource = profile.property(ProfileProperty.CREDENTIAL_SOURCE)
226+
227+
val parentCredentialProvider = when {
228+
sourceProfileName.isPresent -> {
229+
val sourceProfile = profileHolder.getProfile(sourceProfileName.get())
230+
?: throw IllegalStateException("Profile $sourceProfileName looks to have been removed")
231+
createAwsCredentialProvider(sourceProfile, region)
232+
}
233+
credentialSource.isPresent -> {
234+
// Can we parse the credential_source
235+
credentialSourceCredentialProvider(CredentialSourceType.parse(credentialSource.get()), profile)
236+
}
237+
else -> {
238+
throw IllegalArgumentException(message("credentials.profile.assume_role.missing_source", profile.name()))
239+
}
240+
}
222241

223242
return ProfileAssumeRoleProvider(parentCredentialProvider, region, profile)
224243
}
225244

245+
private fun credentialSourceCredentialProvider(credentialSource: CredentialSourceType, profile: Profile): AwsCredentialsProvider =
246+
when (credentialSource) {
247+
CredentialSourceType.ECS_CONTAINER -> ContainerCredentialsProvider.builder().build()
248+
CredentialSourceType.EC2_INSTANCE_METADATA -> {
249+
// The IMDS credentials provider should source the endpoint config properties from the currently active profile
250+
InstanceProfileCredentialsProvider.builder()
251+
.endpoint(profile.getEc2MedataEndpoint())
252+
.build()
253+
}
254+
CredentialSourceType.ENVIRONMENT -> AwsCredentialsProviderChain.builder()
255+
.addCredentialsProvider(SystemPropertyCredentialsProvider.create())
256+
.addCredentialsProvider(EnvironmentVariableCredentialsProvider.create())
257+
.build()
258+
}
259+
226260
private fun createBasicProvider(profile: Profile) = StaticCredentialsProvider.create(
227261
AwsBasicCredentials.create(
228262
profile.requiredProperty(ProfileProperty.AWS_ACCESS_KEY_ID),

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,17 @@ fun validateSsoProfile(profile: Profile) {
5454

5555
private fun validateAssumeRoleProfile(profile: Profile, allProfiles: Map<String, Profile>) {
5656
val rootProfile = profile.traverseCredentialChain(allProfiles).last()
57-
validateProfile(rootProfile, allProfiles)
57+
val credentialSource = rootProfile.property(ProfileProperty.CREDENTIAL_SOURCE)
58+
59+
if (credentialSource.isPresent) {
60+
try {
61+
CredentialSourceType.parse(credentialSource.get())
62+
} catch (e: Exception) {
63+
throw IllegalArgumentException(message("credentials.profile.assume_role.invalid_credential_source", rootProfile.name()))
64+
}
65+
} else {
66+
validateProfile(rootProfile, allProfiles)
67+
}
5868
}
5969

6070
private fun validateStaticSessionProfile(profile: Profile) {

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,30 @@ fun Profile.traverseCredentialChain(profiles: Map<String, Profile>): Sequence<Pr
2121
throw IllegalArgumentException(message("credentials.profile.circular_profiles", name(), chain))
2222
}
2323

24-
val sourceProfile = currentProfile.requiredProperty(ProfileProperty.SOURCE_PROFILE)
25-
currentProfile = profiles[sourceProfile]
26-
?: throw IllegalArgumentException(
27-
message(
28-
"credentials.profile.source_profile_not_found",
29-
currentProfileName,
30-
sourceProfile
24+
val sourceProfile = currentProfile.property(ProfileProperty.SOURCE_PROFILE)
25+
val credentialSource = currentProfile.property(ProfileProperty.CREDENTIAL_SOURCE)
26+
27+
if (sourceProfile.isPresent && credentialSource.isPresent) {
28+
throw IllegalArgumentException(message("credentials.profile.assume_role.duplicate_source", currentProfileName))
29+
}
30+
31+
if (sourceProfile.isPresent) {
32+
val sourceProfileName = sourceProfile.get()
33+
currentProfile = profiles[sourceProfileName]
34+
?: throw IllegalArgumentException(
35+
message(
36+
"credentials.profile.source_profile_not_found",
37+
currentProfileName,
38+
sourceProfileName
39+
)
3140
)
32-
)
3341

34-
yield(currentProfile)
42+
yield(currentProfile)
43+
} else if (credentialSource.isPresent) {
44+
return@sequence
45+
} else {
46+
throw IllegalArgumentException(message("credentials.profile.assume_role.missing_source", currentProfileName))
47+
}
3548
}
3649
}
3750

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2021 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.profiles
5+
6+
import org.assertj.core.api.Assertions.assertThat
7+
import org.assertj.core.api.Assertions.assertThatThrownBy
8+
import org.junit.Rule
9+
import org.junit.Test
10+
import software.aws.toolkits.core.rules.EnvironmentVariableHelper
11+
import software.aws.toolkits.core.rules.SystemPropertyHelper
12+
import software.aws.toolkits.core.utils.test.aString
13+
import software.aws.toolkits.jetbrains.core.credentials.profiles.Ec2MetadataConfigProvider.getEc2MedataEndpoint
14+
import software.aws.toolkits.jetbrains.utils.isInstanceOf
15+
16+
class Ec2MetadataConfigProviderTest {
17+
@Rule
18+
@JvmField
19+
val sysProps = SystemPropertyHelper()
20+
21+
@Rule
22+
@JvmField
23+
val envVars = EnvironmentVariableHelper()
24+
25+
@Test
26+
fun `endpoint can be overridden with system property`() {
27+
val endpoint = aString()
28+
System.setProperty("aws.ec2MetadataServiceEndpoint", endpoint)
29+
30+
assertThat(profile().getEc2MedataEndpoint()).isEqualTo(endpoint)
31+
}
32+
33+
@Test
34+
fun `endpoint can be overridden with env var`() {
35+
val endpoint = aString()
36+
envVars["AWS_EC2_METADATA_SERVICE_ENDPOINT"] = endpoint
37+
38+
assertThat(profile().getEc2MedataEndpoint()).isEqualTo(endpoint)
39+
}
40+
41+
@Test
42+
fun `endpoint can be overridden with profile`() {
43+
val endpoint = aString()
44+
val profile = profile {
45+
put("ec2_metadata_service_endpoint", endpoint)
46+
}
47+
48+
assertThat(profile.getEc2MedataEndpoint()).isEqualTo(endpoint)
49+
}
50+
51+
@Test
52+
fun `endpoint defaults to ipv4 endpoint if nothing is specified`() {
53+
assertThat(profile().getEc2MedataEndpoint()).isEqualTo("http://169.254.169.254")
54+
}
55+
56+
@Test
57+
fun `endpoint defaults to default endpoint if ipv6 is specified`() {
58+
val profile = profile {
59+
put("ec2_metadata_service_endpoint_mode", "ipv6")
60+
}
61+
62+
assertThat(profile.getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
63+
}
64+
65+
@Test
66+
fun `mode is case insensitive`() {
67+
val profile = profile {
68+
put("ec2_metadata_service_endpoint_mode", "ipv6")
69+
}
70+
71+
assertThat(profile.getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
72+
73+
val profile2 = profile {
74+
put("ec2_metadata_service_endpoint_mode", "iPv6")
75+
}
76+
77+
assertThat(profile2.getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
78+
}
79+
80+
@Test
81+
fun `mode can be overridden with system property`() {
82+
System.setProperty("aws.ec2MetadataServiceEndpointMode", "ipv6")
83+
84+
assertThat(profile().getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
85+
}
86+
87+
@Test
88+
fun `mode can be overridden with env var`() {
89+
envVars["AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE"] = "ipv6"
90+
91+
assertThat(profile().getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
92+
}
93+
94+
@Test
95+
fun `invalid mode fails`() {
96+
val profile = profile {
97+
put("ec2_metadata_service_endpoint_mode", "badMode")
98+
}
99+
100+
assertThatThrownBy { profile.getEc2MedataEndpoint() }.isInstanceOf<IllegalArgumentException>()
101+
}
102+
}

jetbrains-core/tst/software/aws/toolkits/jetbrains/core/credentials/profiles/ProfileAssumeRoleProviderTest.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import org.mockito.kotlin.mock
1919
import org.mockito.kotlin.stub
2020
import org.mockito.kotlin.verify
2121
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
22-
import software.amazon.awssdk.profiles.Profile
2322
import software.amazon.awssdk.profiles.ProfileProperty
2423
import software.amazon.awssdk.services.sts.StsClient
2524
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest
@@ -190,9 +189,4 @@ class ProfileAssumeRoleProviderTest {
190189
verify(stsClient).close()
191190
verify(parentProvider as SdkAutoCloseable).close()
192191
}
193-
194-
private fun profile(properties: MutableMap<String, String>.() -> Unit) = Profile.builder()
195-
.name(aString())
196-
.properties(mutableMapOf<String, String>().apply { properties(this) })
197-
.build()
198192
}

0 commit comments

Comments
 (0)