Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

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

import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.sdk.kotlin.runtime.config.imds.ImdsClient
import aws.sdk.kotlin.runtime.http.engine.crt.CrtHttpEngine
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
Expand All @@ -13,8 +14,6 @@ import aws.smithy.kotlin.runtime.util.Platform
import aws.smithy.kotlin.runtime.util.PlatformProvider
import kotlin.time.ExperimentalTime

// TODO - allow region, profile, etc to be passed in

/**
* Default AWS credential provider chain used by most AWS SDKs.
*
Expand All @@ -30,21 +29,25 @@ import kotlin.time.ExperimentalTime
*
* Closing the chain will close all child providers that implement [Closeable].
*
* @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 platformProvider 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.
* @return the newly-constructed credentials provider
*/
public class DefaultChainCredentialsProvider internal constructor(
public class DefaultChainCredentialsProvider constructor(
private val profileName: String? = null,
private val platformProvider: PlatformProvider = Platform,
httpClientEngine: HttpClientEngine? = null
) : CredentialsProvider, Closeable {

public constructor() : this(Platform)

private val manageEngine = httpClientEngine == null
private val httpClientEngine = httpClientEngine ?: CrtHttpEngine()

private val chain = CredentialsProviderChain(
EnvironmentCredentialsProvider(platformProvider::getenv),
ProfileCredentialsProvider(platformProvider = platformProvider, httpClientEngine = httpClientEngine),
ProfileCredentialsProvider(profileName = profileName, platformProvider = platformProvider, httpClientEngine = httpClientEngine),
// STS web identity provider can be constructed from either the profile OR 100% from the environment
StsWebIdentityProvider(platformProvider = platformProvider, httpClientEngine = httpClientEngine),
EcsCredentialsProvider(platformProvider, httpClientEngine),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

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

import aws.sdk.kotlin.runtime.InternalSdkApi
import aws.sdk.kotlin.runtime.auth.credentials.CredentialsProvider
import aws.smithy.kotlin.runtime.io.Closeable

private class BorrowedCredentialsProvider(
private val borrowed: CredentialsProvider
) : CredentialsProvider by borrowed, Closeable {
override fun close() { }
}

/**
* Wraps another [CredentialsProvider] with a no-op close implementation. This inserts a level of indirection for
* use cases when a provider is explicitly given to the SDK, and its ownership should remain with the caller.
* This allows the SDK to treat resources as owned and not have to track ownership state.
*/
@InternalSdkApi
public fun CredentialsProvider.borrow(): CredentialsProvider = when (this) {
is BorrowedCredentialsProvider -> this
else -> BorrowedCredentialsProvider(this)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
package aws.sdk.kotlin.runtime.region

import aws.sdk.kotlin.runtime.ConfigurationException
import aws.sdk.kotlin.runtime.InternalSdkApi
import aws.smithy.kotlin.runtime.io.use
import aws.smithy.kotlin.runtime.util.Platform
import aws.smithy.kotlin.runtime.util.PlatformProvider

/**
* Attempt to resolve the region to make requests to.
* Attempt to resolve the region to make requests to, throws [ConfigurationException] if region could not be
* resolved.
*/
internal suspend fun resolveRegion(platformProvider: PlatformProvider): String =
@InternalSdkApi
public suspend fun resolveRegion(
platformProvider: PlatformProvider = Platform
): String =
DefaultRegionProviderChain(platformProvider).use { providerChain ->
providerChain.getRegion() ?: throw ConfigurationException("unable to auto detect AWS region, tried: $providerChain")
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,18 @@ object AwsRuntimeTypes {
object Types {
val CredentialsProvider = runtimeSymbol("CredentialsProvider", AwsKotlinDependency.AWS_TYPES, "auth.credentials")
val Credentials = runtimeSymbol("Credentials", AwsKotlinDependency.AWS_TYPES, "auth.credentials")
val AwsClientConfig = runtimeSymbol("AwsClientConfig", AwsKotlinDependency.AWS_TYPES, "client")
}

object Config {
object Credentials {
val DefaultChainCredentialsProvider = runtimeSymbol("DefaultChainCredentialsProvider", AwsKotlinDependency.AWS_CONFIG, "auth.credentials")
val StaticCredentialsProvider = runtimeSymbol("StaticCredentialsProvider", AwsKotlinDependency.AWS_CONFIG, "auth.credentials")
val borrow = runtimeSymbol("borrow", AwsKotlinDependency.AWS_CONFIG, "auth.credentials.internal")
}

val AwsClientConfigLoadOptions = runtimeSymbol("AwsClientConfigLoadOptions", AwsKotlinDependency.AWS_CONFIG, "config")
val fromEnvironment = runtimeSymbol("fromEnvironment", AwsKotlinDependency.AWS_CONFIG, "config")
object Region {
val resolveRegion = runtimeSymbol("resolveRegion", AwsKotlinDependency.AWS_CONFIG, "region")
}
}

object Signing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class AwsServiceConfigIntegration : KotlinIntegration {
val RegionProp: ClientConfigProperty = ClientConfigProperty {
name = "region"
symbol = KotlinTypes.String.toBuilder().boxed().build()
baseClass = AwsRuntimeTypes.Types.AwsClientConfig
documentation = """
AWS region to make requests to
""".trimIndent()
Expand All @@ -30,15 +29,26 @@ class AwsServiceConfigIntegration : KotlinIntegration {

val CredentialsProviderProp: ClientConfigProperty = ClientConfigProperty {
symbol = AwsRuntimeTypes.Types.CredentialsProvider
baseClass = AwsRuntimeTypes.Types.AwsClientConfig
documentation = """
The AWS credentials provider to use for authenticating requests. If not provided a
[${symbol?.namespace}.DefaultChainCredentialsProvider] instance will be used.
NOTE: The caller is responsible for managing the lifetime of the provider when set. The SDK
client will not close it when the client is closed.
""".trimIndent()

val defaultProvider = AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider
propertyType = ClientConfigPropertyType.RequiredWithDefault("${defaultProvider.name}()")
additionalImports = listOf(defaultProvider)
propertyType = ClientConfigPropertyType.Custom(render = { prop, writer ->
writer.write(
"val #1L: #2T = builder.#1L?.borrow() ?: #3T()",
prop.propertyName,
prop.symbol,
AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider
)
})

additionalImports = listOf(
AwsRuntimeTypes.Config.Credentials.borrow,
AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider
)
}
}

Expand All @@ -47,50 +57,33 @@ class AwsServiceConfigIntegration : KotlinIntegration {
val serviceSymbol: Symbol = writer.getContextValue(ServiceGenerator.SectionServiceCompanionObject.ServiceSymbol)
writer.withBlock("companion object {", "}") {
withBlock(
"operator fun invoke(sharedConfig: #T? = null, block: Config.Builder.() -> Unit = {}): #L {",
"operator fun invoke(block: Config.Builder.() -> Unit): #L {",
"}",
AwsRuntimeTypes.Types.AwsClientConfig,
serviceSymbol.name
) {
withBlock(
"val config = Config.Builder().apply { ",
"}.apply(block).build()"
) {
write("region = sharedConfig?.region")
write("credentialsProvider = sharedConfig?.credentialsProvider")
write("sdkLogMode = sharedConfig?.sdkLogMode ?: SdkLogMode.Default")
}
write("val config = Config.Builder().apply(block).build()")
write("return Default${serviceSymbol.name}(config)")
}

write("")
write("operator fun invoke(config: Config): ${serviceSymbol.name} = Default${serviceSymbol.name}(config)")

// generate a convenience init to resolve a client from the current environment
listOf(
AwsRuntimeTypes.Types.AwsClientConfig,
AwsRuntimeTypes.Config.AwsClientConfigLoadOptions,
AwsRuntimeTypes.Config.fromEnvironment
).forEach(writer::addImport)

write("")
dokka {
write("Construct a [${serviceSymbol.name}] by resolving the configuration from the current environment.")
write("NOTE: If you are using multiple AWS service clients you may wish to share the configuration among them")
write("by constructing a [#Q] and passing it to each client at construction.", AwsRuntimeTypes.Types.AwsClientConfig)
}
writer.withBlock(
"suspend fun fromEnvironment(block: #1T.() -> Unit = {}): #2T {",
"suspend fun fromEnvironment(block: (Config.Builder.() -> Unit)? = null): #T {",
"}",
AwsRuntimeTypes.Config.AwsClientConfigLoadOptions,
serviceSymbol
) {
write(
"val sharedConfig = #T.#T(block)",
AwsRuntimeTypes.Types.AwsClientConfig,
AwsRuntimeTypes.Config.fromEnvironment
)
write("return #T(sharedConfig)", serviceSymbol)
write("val builder = Config.Builder()")
write("if (block != null) builder.apply(block)")

addImport(AwsRuntimeTypes.Config.Region.resolveRegion)
write("builder.region = builder.region ?: #T()", AwsRuntimeTypes.Config.Region.resolveRegion)
write("return Default${serviceSymbol.name}(builder.build())")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ class PresignerGenerator : KotlinIntegration {
name = "credentialsProvider"
documentation = "The AWS credentials provider to use for authenticating requests. If not provided a [aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider] instance will be used."
baseClass = AwsRuntimeTypes.Signing.ServicePresignConfig
propertyType = ClientConfigPropertyType.RequiredWithDefault("DefaultChainCredentialsProvider()")
// TODO - we could probably relax this and instead create a default chain in createPresignedRequest on-demand in the runtime and close it when done signing
propertyType = ClientConfigPropertyType.Required()
},
ClientConfigProperty {
symbol = AwsRuntimeTypes.Endpoint.AwsEndpointResolver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ open class AwsHttpProtocolClientGenerator(
}
}

override fun renderClose(writer: KotlinWriter) {
writer.addImport(RuntimeTypes.IO.Closeable)
writer.write("")
.openBlock("override fun close() {")
.write("client.close()")
.write("(config.credentialsProvider as? #T)?.close()", RuntimeTypes.IO.Closeable)
Copy link
Contributor

Choose a reason for hiding this comment

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

question

I'm missing something regarding Closable and CredentialsProvider types. The latter doesn't specify a close method, but the WrappedCredentialsProvider does combine the two, and looks here that we check to see if the cp implements closable and then call it if so. Given this would it not be simpler to have CP simply implement Closable? Put another way, what value are we gaining by having only BorrowedCredentialsProvider implement Closable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what value are we gaining by having only BorrowedCredentialsProvider implement Closable

It isn't the only one that implements Closeable. All of the following do:

  • DefaultChainCredentialsProvider
  • EcsCredentialsProvider
  • ImdsCredentialsProvider
  • ProfileCredentialsProvider

Environment, StsWebIdentity, StsAssumeRole, and SSO providers do not implement Closeable.

The issue is that the type is just CredentialsProvider both here and in BorrowedCredentialsProvider so both have to check if the underlying implementation actually needs to be closed or not. I have gone back and forth on this and considered making CredentialsProvider implement Closeable but ultimately decided against it to be more clear about what types do and don't need to be closed when used directly by a customer. I could be persuaded the other way though if you want to make a case for it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I'm not sure what's best either way, but it may be more satisfying of "We ensure accessing AWS services is performant, secure, and reliable for developers" if CredentialProvider implements Closable. I reckon this may be so because if closability varies, intuitively it seems likely that someone may forget that a particular CP needs to be closed. Or perhaps a function that takes a non closing CP is passed a requires-closing CP during a refactor but the nuance is dropped in the refactor. If all CPs must be closed (with some of them being NOP), it seems to be less likely that this kind of bug would occur. This comes at the expense of greater overall complexity of the CP type, however the value may outweigh the cost. @ianbotsf any thoughts on this?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm in favor of explicitly modelling closability. If it can/must be closed, it should be Closeable. Otherwise, I don't think we should unnecessarily attach closing semantics to the CredentialProvider interface because some of the implementations are closeable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya I can see both sides here and I hear the concern. I'm going to leave it as is for now and lets see how it shakes out.

.closeBlock("}")
.write("")
}
Comment on lines +98 to +106
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment: Would be nice to have a new test for this.


private fun renderInternals() {
val endpointsData = javaClass.classLoader.getResource("aws/sdk/kotlin/codegen/endpoints.json")?.readText() ?: throw CodegenException("could not load endpoints.json resource")
val endpointData = Node.parse(endpointsData).expectObjectNode()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ class AwsServiceConfigIntegrationTest {
val contents = writer.toString()

val expectedProps = """
override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider()
val credentialsProvider: CredentialsProvider = builder.credentialsProvider?.borrow() ?: DefaultChainCredentialsProvider()
val endpointResolver: AwsEndpointResolver = builder.endpointResolver ?: DefaultEndpointResolver()
override val region: String = requireNotNull(builder.region) { "region is a required configuration property" }
val region: String = requireNotNull(builder.region) { "region is a required configuration property" }
"""
contents.shouldContainOnlyOnceWithDiff(expectedProps)

val expectedImpl = """
/**
* The AWS credentials provider to use for authenticating requests. If not provided a
* [aws.sdk.kotlin.runtime.auth.credentials.DefaultChainCredentialsProvider] instance will be used.
* NOTE: The caller is responsible for managing the lifetime of the provider when set. The SDK
* client will not close it when the client is closed.
*/
var credentialsProvider: CredentialsProvider? = null
/**
Expand Down
Loading