223 223 | let runtime_components = RuntimeComponentsBuilder::for_tests()
|
224 224 | .with_time_source(Some(time.clone()))
|
225 225 | .with_sleep_impl(Some(TokioSleep::new()))
|
226 226 | .build()
|
227 227 | .unwrap();
|
228 228 |
|
229 229 | let sut = S3ExpressIdentityCache::new(1, time.clone().into_shared(), DEFAULT_BUFFER_TIME);
|
230 230 |
|
231 231 | let identity_resolver = test_identity_resolver(vec![Ok(identity_expiring_in(1000)), Ok(identity_expiring_in(2000))]);
|
232 232 |
|
233 - | let key = sut.key("test-bucket--usw2-az1--x-s3", &Credentials::for_tests_with_session_token());
|
233 + | let key = sut.key("test-bucket--usw2-az1--x-s3", &Credentials::for_tests());
|
234 234 |
|
235 235 | // First call to the cache, populating a cache entry.
|
236 236 | expect_identity(1000, &sut, key.clone(), || {
|
237 237 | let identity_resolver = identity_resolver.clone();
|
238 238 | let runtime_components = runtime_components.clone();
|
239 239 | async move { load(identity_resolver, &runtime_components).await }
|
240 240 | })
|
241 241 | .await;
|
242 242 |
|
243 243 | // Testing for a cache hit by advancing time such that the updated time is before the expiration of the first identity
|
244 244 | // i.e. 500 < 1000.
|
245 245 | time.set_time(epoch_secs(500));
|
246 246 |
|
247 247 | expect_identity(1000, &sut, key.clone(), || async move { panic!("new identity should not be loaded") }).await;
|
248 248 |
|
249 249 | // Testing for a cache miss by advancing time such that the updated time is now after the expiration of the first identity
|
250 250 | // and before the expiration of the second identity i.e. 1000 < 1500 && 1500 < 2000.
|
251 251 | time.set_time(epoch_secs(1500));
|
252 252 |
|
253 253 | expect_identity(2000, &sut, key, || async move { load(identity_resolver, &runtime_components).await }).await;
|
254 254 | }
|
255 255 |
|
256 256 | #[test]
|
257 257 | fn load_contention() {
|
258 258 | let rt = tokio::runtime::Builder::new_multi_thread()
|
259 259 | .enable_time()
|
260 260 | .worker_threads(16)
|
261 261 | .build()
|
262 262 | .unwrap();
|
263 263 |
|
264 264 | let time = ManualTimeSource::new(epoch_secs(0));
|
265 265 | let runtime_components = RuntimeComponentsBuilder::for_tests()
|
266 266 | .with_time_source(Some(time.clone()))
|
267 267 | .with_sleep_impl(Some(TokioSleep::new()))
|
268 268 | .build()
|
269 269 | .unwrap();
|
270 270 |
|
271 271 | let number_of_buckets = 4;
|
272 272 | let sut = Arc::new(S3ExpressIdentityCache::new(
|
273 273 | number_of_buckets,
|
274 274 | time.clone().into_shared(),
|
275 275 | DEFAULT_BUFFER_TIME,
|
276 276 | ));
|
277 277 |
|
278 278 | // Nested for loops below advance time by 200 in total, and each identity has the expiration
|
279 279 | // such that no matter what order async tasks are executed, it never expires.
|
280 280 | let safe_expiration = number_of_buckets as u64 * 50 + DEFAULT_BUFFER_TIME.as_secs() + 1;
|
281 281 | let identity_resolver = test_identity_resolver(vec![
|
282 282 | Ok(identity_expiring_in(safe_expiration)),
|
283 283 | Ok(identity_expiring_in(safe_expiration)),
|
284 284 | Ok(identity_expiring_in(safe_expiration)),
|
285 285 | Ok(identity_expiring_in(safe_expiration)),
|
286 286 | ]);
|
287 287 |
|
288 288 | let mut tasks = Vec::new();
|
289 289 | for i in 0..number_of_buckets {
|
290 - | let key = sut.key(&format!("test-bucket-{i}-usw2-az1--x-s3"), &Credentials::for_tests_with_session_token());
|
290 + | let key = sut.key(&format!("test-bucket-{i}-usw2-az1--x-s3"), &Credentials::for_tests());
|
291 291 | for _ in 0..50 {
|
292 292 | let sut = sut.clone();
|
293 293 | let key = key.clone();
|
294 294 | let identity_resolver = identity_resolver.clone();
|
295 295 | let time = time.clone();
|
296 296 | let runtime_components = runtime_components.clone();
|
297 297 | tasks.push(rt.spawn(async move {
|
298 298 | let now = time.advance(Duration::from_secs(1));
|
299 299 | let identity: Identity = sut
|
300 300 | .get_or_load(key, || async move { load(identity_resolver, &runtime_components).await })
|
301 301 | .await
|
302 302 | .unwrap();
|
303 303 |
|
304 304 | assert!(identity.expiration().unwrap() >= now, "{:?} >= {:?}", identity.expiration(), now);
|
305 305 | }));
|
306 306 | }
|
307 307 | }
|
308 308 | let tasks = tasks.into_iter().collect::<FuturesUnordered<_>>();
|
309 309 | for task in tasks {
|
310 310 | rt.block_on(task).unwrap();
|
311 311 | }
|
312 312 | }
|
313 313 |
|
314 314 | #[tokio::test]
|
315 315 | async fn identity_fetch_triggered_by_lru_eviction() {
|
316 316 | let time = ManualTimeSource::new(UNIX_EPOCH);
|
317 317 | let runtime_components = RuntimeComponentsBuilder::for_tests()
|
318 318 | .with_time_source(Some(time.clone()))
|
319 319 | .with_sleep_impl(Some(TokioSleep::new()))
|
320 320 | .build()
|
321 321 | .unwrap();
|
322 322 |
|
323 323 | // Create a cache of size 2.
|
324 324 | let sut = S3ExpressIdentityCache::new(2, time.into_shared(), DEFAULT_BUFFER_TIME);
|
325 325 |
|
326 326 | let identity_resolver = test_identity_resolver(vec![
|
327 327 | Ok(identity_expiring_in(1000)),
|
328 328 | Ok(identity_expiring_in(2000)),
|
329 329 | Ok(identity_expiring_in(3000)),
|
330 330 | Ok(identity_expiring_in(4000)),
|
331 331 | ]);
|
332 332 |
|
333 - | let [key1, key2, key3] =
|
334 - | [1, 2, 3].map(|i| sut.key(&format!("test-bucket-{i}--usw2-az1--x-s3"), &Credentials::for_tests_with_session_token()));
|
333 + | let [key1, key2, key3] = [1, 2, 3].map(|i| sut.key(&format!("test-bucket-{i}--usw2-az1--x-s3"), &Credentials::for_tests()));
|
335 334 |
|
336 335 | // This should pupulate a cache entry for `key1`.
|
337 336 | expect_identity(1000, &sut, key1.clone(), || {
|
338 337 | let identity_resolver = identity_resolver.clone();
|
339 338 | let runtime_components = runtime_components.clone();
|
340 339 | async move { load(identity_resolver, &runtime_components).await }
|
341 340 | })
|
342 341 | .await;
|
343 342 | // This immediate next call for `key1` should be a cache hit.
|
344 343 | expect_identity(1000, &sut, key1.clone(), || async move { panic!("new identity should not be loaded") }).await;
|
345 344 |
|
346 345 | // This should pupulate a cache entry for `key2`.
|
347 346 | expect_identity(2000, &sut, key2, || {
|
348 347 | let identity_resolver = identity_resolver.clone();
|
349 348 | let runtime_components = runtime_components.clone();
|
350 349 | async move { load(identity_resolver, &runtime_components).await }
|
351 350 | })
|
352 351 | .await;
|
353 352 |
|
354 353 | // This should pupulate a cache entry for `key3`, but evicting a cache entry for `key1` because the cache is full.
|
355 354 | expect_identity(3000, &sut, key3.clone(), || {
|
356 355 | let identity_resolver = identity_resolver.clone();
|
357 356 | let runtime_components = runtime_components.clone();
|
358 357 | async move { load(identity_resolver, &runtime_components).await }
|
359 358 | })
|
360 359 | .await;
|
361 360 |
|
362 361 | // Attempt to get an identity for `key1` should end up fetching a new one since its cache entry has been evicted.
|
363 362 | // This fetch should now evict a cache entry for `key2`.
|
364 363 | expect_identity(4000, &sut, key1, || async move { load(identity_resolver, &runtime_components).await }).await;
|
365 364 |
|
366 365 | // A cache entry for `key3` should still exist in the cache.
|
367 366 | expect_identity(3000, &sut, key3, || async move { panic!("new identity should not be loaded") }).await;
|
368 367 | }
|
369 368 | }
|
370 369 | }
|
371 370 | /// Supporting code for S3 Express identity provider
|
372 371 | pub(crate) mod identity_provider {
|
373 372 | use std::time::{Duration, SystemTime};
|
374 373 |
|
375 374 | use crate::s3_express::identity_cache::S3ExpressIdentityCache;
|
376 375 | use crate::types::SessionCredentials;
|
376 + | use aws_credential_types::credential_feature::AwsCredentialFeature;
|
377 377 | use aws_credential_types::provider::error::CredentialsError;
|
378 378 | use aws_credential_types::Credentials;
|
379 379 | use aws_smithy_async::time::{SharedTimeSource, TimeSource};
|
380 380 | use aws_smithy_runtime_api::box_error::BoxError;
|
381 381 | use aws_smithy_runtime_api::client::endpoint::EndpointResolverParams;
|
382 382 | use aws_smithy_runtime_api::client::identity::{Identity, IdentityCacheLocation, IdentityFuture, ResolveCachedIdentity, ResolveIdentity};
|
383 383 | use aws_smithy_runtime_api::client::interceptors::SharedInterceptor;
|
384 384 | use aws_smithy_runtime_api::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
|
385 385 | use aws_smithy_runtime_api::shared::IntoShared;
|
386 386 | use aws_smithy_types::config_bag::ConfigBag;
|
387 387 |
|
388 388 | use super::identity_cache::{DEFAULT_BUFFER_TIME, DEFAULT_MAX_CACHE_CAPACITY};
|
389 389 |
|
390 390 | #[derive(Debug)]
|
391 391 | pub(crate) struct DefaultS3ExpressIdentityProvider {
|
392 392 | behavior_version: crate::config::BehaviorVersion,
|
393 393 | cache: S3ExpressIdentityCache,
|
394 394 | }
|
395 395 |
|
396 396 | impl TryFrom<SessionCredentials> for Credentials {
|
397 397 | type Error = BoxError;
|
398 398 |
|
399 399 | fn try_from(session_creds: SessionCredentials) -> Result<Self, Self::Error> {
|
400 400 | Ok(Credentials::new(
|
401 401 | session_creds.access_key_id,
|
402 402 | session_creds.secret_access_key,
|
403 403 | Some(session_creds.session_token),
|
404 404 | Some(
|
405 405 | SystemTime::try_from(session_creds.expiration)
|
406 406 | .map_err(|_| CredentialsError::unhandled("credential expiration time cannot be represented by a SystemTime"))?,
|
407 407 | ),
|
408 408 | "s3express",
|
409 409 | ))
|
410 410 | }
|
411 411 | }
|
412 412 |
|
413 413 | impl DefaultS3ExpressIdentityProvider {
|
414 414 | pub(crate) fn builder() -> Builder {
|
415 415 | Builder::default()
|
416 416 | }
|
417 417 |
|
418 418 | async fn identity<'a>(&'a self, runtime_components: &'a RuntimeComponents, config_bag: &'a ConfigBag) -> Result<Identity, BoxError> {
|
419 419 | let bucket_name = self.bucket_name(config_bag)?;
|
420 420 |
|
421 421 | let sigv4_identity_resolver = runtime_components
|
422 422 | .identity_resolver(aws_runtime::auth::sigv4::SCHEME_ID)
|
423 423 | .ok_or("identity resolver for sigv4 should be set for S3")?;
|
424 424 | let aws_identity = runtime_components
|
425 425 | .identity_cache()
|
426 426 | .resolve_cached_identity(sigv4_identity_resolver, runtime_components, config_bag)
|
427 427 | .await?;
|
428 428 |
|
429 429 | let credentials = aws_identity
|
430 430 | .data::<Credentials>()
|
431 431 | .ok_or("wrong identity type for SigV4. Expected AWS credentials but got `{identity:?}")?;
|
432 432 |
|
433 433 | let key = self.cache.key(bucket_name, credentials);
|
434 434 | self.cache
|
435 435 | .get_or_load(key, || async move {
|
436 436 | let creds = self.express_session_credentials(bucket_name, runtime_components, config_bag).await?;
|
437 - | let data = Credentials::try_from(creds)?;
|
437 + | let mut data = Credentials::try_from(creds)?;
|
438 + | data.get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
|
439 + | .push(AwsCredentialFeature::S3ExpressBucket);
|
438 440 | Ok((Identity::new(data.clone(), data.expiry()), data.expiry().unwrap()))
|
439 441 | })
|
440 442 | .await
|
441 443 | }
|
442 444 |
|
443 445 | fn bucket_name<'a>(&'a self, config_bag: &'a ConfigBag) -> Result<&'a str, BoxError> {
|
444 446 | let params = config_bag.load::<EndpointResolverParams>().expect("endpoint resolver params must be set");
|
445 447 | let params = params
|
446 448 | .get::<crate::config::endpoint::Params>()
|
447 449 | .expect("`Params` should be wrapped in `EndpointResolverParams`");
|
517 519 |
|
518 520 | impl ResolveIdentity for DefaultS3ExpressIdentityProvider {
|
519 521 | fn resolve_identity<'a>(&'a self, runtime_components: &'a RuntimeComponents, config_bag: &'a ConfigBag) -> IdentityFuture<'a> {
|
520 522 | IdentityFuture::new(async move { self.identity(runtime_components, config_bag).await })
|
521 523 | }
|
522 524 |
|
523 525 | fn cache_location(&self) -> IdentityCacheLocation {
|
524 526 | IdentityCacheLocation::IdentityResolver
|
525 527 | }
|
526 528 | }
|
529 + |
|
530 + | #[cfg(test)]
|
531 + | mod tests {
|
532 + | use super::*;
|
533 + | use aws_credential_types::credential_feature::AwsCredentialFeature;
|
534 + | use aws_credential_types::Credentials;
|
535 + | use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient};
|
536 + |
|
537 + | #[test]
|
538 + | fn test_s3express_credentials_contain_feature() {
|
539 + | // This test verifies that when SessionCredentials are converted to Credentials
|
540 + | // within the identity provider code path, the S3ExpressBucket feature is embedded.
|
541 + | // We test the conversion logic directly rather than the full identity() method
|
542 + | // to avoid complex mocking of HTTP clients and runtime components.
|
543 + |
|
544 + | let session_creds = SessionCredentials::builder()
|
545 + | .access_key_id("test_access_key")
|
546 + | .secret_access_key("test_secret_key")
|
547 + | .session_token("test_session_token")
|
548 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
549 + | .build()
|
550 + | .expect("valid session credentials");
|
551 + |
|
552 + | // Simulate what the identity provider does: convert SessionCredentials to Credentials
|
553 + | // and embed the S3ExpressBucket feature
|
554 + | let mut credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
555 + | credentials
|
556 + | .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
|
557 + | .push(AwsCredentialFeature::S3ExpressBucket);
|
558 + |
|
559 + | // Verify the feature is embedded in the Credentials
|
560 + | let creds_features = credentials
|
561 + | .get_property::<Vec<AwsCredentialFeature>>()
|
562 + | .expect("features should be present in credentials");
|
563 + | assert!(
|
564 + | creds_features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
565 + | "S3ExpressBucket feature should be embedded in Credentials"
|
566 + | );
|
567 + |
|
568 + | // Verify the feature propagates to Identity when converted
|
569 + | let identity = Identity::from(credentials.clone());
|
570 + | assert!(identity.data::<Credentials>().is_some(), "Identity should contain Credentials");
|
571 + |
|
572 + | let identity_creds = identity.data::<Credentials>().expect("should have credentials");
|
573 + | let identity_features = identity_creds
|
574 + | .get_property::<Vec<AwsCredentialFeature>>()
|
575 + | .expect("features should be present in Identity's credentials");
|
576 + | assert!(
|
577 + | identity_features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
578 + | "S3ExpressBucket feature should propagate to Identity after conversion"
|
579 + | );
|
580 + | }
|
581 + |
|
582 + | #[test]
|
583 + | fn test_session_credentials_conversion() {
|
584 + | let session_creds = SessionCredentials::builder()
|
585 + | .access_key_id("test_access_key")
|
586 + | .secret_access_key("test_secret_key")
|
587 + | .session_token("test_session_token")
|
588 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
589 + | .build()
|
590 + | .expect("valid session credentials");
|
591 + |
|
592 + | let credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
593 + |
|
594 + | assert_eq!(credentials.access_key_id(), "test_access_key");
|
595 + | assert_eq!(credentials.secret_access_key(), "test_secret_key");
|
596 + | assert_eq!(credentials.session_token(), Some("test_session_token"));
|
597 + | }
|
598 + | }
|
527 599 | }
|
528 600 |
|
529 601 | /// Supporting code for S3 Express runtime plugin
|
530 602 | pub(crate) mod runtime_plugin {
|
531 603 | use std::borrow::Cow;
|
532 604 |
|
533 605 | use aws_runtime::auth::SigV4SessionTokenNameOverride;
|
534 606 | use aws_sigv4::http_request::{SignatureLocation, SigningSettings};
|
535 607 | use aws_smithy_runtime_api::{
|
536 608 | box_error::BoxError,
|
644 716 | mod tests {
|
645 717 | use super::*;
|
646 718 | use aws_credential_types::Credentials;
|
647 719 | use aws_smithy_runtime_api::client::identity::ResolveIdentity;
|
648 720 |
|
649 721 | #[test]
|
650 722 | fn disable_option_set_from_service_client_should_take_the_highest_precedence() {
|
651 723 | // Disable option is set from service client.
|
652 724 | let disable_s3_express_session_token = crate::config::DisableS3ExpressSessionAuth(true);
|
653 725 |
|
654 - | // An environment variable says the session auth is _not_ disabled, but it will be
|
655 - | // overruled by what is in `layer`.
|
726 + | // An environment variable says the session auth is _not_ disabled,
|
727 + | // but it will be overruled by what is in `layer`.
|
656 728 | let actual = config(
|
657 729 | Some(disable_s3_express_session_token),
|
658 730 | Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "false")]),
|
659 731 | );
|
660 732 |
|
661 - | // A config layer from this runtime plugin should not provide a new `DisableS3ExpressSessionAuth`
|
662 - | // if the disable option is set from service client.
|
733 + | // A config layer from this runtime plugin should not provide
|
734 + | // a new `DisableS3ExpressSessionAuth` if the disable option is set from service client.
|
663 735 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
664 736 | }
|
665 737 |
|
666 738 | #[test]
|
667 739 | fn disable_option_set_from_env_should_take_the_second_highest_precedence() {
|
668 - | // An environment variable says session auth is disabled
|
740 + | // Disable option is set from environment variable.
|
669 741 | let actual = config(None, Env::from_slice(&[(super::env::S3_DISABLE_EXPRESS_SESSION_AUTH, "true")]));
|
670 742 |
|
743 + | // The config layer should provide `DisableS3ExpressSessionAuth` from the environment variable.
|
671 744 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().unwrap().0);
|
672 745 | }
|
673 746 |
|
674 747 | #[should_panic]
|
675 748 | #[test]
|
676 749 | 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!()
|
750 + | todo!("TODO(aws-sdk-rust#1073): Implement profile file test")
|
680 751 | }
|
681 752 |
|
682 753 | #[test]
|
683 754 | fn disable_option_should_be_unspecified_if_unset() {
|
755 + | // Disable option is not set anywhere.
|
684 756 | let actual = config(None, Env::from_slice(&[]));
|
685 757 |
|
758 + | // The config layer should not provide `DisableS3ExpressSessionAuth` when it's not configured.
|
686 759 | assert!(actual.load::<crate::config::DisableS3ExpressSessionAuth>().is_none());
|
687 760 | }
|
688 761 |
|
689 762 | #[test]
|
690 763 | fn s3_express_runtime_plugin_should_set_default_identity_resolver() {
|
764 + | // Config has SigV4 credentials provider, so S3 Express identity resolver should be set.
|
691 765 | let config = crate::Config::builder()
|
692 766 | .behavior_version_latest()
|
693 767 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
694 768 | .credentials_provider(Credentials::for_tests())
|
695 769 | .build();
|
696 770 |
|
697 771 | let actual = runtime_components_builder(config);
|
772 + | // The runtime plugin should provide a default S3 Express identity resolver.
|
698 773 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_some());
|
699 774 | }
|
700 775 |
|
701 776 | #[test]
|
702 777 | fn s3_express_plugin_should_not_set_default_identity_resolver_without_sigv4_counterpart() {
|
778 + | // Config does not have SigV4 credentials provider.
|
703 779 | let config = crate::Config::builder()
|
704 780 | .behavior_version_latest()
|
705 781 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
706 782 | .build();
|
707 783 |
|
708 784 | let actual = runtime_components_builder(config);
|
785 + | // The runtime plugin should not provide S3 Express identity resolver without SigV4 credentials.
|
709 786 | assert!(actual.identity_resolver(&crate::s3_express::auth::SCHEME_ID).is_none());
|
710 787 | }
|
711 788 |
|
712 789 | #[tokio::test]
|
713 790 | async fn s3_express_plugin_should_not_set_default_identity_resolver_if_user_provided() {
|
791 + | // User provides a custom S3 Express credentials provider.
|
714 792 | let expected_access_key_id = "expected acccess key ID";
|
715 793 | let config = crate::Config::builder()
|
716 794 | .behavior_version_latest()
|
717 795 | .credentials_provider(Credentials::for_tests())
|
718 796 | .express_credentials_provider(Credentials::new(
|
719 797 | expected_access_key_id,
|
720 798 | "secret",
|
721 799 | None,
|
722 800 | None,
|
723 801 | "test express credentials provider",
|
724 802 | ))
|
725 803 | .time_source(aws_smithy_async::time::SystemTimeSource::new())
|
726 804 | .build();
|
727 805 |
|
728 - | // `RuntimeComponentsBuilder` from `S3ExpressRuntimePlugin` should not provide an S3Express identity resolver.
|
806 + | // The runtime plugin should not override the user-provided identity resolver.
|
729 807 | let runtime_components_builder = runtime_components_builder(config.clone());
|
730 808 | assert!(runtime_components_builder
|
731 809 | .identity_resolver(&crate::s3_express::auth::SCHEME_ID)
|
732 810 | .is_none());
|
733 811 |
|
734 - | // Get the S3Express identity resolver from the service config.
|
812 + | // The user-provided identity resolver should be used.
|
735 813 | let express_identity_resolver = config.runtime_components.identity_resolver(&crate::s3_express::auth::SCHEME_ID).unwrap();
|
736 814 | let creds = express_identity_resolver
|
737 815 | .resolve_identity(&RuntimeComponentsBuilder::for_tests().build().unwrap(), &ConfigBag::base())
|
738 816 | .await
|
739 817 | .unwrap();
|
740 818 |
|
741 - | // Verify credentials are the one generated by the S3Express identity resolver user provided.
|
742 819 | assert_eq!(expected_access_key_id, creds.data::<Credentials>().unwrap().access_key_id());
|
743 820 | }
|
744 821 | }
|
745 822 | }
|
746 823 |
|
747 824 | pub(crate) mod checksum {
|
748 825 | use crate::http_request_checksum::DefaultRequestChecksumOverride;
|
749 826 | use aws_smithy_checksums::ChecksumAlgorithm;
|
750 827 | use aws_smithy_types::config_bag::ConfigBag;
|
751 828 |
|