diff --git a/aws/rust-runtime/aws-inlineable/src/lib.rs b/aws/rust-runtime/aws-inlineable/src/lib.rs index 0ae9627703..88459ef03d 100644 --- a/aws/rust-runtime/aws-inlineable/src/lib.rs +++ b/aws/rust-runtime/aws-inlineable/src/lib.rs @@ -31,6 +31,11 @@ pub mod presigning; /// Presigning interceptors pub mod presigning_interceptors; +// This module uses module paths that assume the target crate to which it is copied, e.g. +// `crate::config::endpoint::Params`. If included into `aws-inlineable`, this module would +// fail to compile. +// pub mod s3_express; + /// Special logic for extracting request IDs from S3's responses. #[allow(dead_code)] pub mod s3_request_id; diff --git a/aws/rust-runtime/aws-inlineable/src/s3_express.rs b/aws/rust-runtime/aws-inlineable/src/s3_express.rs new file mode 100644 index 0000000000..76b8a95093 --- /dev/null +++ b/aws/rust-runtime/aws-inlineable/src/s3_express.rs @@ -0,0 +1,125 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// Supporting code for S3 Express auth +pub(crate) mod auth { + use aws_smithy_runtime_api::box_error::BoxError; + use aws_smithy_runtime_api::client::auth::{ + AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, Sign, + }; + use aws_smithy_runtime_api::client::identity::{Identity, SharedIdentityResolver}; + use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::client::runtime_components::{ + GetIdentityResolver, RuntimeComponents, + }; + use aws_smithy_types::config_bag::ConfigBag; + + /// Auth scheme ID for S3 Express. + pub(crate) const SCHEME_ID: AuthSchemeId = AuthSchemeId::new("sigv4-s3express"); + + /// S3 Express auth scheme. + #[derive(Debug, Default)] + pub(crate) struct S3ExpressAuthScheme { + signer: S3ExpressSigner, + } + + impl S3ExpressAuthScheme { + /// Creates a new `S3ExpressAuthScheme`. + pub(crate) fn new() -> Self { + Default::default() + } + } + + impl AuthScheme for S3ExpressAuthScheme { + fn scheme_id(&self) -> AuthSchemeId { + SCHEME_ID + } + + fn identity_resolver( + &self, + identity_resolvers: &dyn GetIdentityResolver, + ) -> Option { + identity_resolvers.identity_resolver(self.scheme_id()) + } + + fn signer(&self) -> &dyn Sign { + &self.signer + } + } + + /// S3 Express signer. + #[derive(Debug, Default)] + pub(crate) struct S3ExpressSigner; + + impl Sign for S3ExpressSigner { + fn sign_http_request( + &self, + _request: &mut HttpRequest, + _identity: &Identity, + _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>, + _runtime_components: &RuntimeComponents, + _config_bag: &ConfigBag, + ) -> Result<(), BoxError> { + todo!() + } + } +} + +/// Supporting code for S3 Express identity cache +pub(crate) mod identity_cache { + /// The caching implementation for S3 Express identity. + /// + /// While customers can either disable S3 Express itself or provide a custom S3 Express identity + /// provider, configuring S3 Express identity cache is not supported. Thus, this is _the_ + /// implementation of S3 Express identity cache. + #[derive(Debug)] + pub(crate) struct S3ExpressIdentityCache; +} + +/// Supporting code for S3 Express identity provider +pub(crate) mod identity_provider { + use crate::s3_express::identity_cache::S3ExpressIdentityCache; + use aws_smithy_runtime_api::client::identity::{ + IdentityCacheLocation, IdentityFuture, ResolveIdentity, + }; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; + use aws_smithy_types::config_bag::ConfigBag; + + #[derive(Debug)] + pub(crate) struct DefaultS3ExpressIdentityProvider { + _cache: S3ExpressIdentityCache, + } + + #[derive(Default)] + pub(crate) struct Builder; + + impl DefaultS3ExpressIdentityProvider { + pub(crate) fn builder() -> Builder { + Builder + } + } + + impl Builder { + pub(crate) fn build(self) -> DefaultS3ExpressIdentityProvider { + DefaultS3ExpressIdentityProvider { + _cache: S3ExpressIdentityCache, + } + } + } + + impl ResolveIdentity for DefaultS3ExpressIdentityProvider { + fn resolve_identity<'a>( + &'a self, + _runtime_components: &'a RuntimeComponents, + _config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + todo!() + } + + fn cache_location(&self) -> IdentityCacheLocation { + IdentityCacheLocation::IdentityResolver + } + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index 3bc616a9b8..2c13e4b2b7 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -18,6 +18,7 @@ import software.amazon.smithy.rustsdk.customize.glacier.GlacierDecorator import software.amazon.smithy.rustsdk.customize.onlyApplyTo import software.amazon.smithy.rustsdk.customize.route53.Route53Decorator import software.amazon.smithy.rustsdk.customize.s3.S3Decorator +import software.amazon.smithy.rustsdk.customize.s3.S3ExpressDecorator import software.amazon.smithy.rustsdk.customize.s3.S3ExtendedRequestIdDecorator import software.amazon.smithy.rustsdk.customize.s3control.S3ControlDecorator import software.amazon.smithy.rustsdk.customize.sso.SSODecorator @@ -64,6 +65,7 @@ val DECORATORS: List = Route53Decorator().onlyApplyTo("com.amazonaws.route53#AWSDnsV20130401"), "com.amazonaws.s3#AmazonS3".applyDecorators( S3Decorator(), + S3ExpressDecorator(), S3ExtendedRequestIdDecorator(), ), S3ControlDecorator().onlyApplyTo("com.amazonaws.s3control#AWSS3ControlServiceV20180820"), diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt new file mode 100644 index 0000000000..8529fdfc47 --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt @@ -0,0 +1,184 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk.customize.s3 + +import software.amazon.smithy.aws.traits.auth.SigV4Trait +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.configReexport +import software.amazon.smithy.rust.codegen.client.smithy.customize.AuthSchemeOption +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginSection +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope +import software.amazon.smithy.rustsdk.AwsRuntimeType +import software.amazon.smithy.rustsdk.InlineAwsDependency + +class S3ExpressDecorator : ClientCodegenDecorator { + override val name: String = "S3ExpressDecorator" + override val order: Byte = 0 + + private fun sigv4S3Express() = + writable { + rust( + "#T", + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFile("s3_express"), + ).resolve("auth::SCHEME_ID"), + ) + } + + override fun authOptions( + codegenContext: ClientCodegenContext, + operationShape: OperationShape, + baseAuthSchemeOptions: List, + ): List = + baseAuthSchemeOptions + + AuthSchemeOption.StaticAuthSchemeOption( + SigV4Trait.ID, + listOf(sigv4S3Express()), + ) + + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations + listOf(S3ExpressServiceRuntimePluginCustomization(codegenContext)) + + override fun configCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = baseCustomizations + listOf(S3ExpressIdentityProviderConfig(codegenContext)) +} + +private class S3ExpressServiceRuntimePluginCustomization(codegenContext: ClientCodegenContext) : + ServiceRuntimePluginCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val codegenScope by lazy { + arrayOf( + "DefaultS3ExpressIdentityProvider" to + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFile("s3_express"), + ).resolve("identity_provider::DefaultS3ExpressIdentityProvider"), + "IdentityCacheLocation" to + RuntimeType.smithyRuntimeApiClient(runtimeConfig) + .resolve("client::identity::IdentityCacheLocation"), + "S3ExpressAuthScheme" to + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFile("s3_express"), + ).resolve("auth::S3ExpressAuthScheme"), + "S3_EXPRESS_SCHEME_ID" to + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFile("s3_express"), + ).resolve("auth::SCHEME_ID"), + "SharedAuthScheme" to + RuntimeType.smithyRuntimeApiClient(runtimeConfig) + .resolve("client::auth::SharedAuthScheme"), + "SharedCredentialsProvider" to + configReexport( + AwsRuntimeType.awsCredentialTypes(runtimeConfig) + .resolve("provider::SharedCredentialsProvider"), + ), + "SharedIdentityResolver" to + RuntimeType.smithyRuntimeApiClient(runtimeConfig) + .resolve("client::identity::SharedIdentityResolver"), + ) + } + + override fun section(section: ServiceRuntimePluginSection): Writable = + writable { + when (section) { + is ServiceRuntimePluginSection.RegisterRuntimeComponents -> { + section.registerAuthScheme(this) { + rustTemplate( + "#{SharedAuthScheme}::new(#{S3ExpressAuthScheme}::new())", + *codegenScope, + ) + } + + section.registerIdentityResolver( + this, + writable { + rustTemplate("#{S3_EXPRESS_SCHEME_ID}", *codegenScope) + }, + writable { + rustTemplate("#{DefaultS3ExpressIdentityProvider}::builder().build()", *codegenScope) + }, + ) + } + + else -> {} + } + } +} + +class S3ExpressIdentityProviderConfig(codegenContext: ClientCodegenContext) : ConfigCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val codegenScope = + arrayOf( + *preludeScope, + "IdentityCacheLocation" to + RuntimeType.smithyRuntimeApiClient(runtimeConfig) + .resolve("client::identity::IdentityCacheLocation"), + "ProvideCredentials" to + configReexport( + AwsRuntimeType.awsCredentialTypes(runtimeConfig) + .resolve("provider::ProvideCredentials"), + ), + "SharedCredentialsProvider" to + configReexport( + AwsRuntimeType.awsCredentialTypes(runtimeConfig) + .resolve("provider::SharedCredentialsProvider"), + ), + "SharedIdentityResolver" to + RuntimeType.smithyRuntimeApiClient(runtimeConfig) + .resolve("client::identity::SharedIdentityResolver"), + "S3_EXPRESS_SCHEME_ID" to + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFile("s3_express"), + ).resolve("auth::SCHEME_ID"), + ) + + override fun section(section: ServiceConfig) = + writable { + when (section) { + ServiceConfig.BuilderImpl -> { + rustTemplate( + """ + /// Sets the credentials provider for S3 Express One Zone + pub fn express_credentials_provider(mut self, credentials_provider: impl #{ProvideCredentials} + 'static) -> Self { + self.set_express_credentials_provider(#{Some}(#{SharedCredentialsProvider}::new(credentials_provider))); + self + } + """, + *codegenScope, + ) + + rustTemplate( + """ + /// Sets the credentials provider for S3 Express One Zone + pub fn set_express_credentials_provider(&mut self, credentials_provider: #{Option}<#{SharedCredentialsProvider}>) -> &mut Self { + if let #{Some}(credentials_provider) = credentials_provider { + self.runtime_components.set_identity_resolver(#{S3_EXPRESS_SCHEME_ID}, credentials_provider); + } + self + } + """, + *codegenScope, + ) + } + + else -> emptySection + } + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt index 5bcc870ae5..544d30ea8f 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt @@ -126,9 +126,17 @@ class OperationInputTestGenerator(_ctx: ClientCodegenContext, private val test: private val model = ctx.model private val instantiator = ClientInstantiator(ctx) + /** tests using S3 Express bucket names need to be disabled until the implementation is in place **/ + private fun EndpointTestCase.isSigV4S3Express() = + expect.endpoint.orNull()?.properties?.get("authSchemes")?.asArrayNode()?.orNull() + ?.map { it.expectObjectNode().expectStringMember("name").value }?.contains("sigv4-s3express") == true + fun generateInput(testOperationInput: EndpointTestOperationInput) = writable { val operationName = testOperationInput.operationName.toSnakeCase() + if (test.isSigV4S3Express()) { + Attribute.shouldPanic("not yet implemented").render(this) + } tokioTest(safeName("operation_input_test_$operationName")) { rustTemplate( """ diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt index 66095c1555..699e5bff19 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt @@ -69,6 +69,14 @@ sealed class ServiceRuntimePluginSection(name: String) : Section(name) { ) { writer.rust("runtime_components.push_retry_classifier(#T);", classifier) } + + fun registerIdentityResolver( + writer: RustWriter, + schemeId: Writable, + identityResolver: Writable, + ) { + writer.rust("runtime_components.set_identity_resolver(#T, #T);", schemeId, identityResolver) + } } } typealias ServiceRuntimePluginCustomization = NamedCustomization diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs index f0e972675a..2671a883a0 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs @@ -159,6 +159,30 @@ pub trait ResolveIdentity: Send + Sync + Debug { fn fallback_on_interrupt(&self) -> Option { None } + + /// Returns the location of an identity cache associated with this identity resolver. + /// + /// By default, identity resolvers will use the identity cache stored in runtime components. + /// Implementing types can change the cache location if they want to. Refer to [`IdentityCacheLocation`] + /// explaining why a concrete identity resolver might want to change the cache location. + fn cache_location(&self) -> IdentityCacheLocation { + IdentityCacheLocation::RuntimeComponents + } +} + +/// Cache location for identity caching. +/// +/// Identities are usually cached in the identity cache owned by [`RuntimeComponents`]. However, +/// we do have identities whose caching mechanism is internally managed by their identity resolver, +/// in which case we want to avoid the `RuntimeComponents`-owned identity cache interfering with +/// the internal caching policy. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum IdentityCacheLocation { + /// Indicates the identity cache is owned by [`RuntimeComponents`]. + RuntimeComponents, + /// Indicates the identity cache is internally managed by the identity resolver. + IdentityResolver, } /// Container for a shared identity resolver. @@ -194,6 +218,10 @@ impl ResolveIdentity for SharedIdentityResolver { ) -> IdentityFuture<'a> { self.inner.resolve_identity(runtime_components, config_bag) } + + fn cache_location(&self) -> IdentityCacheLocation { + self.inner.cache_location() + } } impl_shared_conversions!(convert SharedIdentityResolver from ResolveIdentity using SharedIdentityResolver::new); diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs index 98a39a9a43..0bf5ecf1a7 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs @@ -4,12 +4,14 @@ */ use crate::client::auth::no_auth::NO_AUTH_SCHEME_ID; +use crate::client::identity::IdentityCache; use aws_smithy_runtime_api::box_error::BoxError; use aws_smithy_runtime_api::client::auth::{ AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOptionResolverParams, ResolveAuthSchemeOptions, }; -use aws_smithy_runtime_api::client::identity::ResolveCachedIdentity; +use aws_smithy_runtime_api::client::identity::ResolveIdentity; +use aws_smithy_runtime_api::client::identity::{IdentityCacheLocation, ResolveCachedIdentity}; use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext; use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::ConfigBag; @@ -135,7 +137,13 @@ pub(super) async fn orchestrate_auth( if let Some(auth_scheme) = runtime_components.auth_scheme(scheme_id) { // Use the resolved auth scheme to resolve an identity if let Some(identity_resolver) = auth_scheme.identity_resolver(runtime_components) { - let identity_cache = runtime_components.identity_cache(); + let identity_cache = if identity_resolver.cache_location() + == IdentityCacheLocation::RuntimeComponents + { + runtime_components.identity_cache() + } else { + IdentityCache::no_cache() + }; let signer = auth_scheme.signer(); trace!( auth_scheme = ?auth_scheme,