Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import aws.smithy.kotlin.runtime.util.PlatformEnvironProvider
*/
private const val AWS_CONTAINER_SERVICE_ENDPOINT = "http://169.254.170.2"

private const val PROVIDER_NAME = "EcsContainer"

Comment on lines +41 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Would using the class name be simpler than custom names for each provider?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provider names are actually called out in an SEP

/**
* A [CredentialsProvider] that sources credentials from a local metadata service.
*
Expand All @@ -46,13 +48,20 @@ private const val AWS_CONTAINER_SERVICE_ENDPOINT = "http://169.254.170.2"
*
* For more information on configuring ECS credentials see [IAM Roles for tasks](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html)
*
* @param platform the platform provider
* @param httpClientEngine the [HttpClientEngine] instance to use to make requests. NOTE: This engine's resources and lifetime
* are NOT managed by the provider. Caller is responsible for closing.
*
*/
public class EcsCredentialsProvider internal constructor(
private val platform: PlatformEnvironProvider,
private val httpClientEngine: HttpClientEngine
httpClientEngine: HttpClientEngine? = null
) : CredentialsProvider, Closeable {

public constructor() : this(Platform, CrtHttpEngine())
public constructor() : this(Platform)

private val manageEngine = httpClientEngine == null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style

IMO owned is more descriptive than manage as to the intent of this property.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah true, I made it manageEngine to match the corresponding parameter that sdkHttpClient uses.

I kind of expect this is going to change when we decide on our ownership direction

private val httpClientEngine = httpClientEngine ?: CrtHttpEngine()

private val retryMiddleware = run {
val tokenBucket = StandardRetryTokenBucket(StandardRetryTokenBucketOptions.Default)
Expand Down Expand Up @@ -147,9 +156,12 @@ public class EcsCredentialsProvider internal constructor(
}

override fun close() {
httpClientEngine.close()
if (manageEngine) {
httpClientEngine.close()
}
}
}

private class EcsCredentialsDeserializer : HttpDeserialize<Credentials> {
override suspend fun deserialize(context: ExecutionContext, response: HttpResponse): Credentials {
val payload = response.body.readAll() ?: throw CredentialsProviderException("HTTP credentials response did not contain a payload")
Expand All @@ -159,7 +171,8 @@ private class EcsCredentialsDeserializer : HttpDeserialize<Credentials> {
resp.accessKeyId,
resp.secretAccessKey,
resp.sessionToken,
resp.expiration
resp.expiration,
PROVIDER_NAME
)
is JsonCredentialsResponse.Error -> throw CredentialsProviderException("Error retrieving credentials from container service: code=${resp.code}; message=${resp.message}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package aws.sdk.kotlin.runtime.auth.credentials
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.smithy.kotlin.runtime.util.Platform

private const val PROVIDER_NAME = "Environment"

/**
* A [CredentialsProvider] which reads from `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`.
*/
Expand All @@ -22,5 +24,6 @@ public constructor(private val getEnv: (String) -> String?) : CredentialsProvide
accessKeyId = requireEnv(AwsSdkSetting.AwsAccessKeyId.environmentVariable),
secretAccessKey = requireEnv(AwsSdkSetting.AwsSecretAccessKey.environmentVariable),
sessionToken = getEnv(AwsSdkSetting.AwsSessionToken.environmentVariable),
providerName = PROVIDER_NAME
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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"
private const val PROVIDER_NAME = "IMDSv2"

/**
* [CredentialsProvider] that uses EC2 instance metadata service (IMDS) to provide credentials information.
Expand Down Expand Up @@ -66,7 +67,8 @@ public class ImdsCredentialsProvider(
resp.accessKeyId,
resp.secretAccessKey,
resp.sessionToken,
resp.expiration
resp.expiration,
PROVIDER_NAME
)
is JsonCredentialsResponse.Error -> {
when (resp.code) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,159 @@

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

import aws.sdk.kotlin.crt.auth.credentials.build
import aws.sdk.kotlin.runtime.auth.credentials.profile.LeafProvider
import aws.sdk.kotlin.runtime.auth.credentials.profile.ProfileChain
import aws.sdk.kotlin.runtime.auth.credentials.profile.RoleArn
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.sdk.kotlin.runtime.config.resolve
import aws.sdk.kotlin.runtime.crt.SdkDefaultIO
import aws.sdk.kotlin.runtime.config.imds.ImdsClient
import aws.sdk.kotlin.runtime.config.profile.loadAwsProfiles
import aws.sdk.kotlin.runtime.config.profile.resolveConfigSource
import aws.sdk.kotlin.runtime.region.resolveRegion
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
import aws.smithy.kotlin.runtime.io.Closeable
import aws.smithy.kotlin.runtime.logging.Logger
import aws.smithy.kotlin.runtime.time.TimestampFormat
import aws.smithy.kotlin.runtime.util.Platform
import aws.sdk.kotlin.crt.auth.credentials.ProfileCredentialsProvider as ProfileCredentialsProviderCrt
import aws.smithy.kotlin.runtime.util.PlatformProvider

/**
* A provider that gets credentials from a profile.
* @param profileName The name of the profile to use (or `"default"` if none is specified).
* @param configFileName The name of the config file to use. If none is specified, the default is `".aws/config"` on
* Linux/Mac and`"%USERPROFILE%\.aws\config"` on Windows.
* @param credentialsFileName The name of the credentials file to use. If none is specified, the default is
* `".aws/credentials"` on Linux/Mac and `"%USERPROFILE%\.aws\credentials"` on Windows.
* A [CredentialsProvider] that gets credentials from a profile in `~/.aws/config` or the shared credentials
* file `~/.aws/credentials`. The locations of these files are configurable via environment or system property on
* the JVM (see [AwsSdkSetting.AwsConfigFile] and [AwsSdkSetting.AwsSharedCredentialsFile]).
*
* This provider is part of the [DefaultChainCredentialsProvider] and usually consumed through that provider. However,
* it can be instantiated and used standalone as well.
*
* NOTE: This provider does not implement any caching. It will reload and reparse the profile from the file system
* when called. Use [CachedCredentialsProvider] to decorate the profile provider to get caching behavior.
*
* This provider supports several credentials formats:
*
* ### Credentials defined explicitly within the file
* ```ini
* [default]
* aws_access_key_id = my-access-key
* aws_secret_access_key = my-secret
* ```
*
* ### Assumed role credentials loaded from a credential source
* ```ini
* [default]
* role_arn = arn:aws:iam:123456789:role/RoleA
* credential_source = Environment
* ```
*
* ### Assumed role credentials from a source profile
* ```ini
* [default]
* role_arn = arn:aws:iam:123456789:role/RoleA
* source_profile = base
*
* [profile base]
* aws_access_key_id = my-access-key
* aws_secret_access_key = my-secret
* ```
*
* Other more complex configurations are possible. See the [Configuration and credential file settings](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)
* documentation provided by the AWS CLI.
*
* @param profileName Override the profile name to use. If not provided it will be resolved internally
* via environment (see [AwsSdkSetting.AwsProfile]) or defaulted to `default` if not configured.
* @param region The AWS region to use, this will be resolved internally if not provided.
* @param platform The platform API provider
* @param httpClientEngine the [HttpClientEngine] instance to use to make requests. NOTE: This engine's resources and lifetime
* are NOT managed by the provider. Caller is responsible for closing.
*/
public class ProfileCredentialsProvider public constructor(
profileName: String? = null,
configFileName: String? = null,
credentialsFileName: String? = null,
) : CrtCredentialsProvider {
override val crtProvider: ProfileCredentialsProviderCrt = ProfileCredentialsProviderCrt.build {
clientBootstrap = SdkDefaultIO.ClientBootstrap
tlsContext = SdkDefaultIO.TlsContext
this.profileName = profileName ?: AwsSdkSetting.AwsProfile.resolve(Platform)
this.configFileName = configFileName
this.credentialsFileName = credentialsFileName
public class ProfileCredentialsProvider(
private val profileName: String? = null,
private val region: String? = null,
private val platform: PlatformProvider = Platform,
private val httpClientEngine: HttpClientEngine? = null,
) : CredentialsProvider, Closeable {

private val namedProviders = mapOf(
"Environment" to EnvironmentCredentialsProvider(platform::getenv),
"Ec2InstanceMetadata" to ImdsCredentialsProvider(
profileOverride = profileName,
client = lazy {
ImdsClient {
platformProvider = platform
engine = httpClientEngine
}
},
platformProvider = platform
),
"EcsContainer" to EcsCredentialsProvider(platform, httpClientEngine)
)

override suspend fun getCredentials(): Credentials {
val logger = Logger.getLogger<ProfileCredentialsProvider>()
val source = resolveConfigSource(platform, profileName)
logger.debug { "Loading credentials from profile `${source.profile}`" }
val profiles = loadAwsProfiles(platform, source)
val chain = ProfileChain.resolve(profiles, source.profile)

// if profile is overridden for this provider, attempt to resolve it from there first
val profileOverride = profileName?.let { profiles[it] }
val region = region ?: profileOverride?.get("region") ?: resolveRegion(platform)

val leaf = chain.leaf.toCredentialsProvider(region)
logger.debug { "Resolving credentials from ${chain.leaf.description()}" }
var creds = leaf.getCredentials()

chain.roles.forEach { roleArn ->
logger.debug { "Assuming role `${roleArn.roleArn}`" }
val assumeProvider = roleArn.toCredentialsProvider(creds, region)
creds = assumeProvider.getCredentials()
}

logger.debug { "Obtained credentials from profile; expiration=${creds.expiration?.format(TimestampFormat.ISO_8601)}" }
return creds
}

override fun close() {
namedProviders.forEach { entry ->
(entry.value as? Closeable)?.close()
}
}

private fun LeafProvider.toCredentialsProvider(region: String): CredentialsProvider = when (this) {
is LeafProvider.NamedSource -> namedProviders[name] ?: throw ProviderConfigurationException("unknown credentials source: $name")
is LeafProvider.AccessKey -> StaticCredentialsProvider(credentials)
is LeafProvider.WebIdentityTokenRole -> StsWebIdentityCredentialsProvider(
roleArn,
webIdentityTokenFile,
region = region,
roleSessionName = sessionName,
platformProvider = platform,
httpClientEngine = httpClientEngine
)
is LeafProvider.Sso -> SsoCredentialsProvider(
accountId = ssoAccountId,
roleName = ssoRoleName,
startUrl = ssoStartUrl,
ssoRegion = ssoRegion,
httpClientEngine = httpClientEngine,
platformProvider = platform
)
}

private fun RoleArn.toCredentialsProvider(
creds: Credentials,
region: String
): CredentialsProvider = StsAssumeRoleCredentialsProvider(
credentialsProvider = StaticCredentialsProvider(creds),
roleArn = roleArn,
region = region,
roleSessionName = sessionName,
externalId = externalId,
httpClientEngine = httpClientEngine
)

private fun LeafProvider.description(): String = when (this) {
is LeafProvider.NamedSource -> "named source $name"
is LeafProvider.AccessKey -> "static credentials"
is LeafProvider.WebIdentityTokenRole -> "web identity token"
is LeafProvider.Sso -> "single sign-on"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import aws.smithy.kotlin.runtime.time.Clock
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.util.*

private const val PROVIDER_NAME = "SSO"

/**
* [CredentialsProvider] that uses AWS Single Sign-On (AWS SSO) to source credentials. The
* provider is expected to be configured for the AWS Region where the AWS SSO user portal is hosted.
Expand Down Expand Up @@ -114,7 +116,8 @@ public class SsoCredentialsProvider public constructor(
accessKeyId = checkNotNull(roleCredentials.accessKeyId) { "Expected accessKeyId in SSO roleCredentials response" },
secretAccessKey = checkNotNull(roleCredentials.secretAccessKey) { "Expected secretAccessKey in SSO roleCredentials response" },
sessionToken = roleCredentials.sessionToken,
expiration = Instant.fromEpochSeconds(roleCredentials.expiration)
expiration = Instant.fromEpochSeconds(roleCredentials.expiration),
PROVIDER_NAME
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ public class StsAssumeRoleCredentialsProvider(
accessKeyId = checkNotNull(roleCredentials.accessKeyId) { "Expected accessKeyId in STS assumeRole response" },
secretAccessKey = checkNotNull(roleCredentials.secretAccessKey) { "Expected secretAccessKey in STS assumeRole response" },
sessionToken = roleCredentials.sessionToken,
expiration = roleCredentials.expiration
expiration = roleCredentials.expiration,
providerName = PROVIDER_NAME
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ public class StsWebIdentityCredentialsProvider(
accessKeyId = checkNotNull(roleCredentials.accessKeyId) { "Expected accessKeyId in STS assumeRoleWithWebIdentity response" },
secretAccessKey = checkNotNull(roleCredentials.secretAccessKey) { "Expected secretAccessKey in STS assumeRoleWithWebIdentity response" },
sessionToken = roleCredentials.sessionToken,
expiration = roleCredentials.expiration
expiration = roleCredentials.expiration,
providerName = PROVIDER_NAME
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

package aws.sdk.kotlin.runtime.auth.credentials.profile

import aws.sdk.kotlin.runtime.auth.credentials.Credentials

/**
* A standalone member of the profile chain. Leaf providers do not require
* input credentials to provide their own credentials (e.g. IMDS, ECS, Environment, etc)
*/
internal sealed class LeafProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question

it's not connecting for me, what meaning does 'leaf' mean for this type? Is it that it's terminal, as in a leaf in a tree?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah the provider that doesn't depend on any other providers. In the Rust implementation they called it BaseProvider. I didn't find that descriptive enough for my tastes.


/**
* A profile that specifies a named credentials source
* e.g. `credential_source = Ec2InstanceMetadata`
*/
data class NamedSource(val name: String) : LeafProvider()

/**
* A profile with explicitly configured access keys
*
* Example
* ```ini
* [profile C]
* aws_access_key_id = AKID
* aws_secret_access_key = secret
* ```
*/
data class AccessKey(val credentials: Credentials) : LeafProvider()

/**
* A provider that uses OIDC web identity tokens
*
* Example
* ```ini
* [profile W]
* role_arn = arn:aws:iam:123456789:role/example
* web_identity_token_file = /path/to/token.jwt
* ```
*/
data class WebIdentityTokenRole(
val roleArn: String,
val webIdentityTokenFile: String,
val sessionName: String? = null
) : LeafProvider()

/**
* A provider that uses AWS SSO
*
* Example
* ```ini
* [profile W]
* sso_start_url = https://my-sso-portal.awsapps.com/start
* sso_region = us-east-1
* sso_account_id = 123456789011
* sso_role_name = readOnly
* region = us-west-2
* ```
*/
data class Sso(
val ssoStartUrl: String,
val ssoRegion: String,
val ssoAccountId: String,
val ssoRoleName: String
) : LeafProvider()
}
Loading