Skip to content

Commit 32dea41

Browse files
committed
Revert "feat: add support for account ID in IMDS credentials (#1573)"
This reverts commit a707e1b.
1 parent afb2166 commit 32dea41

File tree

16 files changed

+803
-1017
lines changed

16 files changed

+803
-1017
lines changed

aws-runtime/aws-config/build.gradle.kts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import aws.sdk.kotlin.gradle.codegen.smithyKotlinProjectionSrcDir
88

99
plugins {
1010
alias(libs.plugins.aws.kotlin.repo.tools.smithybuild)
11-
alias(libs.plugins.kotlinx.serialization)
1211
}
1312

1413
description = "Support for AWS configuration"
@@ -53,7 +52,6 @@ kotlin {
5352
implementation(libs.kotlinx.coroutines.test)
5453
implementation(libs.smithy.kotlin.http.test)
5554
implementation(libs.kotlinx.serialization.json)
56-
implementation(libs.kotest.framework.datatest)
5755
}
5856
}
5957
jvmTest {

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/DefaultChainCredentialsProvider.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import aws.smithy.kotlin.runtime.util.PlatformProvider
4141
* @param region the region to make credentials requests to.
4242
* @return the newly-constructed credentials provider
4343
*/
44-
public class DefaultChainCredentialsProvider(
44+
public class DefaultChainCredentialsProvider constructor(
4545
public val profileName: String? = null,
4646
public val platformProvider: PlatformProvider = PlatformProvider.System,
4747
httpClient: HttpClientEngine? = null,
@@ -51,11 +51,6 @@ public class DefaultChainCredentialsProvider(
5151
private val manageEngine = httpClient == null
5252
private val engine = httpClient ?: DefaultHttpEngine()
5353

54-
private val imdsClient = ImdsClient {
55-
platformProvider = this@DefaultChainCredentialsProvider.platformProvider
56-
engine = this@DefaultChainCredentialsProvider.engine
57-
}
58-
5954
private val chain = CredentialsProviderChain(
6055
SystemPropertyCredentialsProvider(platformProvider::getProperty),
6156
EnvironmentCredentialsProvider(platformProvider::getenv),
@@ -64,7 +59,12 @@ public class DefaultChainCredentialsProvider(
6459
ProfileCredentialsProvider(profileName = profileName, platformProvider = platformProvider, httpClient = engine, region = region),
6560
EcsCredentialsProvider(platformProvider, engine),
6661
ImdsCredentialsProvider(
67-
client = imdsClient,
62+
client = lazy {
63+
ImdsClient {
64+
platformProvider = this@DefaultChainCredentialsProvider.platformProvider
65+
engine = this@DefaultChainCredentialsProvider.engine
66+
}
67+
},
6868
platformProvider = platformProvider,
6969
),
7070
)
@@ -75,7 +75,6 @@ public class DefaultChainCredentialsProvider(
7575

7676
override fun close() {
7777
provider.close()
78-
imdsClient.close()
7978
if (manageEngine) {
8079
engine.closeIfCloseable()
8180
}

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt

Lines changed: 84 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -5,232 +5,150 @@
55

66
package aws.sdk.kotlin.runtime.auth.credentials
77

8-
import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials
9-
import aws.sdk.kotlin.runtime.config.imds.*
8+
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
9+
import aws.sdk.kotlin.runtime.config.imds.EC2MetadataError
10+
import aws.sdk.kotlin.runtime.config.imds.ImdsClient
11+
import aws.sdk.kotlin.runtime.config.imds.InstanceMetadataProvider
1012
import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.AwsBusinessMetric
1113
import aws.sdk.kotlin.runtime.http.interceptors.businessmetrics.withBusinessMetric
1214
import aws.smithy.kotlin.runtime.auth.awscredentials.*
1315
import aws.smithy.kotlin.runtime.collections.Attributes
16+
import aws.smithy.kotlin.runtime.config.resolve
1417
import aws.smithy.kotlin.runtime.http.HttpStatusCode
1518
import aws.smithy.kotlin.runtime.io.IOException
1619
import aws.smithy.kotlin.runtime.serde.json.JsonDeserializer
1720
import aws.smithy.kotlin.runtime.telemetry.logging.info
21+
import aws.smithy.kotlin.runtime.telemetry.logging.warn
1822
import aws.smithy.kotlin.runtime.time.Clock
23+
import aws.smithy.kotlin.runtime.time.Instant
1924
import aws.smithy.kotlin.runtime.util.PlatformEnvironProvider
2025
import aws.smithy.kotlin.runtime.util.PlatformProvider
21-
import aws.smithy.kotlin.runtime.util.SingleFlightGroup
22-
import aws.smithy.kotlin.runtime.util.asyncLazy
26+
import kotlinx.coroutines.sync.Mutex
27+
import kotlinx.coroutines.sync.withLock
2328
import kotlin.coroutines.coroutineContext
29+
import kotlin.time.Duration.Companion.seconds
2430

31+
private const val CREDENTIALS_BASE_PATH: String = "/latest/meta-data/iam/security-credentials/"
2532
private const val CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS: String = "AssumeRoleUnauthorizedAccess"
2633
private const val PROVIDER_NAME = "IMDSv2"
2734

2835
/**
2936
* [CredentialsProvider] that uses EC2 instance metadata service (IMDS) to provide credentials information.
30-
* This provider requires that the EC2 instance has an
31-
* [instance profile](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#ec2-instance-profile)
37+
* This provider requires that the EC2 instance has an [instance profile](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#ec2-instance-profile)
3238
* configured.
33-
* @param instanceProfileName overrides the instance profile name. When set, this provider skips querying IMDS for the
34-
* name of the active profile.
35-
* @param client a preconfigured IMDS client with which to retrieve instance metadata. If an instance is passed, the
36-
* caller is responsible for closing it. If no instance is passed, a default instance is created and will be closed when
37-
* this credentials provider is closed.
38-
* @param platformProvider a platform provider used for env vars and system properties
39+
*
40+
* See [EC2 IAM Roles](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) for more
41+
* information.
42+
*
43+
* @param profileOverride override the instance profile name. When retrieving credentials, a call must first be made to
44+
* `<IMDS_BASE_URL>/latest/meta-data/iam/security-credentials/`. This returns the instance profile used. If
45+
* [profileOverride] is set, the initial call to retrieve the profile is skipped and the provided value is used instead.
46+
* @param client the IMDS client to use to resolve credentials information with. This provider takes ownership over
47+
* the lifetime of the given [ImdsClient] and will close it when the provider is closed.
48+
* @param platformProvider the [PlatformEnvironProvider] instance
3949
*/
4050
public class ImdsCredentialsProvider(
41-
instanceProfileName: String? = null,
42-
client: InstanceMetadataProvider? = null,
43-
platformProvider: PlatformProvider = PlatformProvider.System,
51+
public val profileOverride: String? = null,
52+
public val client: Lazy<InstanceMetadataProvider> = lazy { ImdsClient() },
53+
public val platformProvider: PlatformEnvironProvider = PlatformProvider.System,
54+
private val clock: Clock = Clock.System,
4455
) : CloseableCredentialsProvider {
45-
46-
@Deprecated("This constructor supports parameters which are no longer used in the implementation. It will be removed in version 1.5.")
47-
public constructor(
48-
profileOverride: String? = null,
49-
client: Lazy<InstanceMetadataProvider> = lazy { ImdsClient() },
50-
platformProvider: PlatformEnvironProvider = PlatformProvider.System,
51-
@Suppress("UNUSED_PARAMETER") clock: Clock = Clock.System,
52-
) : this(
53-
profileOverride,
54-
client.value,
55-
platformProvider = platformProvider as? PlatformProvider ?: PlatformProvider.System,
56-
)
57-
58-
private val actualPlatformProvider = platformProvider
59-
60-
@Deprecated("This property is retained for backwards compatibility but no longer needs to be public and will be removed in version 1.5.")
61-
public val platformProvider: PlatformEnvironProvider = actualPlatformProvider
62-
63-
private val manageClient: Boolean = client == null
64-
65-
private val actualClient = client ?: ImdsClient {
66-
this.platformProvider = actualPlatformProvider
67-
}
68-
69-
@Deprecated("This property is retained for backwards compatibility but no longer needs to be public and will be removed in version 1.5.")
70-
public val client: Lazy<InstanceMetadataProvider>
71-
get() = lazyOf(actualClient)
72-
73-
private val instanceProfileName = asyncLazy {
74-
instanceProfileName ?: resolveEc2InstanceProfileName(platformProvider)
75-
}
76-
77-
@Deprecated("This property is retained for backwards compatibility but no longer needs to be public and will be removed in version 1.5.")
78-
public val profileOverride: String? = instanceProfileName
79-
80-
private val providerDisabled = asyncLazy {
81-
resolveDisableEc2Metadata(platformProvider) ?: false
82-
}
83-
84-
/**
85-
* Tracks the known-good version of IMDS APIs available in the local environment. This starts as `null` and will be
86-
* updated after the first successful API call.
87-
*/
88-
private var apiVersion: ApiVersion? = null
89-
90-
private val urlBase: String
91-
get() = (apiVersion ?: ApiVersion.EXTENDED).urlBase
92-
9356
private var previousCredentials: Credentials? = null
9457

95-
/**
96-
* Tracks the instance profile name resolved from IMDS. This starts as `null` and will be updated after a
97-
* successful API call. Note that if [instanceProfileName] is set, profile name resolution will be skipped.
98-
*/
99-
private var resolvedProfileName: String? = null
100-
101-
/**
102-
* A deduplicator for resolving credentials and tracking mutable state about IMDS
103-
*/
104-
private val sfg = SingleFlightGroup<Credentials>()
58+
// the time to refresh the Credentials. If set, it will take precedence over the Credentials' expiration time
59+
private var nextRefresh: Instant? = null
10560

106-
override suspend fun resolve(attributes: Attributes): Credentials = sfg.singleFlight(::resolveSingleFlight)
61+
// protects previousCredentials and nextRefresh
62+
private val mu = Mutex()
10763

108-
private suspend fun resolveSingleFlight(): Credentials {
109-
if (providerDisabled.get()) {
64+
override suspend fun resolve(attributes: Attributes): Credentials {
65+
if (AwsSdkSetting.AwsEc2MetadataDisabled.resolve(platformProvider) == true) {
11066
throw CredentialsNotLoadedException("AWS EC2 metadata is explicitly disabled; credentials not loaded")
11167
}
11268

113-
val profileName = instanceProfileName.get() ?: resolvedProfileName ?: try {
114-
actualClient.get(urlBase).also {
115-
if (apiVersion == null) {
116-
// Tried EXTENDED and it worked; remember that for the future
117-
apiVersion = ApiVersion.EXTENDED
69+
// if we have previously served IMDS credentials and it's not time for a refresh, just return the previous credentials
70+
mu.withLock {
71+
previousCredentials?.run {
72+
nextRefresh?.takeIf { clock.now() < it }?.run {
73+
return previousCredentials!!
11874
}
11975
}
120-
} catch (ex: EC2MetadataError) {
121-
when {
122-
apiVersion == null && ex.status == HttpStatusCode.NotFound -> {
123-
// Tried EXTENDED and that didn't work; fallback to LEGACY
124-
apiVersion = ApiVersion.LEGACY
125-
return resolveSingleFlight()
126-
}
127-
128-
ex.status == HttpStatusCode.NotFound -> {
129-
coroutineContext.info<ImdsCredentialsProvider> {
130-
"Received 404 when loading profile name. This instance may not have an associated profile."
131-
}
132-
throw ex
133-
}
134-
135-
else -> return usePreviousCredentials()
136-
?: throw ImdsProfileException(ex).wrapAsCredentialsProviderException()
137-
}
138-
} catch (ex: IOException) {
139-
return usePreviousCredentials() ?: throw ImdsProfileException(ex).wrapAsCredentialsProviderException()
140-
} catch (ex: Exception) {
141-
throw ImdsProfileException(ex).wrapAsCredentialsProviderException()
14276
}
14377

144-
val credsPayload = try {
145-
actualClient.get("$urlBase$profileName")
146-
} catch (ex: EC2MetadataError) {
147-
when {
148-
apiVersion == null && ex.status == HttpStatusCode.NotFound -> {
149-
// Tried EXTENDED and that didn't work; fallback to LEGACY
150-
apiVersion = ApiVersion.LEGACY
151-
return resolveSingleFlight()
152-
}
153-
154-
instanceProfileName.get() == null && ex.status == HttpStatusCode.NotFound -> {
155-
// A previously-resolved profile is now invalid; forget the resolved name and re-resolve
156-
resolvedProfileName = null
157-
return resolveSingleFlight()
158-
}
159-
160-
else -> return usePreviousCredentials()
161-
?: throw ImdsCredentialsException(profileName, ex).wrapAsCredentialsProviderException()
162-
}
163-
} catch (ex: IOException) {
164-
return usePreviousCredentials()
165-
?: throw ImdsCredentialsException(profileName, ex).wrapAsCredentialsProviderException()
78+
val profileName = try {
79+
profileOverride ?: loadProfile()
16680
} catch (ex: Exception) {
167-
throw ImdsCredentialsException(profileName, ex).wrapAsCredentialsProviderException()
81+
return useCachedCredentials(ex) ?: throw CredentialsProviderException("failed to load instance profile", ex)
16882
}
16983

170-
if (instanceProfileName.get() == null) {
171-
// No profile name was provided at construction time; cache the resolved name
172-
resolvedProfileName = profileName
84+
val payload = try {
85+
client.value.get("$CREDENTIALS_BASE_PATH$profileName")
86+
} catch (ex: Exception) {
87+
return useCachedCredentials(ex) ?: throw CredentialsProviderException("failed to load credentials", ex)
17388
}
17489

175-
val deserializer = JsonDeserializer(credsPayload.encodeToByteArray())
90+
val deserializer = JsonDeserializer(payload.encodeToByteArray())
17691

17792
return when (val resp = deserializeJsonCredentials(deserializer)) {
17893
is JsonCredentialsResponse.SessionCredentials -> {
179-
val creds = credentials(
94+
nextRefresh = if (resp.expiration != null && resp.expiration < clock.now()) {
95+
coroutineContext.warn<ImdsCredentialsProvider> {
96+
"Attempting credential expiration extension due to a credential service availability issue. " +
97+
"A refresh of these credentials will be attempted again in " +
98+
"${ DEFAULT_CREDENTIALS_REFRESH_SECONDS / 60 } minutes."
99+
}
100+
clock.now() + DEFAULT_CREDENTIALS_REFRESH_SECONDS.seconds
101+
} else {
102+
null
103+
}
104+
105+
val creds = Credentials(
180106
resp.accessKeyId,
181107
resp.secretAccessKey,
182108
resp.sessionToken,
183109
resp.expiration,
184110
PROVIDER_NAME,
185-
resp.accountId,
186111
).withBusinessMetric(AwsBusinessMetric.Credentials.CREDENTIALS_IMDS)
187112

188-
creds.also { previousCredentials = it }
113+
creds.also {
114+
mu.withLock { previousCredentials = it }
115+
}
189116
}
190-
is JsonCredentialsResponse.Error -> when (resp.code) {
191-
CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS -> throw ProviderConfigurationException("Incorrect IMDS/IAM configuration: [${resp.code}] ${resp.message}. Hint: Does this role have a trust relationship with EC2?")
192-
else -> throw CredentialsProviderException("Error retrieving credentials from IMDS: code=${resp.code}; ${resp.message}")
117+
is JsonCredentialsResponse.Error -> {
118+
when (resp.code) {
119+
CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS -> throw ProviderConfigurationException("Incorrect IMDS/IAM configuration: [${resp.code}] ${resp.message}. Hint: Does this role have a trust relationship with EC2?")
120+
else -> throw CredentialsProviderException("Error retrieving credentials from IMDS: code=${resp.code}; ${resp.message}")
121+
}
193122
}
194123
}
195124
}
196125

197126
override fun close() {
198-
if (manageClient) {
199-
actualClient.close()
127+
if (client.isInitialized()) {
128+
client.value.close()
200129
}
201130
}
202131

203-
private suspend fun usePreviousCredentials(): Credentials? =
204-
previousCredentials?.apply {
132+
private suspend fun loadProfile() = try {
133+
client.value.get(CREDENTIALS_BASE_PATH)
134+
} catch (ex: EC2MetadataError) {
135+
if (ex.statusCode == HttpStatusCode.NotFound.value) {
205136
coroutineContext.info<ImdsCredentialsProvider> {
206-
"Attempting to reuse previously-fetched credentials (expiration = $expiration)"
137+
"Received 404 from IMDS when loading profile information. Hint: This instance may not have an " +
138+
"IAM role associated."
207139
}
208140
}
209-
210-
override fun toString(): String = this.simpleClassName
211-
212-
/**
213-
* Identifies different versions of IMDS APIs for fetching credentials
214-
*/
215-
private enum class ApiVersion(val urlBase: String) {
216-
/**
217-
* The original, now-deprecated API
218-
*/
219-
LEGACY("/latest/meta-data/iam/security-credentials/"),
220-
221-
/**
222-
* The new API which provides `AccountId` and potentially other fields in the future
223-
*/
224-
EXTENDED("/latest/meta-data/iam/security-credentials-extended/"),
141+
throw ex
225142
}
226-
}
227-
228-
internal class ImdsCredentialsException(
229-
profileName: String,
230-
cause: Throwable,
231-
) : RuntimeException("Failed to load credentials for EC2 instance profile \"$profileName\"", cause)
232143

233-
internal class ImdsProfileException(cause: Throwable) : RuntimeException("Failed to load instance profile name", cause)
144+
private suspend fun useCachedCredentials(ex: Exception): Credentials? = when {
145+
ex is IOException || ex is EC2MetadataError && ex.statusCode == HttpStatusCode.InternalServerError.value -> {
146+
mu.withLock {
147+
previousCredentials?.apply { nextRefresh = clock.now() + DEFAULT_CREDENTIALS_REFRESH_SECONDS.seconds }
148+
}
149+
}
150+
else -> null
151+
}
234152

235-
private fun Throwable.wrapAsCredentialsProviderException() =
236-
CredentialsProviderException(message.orEmpty(), this)
153+
override fun toString(): String = this.simpleClassName
154+
}

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ProfileCredentialsProvider.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,12 @@ public class ProfileCredentialsProvider @InternalSdkApi constructor(
106106
private val namedProviders = mapOf(
107107
"Environment" to EnvironmentCredentialsProvider(platformProvider::getenv),
108108
"Ec2InstanceMetadata" to ImdsCredentialsProvider(
109-
instanceProfileName = profileName,
110-
client = ImdsClient {
111-
platformProvider = this@ProfileCredentialsProvider.platformProvider
112-
engine = httpClient
109+
profileOverride = profileName,
110+
client = lazy {
111+
ImdsClient {
112+
platformProvider = this@ProfileCredentialsProvider.platformProvider
113+
engine = httpClient
114+
}
113115
},
114116
platformProvider = platformProvider,
115117
),

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/internal/CredentialsExt.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,3 @@ internal fun credentials(
2929
}
3030
return Credentials(accessKeyId, secretAccessKey, sessionToken, expiration, attributes = attributes)
3131
}
32-
33-
internal val Credentials.accountId: String?
34-
get() = attributes.getOrNull(AwsClientOption.AccountId)

0 commit comments

Comments
 (0)