Skip to content
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

feat: account id routing #1111

Merged
merged 23 commits into from
Nov 17, 2023
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
6 changes: 6 additions & 0 deletions .changes/873fb1ab-bfbb-4110-89cd-bf7ac352bc86.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "873fb1ab-bfbb-4110-89cd-bf7ac352bc86",
"type": "feature",
"description": "⚠️ **IMPORTANT**: Enable account ID based endpoint routing for services that support it",
"requiresMinorVersionBump": true
}
5 changes: 5 additions & 0 deletions .changes/98b36000-c7c2-43e1-8bfe-ec7e93317f3a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "98b36000-c7c2-43e1-8bfe-ec7e93317f3a",
"type": "misc",
"description": "Upgrade dependencies to their latest versions, notably Kotlin 1.9.20"
}
14 changes: 11 additions & 3 deletions aws-runtime/aws-config/api/aws-config.api
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,11 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProv
public fun <init> ()V
public final fun build ()Laws/sdk/kotlin/runtime/auth/credentials/StaticCredentialsProvider;
public final fun getAccessKeyId ()Ljava/lang/String;
public final fun getAccountId ()Ljava/lang/String;
public final fun getSecretAccessKey ()Ljava/lang/String;
public final fun getSessionToken ()Ljava/lang/String;
public final fun setAccessKeyId (Ljava/lang/String;)V
public final fun setAccountId (Ljava/lang/String;)V
public final fun setSecretAccessKey (Ljava/lang/String;)V
public final fun setSessionToken (Ljava/lang/String;)V
}
Expand Down Expand Up @@ -209,7 +211,7 @@ public final class aws/sdk/kotlin/runtime/auth/credentials/internal/ManagedCrede

