347 347 | expect_identity(2000, &sut, key2, || {
|
348 348 | let identity_resolver = identity_resolver.clone();
|
349 349 | let runtime_components = runtime_components.clone();
|
350 350 | async move { load(identity_resolver, &runtime_components).await }
|
351 351 | })
|
352 352 | .await;
|
353 353 |
|
354 354 | // This should pupulate a cache entry for `key3`, but evicting a cache entry for `key1` because the cache is full.
|
355 355 | expect_identity(3000, &sut, key3.clone(), || {
|
356 356 | let identity_resolver = identity_resolver.clone();
|
357 357 | let runtime_components = runtime_components.clone();
|
358 358 | async move { load(identity_resolver, &runtime_components).await }
|
359 359 | })
|
360 360 | .await;
|
361 361 |
|
362 362 | // Attempt to get an identity for `key1` should end up fetching a new one since its cache entry has been evicted.
|
363 363 | // This fetch should now evict a cache entry for `key2`.
|
364 364 | expect_identity(4000, &sut, key1, || async move { load(identity_resolver, &runtime_components).await }).await;
|
365 365 |
|
366 366 | // A cache entry for `key3` should still exist in the cache.
|
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 minimal S3 Express bucket parameters
|
582 + | fn create_test_config_bag(bucket_name: &str) -> aws_smithy_types::config_bag::ConfigBag {
|
583 + | use aws_smithy_runtime_api::client::endpoint::EndpointResolverParams;
|
584 + | use aws_smithy_runtime_api::client::stalled_stream_protection::StalledStreamProtectionConfig;
|
585 + | use aws_smithy_types::config_bag::{ConfigBag, Layer};
|
586 + |
|
587 + | let mut config_bag = ConfigBag::base();
|
588 + | let mut layer = Layer::new("test");
|
589 + |
|
590 + | let endpoint_params = EndpointResolverParams::new(crate::config::endpoint::Params::builder().bucket(bucket_name).build().unwrap());
|
591 + | layer.store_put(endpoint_params);
|
592 + |
|
593 + | layer.store_put(StalledStreamProtectionConfig::disabled());
|
594 + |
|
595 + | layer.store_put(crate::config::Region::new("us-west-2"));
|
596 + |
|
597 + | config_bag.push_layer(layer);
|
598 + |
|
599 + | config_bag
|
600 + | }
|
601 + |
|
602 + | #[test]
|
603 + | fn test_session_credentials_conversion() {
|
604 + | let session_creds = SessionCredentials::builder()
|
605 + | .access_key_id("test_access_key")
|
606 + | .secret_access_key("test_secret_key")
|
607 + | .session_token("test_session_token")
|
608 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
609 + | .build()
|
610 + | .expect("valid session credentials");
|
611 + |
|
612 + | let credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
613 + |
|
614 + | assert_eq!(credentials.access_key_id(), "test_access_key");
|
615 + | assert_eq!(credentials.secret_access_key(), "test_secret_key");
|
616 + | assert_eq!(credentials.session_token(), Some("test_session_token"));
|
617 + | }
|
618 + |
|
619 + | #[tokio::test]
|
620 + | async fn test_identity_provider_embeds_s3express_feature() {
|
621 + | let bucket_name = "test-bucket--usw2-az1--x-s3";
|
622 + |
|
623 + | // Use helper functions to set up test components
|
624 + | let base_credentials = Credentials::for_tests();
|
625 + | let runtime_components = create_test_runtime_components(base_credentials);
|
626 + | let config_bag = create_test_config_bag(bucket_name);
|
627 + |
|
628 + | // Create the identity provider
|
629 + | let provider = DefaultS3ExpressIdentityProvider::builder()
|
630 + | .behavior_version(crate::config::BehaviorVersion::latest())
|
631 + | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
632 + | .build();
|
633 + |
|
634 + | // Call identity() and verify the S3ExpressBucket feature is present
|
635 + | let identity = provider
|
636 + | .identity(&runtime_components, &config_bag)
|
637 + | .await
|
638 + | .expect("identity() should succeed");
|
639 + |
|
640 + | let credentials = identity.data::<Credentials>().expect("Identity should contain Credentials");
|
641 + |
|
642 + | let features = credentials
|
643 + | .get_property::<Vec<AwsCredentialFeature>>()
|
644 + | .expect("Credentials should have features");
|
645 + |
|
646 + | assert!(
|
647 + | features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
648 + | "S3ExpressBucket feature should be present in credentials returned by identity()"
|
649 + | );
|
650 + | }
|
651 + | }
|
527 652 | }
|
528 653 |
|
529 654 | /// Supporting code for S3 Express runtime plugin
|
530 655 | pub(crate) mod runtime_plugin {
|
531 656 | use std::borrow::Cow;
|
532 657 |
|
533 658 | use aws_runtime::auth::SigV4SessionTokenNameOverride;
|
534 659 | use aws_sigv4::http_request::{SignatureLocation, SigningSettings};
|
535 660 | use aws_smithy_runtime_api::{
|
536 661 | box_error::BoxError,
|
644 769 | mod tests {
|
645 770 | use super::*;
|
646 771 | use aws_credential_types::Credentials;
|
647 772 | use aws_smithy_runtime_api::client::identity::ResolveIdentity;
|
648 773 |
|
649 774 | #[test]
|
650 775 | fn disable_option_set_from_service_client_should_take_the_highest_precedence() {
|
651 776 | // Disable option is set from service client.
|
652 777 | let disable_s3_express_session_token = crate::config::DisableS3ExpressSessionAuth(true);
|
653 778 |
|
654 - | // An environment variable says the session auth is _not_ disabled, but it will be
|
655 - | // overruled by what is in `layer`.
|
779 + | // An environment variable says the session auth is _not_ disabled,
|
780 + | // but it will be overruled by what is in `layer`.
|
656 781 | let actual = config(
|
657 782 | Some(disable_s3_express_session_token),
|
658 783 | Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "false")]),
|
659 784 | );
|
660 785 |
|
661 - | // A config layer from this runtime plugin should not provide a new `DisableS3ExpressSessionAuth`
|
662 - | // if the disable option is set from service client.
|
786 + | // A config layer from this runtime plugin should not provide
|
787 + | // a new `DisableS3ExpressSessionAuth` if the disable option is set from service client.
|
663 788 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
664 789 | }
|
665 790 |
|
666 791 | #[test]
|
667 792 | fn disable_option_set_from_env_should_take_the_second_highest_precedence() {
|
668 - | // An environment variable says session auth is disabled
|
793 + | // Disable option is set from environment variable.
|
669 794 | let actual = config(None, Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "true")]));
|
670 795 |
|
796 + | // The config layer should provide `DisableS3ExpressSessionAuth` from the environment variable.
|
671 797 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().unwrap().0);
|
672 798 | }
|
673 799 |
|
674 800 | #[should_panic]
|
675 801 | #[test]
|
676 802 | 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!()
|
803 + | todo!("TODO(aws-sdk-rust#1073): Implement profile file test")
|
680 804 | }
|
681 805 |
|
682 806 | #[test]
|
683 807 | fn disable_option_should_be_unspecified_if_unset() {
|
808 + | // Disable option is not set anywhere.
|
684 809 | let actual = config(None, Env::from_slice(&[]));
|
685 810 |
|
811 + | // The config layer should not provide `DisableS3ExpressSessionAuth` when it's not configured.
|
686 812 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
687 813 | }
|
688 814 |
|
689 815 | #[test]
|
690 816 | fn s3_express_runtime_plugin_should_set_default_identity_resolver() {
|
817 + | // Config has SigV4 credentials provider, so S3 Express identity resolver should be set.
|
691 818 | let config = crate::Config::builder()
|
692 819 | .behavior_version_latest()
|
693 820 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
694 821 | .credentials_provider(Credentials::for_tests())
|
695 822 | .build();
|
696 823 |
|
697 824 | let actual = runtime_components_builder(config);
|
825 + | // The runtime plugin should provide a default S3 Express identity resolver.
|
698 826 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_some());
|
699 827 | }
|
700 828 |
|
701 829 | #[test]
|
702 830 | fn s3_express_plugin_should_not_set_default_identity_resolver_without_sigv4_counterpart() {
|
831 + | // Config does not have SigV4 credentials provider.
|
703 832 | let config = crate::Config::builder()
|
704 833 | .behavior_version_latest()
|
705 834 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
706 835 | .build();
|
707 836 |
|
708 837 | let actual = runtime_components_builder(config);
|
838 + | // The runtime plugin should not provide S3 Express identity resolver without SigV4 credentials.
|
709 839 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_none());
|
710 840 | }
|
711 841 |
|
712 842 | #[tokio::test]
|
713 843 | async fn s3_express_plugin_should_not_set_default_identity_resolver_if_user_provided() {
|
844 + | // User provides a custom S3 Express credentials provider.
|
714 845 | let expected_access_key_id = "expected acccess key ID";
|
715 846 | let config = crate::Config::builder()
|
716 847 | .behavior_version_latest()
|
717 848 | .credentials_provider(Credentials::for_tests())
|
718 849 | .express_credentials_provider(Credentials::new(
|
719 850 | expected_access_key_id,
|
720 851 | "secret",
|
721 852 | None,
|
722 853 | None,
|
723 854 | "test express credentials provider",
|
724 855 | ))
|
725 856 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
726 857 | .build();
|
727 858 |
|
728 - | // `RuntimeComponentsBuilder` from `S3ExpressRuntimePlugin` should not provide an S3Express identity resolver.
|
859 + | // The runtime plugin should not override the user-provided identity resolver.
|
729 860 | let runtime_components_builder = runtime_components_builder(config.clone());
|
730 861 | assert!(runtime_components_builder
|
731 862 | .identity_resolver(&crate::s3_express::auth::SCHEME_ID)
|
732 863 | .is_none());
|
733 864 |
|
734 - | // Get the S3Express identity resolver from the service config.
|
865 + | // The user-provided identity resolver should be used.
|
735 866 | let express_identity_resolver = config.runtime_components.identity_resolver(&crate::s3_express::auth::SCHEME_ID).unwrap();
|
736 867 | let creds = express_identity_resolver
|
737 868 | .resolve_identity(&RuntimeComponentsBuilder::for_tests().build().unwrap(), &ConfigBag::base())
|
738 869 | .await
|
739 870 | .unwrap();
|
740 871 |
|
741 - | // Verify credentials are the one generated by the S3Express identity resolver user provided.
|
742 872 | assert_eq!(expected_access_key_id, creds.data::<Credentials>().unwrap().access_key_id());
|
743 873 | }
|
744 874 | }
|
745 875 | }
|
746 876 |
|
747 877 | pub(crate) mod checksum {
|
748 878 | use crate::http_request_checksum::DefaultRequestChecksumOverride;
|
749 879 | use aws_smithy_checksums::ChecksumAlgorithm;
|
750 880 | use aws_smithy_types::config_bag::ConfigBag;
|
751 881 |
|