diff --git a/.changelog/1752160009.md b/.changelog/1752160009.md new file mode 100644 index 00000000000..ac99af7532a --- /dev/null +++ b/.changelog/1752160009.md @@ -0,0 +1,37 @@ +--- +applies_to: +- aws-sdk-rust +- client +authors: +- ysaito1001 +references: +- smithy-rs#4203 +breaking: false +new_feature: true +bug_fix: false +--- +Add support for configuring auth schemes manually using an auth scheme preference list. +The preference list allows customers to reprioritize the order of auth schemes originally +determined by the auth scheme resolver. +Customers can configure the auth scheme preference at the following locations, listed in order of precedence: +1. Service Client Configuration +```rust +use aws_runtime::auth::sigv4; +use aws_smithy_runtime_api::client::auth::AuthSchemeId; +use aws_smithy_runtime_api::client::auth::http::HTTP_BEARER_AUTH_SCHEME_ID; + +let config = aws_sdk_s3::Config::builder() + .auth_scheme_preference([AuthSchemeId::from("scheme1"), sigv4::SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID]) + // ... + .build(); +``` +2. Environment Variable +``` +AWS_AUTH_SCHEME_PREFERENCE=scheme1, sigv4, httpBearerAuth +``` +3. Configuration File +``` +auth_scheme_preference=scheme1, sigv4, httpBearerAuth +``` +With this configuration, the auth scheme resolver will prefer to select them in the specified order, +if they are supported. diff --git a/aws/rust-runtime/aws-config/Cargo.lock b/aws/rust-runtime/aws-config/Cargo.lock index 390d0189379..788025d4bb8 100644 --- a/aws/rust-runtime/aws-config/Cargo.lock +++ b/aws/rust-runtime/aws-config/Cargo.lock @@ -50,7 +50,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.8.2" +version = "1.8.3" dependencies = [ "aws-credential-types", "aws-runtime", @@ -318,7 +318,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.4" +version = "1.8.5" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -341,7 +341,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.3" +version = "1.8.4" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.7" +version = "1.3.8" dependencies = [ "aws-credential-types", "aws-smithy-async", diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index 17a0fa9abf3..61489eb2b64 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-config" -version = "1.8.2" +version = "1.8.3" authors = [ "AWS Rust SDK Team ", "Russell Cohen ", diff --git a/aws/rust-runtime/aws-config/external-types.toml b/aws/rust-runtime/aws-config/external-types.toml index 362f2ea2921..d5ba73f03c8 100644 --- a/aws/rust-runtime/aws-config/external-types.toml +++ b/aws/rust-runtime/aws-config/external-types.toml @@ -3,10 +3,8 @@ # require manual version bumping every time an automated version bump # to the exposed SDK crates happens. allowed_external_types = [ - "aws_credential_types::cache::CredentialsCache", "aws_credential_types::provider::credentials::ProvideCredentials", "aws_credential_types::provider::credentials::Result", - "aws_credential_types::provider::credentials::SharedCredentialsProvider", "aws_credential_types::provider::token::ProvideToken", "aws_runtime::env_config::error::EnvConfigFileLoadError", "aws_runtime::env_config::file::Builder", @@ -17,30 +15,23 @@ allowed_external_types = [ "aws_runtime::env_config::section::EnvConfigSections", "aws_runtime::env_config::section::Profile", "aws_smithy_async::rt::sleep::AsyncSleep", - "aws_smithy_async::rt::sleep::SharedAsyncSleep", - "aws_smithy_async::time::SharedTimeSource", "aws_smithy_async::time::TimeSource", "aws_smithy_runtime::client::identity::cache::IdentityCache", "aws_smithy_runtime::client::identity::cache::lazy::LazyCacheBuilder", + "aws_smithy_runtime_api::client::auth::AuthSchemePreference", "aws_smithy_runtime_api::box_error::BoxError", "aws_smithy_runtime_api::client::behavior_version::BehaviorVersion", "aws_smithy_runtime_api::client::dns::ResolveDns", - "aws_smithy_runtime_api::client::dns::SharedDnsResolver", "aws_smithy_runtime_api::client::http::HttpClient", - "aws_smithy_runtime_api::client::http::SharedHttpClient", "aws_smithy_runtime_api::client::identity::ResolveCachedIdentity", "aws_smithy_runtime_api::client::identity::ResolveIdentity", "aws_smithy_runtime_api::client::orchestrator::HttpResponse", - "aws_smithy_runtime_api::client::result::SdkError", "aws_smithy_runtime_api::client::retries::classifiers::ClassifyRetry", "aws_smithy_runtime_api::client::retries::classifiers::SharedRetryClassifier", "aws_smithy_runtime_api::client::stalled_stream_protection::StalledStreamProtectionConfig", - "aws_smithy_types::body::SdkBody", "aws_smithy_types::checksum_config::RequestChecksumCalculation", "aws_smithy_types::checksum_config::ResponseChecksumValidation", - "aws_smithy_types::retry", "aws_smithy_types::retry::*", - "aws_smithy_types::timeout", "aws_smithy_types::timeout::OperationTimeoutConfig", "aws_smithy_types::timeout::TimeoutConfig", "aws_smithy_types::timeout::TimeoutConfigBuilder", diff --git a/aws/rust-runtime/aws-config/src/default_provider.rs b/aws/rust-runtime/aws-config/src/default_provider.rs index 2687c16a40b..436778c1399 100644 --- a/aws/rust-runtime/aws-config/src/default_provider.rs +++ b/aws/rust-runtime/aws-config/src/default_provider.rs @@ -69,3 +69,6 @@ pub mod checksums; /// Default provider chain for account-based endpoint mode pub mod account_id_endpoint_mode; + +/// Default provider chain for auth scheme preference list +pub mod auth_scheme_preference; diff --git a/aws/rust-runtime/aws-config/src/default_provider/auth_scheme_preference.rs b/aws/rust-runtime/aws-config/src/default_provider/auth_scheme_preference.rs new file mode 100644 index 00000000000..2a5d765aac0 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/default_provider/auth_scheme_preference.rs @@ -0,0 +1,182 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::provider_config::ProviderConfig; +use aws_runtime::env_config::EnvConfigValue; +use aws_smithy_runtime_api::client::auth::{AuthSchemeId, AuthSchemePreference}; +use aws_smithy_types::error::display::DisplayErrorContext; +use std::borrow::Cow; +use std::fmt; + +mod env { + pub(super) const AUTH_SCHEME_PREFERENCE: &str = "AWS_AUTH_SCHEME_PREFERENCE"; +} + +mod profile_key { + pub(super) const AUTH_SCHEME_PREFERENCE: &str = "auth_scheme_preference"; +} + +/// Load the value for the auth scheme preference +/// +/// This checks the following sources: +/// 1. The environment variable `AWS_AUTH_SCHEME_PREFERENCE=scheme1,scheme2,scheme3` +/// 2. The profile key `auth_scheme_preference=scheme1,scheme2,scheme3` +/// +/// A scheme name can be either a fully qualified name or a shorthand with the namespace prefix trimmed. +/// For example, valid scheme names include "aws.auth#sigv4", "smithy.api#httpBasicAuth", "sigv4", and "httpBasicAuth". +/// Whitespace (spaces or tabs), including leading, trailing, and between names, is ignored. +/// +/// Returns `None` if a parsed string component is empty when creating an `AuthSchemeId`. +pub(crate) async fn auth_scheme_preference_provider( + provider_config: &ProviderConfig, +) -> Option { + let env = provider_config.env(); + let profiles = provider_config.profile().await; + + EnvConfigValue::new() + .env(env::AUTH_SCHEME_PREFERENCE) + .profile(profile_key::AUTH_SCHEME_PREFERENCE) + .validate(&env, profiles, parse_auth_scheme_names) + .map_err(|err| tracing::warn!(err = %DisplayErrorContext(&err), "invalid value for `AuthSchemePreference`")) + .unwrap_or(None) +} + +fn parse_auth_scheme_names(csv: &str) -> Result { + csv.split(',') + .map(|s| { + let trimmed = s.trim().replace([' ', '\t'], ""); + if trimmed.is_empty() { + return Err(InvalidAuthSchemeNamesCsv { + value: format!("Empty name found in `{csv}`."), + }); + } + let scheme_name = trimmed.split('#').next_back().unwrap_or(&trimmed); + Ok(AuthSchemeId::from(Cow::Owned(scheme_name.to_owned()))) + }) + .collect::, _>>() + .map(AuthSchemePreference::from) +} + +#[derive(Debug)] +pub(crate) struct InvalidAuthSchemeNamesCsv { + value: String, +} + +impl fmt::Display for InvalidAuthSchemeNamesCsv { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Not a valid comma-separated auth scheme names: {}", + self.value + ) + } +} + +impl std::error::Error for InvalidAuthSchemeNamesCsv {} + +#[cfg(test)] +mod test { + use super::env; + use crate::{ + default_provider::auth_scheme_preference::auth_scheme_preference_provider, + provider_config::ProviderConfig, + }; + use aws_types::os_shim_internal::Env; + use tracing_test::traced_test; + + #[tokio::test] + #[traced_test] + async fn log_error_on_invalid_value() { + let conf = ProviderConfig::empty().with_env(Env::from_slice(&[( + env::AUTH_SCHEME_PREFERENCE, + "scheme1, , \tscheme2", + )])); + assert_eq!(None, auth_scheme_preference_provider(&conf).await); + assert!(logs_contain( + "Not a valid comma-separated auth scheme names: Empty name found" + )); + } + + #[cfg(feature = "sso")] // for aws-smithy-runtime-api/http-auth + mod http_auth_tests { + use super::env; + #[allow(deprecated)] + use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; + use crate::{ + default_provider::auth_scheme_preference::auth_scheme_preference_provider, + provider_config::ProviderConfig, + }; + use aws_smithy_runtime_api::client::auth::AuthSchemePreference; + use aws_types::os_shim_internal::{Env, Fs}; + + #[tokio::test] + async fn environment_priority() { + let conf = ProviderConfig::empty() + .with_env(Env::from_slice(&[( + env::AUTH_SCHEME_PREFERENCE, + "aws.auth#sigv4, smithy.api#httpBasicAuth, smithy.api#httpDigestAuth, smithy.api#httpBearerAuth, smithy.api#httpApiKeyAuth", + )])) + .with_profile_config( + Some( + #[allow(deprecated)] + ProfileFiles::builder() + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "conf", + ) + .build(), + ), + None, + ) + .with_fs(Fs::from_slice(&[( + "conf", + "[default]\nauth_scheme_preference = scheme1, scheme2 , \tscheme3 \t", + )])); + assert_eq!( + AuthSchemePreference::from([ + aws_runtime::auth::sigv4::SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_BASIC_AUTH_SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_DIGEST_AUTH_SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_BEARER_AUTH_SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_API_KEY_AUTH_SCHEME_ID, + ]), + auth_scheme_preference_provider(&conf).await.unwrap() + ); + } + + #[tokio::test] + async fn load_from_profile() { + let conf = ProviderConfig::empty() + .with_profile_config( + Some( + #[allow(deprecated)] + ProfileFiles::builder() + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "conf", + ) + .build(), + ), + None, + ) + .with_fs(Fs::from_slice(&[( + "conf", + "[default]\nauth_scheme_preference = sigv4, httpBasicAuth, httpDigestAuth, \thttpBearerAuth \t, httpApiKeyAuth ", + )])); + assert_eq!( + AuthSchemePreference::from([ + aws_runtime::auth::sigv4::SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_BASIC_AUTH_SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_DIGEST_AUTH_SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_BEARER_AUTH_SCHEME_ID, + aws_smithy_runtime_api::client::auth::http::HTTP_API_KEY_AUTH_SCHEME_ID, + ]), + auth_scheme_preference_provider(&conf).await.unwrap() + ); + } + } +} diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index ff4e98af683..ba2691ad97e 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -223,6 +223,7 @@ mod loader { use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; use aws_smithy_async::time::{SharedTimeSource, TimeSource}; use aws_smithy_runtime::client::identity::IdentityCache; + use aws_smithy_runtime_api::client::auth::AuthSchemePreference; use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion; use aws_smithy_runtime_api::client::http::HttpClient; use aws_smithy_runtime_api::client::identity::{ResolveCachedIdentity, SharedIdentityCache}; @@ -242,9 +243,10 @@ mod loader { use aws_types::SdkConfig; use crate::default_provider::{ - account_id_endpoint_mode, app_name, checksums, credentials, disable_request_compression, - endpoint_url, ignore_configured_endpoint_urls as ignore_ep, region, - request_min_compression_size_bytes, retry_config, timeout_config, use_dual_stack, use_fips, + account_id_endpoint_mode, app_name, auth_scheme_preference, checksums, credentials, + disable_request_compression, endpoint_url, ignore_configured_endpoint_urls as ignore_ep, + region, request_min_compression_size_bytes, retry_config, timeout_config, use_dual_stack, + use_fips, }; use crate::meta::region::ProvideRegion; #[allow(deprecated)] @@ -271,6 +273,7 @@ mod loader { #[derive(Default, Debug)] pub struct ConfigLoader { app_name: Option, + auth_scheme_preference: Option, identity_cache: Option, credentials_provider: TriStateOption, token_provider: Option, @@ -403,6 +406,28 @@ mod loader { self } + #[doc = docs_for!(auth_scheme_preference)] + /// + /// # Examples + /// ```no_run + /// # use aws_smithy_runtime_api::client::auth::AuthSchemeId; + /// # async fn create_config() { + /// let config = aws_config::from_env() + /// // Favors a custom auth scheme over the SigV4 auth scheme. + /// // Note: This will not result in an error, even if the custom scheme is missing from the resolved auth schemes. + /// .auth_scheme_preference([AuthSchemeId::from("custom"), aws_runtime::auth::sigv4::SCHEME_ID]) + /// .load() + /// .await; + /// # } + /// ``` + pub fn auth_scheme_preference( + mut self, + auth_scheme_preference: impl Into, + ) -> Self { + self.auth_scheme_preference = Some(auth_scheme_preference.into()); + self + } + /// Override the identity cache used to build [`SdkConfig`]. /// /// The identity cache caches AWS credentials and SSO tokens. By default, a lazy cache is used @@ -962,6 +987,13 @@ mod loader { account_id_endpoint_mode::account_id_endpoint_mode_provider(&conf).await }; + let auth_scheme_preference = + if let Some(auth_scheme_preference) = self.auth_scheme_preference { + Some(auth_scheme_preference) + } else { + auth_scheme_preference::auth_scheme_preference_provider(&conf).await + }; + builder.set_request_checksum_calculation(request_checksum_calculation); builder.set_response_checksum_validation(response_checksum_validation); builder.set_identity_cache(identity_cache); @@ -974,6 +1006,7 @@ mod loader { builder.set_request_min_compression_size_bytes(request_min_compression_size_bytes); builder.set_stalled_stream_protection(self.stalled_stream_protection_config); builder.set_account_id_endpoint_mode(account_id_endpoint_mode); + builder.set_auth_scheme_preference(auth_scheme_preference); builder.build() } } diff --git a/aws/rust-runtime/aws-types/Cargo.toml b/aws/rust-runtime/aws-types/Cargo.toml index f99f5e9c720..9df9c7939e8 100644 --- a/aws/rust-runtime/aws-types/Cargo.toml +++ b/aws/rust-runtime/aws-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-types" -version = "1.3.7" +version = "1.3.8" authors = ["AWS Rust SDK Team ", "Russell Cohen "] description = "Cross-service types for the AWS SDK." edition = "2021" diff --git a/aws/rust-runtime/aws-types/external-types.toml b/aws/rust-runtime/aws-types/external-types.toml index ee28a1732a5..215cc7af803 100644 --- a/aws/rust-runtime/aws-types/external-types.toml +++ b/aws/rust-runtime/aws-types/external-types.toml @@ -1,11 +1,11 @@ allowed_external_types = [ - "aws_credential_types::cache::CredentialsCache", "aws_credential_types::provider::credentials::SharedCredentialsProvider", "aws_credential_types::provider::token::SharedTokenProvider", "aws_smithy_async::rt::sleep::AsyncSleep", "aws_smithy_async::rt::sleep::SharedAsyncSleep", "aws_smithy_async::time::SharedTimeSource", "aws_smithy_async::time::TimeSource", + "aws_smithy_runtime_api::client::auth::AuthSchemePreference", "aws_smithy_runtime_api::client::behavior_version::BehaviorVersion", "aws_smithy_runtime_api::client::http::HttpClient", "aws_smithy_runtime_api::client::http::SharedHttpClient", @@ -17,7 +17,6 @@ allowed_external_types = [ "aws_smithy_types::checksum_config::RequestChecksumCalculation", "aws_smithy_types::config_bag::storable::Storable", "aws_smithy_types::config_bag::storable::StoreReplace", - "aws_smithy_types::config_bag::storable::Storer", "aws_smithy_types::error::metadata::Builder", "aws_smithy_types::retry::RetryConfig", "aws_smithy_types::timeout::TimeoutConfig", diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index fc2a198155e..fc1f7080a53 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -20,6 +20,7 @@ pub use aws_credential_types::provider::SharedCredentialsProvider; use aws_smithy_async::rt::sleep::AsyncSleep; pub use aws_smithy_async::rt::sleep::SharedAsyncSleep; pub use aws_smithy_async::time::{SharedTimeSource, TimeSource}; +use aws_smithy_runtime_api::client::auth::AuthSchemePreference; use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion; use aws_smithy_runtime_api::client::http::HttpClient; pub use aws_smithy_runtime_api::client::http::SharedHttpClient; @@ -81,6 +82,17 @@ See the developer guide on [account-based endpoints](https://docs.aws.amazon.com for more information. For services that do not use the account-based endpoints, this setting does nothing. +" }; + (auth_scheme_preference) => { +"Set the auth scheme preference for an auth scheme resolver +(typically the default auth scheme resolver). + +Each operation has a predefined order of auth schemes, as determined by the service, +for auth scheme resolution. By using the auth scheme preference, customers +can reorder the schemes resolved by the auth scheme resolver. + +The preference list is intended as a hint rather than a strict override. +Any schemes not present in the originally resolved auth schemes will be ignored. " }; } } @@ -89,6 +101,7 @@ For services that do not use the account-based endpoints, this setting does noth #[derive(Debug, Clone)] pub struct SdkConfig { app_name: Option, + auth_scheme_preference: Option, identity_cache: Option, credentials_provider: Option, token_provider: Option, @@ -120,6 +133,7 @@ pub struct SdkConfig { #[derive(Debug, Default)] pub struct Builder { app_name: Option, + auth_scheme_preference: Option, identity_cache: Option, credentials_provider: Option, token_provider: Option, @@ -777,6 +791,24 @@ impl Builder { self } + #[doc = docs_for!(auth_scheme_preference)] + pub fn auth_scheme_preference( + mut self, + auth_scheme_preference: impl Into, + ) -> Self { + self.set_auth_scheme_preference(Some(auth_scheme_preference)); + self + } + + #[doc = docs_for!(auth_scheme_preference)] + pub fn set_auth_scheme_preference( + &mut self, + auth_scheme_preference: Option>, + ) -> &mut Self { + self.auth_scheme_preference = auth_scheme_preference.map(|pref| pref.into()); + self + } + /// Set the origin of a setting. /// /// This is used internally to understand how to merge config structs while @@ -789,6 +821,7 @@ impl Builder { pub fn build(self) -> SdkConfig { SdkConfig { app_name: self.app_name, + auth_scheme_preference: self.auth_scheme_preference, identity_cache: self.identity_cache, credentials_provider: self.credentials_provider, token_provider: self.token_provider, @@ -896,6 +929,11 @@ impl SdkConfig { self.account_id_endpoint_mode.as_ref() } + /// Configured auth scheme preference + pub fn auth_scheme_preference(&self) -> Option<&AuthSchemePreference> { + self.auth_scheme_preference.as_ref() + } + /// Configured endpoint URL pub fn endpoint_url(&self) -> Option<&str> { self.endpoint_url.as_deref() @@ -1020,6 +1058,7 @@ impl SdkConfig { pub fn into_builder(self) -> Builder { Builder { app_name: self.app_name, + auth_scheme_preference: self.auth_scheme_preference, identity_cache: self.identity_cache, credentials_provider: self.credentials_provider, token_provider: self.token_provider, diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt index ab00d1fc671..0cfb79aca59 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt @@ -108,6 +108,7 @@ class GenericSmithySdkConfigSettings : ClientCodegenDecorator { ${section.serviceConfigBuilder}.set_http_client(${section.sdkConfig}.http_client()); ${section.serviceConfigBuilder}.set_time_source(${section.sdkConfig}.time_source()); ${section.serviceConfigBuilder}.set_behavior_version(${section.sdkConfig}.behavior_version()); + ${section.serviceConfigBuilder}.set_auth_scheme_preference(${section.sdkConfig}.auth_scheme_preference().cloned()); // setting `None` here removes the default if let Some(config) = ${section.sdkConfig}.stalled_stream_protection() { ${section.serviceConfigBuilder}.set_stalled_stream_protection(Some(config)); diff --git a/aws/sdk/integration-tests/s3/tests/auth_scheme_preference.rs b/aws/sdk/integration-tests/s3/tests/auth_scheme_preference.rs new file mode 100644 index 00000000000..f05912be705 --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/auth_scheme_preference.rs @@ -0,0 +1,68 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_config::Region; +use aws_sdk_s3::{Client, Config}; +use aws_smithy_http_client::test_util::capture_request; + +// S3 is one of the servies that relies on endpoint-based auth scheme resolution. +// An auth scheme preference should not be overridden by other resolution methods. + +#[tracing_test::traced_test] +#[tokio::test] +async fn auth_scheme_preference_at_client_level_should_take_the_highest_priority() { + let (http_client, _) = capture_request(None); + let config = aws_config::from_env() + .http_client(http_client) + .region(Region::new("us-east-2")) + // Explicitly set a preference that favors `sigv4`, otherwise `sigv4a` + // would normally be resolved based on the endpoint authSchemes property. + .auth_scheme_preference([aws_runtime::auth::sigv4::SCHEME_ID]) + .load() + .await; + + let client = Client::new(&config); + let _ = client + .get_object() + .bucket("arn:aws:s3::123456789012:accesspoint/mfzwi23gnjvgw.mrap") + .key("doesnotmatter") + .send() + .await; + + assert!(logs_contain(&format!( + "resolving identity scheme_id=AuthSchemeId {{ scheme_id: \"{auth_scheme_id_str}\" }}", + auth_scheme_id_str = aws_runtime::auth::sigv4::SCHEME_ID.inner(), + ))); +} + +#[tracing_test::traced_test] +#[tokio::test] +async fn auth_scheme_preference_at_operation_level_should_take_the_highest_priority() { + let (http_client, _) = capture_request(None); + let config = aws_config::from_env() + .http_client(http_client) + .region(Region::new("us-east-2")) + .load() + .await; + + let client = Client::new(&config); + let _ = client + .get_object() + .bucket("arn:aws:s3::123456789012:accesspoint/mfzwi23gnjvgw.mrap") + .key("doesnotmatter") + .customize() + .config_override( + // Explicitly set a preference that favors `sigv4`, otherwise `sigv4a` + // would normally be resolved based on the endpoint authSchemes property. + Config::builder().auth_scheme_preference([aws_runtime::auth::sigv4::SCHEME_ID]), + ) + .send() + .await; + + assert!(logs_contain(&format!( + "resolving identity scheme_id=AuthSchemeId {{ scheme_id: \"{auth_scheme_id_str}\" }}", + auth_scheme_id_str = aws_runtime::auth::sigv4::SCHEME_ID.inner(), + ))); +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthDecorator.kt index c353f2a8493..b0d3aadfeb6 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthDecorator.kt @@ -62,7 +62,8 @@ class AuthDecorator : ClientCodegenDecorator { override fun configCustomizations( codegenContext: ClientCodegenContext, baseCustomizations: List, - ): List = baseCustomizations + AuthDecoratorConfigCustomizations(codegenContext) + ): List = + baseCustomizations + AuthDecoratorConfigCustomizations(codegenContext) + AuthSchemePreferenceConfigCustomization(codegenContext) override fun serviceRuntimePluginCustomizations( codegenContext: ClientCodegenContext, @@ -74,7 +75,10 @@ class AuthDecorator : ClientCodegenDecorator { return when (section) { is ServiceRuntimePluginSection.RegisterRuntimeComponents -> writable { - section.registerAuthSchemeOptionResolver(this, defaultAuthSchemeResolver(codegenContext)) + section.registerAuthSchemeOptionResolver( + this, + defaultAuthSchemeResolver(codegenContext), + ) } else -> emptySection @@ -299,3 +303,80 @@ private class AuthDecoratorConfigCustomizations(private val codegenContext: Clie } } } + +private class AuthSchemePreferenceConfigCustomization(codegenContext: ClientCodegenContext) : ConfigCustomization() { + private val codegenScope = + arrayOf( + *preludeScope, + "AuthSchemePreference" to + RuntimeType.smithyRuntimeApiClient(codegenContext.runtimeConfig) + .resolve("client::auth::AuthSchemePreference"), + ) + private val moduleUseName = codegenContext.moduleUseName() + + override fun section(section: ServiceConfig) = + writable { + when (section) { + is ServiceConfig.ConfigImpl -> { + rustTemplate( + """ + /// Returns the configured auth scheme preference + pub fn auth_scheme_preference(&self) -> #{Option}<&#{AuthSchemePreference}> { + self.config.load::<#{AuthSchemePreference}>() + } + """, + *codegenScope, + ) + } + + is ServiceConfig.BuilderImpl -> { + val docs = """ + /// Set the auth scheme preference for an auth scheme resolver + /// (typically the default auth scheme resolver). + /// + /// Each operation has a predefined order of auth schemes, as determined by the service, + /// for auth scheme resolution. By using the auth scheme preference, customers + /// can reorder the schemes resolved by the auth scheme resolver. + /// + /// The preference list is intended as a hint rather than a strict override. + /// Any schemes not present in the originally resolved auth schemes will be ignored. + /// + /// ## Examples + /// + /// ```no_run + /// ## use aws_smithy_runtime_api::client::auth::AuthSchemeId; + /// let config = $moduleUseName::Config::builder() + /// .auth_scheme_preference([AuthSchemeId::from("scheme1"), AuthSchemeId::from("scheme2")]) + /// // ... + /// .build(); + /// let client = $moduleUseName::Client::from_conf(config); + /// ``` + """ + rustTemplate( + """ + $docs + pub fn auth_scheme_preference(mut self, preference: impl #{Into}<#{AuthSchemePreference}>) -> Self { + self.set_auth_scheme_preference(#{Some}(preference.into())); + self + } + + $docs + pub fn set_auth_scheme_preference(&mut self, preference: #{Option}<#{AuthSchemePreference}>) -> &mut Self { + self.config.store_or_unset(preference); + self + } + """, + *codegenScope, + ) + } + + is ServiceConfig.BuilderFromConfigBag -> + rustTemplate( + "${section.builder}.set_auth_scheme_preference(${section.configBag}.load::<#{AuthSchemePreference}>().cloned());", + *codegenScope, + ) + + else -> emptySection + } + } +} diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthTypesGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthTypesGeneratorTest.kt index a8eb9f2376d..838f01c7ff5 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthTypesGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/auth/AuthTypesGeneratorTest.kt @@ -19,7 +19,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.tokioTest -class AuthSchemeResolverGeneratorTest { +class AuthTypesGeneratorTest { val model = """ namespace com.test @@ -29,12 +29,13 @@ class AuthSchemeResolverGeneratorTest { @httpBearerAuth @httpApiKeyAuth(name: "X-Api-Key", in: "header") @httpBasicAuth - @auth([httpApiKeyAuth]) + @auth([httpApiKeyAuth, httpBasicAuth]) service Test { version: "1.0.0", operations: [ GetFooServiceDefault, GetFooOpOverride, + GetFooAnonymous, ] } @@ -42,6 +43,9 @@ class AuthSchemeResolverGeneratorTest { @auth([httpBasicAuth, httpBearerAuth]) operation GetFooOpOverride{} + + @auth([]) + operation GetFooAnonymous{} """.asSmithyModel(smithyVersion = "2.0") @Test @@ -96,7 +100,7 @@ class AuthSchemeResolverGeneratorTest { .iter() .map(|opt| opt.scheme_id().inner()) .collect::>(); - assert_eq!(vec!["http-api-key-auth"], actual); + assert_eq!(vec!["httpApiKeyAuth", "httpBasicAuth"], actual); """, *codegenScope, ) @@ -123,7 +127,7 @@ class AuthSchemeResolverGeneratorTest { .iter() .map(|opt| opt.scheme_id().inner()) .collect::>(); - assert_eq!(vec!["http-basic-auth", "http-bearer-auth"], actual); + assert_eq!(vec!["httpBasicAuth", "httpBearerAuth"], actual); """, *codegenScope, ) diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index 11791ac3504..fe907c86894 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -563,7 +563,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.4" +version = "1.8.5" dependencies = [ "approx", "aws-smithy-async", @@ -591,7 +591,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.3" +version = "1.8.4" dependencies = [ "aws-smithy-async", "aws-smithy-types", diff --git a/rust-runtime/aws-smithy-runtime-api/Cargo.toml b/rust-runtime/aws-smithy-runtime-api/Cargo.toml index bebdc29dfa9..86d83300ccf 100644 --- a/rust-runtime/aws-smithy-runtime-api/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-runtime-api" -version = "1.8.3" +version = "1.8.4" authors = ["AWS Rust SDK Team ", "Zelda Hessler "] description = "Smithy runtime types." edition = "2021" diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs b/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs index 183d36602ec..4ca15dcc78c 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs @@ -420,3 +420,37 @@ impl<'a> From<&'a Document> for AuthSchemeEndpointConfig<'a> { Self(Some(value)) } } + +/// An ordered list of [AuthSchemeId]s +/// +/// Can be used to reorder already-resolved auth schemes by an auth scheme resolver. +/// This list is intended as a hint rather than a strict override; +/// any schemes not present in the resolved auth schemes will be ignored. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AuthSchemePreference { + preference_list: Vec, +} + +impl Storable for AuthSchemePreference { + type Storer = StoreReplace; +} + +impl IntoIterator for AuthSchemePreference { + type Item = AuthSchemeId; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.preference_list.into_iter() + } +} + +impl From for AuthSchemePreference +where + T: AsRef<[AuthSchemeId]>, +{ + fn from(slice: T) -> Self { + AuthSchemePreference { + preference_list: slice.as_ref().to_vec(), + } + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/auth/http.rs b/rust-runtime/aws-smithy-runtime-api/src/client/auth/http.rs index bb6ee4cc494..17e1db80e84 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/auth/http.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/auth/http.rs @@ -6,13 +6,13 @@ use crate::client::auth::AuthSchemeId; /// Auth scheme ID for HTTP API key based authentication. -pub const HTTP_API_KEY_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("http-api-key-auth"); +pub const HTTP_API_KEY_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("httpApiKeyAuth"); /// Auth scheme ID for HTTP Basic Auth. -pub const HTTP_BASIC_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("http-basic-auth"); +pub const HTTP_BASIC_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("httpBasicAuth"); /// Auth scheme ID for HTTP Bearer Auth. -pub const HTTP_BEARER_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("http-bearer-auth"); +pub const HTTP_BEARER_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("httpBearerAuth"); /// Auth scheme ID for HTTP Digest Auth. -pub const HTTP_DIGEST_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("http-digest-auth"); +pub const HTTP_DIGEST_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("httpDigestAuth"); diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index ef735e3e348..eb7b50c4a1d 100644 --- a/rust-runtime/aws-smithy-runtime/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-runtime" -version = "1.8.4" +version = "1.8.5" authors = ["AWS Rust SDK Team ", "Zelda Hessler "] description = "The new smithy runtime crate" edition = "2021" diff --git a/rust-runtime/aws-smithy-runtime/src/client/auth/no_auth.rs b/rust-runtime/aws-smithy-runtime/src/client/auth/no_auth.rs index 195bbd75c86..f1972ccf1c2 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/auth/no_auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/auth/no_auth.rs @@ -20,7 +20,7 @@ use aws_smithy_types::config_bag::ConfigBag; use std::borrow::Cow; /// Auth scheme ID for "no auth". -pub const NO_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("no_auth"); +pub const NO_AUTH_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("noAuth"); /// A [`RuntimePlugin`] that registers a "no auth" identity resolver and auth scheme. /// 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 0e2f69db29b..ecef661d0c6 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs @@ -7,8 +7,8 @@ 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, + AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOption, + AuthSchemeOptionResolverParams, AuthSchemePreference, ResolveAuthSchemeOptions, }; use aws_smithy_runtime_api::client::endpoint::{EndpointResolverParams, ResolveEndpoint}; use aws_smithy_runtime_api::client::identity::{Identity, ResolveIdentity}; @@ -19,6 +19,7 @@ use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace}; use aws_smithy_types::endpoint::Endpoint; use aws_smithy_types::Document; use std::borrow::Cow; +use std::collections::HashMap; use std::error::Error as StdError; use std::fmt; use tracing::trace; @@ -125,6 +126,8 @@ pub(super) async fn resolve_identity( let options = option_resolver .resolve_auth_scheme_options_v2(params, cfg, runtime_components) .await?; + let options = + reprioritize_with_auth_scheme_preference(options, cfg.load::()).await; trace!( auth_scheme_option_resolver_params = ?params, @@ -185,6 +188,40 @@ pub(super) async fn resolve_identity( Err(NoMatchingAuthSchemeError(explored).into()) } +// Re-prioritize `supported_auth_scheme_options` based on `auth_scheme_preference` +// +// Schemes in `auth_scheme_preference` that are not present in `supported_auth_scheme_options` will be ignored. +async fn reprioritize_with_auth_scheme_preference( + supported_auth_scheme_options: Vec, + auth_scheme_preference: Option<&AuthSchemePreference>, +) -> Vec { + match auth_scheme_preference { + Some(preference) => { + // maps auth scheme ID to the index in the preference list + let preference_map: HashMap<_, _> = preference + .clone() + .into_iter() + .enumerate() + .map(|(i, s)| (s, i)) + .collect(); + let (mut preferred, non_preferred): (Vec<_>, Vec<_>) = supported_auth_scheme_options + .into_iter() + .partition(|auth_scheme_option| { + preference_map.contains_key(auth_scheme_option.scheme_id()) + }); + + preferred.sort_by_key(|opt| { + preference_map + .get(opt.scheme_id()) + .expect("guaranteed by `partition`") + }); + preferred.extend(non_preferred); + preferred + } + None => supported_auth_scheme_options, + } +} + pub(super) fn sign_request( scheme_id: &AuthSchemeId, identity: &Identity, @@ -885,4 +922,214 @@ mod tests { panic!("The error should indicate that the explored list was truncated."); } } + + #[cfg(feature = "http-auth")] + #[tokio::test] + async fn test_resolve_identity() { + use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation, BasicAuthScheme}; + use aws_smithy_runtime_api::client::auth::http::{ + HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID, + }; + use aws_smithy_runtime_api::client::identity::http::Token; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; + + #[derive(Debug)] + struct Cache; + impl ResolveCachedIdentity for Cache { + fn resolve_cached_identity<'a>( + &'a self, + identity_resolver: SharedIdentityResolver, + rc: &'a RuntimeComponents, + cfg: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::new( + async move { identity_resolver.resolve_identity(rc, cfg).await }, + ) + } + } + + let mut layer = Layer::new("test"); + layer.store_put(AuthSchemeAndEndpointOrchestrationV2); + layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter")); + let mut cfg = ConfigBag::of_layers(vec![layer]); + + let runtime_components_builder = RuntimeComponentsBuilder::for_tests() + .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new())) + .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new( + "result:", + ApiKeyLocation::Header, + "Authorization", + ))) + .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new( + StaticAuthSchemeOptionResolver::new(vec![ + HTTP_BASIC_AUTH_SCHEME_ID, + HTTP_API_KEY_AUTH_SCHEME_ID, + ]), + ))) + .with_identity_cache(Some(Cache)); + + struct TestCase { + builder_updater: Box RuntimeComponents>, + resolved_auth_scheme: AuthSchemeId, + should_error: bool, + } + + for test_case in [ + TestCase { + builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| { + rcb.with_identity_resolver( + HTTP_BASIC_AUTH_SCHEME_ID, + SharedIdentityResolver::new(Token::new("basic", None)), + ) + .with_identity_resolver( + HTTP_API_KEY_AUTH_SCHEME_ID, + SharedIdentityResolver::new(Token::new("api-key", None)), + ) + .build() + .unwrap() + }), + resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID, + should_error: false, + }, + TestCase { + builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| { + rcb.with_identity_resolver( + HTTP_BASIC_AUTH_SCHEME_ID, + SharedIdentityResolver::new(Token::new("basic", None)), + ) + .build() + .unwrap() + }), + resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID, + should_error: false, + }, + TestCase { + builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| { + rcb.with_identity_resolver( + HTTP_API_KEY_AUTH_SCHEME_ID, + SharedIdentityResolver::new(Token::new("api-key", None)), + ) + .build() + .unwrap() + }), + resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID, + should_error: false, + }, + TestCase { + builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| rcb.build().unwrap()), + resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID, + should_error: true, + }, + ] + .into_iter() + { + let runtime_components = + (test_case.builder_updater)(runtime_components_builder.clone()); + match resolve_identity(&runtime_components, &mut cfg).await { + Ok(resolved) => assert_eq!(test_case.resolved_auth_scheme, resolved.0), + Err(e) if test_case.should_error => { + assert!(e.downcast_ref::().is_some()); + } + _ => { + panic!("`resolve_identity` returned an `Err` when no error was expected in the test."); + } + } + } + } + + #[cfg(feature = "http-auth")] + #[tokio::test] + async fn auth_scheme_preference() { + use aws_smithy_runtime_api::client::auth::http::{ + HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID, + }; + + struct TestCase { + supported: Vec, + preference: Option, + expected_resolved_auths: Vec, + } + + for test_case in [ + TestCase { + supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID] + .map(AuthSchemeOption::from) + .to_vec(), + preference: None, + expected_resolved_auths: vec![ + HTTP_API_KEY_AUTH_SCHEME_ID, + HTTP_BASIC_AUTH_SCHEME_ID, + ], + }, + TestCase { + supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID] + .map(AuthSchemeOption::from) + .to_vec(), + preference: Some([].into()), + expected_resolved_auths: vec![ + HTTP_API_KEY_AUTH_SCHEME_ID, + HTTP_BASIC_AUTH_SCHEME_ID, + ], + }, + TestCase { + supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID] + .map(AuthSchemeOption::from) + .to_vec(), + preference: Some(["bogus"].map(AuthSchemeId::from).into()), + expected_resolved_auths: vec![ + HTTP_API_KEY_AUTH_SCHEME_ID, + HTTP_BASIC_AUTH_SCHEME_ID, + ], + }, + TestCase { + supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID] + .map(AuthSchemeOption::from) + .to_vec(), + preference: Some([HTTP_BASIC_AUTH_SCHEME_ID].into()), + expected_resolved_auths: vec![ + HTTP_BASIC_AUTH_SCHEME_ID, + HTTP_API_KEY_AUTH_SCHEME_ID, + ], + }, + TestCase { + supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID] + .map(AuthSchemeOption::from) + .to_vec(), + preference: Some([HTTP_BASIC_AUTH_SCHEME_ID, HTTP_API_KEY_AUTH_SCHEME_ID].into()), + expected_resolved_auths: vec![ + HTTP_BASIC_AUTH_SCHEME_ID, + HTTP_API_KEY_AUTH_SCHEME_ID, + ], + }, + TestCase { + supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID] + .map(AuthSchemeOption::from) + .to_vec(), + preference: Some( + [ + HTTP_BASIC_AUTH_SCHEME_ID, + HTTP_BEARER_AUTH_SCHEME_ID, + HTTP_API_KEY_AUTH_SCHEME_ID, + ] + .into(), + ), + expected_resolved_auths: vec![ + HTTP_BASIC_AUTH_SCHEME_ID, + HTTP_API_KEY_AUTH_SCHEME_ID, + ], + }, + ] { + let actual = reprioritize_with_auth_scheme_preference( + test_case.supported, + test_case.preference.as_ref(), + ) + .await; + let actual = actual + .iter() + .map(|opt| opt.scheme_id()) + .cloned() + .collect::>(); + assert_eq!(test_case.expected_resolved_auths, actual); + } + } }