aws_secretsmanager_cache/
cache.rs

1use std::num::NonZeroUsize;
2
3use super::cache_item::CacheItem;
4use super::config::CacheConfig;
5use aws_sdk_config::error::SdkError;
6use aws_sdk_secretsmanager::operation::get_secret_value::GetSecretValueError;
7use aws_sdk_secretsmanager::Client as SecretsManagerClient;
8use lru::LruCache;
9
10/// Client for in-process caching of secret values from AWS Secrets Manager.
11///
12/// An LRU (least-recently used) caching scheme is used that provides
13/// O(1) insertions and O(1) lookups for cached values.
14pub struct SecretCache {
15    client: SecretsManagerClient,
16    config: CacheConfig,
17    cache: LruCache<String, CacheItem<String>>,
18}
19
20impl SecretCache {
21    /// Returns a new SecretsCache using the default Cache Configuration options.
22    pub fn new(client: SecretsManagerClient) -> Self {
23        SecretCache::new_cache(client, CacheConfig::new())
24    }
25
26    /// Returns a new SecretsCache using a provided custom Cache Configuration.
27    pub fn new_with_config(client: SecretsManagerClient, config: CacheConfig) -> Self {
28        SecretCache::new_cache(client, config)
29    }
30
31    fn new_cache(client: SecretsManagerClient, config: CacheConfig) -> Self {
32        let cache = LruCache::new(
33            NonZeroUsize::new(config.max_cache_size)
34                .unwrap_or(NonZeroUsize::new(1).expect("Default max_cache_size must be non-zero")),
35        );
36        Self {
37            client,
38            config,
39            cache,
40        }
41    }
42
43    /// Returns a builder for getting secret strings.
44    ///
45    /// Retrieve the secret value with send()
46    pub fn get_secret_string(&mut self, secret_id: String) -> GetSecretStringBuilder {
47        GetSecretStringBuilder::new(self, secret_id)
48    }
49}
50
51/// A builder for the get_secret_string method.
52pub struct GetSecretStringBuilder<'a> {
53    secret_cache: &'a mut SecretCache,
54    secret_id: String,
55    force_refresh: bool,
56}
57
58impl<'a> GetSecretStringBuilder<'a> {
59    pub fn new(secret_cache: &'a mut SecretCache, secret_id: String) -> Self {
60        GetSecretStringBuilder {
61            secret_cache,
62            secret_id,
63            force_refresh: false,
64        }
65    }
66
67    /// Forces a refresh of the secret.
68    ///
69    /// Forces the secret to be fetched from AWS and updates the cache with the fresh value.
70    /// This is required when the cached secret is out of date but not expired, for example due to rotation.
71    pub fn force_refresh(mut self) -> Self {
72        self.force_refresh = true;
73        self
74    }
75
76    /// Fetches the secret value from the cache.
77    ///
78    /// If the secret value exists in the cache and hasn't expired it will be immediately returned.
79    /// The secret will be fetched by calling AWS Secrets Manager and updated in the cache if:
80    /// - the secret value hasn't been stored in the cache
81    /// - the secret stored in the cache but has expired
82    /// - the force_refresh option was provided
83    ///
84    /// Values are stored in the cache with the cache_item_ttl from the CacheConfig.
85    pub async fn send(&mut self) -> Result<String, SdkError<GetSecretValueError>> {
86        if !self.force_refresh {
87            if let Some(cache_item) = self.secret_cache.cache.get(&self.secret_id) {
88                if !cache_item.is_expired() {
89                    return Ok(cache_item.value.clone());
90                }
91            }
92        }
93
94        match self.fetch_secret().await {
95            Ok(secret_value) => {
96                let cache_item = CacheItem::new(
97                    secret_value.clone(),
98                    self.secret_cache.config.cache_item_ttl,
99                );
100                self.secret_cache
101                    .cache
102                    .put(self.secret_id.clone(), cache_item);
103                Ok(secret_value)
104            }
105            Err(e) => Err(e),
106        }
107    }
108
109    async fn fetch_secret(&mut self) -> Result<String, SdkError<GetSecretValueError>> {
110        match self
111            .secret_cache
112            .client
113            .get_secret_value()
114            .secret_id(self.secret_id.clone())
115            .version_stage(self.secret_cache.config.version_stage.clone())
116            .send()
117            .await
118        {
119            Ok(resp) => return Ok(resp.secret_string.as_deref().unwrap().to_string()),
120            Err(e) => Err(e),
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use aws_sdk_config::config::{Credentials, Region};
129    use aws_sdk_secretsmanager::{Client as SecretsManagerClient, Config};
130
131    #[test]
132    fn get_secret_string_builder_defaults() {
133        let mock_secrets_manager_client = get_mock_secretsmanager_client();
134        let mut secret_cache = SecretCache::new(mock_secrets_manager_client);
135
136        let builder = GetSecretStringBuilder::new(&mut secret_cache, "service/secret".to_string());
137
138        assert_eq!(builder.secret_id, "service/secret");
139        assert!(!builder.force_refresh);
140    }
141
142    #[test]
143    fn get_secret_string_builder_force_refresh() {
144        let mock_secrets_manager_client = get_mock_secretsmanager_client();
145        let mut secret_cache = SecretCache::new(mock_secrets_manager_client);
146
147        let builder = GetSecretStringBuilder::new(&mut secret_cache, "service/secret".to_string())
148            .force_refresh();
149
150        assert_eq!(builder.secret_id, "service/secret");
151        assert!(builder.force_refresh);
152    }
153
154    // provides a mocked AWS SecretsManager client for testing
155    fn get_mock_secretsmanager_client() -> SecretsManagerClient {
156        let conf = Config::builder()
157            .region(Region::new("ap-southeast-2"))
158            .credentials_provider(Credentials::new("asdf", "asdf", None, None, "test"))
159            .build();
160
161        SecretsManagerClient::from_conf(conf)
162    }
163}