Skip to content

Commit 9dd0c23

Browse files
authored
Add S3 Express bucket metric tracking-v2 (#4365)
## Motivation and Context Track S3 Express bucket credential usage in User-Agent metrics to provide visibility into S3 Express adoption and usage patterns. ## Description This PR adds tracking for S3 Express bucket credentials by embedding the S3ExpressBucket feature in credentials resolved through the S3 Express identity provider. The feature is reported as metric code J in the User-Agent header. ## Testing - Unit tests verify the feature is correctly embedded in S3 Express credentials - Integration tests verify metric J appears in User-Agent for S3 Express bucket operations - Integration tests verify metric J does NOT appear when S3 Express auth is disabled or for regular buckets ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [ ] I have updated CHANGELOG.next.toml if I made changes to the smithy-rs codegen or runtime crates - [ ] I have updated CHANGELOG.next.toml if I made changes to the AWS SDK, generated SDK code, or SDK runtime crates ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 0a0fb2e commit 9dd0c23

File tree

7 files changed

+220
-21
lines changed

7 files changed

+220
-21
lines changed

aws/rust-runtime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/rust-runtime/aws-credential-types/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-credential-types"
3-
version = "1.2.8"
3+
version = "1.2.9"
44
authors = ["AWS Rust SDK Team <[email protected]>"]
55
description = "Types for AWS SDK credentials."
66
edition = "2021"

aws/rust-runtime/aws-credential-types/src/credential_feature.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub enum AwsCredentialFeature {
4949
CredentialsImds,
5050
/// An operation called using a Bearer token resolved from service-specific environment variables
5151
BearerServiceEnvVars,
52+
/// An operation called using S3 Express bucket credentials
53+
S3ExpressBucket,
5254
}
5355

5456
impl Storable for AwsCredentialFeature {

aws/rust-runtime/aws-inlineable/src/s3_express.rs

Lines changed: 166 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ pub(crate) mod identity_provider {
445445

446446
use crate::s3_express::identity_cache::S3ExpressIdentityCache;
447447
use crate::types::SessionCredentials;
448+
use aws_credential_types::credential_feature::AwsCredentialFeature;
448449
use aws_credential_types::provider::error::CredentialsError;
449450
use aws_credential_types::Credentials;
450451
use aws_smithy_async::time::{SharedTimeSource, TimeSource};
@@ -516,11 +517,11 @@ pub(crate) mod identity_provider {
516517
let creds = self
517518
.express_session_credentials(bucket_name, runtime_components, config_bag)
518519
.await?;
519-
let data = Credentials::try_from(creds)?;
520-
Ok((
521-
Identity::new(data.clone(), data.expiry()),
522-
data.expiry().unwrap(),
523-
))
520+
let mut data = Credentials::try_from(creds)?;
521+
data.get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
522+
.push(AwsCredentialFeature::S3ExpressBucket);
523+
let expiry = data.expiry().unwrap();
524+
Ok((Identity::from(data), expiry))
524525
})
525526
.await
526527
}
@@ -628,6 +629,150 @@ pub(crate) mod identity_provider {
628629
IdentityCacheLocation::IdentityResolver
629630
}
630631
}
632+
633+
#[cfg(test)]
634+
mod tests {
635+
use super::*;
636+
use aws_credential_types::credential_feature::AwsCredentialFeature;
637+
use aws_credential_types::Credentials;
638+
639+
// Helper function to create test runtime components with SigV4 identity resolver
640+
fn create_test_runtime_components(
641+
base_credentials: Credentials,
642+
) -> aws_smithy_runtime_api::client::runtime_components::RuntimeComponents {
643+
use aws_credential_types::provider::SharedCredentialsProvider;
644+
use aws_smithy_runtime::client::http::test_util::infallible_client_fn;
645+
use aws_smithy_runtime::client::orchestrator::endpoints::StaticUriEndpointResolver;
646+
use aws_smithy_runtime::client::retries::strategy::NeverRetryStrategy;
647+
use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
648+
use aws_smithy_runtime_api::client::identity::SharedIdentityResolver;
649+
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
650+
use aws_smithy_types::body::SdkBody;
651+
652+
let sigv4_resolver =
653+
SharedIdentityResolver::new(SharedCredentialsProvider::new(base_credentials));
654+
655+
// Create a simple auth scheme option resolver for testing
656+
let auth_option_resolver =
657+
StaticAuthSchemeOptionResolver::new(vec![aws_runtime::auth::sigv4::SCHEME_ID]);
658+
659+
let http_client = infallible_client_fn(|_req| {
660+
http::Response::builder()
661+
.status(200)
662+
.body(SdkBody::from(
663+
r#"<?xml version="1.0" encoding="UTF-8"?>
664+
<CreateSessionResult>
665+
<Credentials>
666+
<AccessKeyId>session_access_key</AccessKeyId>
667+
<SecretAccessKey>session_secret_key</SecretAccessKey>
668+
<SessionToken>session_token</SessionToken>
669+
<Expiration>2025-01-01T00:00:00Z</Expiration>
670+
</Credentials>
671+
</CreateSessionResult>"#,
672+
))
673+
.unwrap()
674+
});
675+
676+
RuntimeComponentsBuilder::for_tests()
677+
.with_identity_resolver(aws_runtime::auth::sigv4::SCHEME_ID, sigv4_resolver)
678+
.with_http_client(Some(http_client))
679+
.with_time_source(Some(aws_smithy_async::time::SystemTimeSource::new()))
680+
.with_retry_strategy(Some(NeverRetryStrategy::new()))
681+
.with_auth_scheme_option_resolver(Some(auth_option_resolver))
682+
.with_endpoint_resolver(Some(StaticUriEndpointResolver::http_localhost(8080)))
683+
.build()
684+
.unwrap()
685+
}
686+
687+
// Helper function to create config bag with minimal S3 Express bucket parameters
688+
fn create_test_config_bag(bucket_name: &str) -> aws_smithy_types::config_bag::ConfigBag {
689+
use aws_smithy_runtime_api::client::endpoint::EndpointResolverParams;
690+
use aws_smithy_runtime_api::client::stalled_stream_protection::StalledStreamProtectionConfig;
691+
use aws_smithy_types::config_bag::{ConfigBag, Layer};
692+
693+
let mut config_bag = ConfigBag::base();
694+
let mut layer = Layer::new("test");
695+
696+
let endpoint_params = EndpointResolverParams::new(
697+
crate::config::endpoint::Params::builder()
698+
.bucket(bucket_name)
699+
.build()
700+
.unwrap(),
701+
);
702+
layer.store_put(endpoint_params);
703+
704+
layer.store_put(StalledStreamProtectionConfig::disabled());
705+
706+
layer.store_put(crate::config::Region::new("us-west-2"));
707+
708+
config_bag.push_layer(layer);
709+
710+
config_bag
711+
}
712+
713+
#[test]
714+
fn test_session_credentials_conversion() {
715+
let session_creds = SessionCredentials::builder()
716+
.access_key_id("test_access_key")
717+
.secret_access_key("test_secret_key")
718+
.session_token("test_session_token")
719+
.expiration(aws_smithy_types::DateTime::from_secs(1000))
720+
.build()
721+
.expect("valid session credentials");
722+
723+
let credentials =
724+
Credentials::try_from(session_creds).expect("conversion should succeed");
725+
726+
assert_eq!(credentials.access_key_id(), "test_access_key");
727+
assert_eq!(credentials.secret_access_key(), "test_secret_key");
728+
assert_eq!(credentials.session_token(), Some("test_session_token"));
729+
}
730+
731+
#[tokio::test]
732+
async fn test_identity_provider_embeds_s3express_feature() {
733+
let bucket_name = "test-bucket--usw2-az1--x-s3";
734+
735+
// Use helper functions to set up test components
736+
let base_credentials = Credentials::for_tests();
737+
let runtime_components = create_test_runtime_components(base_credentials);
738+
let config_bag = create_test_config_bag(bucket_name);
739+
740+
// Create the identity provider
741+
let provider = DefaultS3ExpressIdentityProvider::builder()
742+
.behavior_version(crate::config::BehaviorVersion::latest())
743+
.time_source(aws_smithy_async::time::SystemTimeSource::new())
744+
.build();
745+
746+
// Call identity() and verify the S3ExpressBucket feature is present
747+
let identity = provider
748+
.identity(&runtime_components, &config_bag)
749+
.await
750+
.expect("identity() should succeed");
751+
752+
let credentials = identity
753+
.data::<Credentials>()
754+
.expect("Identity should contain Credentials");
755+
let features = credentials
756+
.get_property::<Vec<AwsCredentialFeature>>()
757+
.expect("Credentials should have features");
758+
assert!(
759+
features.contains(&AwsCredentialFeature::S3ExpressBucket),
760+
"S3ExpressBucket feature should be present in Credentials' property field"
761+
);
762+
763+
let identity_layer = identity
764+
.property::<aws_smithy_types::config_bag::FrozenLayer>()
765+
.expect("Identity should have a property layer");
766+
let identity_features: Vec<AwsCredentialFeature> = identity_layer
767+
.load::<AwsCredentialFeature>()
768+
.cloned()
769+
.collect();
770+
assert!(
771+
identity_features.contains(&AwsCredentialFeature::S3ExpressBucket),
772+
"S3ExpressBucket feature should be present in Identity's property field"
773+
);
774+
}
775+
}
631776
}
632777

