diff --git a/aws_secretsmanager_agent/src/cache_manager.rs b/aws_secretsmanager_agent/src/cache_manager.rs index 85849fc..0dfeaa2 100644 --- a/aws_secretsmanager_agent/src/cache_manager.rs +++ b/aws_secretsmanager_agent/src/cache_manager.rs @@ -91,8 +91,45 @@ impl CacheManager { } } } -} + /// Evict a secret from the cache. + /// + /// # Arguments + /// + /// * `secret_id` - The name of the secret to evict. + /// * `version` - The version of the secret to evict. + /// * `label` - The label of the secret to evict. + /// + /// # Returns + /// + /// * `Ok(String)` - Success message. + /// * `Err((u16, String))` - The error code and message. + /// + /// # Errors + /// + /// * `HttpError` - The error returned from the SDK. + /// + /// # Example + /// + /// ``` + /// let cache_manager = CacheManager::new().await.unwrap(); + /// let message = cache_manager.evict("my-secret", None, None).unwrap(); + /// ``` + pub async fn evict( + &self, + secret_id: &str, + version: Option<&str>, + label: Option<&str> + ) -> Result { + match self.0.remove_secret_value(secret_id, version, label).await { + Ok(_) => Ok("Secret successfully evicted".to_string()), + Err(e) => { + error!("Failed to evict secret {}: {:?}", secret_id, e); + Err(HttpError(500, err_response("EvictionError", "Failed to evict secret"))) + } + } + } +} /// Private helper to format in internal service error response. #[doc(hidden)] fn int_err() -> HttpError { diff --git a/aws_secretsmanager_agent/src/main.rs b/aws_secretsmanager_agent/src/main.rs index 429a250..891917b 100644 --- a/aws_secretsmanager_agent/src/main.rs +++ b/aws_secretsmanager_agent/src/main.rs @@ -842,9 +842,9 @@ mod tests { // Verify requests using the wrong verbs fail with 405. #[tokio::test] - async fn get_only() { + async fn get_and_post_only() { for verb in [ - "POST", "PUT", "PATCH", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE", + "PUT", "PATCH", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE", ] { let (status, _) = run_requests_with_verb(vec![(verb, "/secretsmanager/get?secretId=MyTest")]) diff --git a/aws_secretsmanager_agent/src/server.rs b/aws_secretsmanager_agent/src/server.rs index 4c1929a..9dc45ec 100644 --- a/aws_secretsmanager_agent/src/server.rs +++ b/aws_secretsmanager_agent/src/server.rs @@ -29,14 +29,14 @@ pub struct Server { /// Handle incoming HTTP requests. /// -/// Implements the HTTP handler. Each incomming request is handled in its own +/// Implements the HTTP handler. Each incoming request is handled in its own /// thread. impl Server { /// Create a server instance. /// /// # Arguments /// - /// * `listener` - The TcpListener to use to accept incomming requests. + /// * `listener` - The TcpListener to use to accept incoming requests. /// * `cfg` - The config object to use for options such header names. /// /// # Returns @@ -87,11 +87,11 @@ impl Server { Ok(()) } - /// Private helper to process the incomming request body and format a response. + /// Private helper to process the incoming request body and format a response. /// /// # Arguments /// - /// * `req` - The incomming HTTP request. + /// * `req` - The incoming HTTP request. /// * `count` - The number of concurrent requets being handled. /// /// # Returns @@ -118,11 +118,11 @@ impl Server { } } - /// Parse an incomming request and provide the response data. + /// Parse an incoming request and provide the response data. /// /// # Arguments /// - /// * `req` - The incomming HTTP request. + /// * `req` - The incoming HTTP request. /// * `count` - The number of concurrent requets being handled. /// /// # Returns @@ -137,13 +137,11 @@ impl Server { ) -> Result { self.validate_max_conn(req, count)?; // Verify connection limits are not exceeded self.validate_token(req)?; // Check for a valid SSRF token - self.validate_method(req)?; // Allow only GET requests + self.validate_method(req)?; // Allow only GET and POST requests - match req.uri().path() { - "/ping" => Ok("healthy".into()), // Standard health check - - // Lambda extension style query - "/secretsmanager/get" => { + match (req.method(), req.uri().path()) { + (&Method::GET, "/ping") => Ok("healthy".into()), // Standard health check + (&Method::GET, "/secretsmanager/get") => { // Lambda extension style query let qry = GSVQuery::try_from_query(&req.uri().to_string())?; Ok(self .cache_mgr @@ -154,9 +152,8 @@ impl Server { ) .await?) } - // Path style request - path if path.starts_with(self.path_prefix.as_str()) => { + (&Method::GET, path) if path.starts_with(self.path_prefix.as_str()) => { let qry = GSVQuery::try_from_path_query(&req.uri().to_string(), &self.path_prefix)?; Ok(self .cache_mgr @@ -167,17 +164,29 @@ impl Server { ) .await?) } + (&Method::POST, "/secretsmanager/evict") => { + let qry = GSVQuery::try_from_query(&req.uri().to_string())?; + Ok(self + .cache_mgr + .evict( + &qry.secret_id, + qry.version_id.as_deref(), + qry.version_stage.as_deref(), + ) + .await?) + } _ => Err(HttpError(404, "Not found".into())), } } - /// Verify the incomming request does not exceed the maximum connection limit. + + /// Verify the incoming request does not exceed the maximum connection limit. /// /// The limit is not enforced for ping/health checks. /// /// # Arguments /// - /// * `req` - The incomming HTTP request. + /// * `req` - The incoming HTTP request. /// * `count` - The number of concurrent requets being handled. /// /// # Returns @@ -209,7 +218,7 @@ impl Server { /// /// # Arguments /// - /// * `req` - The incomming HTTP request. + /// * `req` - The incoming HTTP request. /// /// # Returns /// @@ -241,22 +250,21 @@ impl Server { Err(HttpError(403, "Bad Token".into())) } - /// Verify the request is using the GET HTTP verb. + /// Verify the request is using the GET or POST HTTP verb. /// /// # Arguments /// - /// * `req` - The incomming HTTP request. + /// * `req` - The incoming HTTP request. /// /// # Returns /// - /// * `Ok(())` - If the GET verb/method is use. - /// * `Err((u16, String))` - A 405 error codde and message when GET is not used. + /// * `Ok(())` - If the GET or POST verb/method is use. + /// * `Err((u16, String))` - A 405 error codde and message when GET or POST is not used. #[doc(hidden)] fn validate_method(&self, req: &Request) -> Result<(), HttpError> { - if *req.method() == Method::GET { - return Ok(()); + match *req.method() { + Method::GET | Method::POST => Ok(()), + _ => Err(HttpError(405, "Method not allowed".into())), } - - Err(HttpError(405, "Not allowed".into())) } -} +} \ No newline at end of file diff --git a/aws_secretsmanager_caching/src/lib.rs b/aws_secretsmanager_caching/src/lib.rs index c8a5ca6..b1b1807 100644 --- a/aws_secretsmanager_caching/src/lib.rs +++ b/aws_secretsmanager_caching/src/lib.rs @@ -296,6 +296,24 @@ impl SecretsManagerCachingClient { Ok(false) } + + /// Removes a secret in the cache, forcing a refresh on the next retrieval. + /// + /// # Arguments + /// + /// * `secret_id` - The ARN or name of the secret to remove. + /// * `version_id` - The version id of the secret version to remove. + /// * `version_stage` - The staging label of the version of the secret to remove. + pub async fn remove_secret_value( + &self, + secret_id: &str, + version_id: Option<&str>, + version_stage: Option<&str>, + ) -> Result<(), Box> { + let mut write_lock = self.store.write().await; + write_lock.remove_secret_value(secret_id, version_id, version_stage)?; + Ok(()) + } } #[cfg(test)] diff --git a/aws_secretsmanager_caching/src/secret_store/memory_store/cache.rs b/aws_secretsmanager_caching/src/secret_store/memory_store/cache.rs index 02b4bea..99299f6 100644 --- a/aws_secretsmanager_caching/src/secret_store/memory_store/cache.rs +++ b/aws_secretsmanager_caching/src/secret_store/memory_store/cache.rs @@ -48,8 +48,17 @@ impl Cache { { self.entries.get(key) } -} + /// Removes a key-value pair from the cache. + /// Returns the value if the key was present in the cache, None otherwise. + pub fn remove(&mut self, key: &Q) -> Option + where + Q: ?Sized + Hash + Eq, + K: Borrow, + { + self.entries.remove(key) + } +} #[cfg(test)] mod tests { use super::*; @@ -113,4 +122,19 @@ mod tests { let items: Vec = cache.entries.iter().map(|t| (*t.1)).collect(); assert_eq!(items, [2]); } + + #[test] + fn remove_evicts_entry() { + let mut cache = TestIntCache::new(NonZeroUsize::new(4).unwrap()); + + cache.insert("test1".to_string(), 1); + cache.insert("test2".to_string(), 2); + + assert_eq!(cache.remove("test1"), Some(1)); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get("test1"), None); + assert_eq!(cache.get("test2"), Some(&2)); + + assert_eq!(cache.remove("non_existent"), None); + } } diff --git a/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs b/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs index df405dd..8748601 100644 --- a/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs +++ b/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs @@ -94,6 +94,24 @@ impl SecretStore for MemoryStore { Ok(()) } + + fn remove_secret_value( + &mut self, + secret_id: &str, + version_id: Option<&str>, + version_stage: Option<&str>, + ) -> Result<(), SecretStoreError> { + let key = Key { + secret_id: secret_id.to_string(), + version_id: version_id.map(String::from), + version_stage: version_stage.map(String::from), + }; + if self.gsv_cache.remove(&key).is_none() { + Err(SecretStoreError::ResourceNotFound) + } else { + Ok(()) + } + } } /// Write the secret value to the store @@ -275,4 +293,29 @@ mod tests { Err(e) => panic!("Unexpected error: {}", e), } } + + #[test] + fn memory_store_remove_secret_value() { + let mut store = MemoryStore::default(); + + store_secret(&mut store, None, None, None); + + // Verify the secret exists + assert!(store.get_secret_value(NAME, None, None).is_ok()); + + // Remove the secret + assert!(store.remove_secret_value(NAME, None, None).is_ok()); + + // Verify the secret no longer exists + match store.get_secret_value(NAME, None, None) { + Err(SecretStoreError::ResourceNotFound) => (), + _ => panic!("Expected ResourceNotFound error"), + } + + // Attempt to remove a non-existent secret + match store.remove_secret_value("non_existent", None, None) { + Err(SecretStoreError::ResourceNotFound) => (), + _ => panic!("Expected ResourceNotFound error"), + } + } } diff --git a/aws_secretsmanager_caching/src/secret_store/mod.rs b/aws_secretsmanager_caching/src/secret_store/mod.rs index f566727..d7af003 100644 --- a/aws_secretsmanager_caching/src/secret_store/mod.rs +++ b/aws_secretsmanager_caching/src/secret_store/mod.rs @@ -29,6 +29,14 @@ pub trait SecretStore: Debug + Send + Sync { version_stage: Option, data: GetSecretValueOutputDef, ) -> Result<(), SecretStoreError>; + + /// Remove the secret value from the store + fn remove_secret_value( + &mut self, + secret_id: &str, + version_id: Option<&str>, + version_stage: Option<&str>, + ) -> Result<(), SecretStoreError>; } /// All possible error types