Skip to content

Commit

Permalink
feat: account id routing (#1111)
Browse files Browse the repository at this point in the history
  • Loading branch information
aajtodd authored Nov 17, 2023
1 parent 5d6588f commit 3270089
Show file tree
Hide file tree
Showing 55 changed files with 730 additions and 193 deletions.
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 {

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
}

public class Builder {
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 {
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)
}
}
}
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

0 comments on commit 3270089

Please sign in to comment.