llm_shield_cloud_aws/
secrets.rs1use aws_sdk_secretsmanager::Client;
6use llm_shield_cloud::{
7 async_trait, CloudError, CloudSecretManager, Result, SecretCache, SecretMetadata, SecretValue,
8};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12pub struct AwsSecretsManager {
35 client: Client,
36 cache: SecretCache,
37 region: String,
38}
39
40impl AwsSecretsManager {
41 pub async fn new() -> Result<Self> {
54 let config = aws_config::load_from_env().await;
55 let region = config
56 .region()
57 .map(|r| r.to_string())
58 .unwrap_or_else(|| "us-east-1".to_string());
59
60 let client = Client::new(&config);
61 let cache = SecretCache::new(300); tracing::info!("Initialized AWS Secrets Manager client in region: {}", region);
64
65 Ok(Self {
66 client,
67 cache,
68 region,
69 })
70 }
71
72 pub async fn new_with_region(region: impl Into<String>) -> Result<Self> {
82 let region_str = region.into();
83 let config = aws_config::from_env()
84 .region(aws_config::Region::new(region_str.clone()))
85 .load()
86 .await;
87
88 let client = Client::new(&config);
89 let cache = SecretCache::new(300);
90
91 tracing::info!("Initialized AWS Secrets Manager client in region: {}", region_str);
92
93 Ok(Self {
94 client,
95 cache,
96 region: region_str,
97 })
98 }
99
100 pub async fn new_with_cache_ttl(
111 region: impl Into<String>,
112 cache_ttl_seconds: u64,
113 ) -> Result<Self> {
114 let region_str = region.into();
115 let config = aws_config::from_env()
116 .region(aws_config::Region::new(region_str.clone()))
117 .load()
118 .await;
119
120 let client = Client::new(&config);
121 let cache = SecretCache::new(cache_ttl_seconds);
122
123 tracing::info!(
124 "Initialized AWS Secrets Manager client in region: {} with {}s cache TTL",
125 region_str,
126 cache_ttl_seconds
127 );
128
129 Ok(Self {
130 client,
131 cache,
132 region: region_str,
133 })
134 }
135
136 pub fn region(&self) -> &str {
138 &self.region
139 }
140
141 pub async fn clear_cache(&self) {
143 self.cache.clear().await;
144 tracing::debug!("Cleared AWS Secrets Manager cache");
145 }
146
147 pub async fn cache_size(&self) -> usize {
149 self.cache.len().await
150 }
151}
152
153#[async_trait]
154impl CloudSecretManager for AwsSecretsManager {
155 async fn get_secret(&self, name: &str) -> Result<SecretValue> {
156 if let Some(cached) = self.cache.get(name).await {
158 tracing::debug!("Cache hit for secret: {}", name);
159 return Ok(cached);
160 }
161
162 tracing::debug!("Fetching secret from AWS Secrets Manager: {}", name);
163
164 let response = self
166 .client
167 .get_secret_value()
168 .secret_id(name)
169 .send()
170 .await
171 .map_err(|e| CloudError::secret_fetch(name, e.to_string()))?;
172
173 let value = if let Some(secret_string) = response.secret_string() {
175 SecretValue::from_string(secret_string.to_string())
176 } else if let Some(secret_binary) = response.secret_binary() {
177 SecretValue::from_bytes(secret_binary.clone().into_inner())
178 } else {
179 return Err(CloudError::SecretFormat {
180 name: name.to_string(),
181 reason: "Secret has no string or binary value".to_string(),
182 });
183 };
184
185 self.cache.set(name.to_string(), value.clone()).await;
187
188 tracing::info!("Successfully fetched secret: {}", name);
189
190 Ok(value)
191 }
192
193 async fn list_secrets(&self) -> Result<Vec<String>> {
194 tracing::debug!("Listing secrets from AWS Secrets Manager");
195
196 let mut secret_names = Vec::new();
197 let mut next_token: Option<String> = None;
198
199 loop {
200 let mut request = self.client.list_secrets();
201
202 if let Some(token) = next_token {
203 request = request.next_token(token);
204 }
205
206 let response = request
207 .send()
208 .await
209 .map_err(|e| CloudError::SecretList(e.to_string()))?;
210
211 for secret in response.secret_list() {
212 if let Some(name) = secret.name() {
213 secret_names.push(name.to_string());
214 }
215 }
216
217 next_token = response.next_token().map(String::from);
218
219 if next_token.is_none() {
220 break;
221 }
222 }
223
224 tracing::info!("Listed {} secrets", secret_names.len());
225
226 Ok(secret_names)
227 }
228
229 async fn create_secret(&self, name: &str, value: &SecretValue) -> Result<()> {
230 tracing::debug!("Creating secret in AWS Secrets Manager: {}", name);
231
232 self.client
233 .create_secret()
234 .name(name)
235 .secret_string(value.as_string())
236 .send()
237 .await
238 .map_err(|e| CloudError::secret_create(name, e.to_string()))?;
239
240 tracing::info!("Successfully created secret: {}", name);
241
242 Ok(())
243 }
244
245 async fn update_secret(&self, name: &str, value: &SecretValue) -> Result<()> {
246 tracing::debug!("Updating secret in AWS Secrets Manager: {}", name);
247
248 self.client
249 .update_secret()
250 .secret_id(name)
251 .secret_string(value.as_string())
252 .send()
253 .await
254 .map_err(|e| CloudError::secret_update(name, e.to_string()))?;
255
256 self.cache.invalidate(name).await;
258
259 tracing::info!("Successfully updated secret: {}", name);
260
261 Ok(())
262 }
263
264 async fn delete_secret(&self, name: &str) -> Result<()> {
265 tracing::debug!("Deleting secret from AWS Secrets Manager: {}", name);
266
267 self.client
268 .delete_secret()
269 .secret_id(name)
270 .force_delete_without_recovery(false) .send()
272 .await
273 .map_err(|e| CloudError::secret_delete(name, e.to_string()))?;
274
275 self.cache.invalidate(name).await;
277
278 tracing::info!("Successfully deleted secret (30-day recovery): {}", name);
279
280 Ok(())
281 }
282
283 async fn get_secret_metadata(&self, name: &str) -> Result<SecretMetadata> {
284 tracing::debug!("Fetching secret metadata from AWS Secrets Manager: {}", name);
285
286 let response = self
287 .client
288 .describe_secret()
289 .secret_id(name)
290 .send()
291 .await
292 .map_err(|e| CloudError::secret_fetch(name, e.to_string()))?;
293
294 let created_at = response
295 .created_date()
296 .and_then(|dt| {
297 chrono::DateTime::from_timestamp(dt.secs(), dt.subsec_nanos())
298 })
299 .unwrap_or_else(chrono::Utc::now);
300
301 let updated_at = response
302 .last_changed_date()
303 .and_then(|dt| {
304 chrono::DateTime::from_timestamp(dt.secs(), dt.subsec_nanos())
305 })
306 .unwrap_or(created_at);
307
308 let mut tags = HashMap::new();
309 for tag in response.tags() {
310 if let (Some(key), Some(value)) = (tag.key(), tag.value()) {
311 tags.insert(key.to_string(), value.to_string());
312 }
313 }
314
315 let version = response.version_ids_to_stages().and_then(|versions| {
316 versions
317 .iter()
318 .find(|(_, stages)| stages.contains(&"AWSCURRENT".to_string()))
319 .map(|(version_id, _)| version_id.clone())
320 });
321
322 Ok(SecretMetadata {
323 name: name.to_string(),
324 created_at,
325 updated_at,
326 tags,
327 version,
328 })
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_aws_secrets_manager_region() {
338 let region = "us-west-2".to_string();
341 assert_eq!(region, "us-west-2");
343 }
344
345 #[tokio::test]
346 async fn test_cache_operations() {
347 let cache = SecretCache::new(300);
349
350 let test_secret = SecretValue::from_string("test-value".to_string());
351 cache.set("test-key".to_string(), test_secret.clone()).await;
352
353 let retrieved = cache.get("test-key").await;
354 assert!(retrieved.is_some());
355 assert_eq!(retrieved.unwrap().as_string(), "test-value");
356
357 cache.clear().await;
358 assert_eq!(cache.len().await, 0);
359 }
360}