1use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
5
6#[cfg(test)]
7use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
8
9#[cfg(not(test))]
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use object_store::ObjectStore as OSObjectStore;
13use object_store_opendal::OpendalStore;
14use opendal::{Operator, services::S3};
15
16use aws_config::default_provider::credentials::DefaultCredentialsChain;
17use aws_credential_types::provider::ProvideCredentials;
18use object_store::{
19 ClientOptions, CredentialProvider, Result as ObjectStoreResult, RetryConfig,
20 StaticCredentialProvider,
21 aws::{
22 AmazonS3Builder, AmazonS3ConfigKey, AwsCredential as ObjectStoreAwsCredential,
23 AwsCredentialProvider,
24 },
25};
26use tokio::sync::RwLock;
27use url::Url;
28
29use crate::object_store::{
30 DEFAULT_CLOUD_BLOCK_SIZE, DEFAULT_CLOUD_IO_PARALLELISM, DEFAULT_MAX_IOP_SIZE, ObjectStore,
31 ObjectStoreParams, ObjectStoreProvider, StorageOptions, StorageOptionsAccessor,
32 dynamic_credentials::{NamespaceCredentialsProvider, build_dynamic_credential_provider},
33 throttle::{AimdThrottleConfig, AimdThrottledStore},
34};
35use lance_core::error::{Error, Result};
36
37#[derive(Default, Debug)]
38pub struct AwsStoreProvider;
39
40impl AwsStoreProvider {
41 async fn build_amazon_s3_store(
42 &self,
43 base_path: &mut Url,
44 params: &ObjectStoreParams,
45 storage_options: &StorageOptions,
46 is_s3_express: bool,
47 ) -> Result<Arc<dyn OSObjectStore>> {
48 let retry_config = RetryConfig {
51 backoff: Default::default(),
52 max_retries: storage_options.client_max_retries(),
53 retry_timeout: Duration::from_secs(storage_options.client_retry_timeout()),
54 };
55
56 let mut s3_storage_options = storage_options.as_s3_options();
57 let region = resolve_s3_region(base_path, &s3_storage_options).await?;
58
59 let accessor = params.get_accessor();
61
62 let (aws_creds, region) = build_aws_credential(
63 params.s3_credentials_refresh_offset,
64 params.aws_credentials.clone(),
65 Some(&s3_storage_options),
66 region,
67 accessor,
68 )
69 .await?;
70
71 if is_s3_express {
73 s3_storage_options.insert(AmazonS3ConfigKey::S3Express, true.to_string());
74 }
75
76 #[cfg(feature = "metrics")]
79 let store_prefix =
80 self.calculate_object_store_prefix(base_path, Some(&storage_options.0))?;
81
82 base_path.set_scheme("s3").unwrap();
84 base_path.set_query(None);
85
86 let mut builder =
88 AmazonS3Builder::new().with_client_options(storage_options.client_options()?);
89 for (key, value) in s3_storage_options {
90 builder = builder.with_config(key, value);
91 }
92 builder = builder
93 .with_url(base_path.as_ref())
94 .with_credentials(aws_creds)
95 .with_retry(retry_config)
96 .with_region(region);
97
98 #[cfg(feature = "metrics")]
99 {
100 builder = builder.with_http_connector(
101 crate::object_store::metrics::MeteringHttpConnector::new(store_prefix),
102 );
103 }
104
105 Ok(Arc::new(builder.build()?) as Arc<dyn OSObjectStore>)
106 }
107
108 async fn build_opendal_s3_store(
109 &self,
110 base_path: &Url,
111 storage_options: &StorageOptions,
112 ) -> Result<Arc<dyn OSObjectStore>> {
113 let bucket = base_path
114 .host_str()
115 .ok_or_else(|| Error::invalid_input("S3 URL must contain bucket name"))?
116 .to_string();
117
118 let prefix = base_path.path().trim_start_matches('/').to_string();
119
120 let mut config_map: HashMap<String, String> = storage_options.0.clone();
123
124 config_map.insert("bucket".to_string(), bucket);
126
127 if !prefix.is_empty() {
128 config_map.insert("root".to_string(), "/".to_string());
129 }
130
131 let operator = Operator::from_iter::<S3>(config_map)
132 .map_err(|e| Error::invalid_input(format!("Failed to create S3 operator: {:?}", e)))?
133 .finish();
134
135 Ok(Arc::new(OpendalStore::new(operator)) as Arc<dyn OSObjectStore>)
136 }
137}
138
139#[async_trait::async_trait]
140impl ObjectStoreProvider for AwsStoreProvider {
141 async fn new_store(
142 &self,
143 mut base_path: Url,
144 params: &ObjectStoreParams,
145 ) -> Result<ObjectStore> {
146 let block_size = params.block_size.unwrap_or(DEFAULT_CLOUD_BLOCK_SIZE);
147 let mut storage_options =
148 StorageOptions::new(params.storage_options().cloned().unwrap_or_default());
149 storage_options.with_env_s3();
150 let download_retry_count = storage_options.download_retry_count();
151
152 let use_opendal = storage_options
153 .0
154 .get("use_opendal")
155 .map(|v| v == "true")
156 .unwrap_or(false);
157
158 let is_s3_express = check_s3_express(&base_path, &storage_options);
160
161 let use_constant_size_upload_parts = storage_options
162 .0
163 .get("aws_endpoint")
164 .map(|endpoint| endpoint.contains("r2.cloudflarestorage.com"))
165 .unwrap_or(false);
166
167 let inner = if use_opendal {
168 self.build_opendal_s3_store(&base_path, &storage_options)
170 .await?
171 } else {
172 self.build_amazon_s3_store(&mut base_path, params, &storage_options, is_s3_express)
174 .await?
175 };
176 let throttle_config = AimdThrottleConfig::from_storage_options(params.storage_options())?;
177 let inner = if throttle_config.is_disabled() {
178 inner
179 } else {
180 Arc::new(AimdThrottledStore::new(inner, throttle_config)?) as Arc<dyn OSObjectStore>
181 };
182
183 Ok(ObjectStore {
184 inner,
185 scheme: String::from(base_path.scheme()),
186 block_size,
187 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
188 use_constant_size_upload_parts,
189 list_is_lexically_ordered: !is_s3_express,
190 io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
191 download_retry_count,
192 io_tracker: Default::default(),
193 store_prefix: self
194 .calculate_object_store_prefix(&base_path, params.storage_options())?,
195 })
196 }
197}
198
199fn check_s3_express(url: &Url, storage_options: &StorageOptions) -> bool {
201 storage_options
202 .0
203 .get("s3_express")
204 .map(|v| v == "true")
205 .unwrap_or(false)
206 || url.authority().ends_with("--x-s3")
207}
208
209async fn resolve_s3_region(
217 url: &Url,
218 storage_options: &HashMap<AmazonS3ConfigKey, String>,
219) -> Result<Option<String>> {
220 if let Some(region) = storage_options.get(&AmazonS3ConfigKey::Region) {
221 Ok(Some(region.clone()))
222 } else if storage_options.get(&AmazonS3ConfigKey::Endpoint).is_none() {
223 let bucket = url.host_str().ok_or_else(|| {
226 Error::invalid_input(format!("Could not parse bucket from url: {}", url))
227 })?;
228
229 let mut client_options = ClientOptions::default();
230 for (key, value) in storage_options {
231 if let AmazonS3ConfigKey::Client(client_key) = key {
232 client_options = client_options.with_config(*client_key, value.clone());
233 }
234 }
235
236 let bucket_region =
237 object_store::aws::resolve_bucket_region(bucket, &client_options).await?;
238 Ok(Some(bucket_region))
239 } else {
240 Ok(None)
241 }
242}
243
244pub async fn build_aws_credential(
261 credentials_refresh_offset: Duration,
262 credentials: Option<AwsCredentialProvider>,
263 storage_options: Option<&HashMap<AmazonS3ConfigKey, String>>,
264 region: Option<String>,
265 storage_options_accessor: Option<Arc<StorageOptionsAccessor>>,
266) -> Result<(AwsCredentialProvider, String)> {
267 use aws_config::meta::region::RegionProviderChain;
268 const DEFAULT_REGION: &str = "us-west-2";
269
270 let region = if let Some(region) = region {
271 region
272 } else {
273 RegionProviderChain::default_provider()
274 .or_else(DEFAULT_REGION)
275 .region()
276 .await
277 .map(|r| r.as_ref().to_string())
278 .unwrap_or(DEFAULT_REGION.to_string())
279 };
280
281 let storage_options_credentials = storage_options.and_then(extract_static_s3_credentials);
282
283 if credentials.is_none()
285 && let Some(dynamic_creds) = build_dynamic_credential_provider::<ObjectStoreAwsCredential>(
286 storage_options_accessor.clone(),
287 )
288 .await?
289 {
290 return Ok((dynamic_creds, region));
291 }
292
293 if storage_options_accessor
294 .as_ref()
295 .is_some_and(|a| a.has_provider())
296 {
297 log::debug!(
298 "Storage options from provider do not contain explicit AWS credentials, \
299 falling back to default AWS credentials chain."
300 );
301 }
302
303 if let Some(creds) = credentials {
305 Ok((creds, region))
306 } else if let Some(creds) = storage_options_credentials {
307 Ok((Arc::new(creds), region))
308 } else {
309 let credentials_provider = DefaultCredentialsChain::builder().build().await;
310
311 Ok((
312 Arc::new(AwsCredentialAdapter::new(
313 Arc::new(credentials_provider),
314 credentials_refresh_offset,
315 )),
316 region,
317 ))
318 }
319}
320
321fn extract_static_s3_credentials(
322 options: &HashMap<AmazonS3ConfigKey, String>,
323) -> Option<StaticCredentialProvider<ObjectStoreAwsCredential>> {
324 let key_id = options.get(&AmazonS3ConfigKey::AccessKeyId).cloned();
325 let secret_key = options.get(&AmazonS3ConfigKey::SecretAccessKey).cloned();
326 let token = options.get(&AmazonS3ConfigKey::Token).cloned();
327 match (key_id, secret_key, token) {
328 (Some(key_id), Some(secret_key), token) => {
329 Some(StaticCredentialProvider::new(ObjectStoreAwsCredential {
330 key_id,
331 secret_key,
332 token,
333 }))
334 }
335 _ => None,
336 }
337}
338
339#[derive(Debug)]
341pub struct AwsCredentialAdapter {
342 pub inner: Arc<dyn ProvideCredentials>,
343
344 cache: Arc<RwLock<HashMap<String, Arc<aws_credential_types::Credentials>>>>,
346
347 credentials_refresh_offset: Duration,
349}
350
351impl AwsCredentialAdapter {
352 pub fn new(
353 provider: Arc<dyn ProvideCredentials>,
354 credentials_refresh_offset: Duration,
355 ) -> Self {
356 Self {
357 inner: provider,
358 cache: Arc::new(RwLock::new(HashMap::new())),
359 credentials_refresh_offset,
360 }
361 }
362}
363
364const AWS_CREDS_CACHE_KEY: &str = "aws_credentials";
365
366fn to_system_time(time: std::time::SystemTime) -> SystemTime {
368 let duration_since_epoch = time
369 .duration_since(std::time::UNIX_EPOCH)
370 .expect("time should be after UNIX_EPOCH");
371 UNIX_EPOCH + duration_since_epoch
372}
373
374#[async_trait::async_trait]
375impl CredentialProvider for AwsCredentialAdapter {
376 type Credential = ObjectStoreAwsCredential;
377
378 async fn get_credential(&self) -> ObjectStoreResult<Arc<Self::Credential>> {
379 let cached_creds = {
380 let cache_value = self.cache.read().await.get(AWS_CREDS_CACHE_KEY).cloned();
381 let expired = cache_value
382 .clone()
383 .map(|cred| {
384 cred.expiry()
385 .map(|exp| {
386 to_system_time(exp)
387 .checked_sub(self.credentials_refresh_offset)
388 .expect("this time should always be valid")
389 < SystemTime::now()
390 })
391 .unwrap_or(false)
393 })
394 .unwrap_or(true); if expired { None } else { cache_value.clone() }
396 };
397
398 if let Some(creds) = cached_creds {
399 Ok(Arc::new(Self::Credential {
400 key_id: creds.access_key_id().to_string(),
401 secret_key: creds.secret_access_key().to_string(),
402 token: creds.session_token().map(|s| s.to_string()),
403 }))
404 } else {
405 let refreshed_creds =
406 Arc::new(self.inner.provide_credentials().await.map_err(|e| {
407 Error::internal(format!("Failed to get AWS credentials: {:?}", e))
408 })?);
409
410 self.cache
411 .write()
412 .await
413 .insert(AWS_CREDS_CACHE_KEY.to_string(), refreshed_creds.clone());
414
415 Ok(Arc::new(Self::Credential {
416 key_id: refreshed_creds.access_key_id().to_string(),
417 secret_key: refreshed_creds.secret_access_key().to_string(),
418 token: refreshed_creds.session_token().map(|s| s.to_string()),
419 }))
420 }
421 }
422}
423
424impl StorageOptions {
425 pub fn with_env_s3(&mut self) {
427 for (os_key, os_value) in std::env::vars_os() {
428 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str())
429 && let Ok(config_key) = AmazonS3ConfigKey::from_str(&key.to_ascii_lowercase())
430 && !self.0.contains_key(config_key.as_ref())
431 {
432 self.0
433 .insert(config_key.as_ref().to_string(), value.to_string());
434 }
435 }
436 }
437
438 pub fn as_s3_options(&self) -> HashMap<AmazonS3ConfigKey, String> {
440 self.0
441 .iter()
442 .filter_map(|(key, value)| {
443 let s3_key = AmazonS3ConfigKey::from_str(&key.to_ascii_lowercase()).ok()?;
444 Some((s3_key, value.clone()))
445 })
446 .collect()
447 }
448}
449
450impl ObjectStoreParams {
451 pub fn with_aws_credentials(
453 aws_credentials: Option<AwsCredentialProvider>,
454 region: Option<String>,
455 ) -> Self {
456 let storage_options_accessor = region.map(|region| {
457 let opts: HashMap<String, String> =
458 [("region".into(), region)].iter().cloned().collect();
459 Arc::new(StorageOptionsAccessor::with_static_options(opts))
460 });
461 Self {
462 aws_credentials,
463 storage_options_accessor,
464 ..Default::default()
465 }
466 }
467}
468
469pub type DynamicStorageOptionsCredentialProvider =
470 NamespaceCredentialsProvider<ObjectStoreAwsCredential>;
471
472#[cfg(test)]
473mod tests {
474 use crate::object_store::ObjectStoreRegistry;
475 use crate::object_store::StorageOptionsProvider;
476 use mock_instant::thread_local::MockClock;
477 use object_store::path::Path;
478 use std::sync::atomic::{AtomicBool, Ordering};
479
480 use super::*;
481
482 #[derive(Debug, Default)]
483 struct MockAwsCredentialsProvider {
484 called: AtomicBool,
485 }
486
487 #[async_trait::async_trait]
488 impl CredentialProvider for MockAwsCredentialsProvider {
489 type Credential = ObjectStoreAwsCredential;
490
491 async fn get_credential(&self) -> ObjectStoreResult<Arc<Self::Credential>> {
492 self.called.store(true, Ordering::Relaxed);
493 Ok(Arc::new(Self::Credential {
494 key_id: "".to_string(),
495 secret_key: "".to_string(),
496 token: None,
497 }))
498 }
499 }
500
501 #[tokio::test]
502 async fn test_injected_aws_creds_option_is_used() {
503 let mock_provider = Arc::new(MockAwsCredentialsProvider::default());
504 let registry = Arc::new(ObjectStoreRegistry::default());
505
506 let params = ObjectStoreParams {
507 aws_credentials: Some(mock_provider.clone() as AwsCredentialProvider),
508 ..ObjectStoreParams::default()
509 };
510
511 assert!(!mock_provider.called.load(Ordering::Relaxed));
513
514 let (store, _) = ObjectStore::from_uri_and_params(registry, "s3://not-a-bucket", ¶ms)
515 .await
516 .unwrap();
517
518 let _ = store
520 .open(&Path::parse("/").unwrap())
521 .await
522 .unwrap()
523 .get_range(0..1)
524 .await;
525
526 assert!(mock_provider.called.load(Ordering::Relaxed));
528 }
529
530 #[test]
531 fn test_s3_path_parsing() {
532 let provider = AwsStoreProvider;
533
534 let cases = [
535 ("s3://bucket/path/to/file", "path/to/file"),
536 ("s3://bucket/测试path/to/file", "测试path/to/file"),
538 ("s3://bucket/path/&to/file", "path/&to/file"),
539 ("s3://bucket/path/=to/file", "path/=to/file"),
540 (
541 "s3+ddb://bucket/path/to/file?ddbTableName=test",
542 "path/to/file",
543 ),
544 ];
545
546 for (uri, expected_path) in cases {
547 let url = Url::parse(uri).unwrap();
548 let path = provider.extract_path(&url).unwrap();
549 let expected_path = Path::parse(expected_path).unwrap();
553 assert_eq!(path, expected_path)
554 }
555 }
556
557 #[test]
563 fn test_s3_non_ascii_path_no_double_encoding() {
564 let provider = AwsStoreProvider;
565
566 let url = Url::parse("s3://bucket/中文路径").unwrap();
571 let path = provider.extract_path(&url).unwrap();
572
573 assert_eq!(path.as_ref(), "中文路径");
575 }
576
577 #[test]
578 fn test_is_s3_express() {
579 let cases = [
580 (
581 "s3://bucket/path/to/file",
582 HashMap::from([("s3_express".to_string(), "true".to_string())]),
583 true,
584 ),
585 (
586 "s3://bucket/path/to/file",
587 HashMap::from([("s3_express".to_string(), "false".to_string())]),
588 false,
589 ),
590 ("s3://bucket/path/to/file", HashMap::from([]), false),
591 (
592 "s3://bucket--x-s3/path/to/file",
593 HashMap::from([("s3_express".to_string(), "true".to_string())]),
594 true,
595 ),
596 (
597 "s3://bucket--x-s3/path/to/file",
598 HashMap::from([("s3_express".to_string(), "false".to_string())]),
599 true, ),
601 ("s3://bucket--x-s3/path/to/file", HashMap::from([]), true),
602 ];
603
604 for (uri, storage_map, expected) in cases {
605 let url = Url::parse(uri).unwrap();
606 let storage_options = StorageOptions(storage_map);
607 let is_s3_express = check_s3_express(&url, &storage_options);
608 assert_eq!(is_s3_express, expected);
609 }
610 }
611
612 #[tokio::test]
613 async fn test_use_opendal_flag() {
614 use crate::object_store::StorageOptionsAccessor;
615 let provider = AwsStoreProvider;
616 let url = Url::parse("s3://test-bucket/path").unwrap();
617 let params_with_flag = ObjectStoreParams {
618 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
619 HashMap::from([
620 ("use_opendal".to_string(), "true".to_string()),
621 ("region".to_string(), "us-west-2".to_string()),
622 ]),
623 ))),
624 ..Default::default()
625 };
626
627 let store = provider
628 .new_store(url.clone(), ¶ms_with_flag)
629 .await
630 .unwrap();
631 assert_eq!(store.scheme, "s3");
632 }
633
634 #[derive(Debug)]
635 struct MockStorageOptionsProvider {
636 call_count: Arc<RwLock<usize>>,
637 expires_in_millis: Option<u64>,
638 }
639
640 impl MockStorageOptionsProvider {
641 fn new(expires_in_millis: Option<u64>) -> Self {
642 Self {
643 call_count: Arc::new(RwLock::new(0)),
644 expires_in_millis,
645 }
646 }
647
648 async fn get_call_count(&self) -> usize {
649 *self.call_count.read().await
650 }
651 }
652
653 #[async_trait::async_trait]
654 impl StorageOptionsProvider for MockStorageOptionsProvider {
655 async fn fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
656 let count = {
657 let mut c = self.call_count.write().await;
658 *c += 1;
659 *c
660 };
661
662 let mut options = HashMap::from([
663 ("aws_access_key_id".to_string(), format!("AKID_{}", count)),
664 (
665 "aws_secret_access_key".to_string(),
666 format!("SECRET_{}", count),
667 ),
668 ("aws_session_token".to_string(), format!("TOKEN_{}", count)),
669 ]);
670
671 if let Some(expires_in) = self.expires_in_millis {
672 let now_ms = SystemTime::now()
673 .duration_since(UNIX_EPOCH)
674 .unwrap()
675 .as_millis() as u64;
676 let expires_at = now_ms + expires_in;
677 options.insert("expires_at_millis".to_string(), expires_at.to_string());
678 }
679
680 Ok(Some(options))
681 }
682
683 fn provider_id(&self) -> String {
684 let ptr = Arc::as_ptr(&self.call_count) as usize;
685 format!("MockStorageOptionsProvider {{ id: {} }}", ptr)
686 }
687 }
688
689 #[tokio::test]
690 async fn test_dynamic_credential_provider_with_initial_cache() {
691 MockClock::set_system_time(Duration::from_secs(100_000));
692
693 let now_ms = MockClock::system_time().as_millis() as u64;
694
695 let mock = Arc::new(MockStorageOptionsProvider::new(Some(
697 600_000, )));
699
700 let expires_at = now_ms + 600_000; let initial_options = HashMap::from([
703 ("aws_access_key_id".to_string(), "AKID_CACHED".to_string()),
704 (
705 "aws_secret_access_key".to_string(),
706 "SECRET_CACHED".to_string(),
707 ),
708 ("aws_session_token".to_string(), "TOKEN_CACHED".to_string()),
709 ("expires_at_millis".to_string(), expires_at.to_string()),
710 ("refresh_offset_millis".to_string(), "300000".to_string()), ]);
712
713 let provider = DynamicStorageOptionsCredentialProvider::from_provider_with_initial(
714 mock.clone(),
715 initial_options,
716 );
717
718 let cred = provider.get_credential().await.unwrap();
720 assert_eq!(cred.key_id, "AKID_CACHED");
721 assert_eq!(cred.secret_key, "SECRET_CACHED");
722 assert_eq!(cred.token, Some("TOKEN_CACHED".to_string()));
723
724 assert_eq!(mock.get_call_count().await, 0);
726 }
727
728 #[tokio::test]
729 async fn test_dynamic_credential_provider_with_expired_cache() {
730 MockClock::set_system_time(Duration::from_secs(100_000));
731
732 let now_ms = MockClock::system_time().as_millis() as u64;
733
734 let mock = Arc::new(MockStorageOptionsProvider::new(Some(
736 600_000, )));
738
739 let expired_time = now_ms - 1_000; let initial_options = HashMap::from([
742 ("aws_access_key_id".to_string(), "AKID_EXPIRED".to_string()),
743 (
744 "aws_secret_access_key".to_string(),
745 "SECRET_EXPIRED".to_string(),
746 ),
747 ("expires_at_millis".to_string(), expired_time.to_string()),
748 ("refresh_offset_millis".to_string(), "300000".to_string()), ]);
750
751 let provider = DynamicStorageOptionsCredentialProvider::from_provider_with_initial(
752 mock.clone(),
753 initial_options,
754 );
755
756 let cred = provider.get_credential().await.unwrap();
758 assert_eq!(cred.key_id, "AKID_1");
759 assert_eq!(cred.secret_key, "SECRET_1");
760 assert_eq!(cred.token, Some("TOKEN_1".to_string()));
761
762 assert_eq!(mock.get_call_count().await, 1);
764 }
765
766 #[tokio::test]
767 async fn test_dynamic_credential_provider_refresh_lead_time() {
768 MockClock::set_system_time(Duration::from_secs(100_000));
769
770 let mock = Arc::new(MockStorageOptionsProvider::new(Some(
772 30_000, )));
774
775 let provider = DynamicStorageOptionsCredentialProvider::from_provider(mock.clone());
778
779 let cred = provider.get_credential().await.unwrap();
783 assert_eq!(cred.key_id, "AKID_1");
784 assert_eq!(mock.get_call_count().await, 1);
785
786 let cred = provider.get_credential().await.unwrap();
790 assert_eq!(cred.key_id, "AKID_2");
791 assert_eq!(mock.get_call_count().await, 2);
792 }
793
794 #[tokio::test]
795 async fn test_dynamic_credential_provider_no_initial_cache() {
796 MockClock::set_system_time(Duration::from_secs(100_000));
797
798 let mock = Arc::new(MockStorageOptionsProvider::new(Some(
800 120_000, )));
802
803 let provider = DynamicStorageOptionsCredentialProvider::from_provider(mock.clone());
805
806 let cred = provider.get_credential().await.unwrap();
808 assert_eq!(cred.key_id, "AKID_1");
809 assert_eq!(cred.secret_key, "SECRET_1");
810 assert_eq!(cred.token, Some("TOKEN_1".to_string()));
811 assert_eq!(mock.get_call_count().await, 1);
812
813 let cred = provider.get_credential().await.unwrap();
815 assert_eq!(cred.key_id, "AKID_1");
816 assert_eq!(mock.get_call_count().await, 1); MockClock::set_system_time(Duration::from_secs(100_000 + 90));
821 let cred = provider.get_credential().await.unwrap();
822 assert_eq!(cred.key_id, "AKID_2");
823 assert_eq!(cred.secret_key, "SECRET_2");
824 assert_eq!(cred.token, Some("TOKEN_2".to_string()));
825 assert_eq!(mock.get_call_count().await, 2);
826
827 MockClock::set_system_time(Duration::from_secs(100_000 + 210));
829 let cred = provider.get_credential().await.unwrap();
830 assert_eq!(cred.key_id, "AKID_3");
831 assert_eq!(cred.secret_key, "SECRET_3");
832 assert_eq!(mock.get_call_count().await, 3);
833 }
834
835 #[tokio::test]
836 async fn test_dynamic_credential_provider_with_initial_options() {
837 MockClock::set_system_time(Duration::from_secs(100_000));
838
839 let now_ms = MockClock::system_time().as_millis() as u64;
840
841 let mock = Arc::new(MockStorageOptionsProvider::new(Some(
843 600_000, )));
845
846 let expires_at = now_ms + 600_000; let initial_options = HashMap::from([
849 ("aws_access_key_id".to_string(), "AKID_INITIAL".to_string()),
850 (
851 "aws_secret_access_key".to_string(),
852 "SECRET_INITIAL".to_string(),
853 ),
854 ("aws_session_token".to_string(), "TOKEN_INITIAL".to_string()),
855 ("expires_at_millis".to_string(), expires_at.to_string()),
856 ("refresh_offset_millis".to_string(), "300000".to_string()), ]);
858
859 let provider = DynamicStorageOptionsCredentialProvider::from_provider_with_initial(
861 mock.clone(),
862 initial_options,
863 );
864
865 let cred = provider.get_credential().await.unwrap();
867 assert_eq!(cred.key_id, "AKID_INITIAL");
868 assert_eq!(cred.secret_key, "SECRET_INITIAL");
869 assert_eq!(cred.token, Some("TOKEN_INITIAL".to_string()));
870
871 assert_eq!(mock.get_call_count().await, 0);
873
874 MockClock::set_system_time(Duration::from_secs(100_000 + 360));
877 let cred = provider.get_credential().await.unwrap();
878 assert_eq!(cred.key_id, "AKID_1");
879 assert_eq!(cred.secret_key, "SECRET_1");
880 assert_eq!(cred.token, Some("TOKEN_1".to_string()));
881
882 assert_eq!(mock.get_call_count().await, 1);
884
885 MockClock::set_system_time(Duration::from_secs(100_000 + 660));
887 let cred = provider.get_credential().await.unwrap();
888 assert_eq!(cred.key_id, "AKID_2");
889 assert_eq!(cred.secret_key, "SECRET_2");
890 assert_eq!(cred.token, Some("TOKEN_2".to_string()));
891
892 assert_eq!(mock.get_call_count().await, 2);
894
895 MockClock::set_system_time(Duration::from_secs(100_000 + 960));
897 let cred = provider.get_credential().await.unwrap();
898 assert_eq!(cred.key_id, "AKID_3");
899 assert_eq!(cred.secret_key, "SECRET_3");
900 assert_eq!(cred.token, Some("TOKEN_3".to_string()));
901
902 assert_eq!(mock.get_call_count().await, 3);
904 }
905
906 #[tokio::test]
907 async fn test_dynamic_credential_provider_concurrent_access() {
908 let mock = Arc::new(MockStorageOptionsProvider::new(Some(9999999999999)));
910
911 let provider = Arc::new(DynamicStorageOptionsCredentialProvider::from_provider(
912 mock.clone(),
913 ));
914
915 let mut handles = vec![];
917 for i in 0..10 {
918 let provider = provider.clone();
919 let handle = tokio::spawn(async move {
920 let cred = provider.get_credential().await.unwrap();
921 assert_eq!(cred.key_id, "AKID_1");
923 assert_eq!(cred.secret_key, "SECRET_1");
924 assert_eq!(cred.token, Some("TOKEN_1".to_string()));
925 i });
927 handles.push(handle);
928 }
929
930 let results: Vec<_> = futures::future::join_all(handles)
932 .await
933 .into_iter()
934 .map(|r| r.unwrap())
935 .collect();
936
937 assert_eq!(results.len(), 10);
939 for i in 0..10 {
940 assert!(results.contains(&i));
941 }
942
943 let call_count = mock.get_call_count().await;
946 assert_eq!(
947 call_count, 1,
948 "Provider should be called exactly once despite concurrent access"
949 );
950 }
951
952 #[tokio::test]
953 async fn test_dynamic_credential_provider_concurrent_refresh() {
954 MockClock::set_system_time(Duration::from_secs(100_000));
955
956 let now_ms = MockClock::system_time().as_millis() as u64;
957
958 let expires_at = now_ms - 1_000_000;
960 let initial_options = HashMap::from([
961 ("aws_access_key_id".to_string(), "AKID_OLD".to_string()),
962 (
963 "aws_secret_access_key".to_string(),
964 "SECRET_OLD".to_string(),
965 ),
966 ("aws_session_token".to_string(), "TOKEN_OLD".to_string()),
967 ("expires_at_millis".to_string(), expires_at.to_string()),
968 ("refresh_offset_millis".to_string(), "300000".to_string()), ]);
970
971 let mock = Arc::new(MockStorageOptionsProvider::new(Some(
973 3_600_000, )));
975
976 let provider = Arc::new(
977 DynamicStorageOptionsCredentialProvider::from_provider_with_initial(
978 mock.clone(),
979 initial_options,
980 ),
981 );
982
983 let mut handles = vec![];
986 for i in 0..20 {
987 let provider = provider.clone();
988 let handle = tokio::spawn(async move {
989 let cred = provider.get_credential().await.unwrap();
990 assert_eq!(cred.key_id, "AKID_1");
992 assert_eq!(cred.secret_key, "SECRET_1");
993 assert_eq!(cred.token, Some("TOKEN_1".to_string()));
994 i
995 });
996 handles.push(handle);
997 }
998
999 let results: Vec<_> = futures::future::join_all(handles)
1001 .await
1002 .into_iter()
1003 .map(|r| r.unwrap())
1004 .collect();
1005
1006 assert_eq!(results.len(), 20);
1008
1009 let call_count = mock.get_call_count().await;
1012 assert!(
1013 call_count >= 1,
1014 "Provider should be called at least once, was called {} times",
1015 call_count
1016 );
1017
1018 assert!(
1020 call_count < 10,
1021 "Provider should not be called too many times due to lock contention, was called {} times",
1022 call_count
1023 );
1024 }
1025
1026 #[tokio::test]
1027 async fn test_explicit_aws_credentials_takes_precedence_over_accessor() {
1028 let mock_storage_provider = Arc::new(MockStorageOptionsProvider::new(Some(600_000)));
1030
1031 let accessor = Arc::new(StorageOptionsAccessor::with_provider(
1033 mock_storage_provider.clone(),
1034 ));
1035
1036 let explicit_cred_provider = Arc::new(MockAwsCredentialsProvider::default());
1038
1039 let (result, _region) = build_aws_credential(
1042 Duration::from_secs(300),
1043 Some(explicit_cred_provider.clone() as AwsCredentialProvider),
1044 None, Some("us-west-2".to_string()),
1046 Some(accessor),
1047 )
1048 .await
1049 .unwrap();
1050
1051 let cred = result.get_credential().await.unwrap();
1053
1054 assert!(explicit_cred_provider.called.load(Ordering::Relaxed));
1056
1057 assert_eq!(
1059 mock_storage_provider.get_call_count().await,
1060 0,
1061 "Storage options provider should not be called when explicit aws_credentials is provided"
1062 );
1063
1064 assert_eq!(cred.key_id, "");
1066 assert_eq!(cred.secret_key, "");
1067 }
1068
1069 #[tokio::test]
1070 async fn test_accessor_used_when_no_explicit_aws_credentials() {
1071 MockClock::set_system_time(Duration::from_secs(100_000));
1072
1073 let now_ms = MockClock::system_time().as_millis() as u64;
1074
1075 let mock_storage_provider = Arc::new(MockStorageOptionsProvider::new(Some(600_000)));
1077
1078 let expires_at = now_ms + 600_000; let initial_options = HashMap::from([
1081 (
1082 "aws_access_key_id".to_string(),
1083 "AKID_FROM_ACCESSOR".to_string(),
1084 ),
1085 (
1086 "aws_secret_access_key".to_string(),
1087 "SECRET_FROM_ACCESSOR".to_string(),
1088 ),
1089 (
1090 "aws_session_token".to_string(),
1091 "TOKEN_FROM_ACCESSOR".to_string(),
1092 ),
1093 ("expires_at_millis".to_string(), expires_at.to_string()),
1094 ("refresh_offset_millis".to_string(), "300000".to_string()), ]);
1096
1097 let accessor = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
1099 initial_options,
1100 mock_storage_provider.clone(),
1101 ));
1102
1103 let (result, _region) = build_aws_credential(
1105 Duration::from_secs(300),
1106 None, None, Some("us-west-2".to_string()),
1109 Some(accessor),
1110 )
1111 .await
1112 .unwrap();
1113
1114 let cred = result.get_credential().await.unwrap();
1116 assert_eq!(cred.key_id, "AKID_FROM_ACCESSOR");
1117 assert_eq!(cred.secret_key, "SECRET_FROM_ACCESSOR");
1118
1119 assert_eq!(mock_storage_provider.get_call_count().await, 0);
1121
1122 MockClock::set_system_time(Duration::from_secs(100_000 + 360));
1124
1125 let cred = result.get_credential().await.unwrap();
1127 assert_eq!(cred.key_id, "AKID_1");
1128 assert_eq!(cred.secret_key, "SECRET_1");
1129
1130 assert_eq!(mock_storage_provider.get_call_count().await, 1);
1132 }
1133}