633778
/// Supporting code for S3 Express runtime plugin
@@ -787,28 +932,29 @@ pub(crate) mod runtime_plugin {
787932
// Disable option is set from service client.
788933
let disable_s3_express_session_token = crate::config::DisableS3ExpressSessionAuth(true);
789934

790-
// An environment variable says the session auth is _not_ disabled, but it will be
791-
// overruled by what is in `layer`.
935+
// An environment variable says the session auth is _not_ disabled,
936+
// but it will be overruled by what is in `layer`.
792937
let actual = config(
793938
Some(disable_s3_express_session_token),
794939
Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "false")]),
795940
);
796941

797-
// A config layer from this runtime plugin should not provide a new `DisableS3ExpressSessionAuth`
798-
// if the disable option is set from service client.
942+
// A config layer from this runtime plugin should not provide
943+
// a new `DisableS3ExpressSessionAuth` if the disable option is set from service client.
799944
assert!(actual
800945
.load::<crate::config::DisableS3ExpressSessionAuth>()
801946
.is_none());
802947
}
803948

804949
#[test]
805950
fn disable_option_set_from_env_should_take_the_second_highest_precedence() {
806-
// An environment variable says session auth is disabled
951+
// Disable option is set from environment variable.
807952
let actual = config(
808953
None,
809954
Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "true")]),
810955
);
811956

