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 + | // Verify SessionCredentials conversion to Credentials embeds S3ExpressBucket feature
|
540 + | let session_creds = SessionCredentials::builder()
|
541 + | .access_key_id("test_access_key")
|
542 + | .secret_access_key("test_secret_key")
|
543 + | .session_token("test_session_token")
|
544 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
545 + | .build()
|
546 + | .expect("valid session credentials");
|
547 + |
|
548 + | let mut credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
549 + |
|
550 + | // Embed the feature as done in the identity() method
|
551 + | credentials
|
552 + | .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
|
553 + | .push(AwsCredentialFeature::S3ExpressBucket);
|
554 + |
|
555 + | // Verify the feature is present in credentials
|
556 + | let features = credentials
|
557 + | .get_property::<Vec<AwsCredentialFeature>>()
|
558 + | .expect("features should be present");
|
559 + | assert!(
|
560 + | features.contains(&AwsCredentialFeature::S3ExpressBucket),
|
561 + | "S3ExpressBucket feature should be embedded in credentials"
|
562 + | );
|
563 + |
|
564 + | // The feature is successfully embedded in credentials
|
565 + | // When converted to Identity, the credentials (with features) are preserved
|
566 + | // This is sufficient to verify the feature tracking mechanism works
|
567 + | }
|
568 + |
|
569 + | #[test]
|
570 + | fn test_session_credentials_conversion() {
|
571 + | // Verify SessionCredentials can be converted to Credentials
|
572 + | let session_creds = SessionCredentials::builder()
|
573 + | .access_key_id("test_access_key")
|
574 + | .secret_access_key("test_secret_key")
|
575 + | .session_token("test_session_token")
|
576 + | .expiration(aws_smithy_types::DateTime::from_secs(1000))
|
577 + | .build()
|
578 + | .expect("valid session credentials");
|
579 + |
|
580 + | let credentials = Credentials::try_from(session_creds).expect("conversion should succeed");
|
581 + |
|
582 + | assert_eq!(credentials.access_key_id(), "test_access_key");
|
583 + | assert_eq!(credentials.secret_access_key(), "test_secret_key");
|
584 + | assert_eq!(credentials.session_token(), Some("test_session_token"));
|
585 + | }
|
586 + | }
|
527 587 | }
|
528 588 |
|
529 589 | /// Supporting code for S3 Express runtime plugin
|
530 590 | pub(crate) mod runtime_plugin {
|
531 591 | use std::borrow::Cow;
|
532 592 |
|
533 593 | use aws_runtime::auth::SigV4SessionTokenNameOverride;
|
534 594 | use aws_sigv4::http_request::{SignatureLocation, SigningSettings};
|
535 595 | use aws_smithy_runtime_api::{
|
536 596 | box_error::BoxError,
|
537 597 | client::{runtime_components::RuntimeComponentsBuilder, runtime_plugin::RuntimePlugin},
|
538 598 | };
|
539 599 | use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Layer};
|
540 600 | use aws_types::os_shim_internal::Env;
|
541 601 |
|
542 602 | mod env {
|
543 603 | pub(super) const S3_DISABLE_EXPRESS_SESSION_AUTH: &str = "AWS_S3_DISABLE_EXPRESS_SESSION_AUTH";
|
544 604 | }
|
545 605 |
|
546 606 | #[derive(Debug)]
|
547 607 | pub(crate) struct S3ExpressRuntimePlugin {
|
548 608 | config: FrozenLayer,
|
549 609 | runtime_components_builder: RuntimeComponentsBuilder,
|
550 610 | }
|
551 611 |
|
552 612 | impl S3ExpressRuntimePlugin {
|
553 613 | // `new` will be called as `additional_client_plugins` within `base_client_runtime_plugins`.
|
554 614 | // This guarantees that `new` receives a fully constructed service config, with required
|
555 615 | // runtime components registered with `RuntimeComponents`.
|
556 616 | pub(crate) fn new(service_config: crate::config::Config) -> Self {
|