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`");
|
448 451 | params.bucket().ok_or("A bucket was not set in endpoint params".into())
|
449 452 | }
|
450 453 |
|
451 454 | async fn express_session_credentials<'a>(
|
452 455 | &'a self,
|
453 456 | bucket_name: &'a str,
|
454 457 | runtime_components: &'a RuntimeComponents,
|
455 458 | config_bag: &'a ConfigBag,
|
456 459 | ) -> Result<SessionCredentials, BoxError> {
|
457 460 | let mut config_builder = crate::config::Builder::from_config_bag(config_bag).behavior_version(self.behavior_version);
|
458 461 |
|
459 462 | // inherits all runtime components from a current S3 operation but clears out
|
460 463 | // out interceptors configured for that operation
|
461 464 | let mut rc_builder = runtime_components.to_builder();
|
462 465 | rc_builder.set_interceptors(std::iter::empty::<SharedInterceptor>());
|
463 466 | config_builder.runtime_components = rc_builder;
|
464 467 |
|
465 468 | let client = crate::Client::from_conf(config_builder.build());
|
466 469 | let response = client.create_session().bucket(bucket_name).send().await?;
|
467 470 |
|
497 500 | pub(crate) fn buffer_time(mut self, buffer_time: Duration) -> Self {
|
498 501 | self.set_buffer_time(Some(buffer_time));
|
499 502 | self
|
500 503 | }
|
501 504 | #[allow(dead_code)]
|
502 505 | pub(crate) fn set_buffer_time(&mut self, buffer_time: Option<Duration>) -> &mut Self {
|
503 506 | self.buffer_time = buffer_time;
|
504 507 | self
|
505 508 | }
|
506 509 | pub(crate) fn build(self) -> DefaultS3ExpressIdentityProvider {
|
507 510 | DefaultS3ExpressIdentityProvider {
|
508 511 | behavior_version: self.behavior_version.expect("required field `behavior_version` should be set"),
|
509 512 | cache: S3ExpressIdentityCache::new(
|
510 513 | DEFAULT_MAX_CACHE_CAPACITY,
|
511 514 | self.time_source.unwrap_or_default(),
|
512 515 | self.buffer_time.unwrap_or(DEFAULT_BUFFER_TIME),
|
513 516 | ),
|
514 517 | }
|
515 518 | }
|
516 519 | }
|
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 + | #[test]
|
538 + | fn test_s3express_identity_contains_feature() {
|
539 + | let session_creds = SessionCredentials::builder()
|
540 + | .access_key_id("test_access_key")
|
541 + | .secret_access_key("test_secret_key")
|
542 + | .session_token("test_session_token")
|
543 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
544 + | .build()
|
545 + | .expect("valid session credentials");
|
546 + |
|
547 + | let mut credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
548 + | credentials
|
549 + | .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
|
550 + | .push(AwsCredentialFeature::S3ExpressBucket);
|
551 + |
|
552 + | let creds_features = credentials
|
553 + | .get_property::<Vec<AwsCredentialFeature>>()
|
554 + | .expect("features should be present in credentials");
|
555 + | assert!(
|
556 + | creds_features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
557 + | "S3ExpressBucket feature should be embedded in Credentials"
|
558 + | );
|
559 + |
|
560 + | let identity = Identity::from(credentials.clone());
|
561 + | assert!(identity.data::<Credentials>().is_some(), "Identity should contain Credentials");
|
562 + |
|
563 + | let identity_creds = identity.data::<Credentials>().expect("should have credentials");
|
564 + | let identity_features = identity_creds
|
565 + | .get_property::<Vec<AwsCredentialFeature>>()
|
566 + | .expect("features should be present in Identity's credentials");
|
567 + | assert!(
|
568 + | identity_features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
569 + | "S3ExpressBucket feature should be present in Identity's Credentials after conversion"
|
570 + | );
|
571 + | }
|
572 + |
|
573 + | #[test]
|
574 + | fn test_session_credentials_conversion() {
|
575 + | let session_creds = SessionCredentials::builder()
|
576 + | .access_key_id("test_access_key")
|
577 + | .secret_access_key("test_secret_key")
|
578 + | .session_token("test_session_token")
|
579 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
580 + | .build()
|
581 + | .expect("valid session credentials");
|
582 + |
|
583 + | let credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
584 + |
|
585 + | assert_eq!(credentials.access_key_id(), "test_access_key");
|
586 + | assert_eq!(credentials.secret_access_key(), "test_secret_key");
|
587 + | assert_eq!(credentials.session_token(), Some("test_session_token"));
|
588 + | }
|
589 + | }
|
527 590 | }
|
528 591 |
|
529 592 | /// Supporting code for S3 Express runtime plugin
|
530 593 | pub(crate) mod runtime_plugin {
|
531 594 | use std::borrow::Cow;
|
532 595 |
|
533 596 | use aws_runtime::auth::SigV4SessionTokenNameOverride;
|
534 597 | use aws_sigv4::http_request::{SignatureLocation, SigningSettings};
|
535 598 | use aws_smithy_runtime_api::{
|
536 599 | box_error::BoxError,
|
537 600 | client::{runtime_components::RuntimeComponentsBuilder, runtime_plugin::RuntimePlugin},
|
538 601 | };
|
539 602 | use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Layer};
|
540 603 | use aws_types::os_shim_internal::Env;
|
541 604 |
|
542 605 | mod env {
|
543 606 | pub(super) const S3_DISABLE_EXPRESS_SESSION_AUTH: &str = "AWS_S3_DISABLE_EXPRESS_SESSION_AUTH";
|
544 607 | }
|
545 608 |
|
546 609 | #[derive(Debug)]
|
547 610 | pub(crate) struct S3ExpressRuntimePlugin {
|
548 611 | config: FrozenLayer,
|
549 612 | runtime_components_builder: RuntimeComponentsBuilder,
|
550 613 | }
|
551 614 |
|
552 615 | impl S3ExpressRuntimePlugin {
|
553 616 | // `new` will be called as `additional_client_plugins` within `base_client_runtime_plugins`.
|
554 617 | // This guarantees that `new` receives a fully constructed service config, with required
|
555 618 | // runtime components registered with `RuntimeComponents`.
|
556 619 | pub(crate) fn new(service_config: crate::config::Config) -> Self {
|
641 704 | }
|
642 705 |
|
643 706 | #[cfg(test)]
|
644 707 | mod tests {
|
645 708 | use super::*;
|
646 709 | use aws_credential_types::Credentials;
|
647 710 | use aws_smithy_runtime_api::client::identity::ResolveIdentity;
|
648 711 |
|
649 712 | #[test]
|
650 713 | fn disable_option_set_from_service_client_should_take_the_highest_precedence() {
|
651 - | // Disable option is set from service client.
|
652 714 | let disable_s3_express_session_token = crate::config::DisableS3ExpressSessionAuth(true);
|
653 715 |
|
654 - | // An environment variable says the session auth is _not_ disabled, but it will be
|
655 - | // overruled by what is in `layer`.
|
656 716 | let actual = config(
|
657 717 | Some(disable_s3_express_session_token),
|
658 718 | Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "false")]),
|
659 719 | );
|
660 720 |
|
661 - | // A config layer from this runtime plugin should not provide a new `DisableS3ExpressSessionAuth`
|
662 - | // if the disable option is set from service client.
|
663 721 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
664 722 | }
|
665 723 |
|
666 724 | #[test]
|
667 725 | fn disable_option_set_from_env_should_take_the_second_highest_precedence() {
|
668 - | // An environment variable says session auth is disabled
|
669 726 | let actual = config(None, Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "true")]));
|
670 727 |
|
671 728 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().unwrap().0);
|
672 729 | }
|
673 730 |
|
674 731 | #[should_panic]
|
675 732 | #[test]
|
676 733 | 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!()
|
734 + | todo!("TODO(aws-sdk-rust#1073): Implement profile file test")
|
680 735 | }
|
681 736 |
|
682 737 | #[test]
|
683 738 | fn disable_option_should_be_unspecified_if_unset() {
|
684 739 | let actual = config(None, Env::from_slice(&[]));
|
685 740 |
|
686 741 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
687 742 | }
|
688 743 |
|
689 744 | #[test]
|
690 745 | fn s3_express_runtime_plugin_should_set_default_identity_resolver() {
|
691 746 | let config = crate::Config::builder()
|
692 747 | .behavior_version_latest()
|
693 748 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
694 749 | .credentials_provider(Credentials::for_tests())
|
695 750 | .build();
|
696 751 |
|
697 752 | let actual = runtime_components_builder(config);
|
698 753 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_some());
|
699 754 | }
|
700 755 |
|
701 756 | #[test]
|
702 757 | fn s3_express_plugin_should_not_set_default_identity_resolver_without_sigv4_counterpart() {
|
703 758 | let config = crate::Config::builder()
|
704 759 | .behavior_version_latest()
|
705 760 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
706 761 | .build();
|
707 762 |
|
708 763 | let actual = runtime_components_builder(config);
|
709 764 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_none());
|
710 765 | }
|
711 766 |
|
712 767 | #[tokio::test]
|
713 768 | async fn s3_express_plugin_should_not_set_default_identity_resolver_if_user_provided() {
|
714 769 | let expected_access_key_id = "expected acccess key ID";
|
715 770 | let config = crate::Config::builder()
|
716 771 | .behavior_version_latest()
|
717 772 | .credentials_provider(Credentials::for_tests())
|
718 773 | .express_credentials_provider(Credentials::new(
|
719 774 | expected_access_key_id,
|
720 775 | "secret",
|
721 776 | None,
|
722 777 | None,
|
723 778 | "test express credentials provider",
|
724 779 | ))
|
725 780 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
726 781 | .build();
|
727 782 |
|
728 - | // `RuntimeComponentsBuilder` from `S3ExpressRuntimePlugin` should not provide an S3Express identity resolver.
|
729 783 | let runtime_components_builder = runtime_components_builder(config.clone());
|
730 784 | assert!(runtime_components_builder
|
731 785 | .identity_resolver(&crate::s3_express::auth::SCHEME_ID)
|
732 786 | .is_none());
|
733 787 |
|
734 - | // Get the S3Express identity resolver from the service config.
|
735 788 | let express_identity_resolver = config.runtime_components.identity_resolver(&crate::s3_express::auth::SCHEME_ID).unwrap();
|
736 789 | let creds = express_identity_resolver
|
737 790 | .resolve_identity(&RuntimeComponentsBuilder::for_tests().build().unwrap(), &ConfigBag::base())
|
738 791 | .await
|
739 792 | .unwrap();
|
740 793 |
|
741 - | // Verify credentials are the one generated by the S3Express identity resolver user provided.
|
742 794 | assert_eq!(expected_access_key_id, creds.data::<Credentials>().unwrap().access_key_id());
|
743 795 | }
|
744 796 | }
|
745 797 | }
|
746 798 |
|
747 799 | pub(crate) mod checksum {
|
748 800 | use crate::http_request_checksum::DefaultRequestChecksumOverride;
|
749 801 | use aws_smithy_checksums::ChecksumAlgorithm;
|
750 802 | use aws_smithy_types::config_bag::ConfigBag;
|
751 803 |
|