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 - | Ok((Identity::new(data.clone(), data.expiry()), data.expiry().unwrap()))
|
438 + | let mut data = Credentials::try_from(creds)?;
|
439 + | data.get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
|
440 + | .push(AwsCredentialFeature::S3ExpressBucket);
|
441 + | let expiry = data.expiry().unwrap();
|
442 + | Ok((Identity::from(data), expiry))
|
439 443 | })
|
440 444 | .await
|
441 445 | }
|
442 446 |
|
443 447 | fn bucket_name<'a>(&'a self, config_bag: &'a ConfigBag) -> Result<&'a str, BoxError> {
|
444 448 | let params = config_bag.load::<EndpointResolverParams>().expect("endpoint resolver params must be set");
|
445 449 | let params = params
|
446 450 | .get::<crate::config::endpoint::Params>()
|
447 451 | .expect("`Params` should be wrapped in `EndpointResolverParams`");
|
448 452 | params.bucket().ok_or("A bucket was not set in endpoint params".into())
|
517 521 |
|
518 522 | impl ResolveIdentity for DefaultS3ExpressIdentityProvider {
|
519 523 | fn resolve_identity<'a>(&'a self, runtime_components: &'a RuntimeComponents, config_bag: &'a ConfigBag) -> IdentityFuture<'a> {
|
520 524 | IdentityFuture::new(async move { self.identity(runtime_components, config_bag).await })
|
521 525 | }
|
522 526 |
|
523 527 | fn cache_location(&self) -> IdentityCacheLocation {
|
524 528 | IdentityCacheLocation::IdentityResolver
|
525 529 | }
|
526 530 | }
|
531 + |
|
532 + | #[cfg(test)]
|
533 + | mod tests {
|
534 + | use super::*;
|
535 + | use aws_credential_types::credential_feature::AwsCredentialFeature;
|
536 + | use aws_credential_types::Credentials;
|
537 + |
|
538 + | // Helper function to create test runtime components with SigV4 identity resolver
|
539 + | fn create_test_runtime_components(base_credentials: Credentials) -> aws_smithy_runtime_api::client::runtime_components::RuntimeComponents {
|
540 + | use aws_credential_types::provider::SharedCredentialsProvider;
|
541 + | use aws_smithy_runtime::client::http::test_util::infallible_client_fn;
|
542 + | use aws_smithy_runtime::client::orchestrator::endpoints::StaticUriEndpointResolver;
|
543 + | use aws_smithy_runtime::client::retries::strategy::NeverRetryStrategy;
|
544 + | use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
|
545 + | use aws_smithy_runtime_api::client::identity::SharedIdentityResolver;
|
546 + | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
|
547 + | use aws_smithy_types::body::SdkBody;
|
548 + |
|
549 + | let sigv4_resolver = SharedIdentityResolver::new(SharedCredentialsProvider::new(base_credentials));
|
550 + |
|
551 + | // Create a simple auth scheme option resolver for testing
|
552 + | let auth_option_resolver = StaticAuthSchemeOptionResolver::new(vec![aws_runtime::auth::sigv4::SCHEME_ID]);
|
553 + |
|
554 + | let http_client = infallible_client_fn(|_req| {
|
555 + | http::Response::builder()
|
556 + | .status(200)
|
557 + | .body(SdkBody::from(
|
558 + | r#"<?xml version="1.0" encoding="UTF-8"?>
|
559 + | <CreateSessionResult>
|
560 + | <Credentials>
|
561 + | <AccessKeyId>session_access_key</AccessKeyId>
|
562 + | <SecretAccessKey>session_secret_key</SecretAccessKey>
|
563 + | <SessionToken>session_token</SessionToken>
|
564 + | <Expiration>2025-01-01T00:00:00Z</Expiration>
|
565 + | </Credentials>
|
566 + | </CreateSessionResult>"#,
|
567 + | ))
|
568 + | .unwrap()
|
569 + | });
|
570 + |
|
571 + | RuntimeComponentsBuilder::for_tests()
|
572 + | .with_identity_resolver(aws_runtime::auth::sigv4::SCHEME_ID, sigv4_resolver)
|
573 + | .with_http_client(Some(http_client))
|
574 + | .with_time_source(Some(aws_smithy_async::time::SystemTimeSource::new()))
|
575 + | .with_retry_strategy(Some(NeverRetryStrategy::new()))
|
576 + | .with_auth_scheme_option_resolver(Some(auth_option_resolver))
|
577 + | .with_endpoint_resolver(Some(StaticUriEndpointResolver::http_localhost(8080)))
|
578 + | .build()
|
579 + | .unwrap()
|
580 + | }
|
581 + |
|
582 + | // Helper function to create config bag with minimal S3 Express bucket parameters
|
583 + | fn create_test_config_bag(bucket_name: &str) -> aws_smithy_types::config_bag::ConfigBag {
|
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 + |
|
588 + | let mut config_bag = ConfigBag::base();
|
589 + | let mut layer = Layer::new("test");
|
590 + |
|
591 + | let endpoint_params = EndpointResolverParams::new(crate::config::endpoint::Params::builder().bucket(bucket_name).build().unwrap());
|
592 + | layer.store_put(endpoint_params);
|
593 + |
|
594 + | layer.store_put(StalledStreamProtectionConfig::disabled());
|
595 + |
|
596 + | layer.store_put(crate::config::Region::new("us-west-2"));
|
597 + |
|
598 + | config_bag.push_layer(layer);
|
599 + |
|
600 + | config_bag
|
601 + | }
|
602 + |
|
603 + | #[test]
|
604 + | fn test_session_credentials_conversion() {
|
605 + | let session_creds = SessionCredentials::builder()
|
606 + | .access_key_id("test_access_key")
|
607 + | .secret_access_key("test_secret_key")
|
608 + | .session_token("test_session_token")
|
609 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
610 + | .build()
|
611 + | .expect("valid session credentials");
|
612 + |
|
613 + | let credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
614 + |
|
615 + | assert_eq!(credentials.access_key_id(), "test_access_key");
|
616 + | assert_eq!(credentials.secret_access_key(), "test_secret_key");
|
617 + | assert_eq!(credentials.session_token(), Some("test_session_token"));
|
618 + | }
|
619 + |
|
620 + | #[tokio::test]
|
621 + | async fn test_identity_provider_embeds_s3express_feature() {
|
622 + | let bucket_name = "test-bucket--usw2-az1--x-s3";
|
623 + |
|
624 + | // Use helper functions to set up test components
|
625 + | let base_credentials = Credentials::for_tests();
|
626 + | let runtime_components = create_test_runtime_components(base_credentials);
|
627 + | let config_bag = create_test_config_bag(bucket_name);
|
628 + |
|
629 + | // Create the identity provider
|
630 + | let provider = DefaultS3ExpressIdentityProvider::builder()
|
631 + | .behavior_version(crate::config::BehaviorVersion::latest())
|
632 + | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
633 + | .build();
|
634 + |
|
635 + | // Call identity() and verify the S3ExpressBucket feature is present
|
636 + | let identity = provider
|
637 + | .identity(&runtime_components, &config_bag)
|
638 + | .await
|
639 + | .expect("identity() should succeed");
|
640 + |
|
641 + | let credentials = identity.data::<Credentials>().expect("Identity should contain Credentials");
|
642 + | let features = credentials
|
643 + | .get_property::<Vec<AwsCredentialFeature>>()
|
644 + | .expect("Credentials should have features");
|
645 + | assert!(
|
646 + | features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
647 + | "S3ExpressBucket feature should be present in Credentials' property field"
|
648 + | );
|
649 + |
|
650 + | let identity_layer = identity
|
651 + | .property::<aws_smithy_types::config_bag::FrozenLayer>()
|
652 + | .expect("Identity should have a property layer");
|
653 + | let identity_features: Vec<AwsCredentialFeature> = identity_layer.load::<AwsCredentialFeature>().cloned().collect();
|
654 + | assert!(
|
655 + | identity_features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
656 + | "S3ExpressBucket feature should be present in Identity's property field"
|
657 + | );
|
658 + | }
|
659 + | }
|
527 660 | }
|
528 661 |
|
529 662 | /// Supporting code for S3 Express runtime plugin
|
530 663 | pub(crate) mod runtime_plugin {
|
531 664 | use std::borrow::Cow;
|
532 665 |
|
533 666 | use aws_runtime::auth::SigV4SessionTokenNameOverride;
|
534 667 | use aws_sigv4::http_request::{SignatureLocation, SigningSettings};
|
535 668 | use aws_smithy_runtime_api::{
|
536 669 | box_error::BoxError,
|
644 777 | mod tests {
|
645 778 | use super::*;
|
646 779 | use aws_credential_types::Credentials;
|
647 780 | use aws_smithy_runtime_api::client::identity::ResolveIdentity;
|
648 781 |
|
649 782 | #[test]
|
650 783 | fn disable_option_set_from_service_client_should_take_the_highest_precedence() {
|
651 784 | // Disable option is set from service client.
|
652 785 | let disable_s3_express_session_token = crate::config::DisableS3ExpressSessionAuth(true);
|
653 786 |
|
654 - | // An environment variable says the session auth is _not_ disabled, but it will be
|
655 - | // overruled by what is in `layer`.
|
787 + | // An environment variable says the session auth is _not_ disabled,
|
788 + | // but it will be overruled by what is in `layer`.
|
656 789 | let actual = config(
|
657 790 | Some(disable_s3_express_session_token),
|
658 791 | Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "false")]),
|
659 792 | );
|
660 793 |
|
661 - | // A config layer from this runtime plugin should not provide a new `DisableS3ExpressSessionAuth`
|
662 - | // if the disable option is set from service client.
|
794 + | // A config layer from this runtime plugin should not provide
|
795 + | // a new `DisableS3ExpressSessionAuth` if the disable option is set from service client.
|
663 796 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
664 797 | }
|
665 798 |
|
666 799 | #[test]
|
667 800 | fn disable_option_set_from_env_should_take_the_second_highest_precedence() {
|
668 - | // An environment variable says session auth is disabled
|
801 + | // Disable option is set from environment variable.
|
669 802 | let actual = config(None, Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "true")]));
|
670 803 |
|
804 + | // The config layer should provide `DisableS3ExpressSessionAuth` from the environment variable.
|
671 805 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().unwrap().0);
|
672 806 | }
|
673 807 |
|
674 808 | #[should_panic]
|
675 809 | #[test]
|
676 810 | 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!()
|
811 + | todo!("TODO(aws-sdk-rust#1073): Implement profile file test")
|
680 812 | }
|
681 813 |
|
682 814 | #[test]
|
683 815 | fn disable_option_should_be_unspecified_if_unset() {
|
816 + | // Disable option is not set anywhere.
|
684 817 | let actual = config(None, Env::from_slice(&[]));
|
685 818 |
|
819 + | // The config layer should not provide `DisableS3ExpressSessionAuth` when it's not configured.
|
686 820 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
687 821 | }
|
688 822 |
|
689 823 | #[test]
|
690 824 | fn s3_express_runtime_plugin_should_set_default_identity_resolver() {
|
825 + | // Config has SigV4 credentials provider, so S3 Express identity resolver should be set.
|
691 826 | let config = crate::Config::builder()
|
692 827 | .behavior_version_latest()
|
693 828 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
694 829 | .credentials_provider(Credentials::for_tests())
|
695 830 | .build();
|
696 831 |
|
697 832 | let actual = runtime_components_builder(config);
|
833 + | // The runtime plugin should provide a default S3 Express identity resolver.
|
698 834 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_some());
|
699 835 | }
|
700 836 |
|
701 837 | #[test]
|
702 838 | fn s3_express_plugin_should_not_set_default_identity_resolver_without_sigv4_counterpart() {
|
839 + | // Config does not have SigV4 credentials provider.
|
703 840 | let config = crate::Config::builder()
|
704 841 | .behavior_version_latest()
|
705 842 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
706 843 | .build();
|
707 844 |
|
708 845 | let actual = runtime_components_builder(config);
|
846 + | // The runtime plugin should not provide S3 Express identity resolver without SigV4 credentials.
|
709 847 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_none());
|
710 848 | }
|
711 849 |
|
712 850 | #[tokio::test]
|
713 851 | async fn s3_express_plugin_should_not_set_default_identity_resolver_if_user_provided() {
|
852 + | // User provides a custom S3 Express credentials provider.
|
714 853 | let expected_access_key_id = "expected acccess key ID";
|
715 854 | let config = crate::Config::builder()
|
716 855 | .behavior_version_latest()
|
717 856 | .credentials_provider(Credentials::for_tests())
|
718 857 | .express_credentials_provider(Credentials::new(
|
719 858 | expected_access_key_id,
|
720 859 | "secret",
|
721 860 | None,
|
722 861 | None,
|
723 862 | "test express credentials provider",
|
724 863 | ))
|
725 864 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
726 865 | .build();
|
727 866 |
|
728 - | // `RuntimeComponentsBuilder` from `S3ExpressRuntimePlugin` should not provide an S3Express identity resolver.
|
867 + | // The runtime plugin should not override the user-provided identity resolver.
|
729 868 | let runtime_components_builder = runtime_components_builder(config.clone());
|
730 869 | assert!(runtime_components_builder
|
731 870 | .identity_resolver(&crate::s3_express::auth::SCHEME_ID)
|
732 871 | .is_none());
|
733 872 |
|
734 - | // Get the S3Express identity resolver from the service config.
|
873 + | // The user-provided identity resolver should be used.
|
735 874 | let express_identity_resolver = config.runtime_components.identity_resolver(&crate::s3_express::auth::SCHEME_ID).unwrap();
|
736 875 | let creds = express_identity_resolver
|
737 876 | .resolve_identity(&RuntimeComponentsBuilder::for_tests().build().unwrap(), &ConfigBag::base())
|
738 877 | .await
|
739 878 | .unwrap();
|
740 879 |
|
741 - | // Verify credentials are the one generated by the S3Express identity resolver user provided.
|
742 880 | assert_eq!(expected_access_key_id, creds.data::<Credentials>().unwrap().access_key_id());
|
743 881 | }
|
744 882 | }
|
745 883 | }
|
746 884 |
|
747 885 | pub(crate) mod checksum {
|
748 886 | use crate::http_request_checksum::DefaultRequestChecksumOverride;
|
749 887 | use aws_smithy_checksums::ChecksumAlgorithm;
|
750 888 | use aws_smithy_types::config_bag::ConfigBag;
|
751 889 |
|