public abstract class aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory : aws/smithy/kotlin/runtime/client/SdkClientFactory {
public fun <init> ()V
protected fun finalizeConfig (Laws/smithy/kotlin/runtime/client/SdkClient$Builder;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
protected fun finalizeConfig (Laws/smithy/kotlin/runtime/client/SdkClient$Builder;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun fromEnvironment (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun fromEnvironment$default (Laws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/client/SdkClient;
Expand All @@ -218,10 +220,16 @@ public abstract class aws/sdk/kotlin/runtime/config/AbstractAwsSdkClientFactory
public final class aws/sdk/kotlin/runtime/config/AwsSdkSettingKt {
}

public final class aws/sdk/kotlin/runtime/config/endpoints/EndpointsKt {
public final class aws/sdk/kotlin/runtime/config/endpoints/AccountIdEndpointMode : java/lang/Enum {
public static final field DISABLED Laws/sdk/kotlin/runtime/config/endpoints/AccountIdEndpointMode;
public static final field PREFERRED Laws/sdk/kotlin/runtime/config/endpoints/AccountIdEndpointMode;
public static final field REQUIRED Laws/sdk/kotlin/runtime/config/endpoints/AccountIdEndpointMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/runtime/config/endpoints/AccountIdEndpointMode;
public static fun values ()[Laws/sdk/kotlin/runtime/config/endpoints/AccountIdEndpointMode;
}

public final class aws/sdk/kotlin/runtime/config/endpoints/ResolveEndpointUrlKt {
public final class aws/sdk/kotlin/runtime/config/endpoints/ResolversKt {
}

public final class aws/sdk/kotlin/runtime/config/imds/EC2MetadataError : aws/sdk/kotlin/runtime/AwsServiceException {
Expand Down
114 changes: 114 additions & 0 deletions aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/arns/Arn.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.runtime.arns

private const val ARN_COMPONENT_COUNT = 6

/**
* Represents an [Amazon Resource Name (ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html).
*
* The following Arn formats are supported:
* * `arn:partition:service:region:account-id:resource-id`
* * `arn:partition:service:region:account-id:resource-type/resource-id`
* * `arn:partition:service:region:account-id:resource-type:resource-id`
* * `arn:partition:service:region:account-id:resource-type:resource-id:qualifier`
* * `arn:partition:service:region:account-id:resource-type:resource-id/qualifier`
*
* The exact format of an ARN depends on the service and resource type. Some resource ARNs can include a path or
* wildcard. To look up the ARN format for a specific AWS resource, open the
* [Service Authorization Reference](https://docs.aws.amazon.com/service-authorization/latest/reference/),
* open the page for the service, and navigate to the resource types table.
*/
internal class Arn(
public val partition: String,
public val service: String,
public val region: String?,
public val accountId: String?,
public val resource: String,
) {
public companion object {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Answered elsewhere


public inline operator fun invoke(block: Builder.() -> Unit): Arn = Builder().apply(block).build()

/**
* Parse a string into an [Arn]
*/
public fun parse(arn: String): Arn {
val parts = arn.split(':', limit = ARN_COMPONENT_COUNT)
require(parts.size == ARN_COMPONENT_COUNT) { "Malformed ARN ($arn) does not have the expected number of components" }
require(parts[0] == "arn") { "Malformed ARN - does not start with `arn:`" }
require(parts[1].isNotBlank()) { "Malformed ARN - no AWS partition specified" }
require(parts[2].isNotBlank()) { "Malformed ARN - no AWS service specified" }

return Arn {
partition = parts[1]
service = parts[2]
region = parts[3].takeIf(String::isNotBlank)
accountId = parts[4].takeIf(String::isNotBlank)
resource = parts[5]
}
}
}

internal constructor(builder: Builder) : this(
builder.partition!!,
builder.service!!,
builder.region,
builder.accountId,
builder.resource!!,
)

init {
require(region == null || region.isNotBlank()) { "ARN region must not be blank" }
require(accountId == null || accountId.isNotBlank()) { "ARN accountId must not be blank" }
}

override fun toString(): String = buildString {
append("arn:$partition:$service:")
if (region != null) {
append(region)
}
append(":")
if (accountId != null) {
append(accountId)
}
append(":$resource")
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Arn) return false
if (partition != other.partition) return false
if (service != other.service) return false
if (region != other.region) return false
if (accountId != other.accountId) return false
return resource == other.resource
}

override fun hashCode(): Int {
var result = partition.hashCode()
result = 31 * result + service.hashCode()
result = 31 * result + (region?.hashCode() ?: 0)
result = 31 * result + (accountId?.hashCode() ?: 0)
result = 31 * result + resource.hashCode()
return result
}
Comment on lines +80 to +97
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: If this is an internal class, it could be a data class to avoid writing/maintaining these.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could be, I didn't refactor this when moving from public to internal though and didn't see a reason to.


public class Builder {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why public?

Copy link
Contributor Author

@aajtodd aajtodd Nov 15, 2023

Choose a reason for hiding this comment

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

It was originally public. My understanding is that any client who sees the declaring class sees its public members. Because the outer class visibility is internal though this is only visible wherever the outer class is. The binary diff tooling seems to agree.

public var partition: String? = null
public var service: String? = null
public var region: String? = null
public var accountId: String? = null
public var resource: String? = null

@PublishedApi
internal fun build(): Arn {
Comment on lines +106 to +107
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Does @PublishedApi have any effect on members of inner classes of internal classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

N, this was originally all public so I left it as is.

require(!partition.isNullOrBlank()) { "ARN partition must not be null or blank" }
require(!service.isNullOrBlank()) { "ARN service must not be null or blank" }
requireNotNull(resource) { "ARN resource must not be null" }
return Arn(this)
}
Comment on lines +106 to +112
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why validate some of the fields in the builder and the other fields in the Arn class itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because it has a constructor that can be used independent of the builder. The builder itself has nullable fields but the constructor doesn't. We only validate the things the builder needs to be responsible for here and elsewhere.

}
}
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.auth.credentials.internal.credentials
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.sdk.kotlin.runtime.config.AwsSdkSetting.AwsContainerCredentialsRelativeUri
import aws.smithy.kotlin.runtime.ErrorMetadata
Expand Down Expand Up @@ -188,12 +189,13 @@ private class EcsCredentialsDeserializer : HttpDeserialize<Credentials> {
throw CredentialsProviderException("HTTP credentials response was not of expected format")
}

return Credentials(
return credentials(
resp.accessKeyId,
resp.secretAccessKey,
resp.sessionToken,
resp.expiration,
PROVIDER_NAME,
resp.accountId,
)
}
}
Expand Down
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.auth.credentials.internal.credentials
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
Expand All @@ -18,6 +19,7 @@ private const val PROVIDER_NAME = "Environment"
private val ACCESS_KEY_ID = AwsSdkSetting.AwsAccessKeyId.envVar
private val SECRET_ACCESS_KEY = AwsSdkSetting.AwsSecretAccessKey.envVar
private val SESSION_TOKEN = AwsSdkSetting.AwsSessionToken.envVar
private val ACCOUNT_ID = AwsSdkSetting.AwsAccountId.envVar

/**
* A [CredentialsProvider] which reads from `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`.
Expand All @@ -33,11 +35,12 @@ public class EnvironmentCredentialsProvider(
coroutineContext.trace<EnvironmentCredentialsProvider> {
"Attempting to load credentials from env vars $ACCESS_KEY_ID/$SECRET_ACCESS_KEY/$SESSION_TOKEN"
}
return Credentials(
return credentials(
accessKeyId = requireEnv(ACCESS_KEY_ID),
secretAccessKey = requireEnv(SECRET_ACCESS_KEY),
sessionToken = getEnv(SESSION_TOKEN),
providerName = PROVIDER_NAME,
accountId = getEnv(ACCOUNT_ID),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal sealed class JsonCredentialsResponse {
val secretAccessKey: String,
val sessionToken: String,
val expiration: Instant?,
val accountId: String? = null,
) : JsonCredentialsResponse()

// TODO - add support for static credentials
Expand Down Expand Up @@ -78,6 +79,7 @@ internal suspend fun deserializeJsonCredentials(deserializer: Deserializer): Jso
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 ACCOUNT_ID_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("AccountId"))
val MESSAGE_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("Message"))

val OBJ_DESCRIPTOR = SdkObjectDescriptor.build {
Expand All @@ -86,6 +88,7 @@ internal suspend fun deserializeJsonCredentials(deserializer: Deserializer): Jso
field(SECRET_ACCESS_KEY_ID_DESCRIPTOR)
field(SESSION_TOKEN_DESCRIPTOR)
field(EXPIRATION_DESCRIPTOR)
field(ACCOUNT_ID_DESCRIPTOR)
field(MESSAGE_DESCRIPTOR)
}

Expand All @@ -95,6 +98,7 @@ internal suspend fun deserializeJsonCredentials(deserializer: Deserializer): Jso
var sessionToken: String? = null
var expiration: Instant? = null
var message: String? = null
var accountId: String? = null

try {
deserializer.deserializeStruct(OBJ_DESCRIPTOR) {
Expand All @@ -105,6 +109,7 @@ internal suspend fun deserializeJsonCredentials(deserializer: Deserializer): Jso
SECRET_ACCESS_KEY_ID_DESCRIPTOR.index -> secretAccessKey = deserializeString()
SESSION_TOKEN_DESCRIPTOR.index -> sessionToken = deserializeString()
EXPIRATION_DESCRIPTOR.index -> expiration = Instant.fromIso8601(deserializeString())
ACCOUNT_ID_DESCRIPTOR.index -> accountId = deserializeString()

// error responses
MESSAGE_DESCRIPTOR.index -> message = deserializeString()
Expand All @@ -124,7 +129,7 @@ internal suspend fun deserializeJsonCredentials(deserializer: Deserializer): Jso
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!!)
JsonCredentialsResponse.SessionCredentials(accessKeyId!!, secretAccessKey!!, sessionToken!!, expiration!!, accountId)
}
else -> JsonCredentialsResponse.Error(code, message)
}
Expand All @@ -141,20 +146,23 @@ internal fun deserializeJsonProcessCredentials(deserializer: Deserializer): Json
val SESSION_TOKEN_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("SessionToken"))
val EXPIRATION_DESCRIPTOR = SdkFieldDescriptor(SerialKind.Timestamp, JsonSerialName("Expiration"))
val VERSION_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("Version"))
val ACCOUNT_ID_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, JsonSerialName("AccountId"))

val OBJ_DESCRIPTOR = SdkObjectDescriptor.build {
field(ACCESS_KEY_ID_DESCRIPTOR)
field(SECRET_ACCESS_KEY_ID_DESCRIPTOR)
field(SESSION_TOKEN_DESCRIPTOR)
field(EXPIRATION_DESCRIPTOR)
field(VERSION_DESCRIPTOR)
field(ACCOUNT_ID_DESCRIPTOR)
}

var accessKeyId: String? = null
var secretAccessKey: String? = null
var sessionToken: String? = null
var expiration: Instant? = null
var version: Int? = null
var accountId: String? = null

try {
deserializer.deserializeStruct(OBJ_DESCRIPTOR) {
Expand All @@ -165,7 +173,7 @@ internal fun deserializeJsonProcessCredentials(deserializer: Deserializer): Json
SESSION_TOKEN_DESCRIPTOR.index -> sessionToken = deserializeString()
EXPIRATION_DESCRIPTOR.index -> expiration = Instant.fromIso8601(deserializeString())
VERSION_DESCRIPTOR.index -> version = deserializeInt()

ACCOUNT_ID_DESCRIPTOR.index -> accountId = deserializeString()
null -> break@loop
else -> skipValue()
}
Expand All @@ -180,5 +188,5 @@ internal fun deserializeJsonProcessCredentials(deserializer: Deserializer): Json
if (sessionToken == null) throw InvalidJsonCredentialsException("missing field `${SESSION_TOKEN_DESCRIPTOR.serialName}`")
if (version == null) throw InvalidJsonCredentialsException("missing field `${VERSION_DESCRIPTOR.serialName}`")
if (version != 1) throw InvalidJsonCredentialsException("version $version is not supported")
return JsonCredentialsResponse.SessionCredentials(accessKeyId!!, secretAccessKey!!, sessionToken!!, expiration)
return JsonCredentialsResponse.SessionCredentials(accessKeyId!!, secretAccessKey!!, sessionToken!!, expiration, accountId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package aws.sdk.kotlin.runtime.auth.credentials

import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException
Expand Down Expand Up @@ -66,12 +67,13 @@ public class ProcessCredentialsProvider(
val deserializer = JsonDeserializer(payload)

return when (val resp = deserializeJsonProcessCredentials(deserializer)) {
is JsonCredentialsResponse.SessionCredentials -> Credentials(
is JsonCredentialsResponse.SessionCredentials -> credentials(
resp.accessKeyId,
resp.secretAccessKey,
resp.sessionToken,
resp.expiration ?: Instant.MAX_VALUE,
PROVIDER_NAME,
resp.accountId,
)
else -> throw CredentialsProviderException("Credentials response was not of expected format")
}
Expand Down
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.auth.credentials.internal.credentials
import aws.sdk.kotlin.runtime.auth.credentials.internal.sso.SsoClient
import aws.sdk.kotlin.runtime.auth.credentials.internal.sso.getRoleCredentials
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
Expand Down Expand Up @@ -116,12 +117,13 @@ public class SsoCredentialsProvider public constructor(

val roleCredentials = resp.roleCredentials ?: throw CredentialsProviderException("Expected SSO roleCredentials to not be null")

return Credentials(
return credentials(
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.fromEpochMilliseconds(roleCredentials.expiration),
PROVIDER_NAME,
accountId = accountId,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@

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

import aws.sdk.kotlin.runtime.auth.credentials.internal.credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
import aws.smithy.kotlin.runtime.util.Attributes

private const val PROVIDER_NAME = "Static"

/**
* A credentials provider for a fixed set of credentials
*
* @param credentials The set of static credentials this provider will return
*/
public class StaticCredentialsProvider(public val credentials: Credentials) : CredentialsProvider {

private constructor(builder: Builder) : this(Credentials(builder.accessKeyId!!, builder.secretAccessKey!!, builder.sessionToken))
private constructor(builder: Builder) : this(
credentials(
builder.accessKeyId!!,
builder.secretAccessKey!!,
builder.sessionToken,
providerName = PROVIDER_NAME,
accountId = builder.accountId,
),
)

override suspend fun resolve(attributes: Attributes): Credentials = credentials

Expand All @@ -31,6 +42,7 @@ public class StaticCredentialsProvider(public val credentials: Credentials) : Cr
public var accessKeyId: String? = null
public var secretAccessKey: String? = null
public var sessionToken: String? = null
public var accountId: String? = null

public fun build(): StaticCredentialsProvider {
if (accessKeyId == null || secretAccessKey == null) {
Expand Down
Loading