@@ -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( )
0 commit comments