957+
// The config layer should provide `DisableS3ExpressSessionAuth` from the environment variable.
812958
assert!(
813959
actual
814960
.load::<crate::config::DisableS3ExpressSessionAuth>()
@@ -820,49 +966,54 @@ pub(crate) mod runtime_plugin {
820966
#[should_panic]
821967
#[test]
822968
fn disable_option_set_from_profile_file_should_take_the_lowest_precedence() {
823-
// TODO(aws-sdk-rust#1073): Implement a test that mimics only setting
824-
// `s3_disable_express_session_auth` in a profile file
825-
todo!()
969+
todo!("TODO(aws-sdk-rust#1073): Implement profile file test")
826970
}
827971

828972
#[test]
829973
fn disable_option_should_be_unspecified_if_unset() {
974+
// Disable option is not set anywhere.
830975
let actual = config(None, Env::from_slice(&[]));
831976

977+
// The config layer should not provide `DisableS3ExpressSessionAuth` when it's not configured.
832978
assert!(actual
833979
.load::<crate::config::DisableS3ExpressSessionAuth>()
834980
.is_none());
835981
}
836982

837983
#[test]
838984
fn s3_express_runtime_plugin_should_set_default_identity_resolver() {
985+
// Config has SigV4 credentials provider, so S3 Express identity resolver should be set.
839986
let config = crate::Config::builder()
840987
.behavior_version_latest()
841988
.time_source(aws_smithy_async::time::SystemTimeSource::new())
842989
.credentials_provider(Credentials::for_tests())
843990
.build();
844991

845992
let actual = runtime_components_builder(config);
993+
// The runtime plugin should provide a default S3 Express identity resolver.
846994
assert!(actual
847995
.identity_resolver(&crate::s3_express::auth::SCHEME_ID)
848996
.is_some());
849997
}
850998

851999
#[test]
8521000
fn s3_express_plugin_should_not_set_default_identity_resolver_without_sigv4_counterpart() {
1001+
// Config does not have SigV4 credentials provider.
8531002
let config = crate::Config::builder()
8541003
.behavior_version_latest()
8551004
.time_source(aws_smithy_async::time::SystemTimeSource::new())
8561005
.build();
8571006

8581007
let actual = runtime_components_builder(config);
1008+
// The runtime plugin should not provide S3 Express identity resolver without SigV4 credentials.
8591009
assert!(actual
8601010
.identity_resolver(&crate::s3_express::auth::SCHEME_ID)
8611011
.is_none());
8621012
}
8631013

8641014
#[tokio::test]
8651015
async fn s3_express_plugin_should_not_set_default_identity_resolver_if_user_provided() {
1016+
// User provides a custom S3 Express credentials provider.
8661017
let expected_access_key_id = "expected acccess key ID";
8671018
let config = crate::Config::builder()
8681019
.behavior_version_latest()
@@ -877,13 +1028,13 @@ pub(crate) mod runtime_plugin {
8771028
.time_source(aws_smithy_async::time::SystemTimeSource::new())
8781029
.build();
8791030

880-
// `RuntimeComponentsBuilder` from `S3ExpressRuntimePlugin` should not provide an S3Express identity resolver.
1031+
// The runtime plugin should not override the user-provided identity resolver.
8811032
let runtime_components_builder = runtime_components_builder(config.clone());
8821033
assert!(runtime_components_builder
8831034
.identity_resolver(&crate::s3_express::auth::SCHEME_ID)
8841035
.is_none());
8851036

886-
// Get the S3Express identity resolver from the service config.
1037+
// The user-provided identity resolver should be used.
8871038
let express_identity_resolver = config
8881039
.runtime_components
8891040
.identity_resolver(&crate::s3_express::auth::SCHEME_ID)
@@ -896,7 +1047,6 @@ pub(crate) mod runtime_plugin {
8961047
.await
8971048
.unwrap();
8981049

899-
// Verify credentials are the one generated by the S3Express identity resolver user provided.
9001050
assert_eq!(
9011051
expected_access_key_id,
9021052
creds.data::<Credentials>().unwrap().access_key_id()

aws/rust-runtime/aws-runtime/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-runtime"
3-
version = "1.5.13"
3+
version = "1.5.14"
44
authors = ["AWS Rust SDK Team <[email protected]>"]
55
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
66
edition = "2021"

aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ impl ProvideBusinessMetric for AwsCredentialFeature {
258258
CredentialsHttp => Some(BusinessMetric::CredentialsHttp),
259259
CredentialsImds => Some(BusinessMetric::CredentialsImds),
260260
BearerServiceEnvVars => Some(BusinessMetric::BearerServiceEnvVars),
261+
S3ExpressBucket => Some(BusinessMetric::S3ExpressBucket),
261262
otherwise => {
262263
// This may occur if a customer upgrades only the `aws-smithy-runtime-api` crate
263264
// while continuing to use an outdated version of an SDK crate or the `aws-credential-types`

0 commit comments

Comments
 (0)