367 367 | expect_identity(3000, &sut, key3, || async move { panic!("new identity should not be loaded") }).await;
|
368 368 | }
|
369 369 | }
|
370 370 | }
|
371 371 | /// Supporting code for S3 Express identity provider
|
372 372 | pub(crate) mod identity_provider {
|
373 373 | use std::time::{Duration, SystemTime};
|
374 374 |
|
375 375 | use crate::s3_express::identity_cache::S3ExpressIdentityCache;
|
376 376 | use crate::types::SessionCredentials;
|
377 + | use aws_credential_types::credential_feature::AwsCredentialFeature;
|
377 378 | use aws_credential_types::provider::error::CredentialsError;
|
378 379 | use aws_credential_types::Credentials;
|
379 380 | use aws_smithy_async::time::{SharedTimeSource, TimeSource};
|
380 381 | use aws_smithy_runtime_api::box_error::BoxError;
|
381 382 | use aws_smithy_runtime_api::client::endpoint::EndpointResolverParams;
|
382 383 | use aws_smithy_runtime_api::client::identity::{Identity, IdentityCacheLocation, IdentityFuture, ResolveCachedIdentity, ResolveIdentity};
|
383 384 | use aws_smithy_runtime_api::client::interceptors::SharedInterceptor;
|
384 385 | use aws_smithy_runtime_api::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
|
385 386 | use aws_smithy_runtime_api::shared::IntoShared;
|
386 387 | use aws_smithy_types::config_bag::ConfigBag;
|
387 388 |
|
388 389 | use super::identity_cache::{DEFAULT_BUFFER_TIME, DEFAULT_MAX_CACHE_CAPACITY};
|
389 390 |
|
390 391 | #[derive(Debug)]
|
391 392 | pub(crate) struct DefaultS3ExpressIdentityProvider {
|
392 393 | behavior_version: crate::config::BehaviorVersion,
|
393 394 | cache: S3ExpressIdentityCache,
|
394 395 | }
|
395 396 |
|
396 397 | impl TryFrom<SessionCredentials> for Credentials {
|
397 398 | type Error = BoxError;
|
398 399 |
|
399 400 | fn try_from(session_creds: SessionCredentials) -> Result<Self, Self::Error> {
|
400 401 | Ok(Credentials::new(
|
401 402 | session_creds.access_key_id,
|
402 403 | session_creds.secret_access_key,
|
403 404 | Some(session_creds.session_token),
|
404 405 | Some(
|
405 406 | SystemTime::try_from(session_creds.expiration)
|
406 407 | .map_err(|_| CredentialsError::unhandled("credential expiration time cannot be represented by a SystemTime"))?,
|
407 408 | ),
|
408 409 | "s3express",
|
409 410 | ))
|
410 411 | }
|
411 412 | }
|
412 413 |
|
413 414 | impl DefaultS3ExpressIdentityProvider {
|
414 415 | pub(crate) fn builder() -> Builder {
|
415 416 | Builder::default()
|
416 417 | }
|
417 418 |
|
418 419 | async fn identity<'a>(&'a self, runtime_components: &'a RuntimeComponents, config_bag: &'a ConfigBag) -> Result<Identity, BoxError> {
|
419 420 | let bucket_name = self.bucket_name(config_bag)?;
|
420 421 |
|
421 422 | let sigv4_identity_resolver = runtime_components
|
422 423 | .identity_resolver(aws_runtime::auth::sigv4::SCHEME_ID)
|
423 424 | .ok_or("identity resolver for sigv4 should be set for S3")?;
|
424 425 | let aws_identity = runtime_components
|
425 426 | .identity_cache()
|
426 427 | .resolve_cached_identity(sigv4_identity_resolver, runtime_components, config_bag)
|
427 428 | .await?;
|
428 429 |
|
429 430 | let credentials = aws_identity
|
430 431 | .data::<Credentials>()
|
431 432 | .ok_or("wrong identity type for SigV4. Expected AWS credentials but got `{identity:?}")?;
|
432 433 |
|
433 434 | let key = self.cache.key(bucket_name, credentials);
|
434 435 | self.cache
|
435 436 | .get_or_load(key, || async move {
|
436 437 | let creds = self.express_session_credentials(bucket_name, runtime_components, config_bag).await?;
|
437 - | let data = Credentials::try_from(creds)?;
|
438 + | let mut data = Credentials::try_from(creds)?;
|
439 + | data.get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
|
440 + | .push(AwsCredentialFeature::S3ExpressBucket);
|
438 441 | Ok((Identity::new(data.clone(), data.expiry()), data.expiry().unwrap()))
|
439 442 | })
|
440 443 | .await
|
441 444 | }
|
442 445 |
|
443 446 | fn bucket_name<'a>(&'a self, config_bag: &'a ConfigBag) -> Result<&'a str, BoxError> {
|
444 447 | let params = config_bag.load::<EndpointResolverParams>().expect("endpoint resolver params must be set");
|
445 448 | let params = params
|
446 449 | .get::<crate::config::endpoint::Params>()
|
447 450 | .expect("`Params` should be wrapped in `EndpointResolverParams`");
|
517 520 |
|
518 521 | impl ResolveIdentity for DefaultS3ExpressIdentityProvider {
|
519 522 | fn resolve_identity<'a>(&'a self, runtime_components: &'a RuntimeComponents, config_bag: &'a ConfigBag) -> IdentityFuture<'a> {
|
520 523 | IdentityFuture::new(async move { self.identity(runtime_components, config_bag).await })
|
521 524 | }
|
522 525 |
|
523 526 | fn cache_location(&self) -> IdentityCacheLocation {
|
524 527 | IdentityCacheLocation::IdentityResolver
|
525 528 | }
|
526 529 | }
|
530 + |
|
531 + | #[cfg(test)]
|
532 + | mod tests {
|
533 + | use super::*;
|
534 + | use aws_credential_types::credential_feature::AwsCredentialFeature;
|
535 + | use aws_credential_types::Credentials;
|
536 + |
|
537 + | // Helper function to create test runtime components with SigV4 identity resolver
|
538 + | fn create_test_runtime_components(base_credentials: Credentials) -> aws_smithy_runtime_api::client::runtime_components::RuntimeComponents {
|
539 + | use aws_credential_types::provider::SharedCredentialsProvider;
|
540 + | use aws_smithy_runtime::client::http::test_util::infallible_client_fn;
|
541 + | use aws_smithy_runtime::client::orchestrator::endpoints::StaticUriEndpointResolver;
|
542 + | use aws_smithy_runtime::client::retries::strategy::NeverRetryStrategy;
|
543 + | use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
|
544 + | use aws_smithy_runtime_api::client::identity::SharedIdentityResolver;
|
545 + | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
|
546 + | use aws_smithy_types::body::SdkBody;
|
547 + |
|
548 + | let sigv4_resolver = SharedIdentityResolver::new(SharedCredentialsProvider::new(base_credentials));
|
549 + |
|
550 + | // Create a simple auth scheme option resolver for testing
|
551 + | let auth_option_resolver = StaticAuthSchemeOptionResolver::new(vec![aws_runtime::auth::sigv4::SCHEME_ID]);
|
552 + |
|
553 + | let http_client = infallible_client_fn(|_req| {
|
554 + | http::Response::builder()
|
555 + | .status(200)
|
556 + | .body(SdkBody::from(
|
557 + | r#"<?xml version="1.0" encoding="UTF-8"?>
|
558 + | <CreateSessionResult>
|
559 + | <Credentials>
|
560 + | <AccessKeyId>session_access_key</AccessKeyId>
|
561 + | <SecretAccessKey>session_secret_key</SecretAccessKey>
|
562 + | <SessionToken>session_token</SessionToken>
|
563 + | <Expiration>2025-01-01T00:00:00Z</Expiration>
|
564 + | </Credentials>
|
565 + | </CreateSessionResult>"#,
|
566 + | ))
|
567 + | .unwrap()
|
568 + | });
|
569 + |
|
570 + | RuntimeComponentsBuilder::for_tests()
|
571 + | .with_identity_resolver(aws_runtime::auth::sigv4::SCHEME_ID, sigv4_resolver)
|
572 + | .with_http_client(Some(http_client))
|
573 + | .with_time_source(Some(aws_smithy_async::time::SystemTimeSource::new()))
|
574 + | .with_retry_strategy(Some(NeverRetryStrategy::new()))
|
575 + | .with_auth_scheme_option_resolver(Some(auth_option_resolver))
|
576 + | .with_endpoint_resolver(Some(StaticUriEndpointResolver::http_localhost(8080)))
|
577 + | .build()
|
578 + | .unwrap()
|
579 + | }
|
580 + |
|
581 + | // Helper function to create config bag with S3 Express bucket endpoint parameters
|
582 + | fn create_test_config_bag(bucket_name: &str) -> aws_smithy_types::config_bag::ConfigBag {
|
583 + | use aws_runtime::auth::SigV4OperationSigningConfig;
|
584 + | use aws_smithy_runtime_api::client::endpoint::EndpointResolverParams;
|
585 + | use aws_smithy_runtime_api::client::stalled_stream_protection::StalledStreamProtectionConfig;
|
586 + | use aws_smithy_types::config_bag::{ConfigBag, Layer};
|
587 + | use aws_smithy_types::endpoint::Endpoint;
|
588 + | use aws_types::region::{Region, SigningRegion};
|
589 + | use aws_types::SigningName;
|
590 + |
|
591 + | let mut config_bag = ConfigBag::base();
|
592 + | let mut layer = Layer::new("test");
|
593 + |
|
594 + | let endpoint_params = EndpointResolverParams::new(crate::config::endpoint::Params::builder().bucket(bucket_name).build().unwrap());
|
595 + | layer.store_put(endpoint_params);
|
596 + |
|
597 + | // Add a test endpoint
|
598 + | let endpoint = Endpoint::builder()
|
599 + | .url(format!("https://{}.s3express-usw2-az1.us-west-2.amazonaws.com", bucket_name))
|
600 + | .build();
|
601 + | layer.store_put(endpoint);
|
602 + |
|
603 + | // Add stalled stream protection config
|
604 + | layer.store_put(StalledStreamProtectionConfig::disabled());
|
605 + |
|
606 + | // Add region for the S3 client config
|
607 + | layer.store_put(crate::config::Region::new("us-west-2"));
|
608 + |
|
609 + | // Add SigV4 operation signing config
|
610 + | let signing_config = SigV4OperationSigningConfig {
|
611 + | region: Some(SigningRegion::from(Region::new("us-west-2"))),
|
612 + | name: Some(SigningName::from_static("s3")),
|
613 + | ..Default::default()
|
614 + | };
|
615 + | layer.store_put(signing_config);
|
616 + |
|
617 + | config_bag.push_layer(layer);
|
618 + |
|
619 + | config_bag
|
620 + | }
|
621 + |
|
622 + | #[test]
|
623 + | fn test_session_credentials_conversion() {
|
624 + | let session_creds = SessionCredentials::builder()
|
625 + | .access_key_id("test_access_key")
|
626 + | .secret_access_key("test_secret_key")
|
627 + | .session_token("test_session_token")
|
628 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
629 + | .build()
|
630 + | .expect("valid session credentials");
|
631 + |
|
632 + | let credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
633 + |
|
634 + | assert_eq!(credentials.access_key_id(), "test_access_key");
|
635 + | assert_eq!(credentials.secret_access_key(), "test_secret_key");
|
636 + | assert_eq!(credentials.session_token(), Some("test_session_token"));
|
637 + | }
|
638 + |
|
639 + | #[tokio::test]
|
640 + | async fn test_identity_provider_embeds_s3express_feature() {
|
641 + | let bucket_name = "test-bucket--usw2-az1--x-s3";
|
642 + |
|
643 + | // Use helper functions to set up test components
|
644 + | let base_credentials = Credentials::for_tests();
|
645 + | let runtime_components = create_test_runtime_components(base_credentials);
|
646 + | let config_bag = create_test_config_bag(bucket_name);
|
647 + |
|
648 + | // Create the identity provider
|
649 + | let provider = DefaultS3ExpressIdentityProvider::builder()
|
650 + | .behavior_version(crate::config::BehaviorVersion::latest())
|
651 + | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
652 + | .build();
|
653 + |
|
654 + | // Call identity() and verify the S3ExpressBucket feature is present
|
655 + | let identity = provider
|
656 + | .identity(&runtime_components, &config_bag)
|
657 + | .await
|
658 + | .expect("identity() should succeed");
|
659 + |
|
660 + | let credentials = identity.data::<Credentials>().expect("Identity should contain Credentials");
|
661 + |
|
662 + | let features = credentials
|
663 + | .get_property::<Vec<AwsCredentialFeature>>()
|
664 + | .expect("Credentials should have features");
|
665 + |
|
666 + | assert!(
|
667 + | features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
668 + | "S3ExpressBucket feature should be present in credentials returned by identity()"
|
669 + | );
|
670 + | }
|
671 + | }
|
527 672 | }
|
528 673 |
|
529 674 | /// Supporting code for S3 Express runtime plugin
|
530 675 | pub(crate) mod runtime_plugin {
|
531 676 | use std::borrow::Cow;
|
532 677 |
|
533 678 | use aws_runtime::auth::SigV4SessionTokenNameOverride;
|
534 679 | use aws_sigv4::http_request::{SignatureLocation, SigningSettings};
|
535 680 | use aws_smithy_runtime_api::{
|
536 681 | box_error::BoxError,
|
644 789 | mod tests {
|
645 790 | use super::*;
|
646 791 | use aws_credential_types::Credentials;
|
647 792 | use aws_smithy_runtime_api::client::identity::ResolveIdentity;
|
648 793 |
|
649 794 | #[test]
|
650 795 | fn disable_option_set_from_service_client_should_take_the_highest_precedence() {
|
651 796 | // Disable option is set from service client.
|
652 797 | let disable_s3_express_session_token = crate::config::DisableS3ExpressSessionAuth(true);
|
653 798 |
|
654 - | // An environment variable says the session auth is _not_ disabled, but it will be
|
655 - | // overruled by what is in `layer`.
|
799 + | // An environment variable says the session auth is _not_ disabled,
|
800 + | // but it will be overruled by what is in `layer`.
|
656 801 | let actual = config(
|
657 802 | Some(disable_s3_express_session_token),
|
658 803 | Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "false")]),
|
659 804 | );
|
660 805 |
|
661 - | // A config layer from this runtime plugin should not provide a new `DisableS3ExpressSessionAuth`
|
662 - | // if the disable option is set from service client.
|
806 + | // A config layer from this runtime plugin should not provide
|
807 + | // a new `DisableS3ExpressSessionAuth` if the disable option is set from service client.
|
663 808 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
664 809 | }
|
665 810 |
|
666 811 | #[test]
|
667 812 | fn disable_option_set_from_env_should_take_the_second_highest_precedence() {
|
668 - | // An environment variable says session auth is disabled
|
813 + | // Disable option is set from environment variable.
|
669 814 | let actual = config(None, Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "true")]));
|
670 815 |
|
816 + | // The config layer should provide `DisableS3ExpressSessionAuth` from the environment variable.
|
671 817 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().unwrap().0);
|
672 818 | }
|
673 819 |
|
674 820 | #[should_panic]
|
675 821 | #[test]
|
676 822 | fn disable_option_set_from_profile_file_should_take_the_lowest_precedence() {
|
677 - | // TODO(aws-sdk-rust#1073): Implement a test that mimics only setting
|
678 - | // `s3_disable_express_session_auth` in a profile file
|
679 - | todo!()
|
823 + | todo!("TODO(aws-sdk-rust#1073): Implement profile file test")
|
680 824 | }
|
681 825 |
|
682 826 | #[test]
|
683 827 | fn disable_option_should_be_unspecified_if_unset() {
|
828 + | // Disable option is not set anywhere.
|
684 829 | let actual = config(None, Env::from_slice(&[]));
|
685 830 |
|
831 + | // The config layer should not provide `DisableS3ExpressSessionAuth` when it's not configured.
|
686 832 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
687 833 | }
|
688 834 |
|
689 835 | #[test]
|
690 836 | fn s3_express_runtime_plugin_should_set_default_identity_resolver() {
|
837 + | // Config has SigV4 credentials provider, so S3 Express identity resolver should be set.
|
691 838 | let config = crate::Config::builder()
|
692 839 | .behavior_version_latest()
|
693 840 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
694 841 | .credentials_provider(Credentials::for_tests())
|
695 842 | .build();
|
696 843 |
|
697 844 | let actual = runtime_components_builder(config);
|
845 + | // The runtime plugin should provide a default S3 Express identity resolver.
|
698 846 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_some());
|
699 847 | }
|
700 848 |
|
701 849 | #[test]
|
702 850 | fn s3_express_plugin_should_not_set_default_identity_resolver_without_sigv4_counterpart() {
|
851 + | // Config does not have SigV4 credentials provider.
|
703 852 | let config = crate::Config::builder()
|
704 853 | .behavior_version_latest()
|
705 854 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
706 855 | .build();
|
707 856 |
|
708 857 | let actual = runtime_components_builder(config);
|
858 + | // The runtime plugin should not provide S3 Express identity resolver without SigV4 credentials.
|
709 859 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_none());
|
710 860 | }
|
711 861 |
|
712 862 | #[tokio::test]
|
713 863 | async fn s3_express_plugin_should_not_set_default_identity_resolver_if_user_provided() {
|
864 + | // User provides a custom S3 Express credentials provider.
|
714 865 | let expected_access_key_id = "expected acccess key ID";
|
715 866 | let config = crate::Config::builder()
|
716 867 | .behavior_version_latest()
|
717 868 | .credentials_provider(Credentials::for_tests())
|
718 869 | .express_credentials_provider(Credentials::new(
|
719 870 | expected_access_key_id,
|
720 871 | "secret",
|
721 872 | None,
|
722 873 | None,
|
723 874 | "test express credentials provider",
|
724 875 | ))
|
725 876 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
726 877 | .build();
|
727 878 |
|
728 - | // `RuntimeComponentsBuilder` from `S3ExpressRuntimePlugin` should not provide an S3Express identity resolver.
|
879 + | // The runtime plugin should not override the user-provided identity resolver.
|
729 880 | let runtime_components_builder = runtime_components_builder(config.clone());
|
730 881 | assert!(runtime_components_builder
|
731 882 | .identity_resolver(&crate::s3_express::auth::SCHEME_ID)
|
732 883 | .is_none());
|
733 884 |
|
734 - | // Get the S3Express identity resolver from the service config.
|
885 + | // The user-provided identity resolver should be used.
|
735 886 | let express_identity_resolver = config.runtime_components.identity_resolver(&crate::s3_express::auth::SCHEME_ID).unwrap();
|
736 887 | let creds = express_identity_resolver
|
737 888 | .resolve_identity(&RuntimeComponentsBuilder::for_tests().build().unwrap(), &ConfigBag::base())
|
738 889 | .await
|
739 890 | .unwrap();
|
740 891 |
|
741 - | // Verify credentials are the one generated by the S3Express identity resolver user provided.
|
742 892 | assert_eq!(expected_access_key_id, creds.data::<Credentials>().unwrap().access_key_id());
|
743 893 | }
|
744 894 | }
|
745 895 | }
|
746 896 |
|
747 897 | pub(crate) mod checksum {
|
748 898 | use crate::http_request_checksum::DefaultRequestChecksumOverride;
|
749 899 | use aws_smithy_checksums::ChecksumAlgorithm;
|
750 900 | use aws_smithy_types::config_bag::ConfigBag;
|
751 901 |
|