-
Notifications
You must be signed in to change notification settings - Fork 55
feat(rt): ec2 credentials provider #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
73b826c
0898c5f
95b3a7c
d270995
8384158
d2351d2
61a4214
6cdf19a
0e98094
a1e6709
a3e1457
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
|
|
||
| package aws.sdk.kotlin.runtime.auth.credentials | ||
|
|
||
| import aws.sdk.kotlin.runtime.ClientException | ||
| import aws.sdk.kotlin.runtime.ConfigurationException | ||
|
|
||
| /** | ||
| * No credentials were available from this [CredentialsProvider] | ||
| */ | ||
| public class CredentialsNotLoadedException(message: String?, cause: Throwable? = null) : | ||
| ClientException(message ?: "The provider could not provide credentials or required configuration was not set", cause) | ||
|
|
||
| /** | ||
| * The [CredentialsProvider] was given an invalid configuration (e.g. invalid aws configuration file, invalid IMDS endpoint, etc) | ||
| */ | ||
| public class ProviderConfigurationException(message: String, cause: Throwable? = null) : ConfigurationException(message, cause) | ||
|
|
||
| /** | ||
| * The [CredentialsProvider] experienced an error during credentials resolution | ||
| */ | ||
| public class CredentialsProviderException(message: String, cause: Throwable? = null) : ClientException(message, cause) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
|
|
||
| package aws.sdk.kotlin.runtime.auth.credentials | ||
|
|
||
| import aws.sdk.kotlin.runtime.config.AwsSdkSetting | ||
| import aws.sdk.kotlin.runtime.config.imds.EC2MetadataError | ||
| import aws.sdk.kotlin.runtime.config.imds.ImdsClient | ||
| import aws.sdk.kotlin.runtime.config.resolve | ||
| import aws.smithy.kotlin.runtime.http.HttpStatusCode | ||
| import aws.smithy.kotlin.runtime.io.Closeable | ||
| import aws.smithy.kotlin.runtime.logging.Logger | ||
| import aws.smithy.kotlin.runtime.serde.json.JsonDeserializer | ||
| import aws.smithy.kotlin.runtime.util.Platform | ||
| import aws.smithy.kotlin.runtime.util.PlatformEnvironProvider | ||
| import aws.smithy.kotlin.runtime.util.asyncLazy | ||
|
|
||
| private const val CREDENTIALS_BASE_PATH: String = "/latest/meta-data/iam/security-credentials" | ||
| private const val CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS: String = "AssumeRoleUnauthorizedAccess" | ||
|
|
||
| /** | ||
| * [CredentialsProvider] that uses EC2 instance metadata service (IMDS) to provide credentials information. | ||
| * 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) | ||
| * configured. | ||
| * | ||
| * See [EC2 IAM Roles](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) for more | ||
| * information. | ||
| * | ||
| * @param profileOverride override the instance profile name. When retrieving credentials, a call must first be made to | ||
| * `<IMDS_BASE_URL>/latest/meta-data/iam/security-credentials`. This returns the instance profile used. If | ||
| * [profileOverride] is set, the initial call to retrieve the profile is skipped and the provided value is used instead. | ||
| * @param client the IMDS client to use to resolve credentials information with | ||
| * @param platformProvider the [PlatformEnvironProvider] instance | ||
| */ | ||
| public class ImdsCredentialsProvider( | ||
| private val profileOverride: String? = null, | ||
| private val client: Lazy<ImdsClient> = lazy { ImdsClient() }, | ||
| private val platformProvider: PlatformEnvironProvider = Platform | ||
| ) : CredentialsProvider, Closeable { | ||
| private val logger = Logger.getLogger<ImdsCredentialsProvider>() | ||
|
|
||
| private val profile = asyncLazy { | ||
| if (profileOverride != null) return@asyncLazy profileOverride | ||
| loadProfile() | ||
| } | ||
|
|
||
| override suspend fun getCredentials(): Credentials { | ||
| if (AwsSdkSetting.AwsEc2MetadataDisabled.resolve(platformProvider) == true) { | ||
| throw CredentialsNotLoadedException("AWS EC2 metadata is explicitly disabled; credentials not loaded") | ||
| } | ||
|
|
||
| val profileName = try { | ||
| profile.get() | ||
| } catch (ex: Exception) { | ||
| throw CredentialsProviderException("failed to load instance profile", ex) | ||
| } | ||
|
|
||
| val payload = client.value.get("$CREDENTIALS_BASE_PATH/$profileName") | ||
| val deserializer = JsonDeserializer(payload.encodeToByteArray()) | ||
|
|
||
| return when (val resp = deserializeJsonCredentials(deserializer)) { | ||
| is JsonCredentialsResponse.SessionCredentials -> Credentials( | ||
| resp.accessKeyId, | ||
| resp.secretAccessKey, | ||
| resp.sessionToken, | ||
| resp.expiration | ||
| ) | ||
| is JsonCredentialsResponse.Error -> { | ||
| when (resp.code) { | ||
| 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?") | ||
| else -> throw CredentialsProviderException("Error retrieving credentials from IMDS: code=${resp.code}; ${resp.message}") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun close() { | ||
| if (client.isInitialized()) { | ||
| client.value.close() | ||
| } | ||
| } | ||
|
|
||
| private suspend fun loadProfile(): String { | ||
| return try { | ||
| client.value.get(CREDENTIALS_BASE_PATH) | ||
| } catch (ex: EC2MetadataError) { | ||
| if (ex.statusCode == HttpStatusCode.NotFound.value) { | ||
| logger.info { "Received 404 from IMDS when loading profile information. Hint: This instance may not have an IAM role associated." } | ||
| } | ||
| throw ex | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
|
|
||
| package aws.sdk.kotlin.runtime.auth.credentials | ||
|
|
||
| import aws.sdk.kotlin.runtime.ClientException | ||
| import aws.smithy.kotlin.runtime.serde.* | ||
| import aws.smithy.kotlin.runtime.serde.json.JsonSerialName | ||
| import aws.smithy.kotlin.runtime.time.Instant | ||
|
|
||
| /** | ||
| * Exception thrown when credentials from response do not contain valid credentials or malformed JSON | ||
| */ | ||
| public class InvalidJsonCredentialsException(message: String, cause: Throwable? = null) : ClientException(message, cause) | ||
|
|
||
| /** | ||
| * Common response elements for multiple HTTP credential providers (e.g. IMDS and ECS) | ||
| */ | ||
| internal sealed class JsonCredentialsResponse { | ||
| /** | ||
| * Credentials that can expire | ||
| */ | ||
| data class SessionCredentials( | ||
| val accessKeyId: String, | ||
| val secretAccessKey: String, | ||
| val sessionToken: String, | ||
| val expiration: Instant, | ||
| ) : JsonCredentialsResponse() | ||
|
|
||
| // TODO - add support for static credentials | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question these will come in subsequent PR associated w/ the imds client?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not as much to do with IMDS so much as other JSON based credential responses. IMDS, STS, ECS all have the same JSON response structure (or rather are subsets of one another) |
||
| // { | ||
| // "AccessKeyId" : "MUA...", | ||
| // "SecretAccessKey" : "/7PC5om...." | ||
| // } | ||
|
|
||
| // TODO - add support for assume role credentials | ||
| // { | ||
| // // fields to construct STS client: | ||
| // "Region": "sts-region-name", | ||
| // "AccessKeyId" : "MUA...", | ||
| // "Expiration" : "2016-02-25T06:03:31Z", // optional | ||
| // "SecretAccessKey" : "/7PC5om....", | ||
| // "Token" : "AQoDY....=", // optional | ||
| // // fields controlling the STS role: | ||
| // "RoleArn": "...", // required | ||
| // "RoleSessionName": "...", // required | ||
| // // and also: DurationSeconds, ExternalId, SerialNumber, TokenCode, Policy | ||
| // ... | ||
| // } | ||
|
|
||
| /** | ||
| * Response successfully parsed as an error response | ||
| */ | ||
| data class Error(val code: String, val message: String?) : JsonCredentialsResponse() | ||
| } | ||
|
|
||
| /** | ||
| * In general, the document looks something like: | ||
| * | ||
| * ``` | ||
| * { | ||
| * "Code" : "Success", | ||
| * "LastUpdated" : "2019-05-28T18:03:09Z", | ||
| * "Type" : "AWS-HMAC", | ||
| * "AccessKeyId" : "...", | ||
| * "SecretAccessKey" : "...", | ||
| * "Token" : "...", | ||
| * "Expiration" : "2019-05-29T00:21:43Z" | ||
| * } | ||
| * ``` | ||
| */ | ||
| internal suspend fun deserializeJsonCredentials(deserializer: Deserializer): JsonCredentialsResponse { | ||
| val CODE_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("Code")) | ||
| val ACCESS_KEY_ID_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("AccessKeyId")) | ||
| val SECRET_ACCESS_KEY_ID_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("SecretAccessKey")) | ||
| val SESSION_TOKEN_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("Token")) | ||
| val EXPIRATION_DESCRIPTOR = SdkFieldDescriptor(SerialKind.Timestamp, JsonSerialName("Expiration")) | ||
| val MESSAGE_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("Message")) | ||
|
|
||
| val OBJ_DESCRIPTOR = SdkObjectDescriptor.build { | ||
| field(CODE_DESCRIPTOR) | ||
| field(ACCESS_KEY_ID_DESCRIPTOR) | ||
| field(SECRET_ACCESS_KEY_ID_DESCRIPTOR) | ||
| field(SESSION_TOKEN_DESCRIPTOR) | ||
| field(EXPIRATION_DESCRIPTOR) | ||
| field(MESSAGE_DESCRIPTOR) | ||
| } | ||
|
|
||
| var code: String? = null | ||
| var accessKeyId: String? = null | ||
| var secretAccessKey: String? = null | ||
| var sessionToken: String? = null | ||
| var expiration: Instant? = null | ||
| var message: String? = null | ||
|
|
||
| try { | ||
| deserializer.deserializeStruct(OBJ_DESCRIPTOR) { | ||
| loop@while (true) { | ||
| when (findNextFieldIndex()) { | ||
| CODE_DESCRIPTOR.index -> code = deserializeString() | ||
| ACCESS_KEY_ID_DESCRIPTOR.index -> accessKeyId = deserializeString() | ||
| SECRET_ACCESS_KEY_ID_DESCRIPTOR.index -> secretAccessKey = deserializeString() | ||
| SESSION_TOKEN_DESCRIPTOR.index -> sessionToken = deserializeString() | ||
| EXPIRATION_DESCRIPTOR.index -> expiration = Instant.fromIso8601(deserializeString()) | ||
|
|
||
| // error responses | ||
| MESSAGE_DESCRIPTOR.index -> message = deserializeString() | ||
| null -> break@loop | ||
| else -> skipValue() | ||
| } | ||
| } | ||
| } | ||
| } catch (ex: DeserializationException) { | ||
| throw InvalidJsonCredentialsException("invalid JSON credentials response", ex) | ||
| } | ||
|
|
||
| return when (code?.lowercase()) { | ||
| // IMDS does not appear to reply with `Code` missing but documentation indicates it may be possible | ||
| "success", null -> { | ||
| if (accessKeyId == null) throw InvalidJsonCredentialsException("missing field `AccessKeyId`") | ||
| if (secretAccessKey == null) throw InvalidJsonCredentialsException("missing field `SecretAccessKey`") | ||
| if (sessionToken == null) throw InvalidJsonCredentialsException("missing field `Token`") | ||
| if (expiration == null) throw InvalidJsonCredentialsException("missing field `Expiration`") | ||
| JsonCredentialsResponse.SessionCredentials(accessKeyId!!, secretAccessKey!!, sessionToken!!, expiration!!) | ||
| } | ||
| else -> JsonCredentialsResponse.Error(code!!, message) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment
it would be nice to get lazy eval without having the additional
valueaccess. I don't see a way to do it offhand but 🤷There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya you can't have properties/delegated properties be
suspendso I don't see a way around it