diff --git a/.gitignore b/.gitignore index c335994..8ab2f81 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,6 @@ fabric.properties # Visual Studio Code .vscode/* -!.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json diff --git a/aws_secretsmanager_agent/src/main.rs b/aws_secretsmanager_agent/src/main.rs index 429a250..d7655d7 100644 --- a/aws_secretsmanager_agent/src/main.rs +++ b/aws_secretsmanager_agent/src/main.rs @@ -222,14 +222,14 @@ mod tests { use hyper::{client, Request, StatusCode}; use hyper_util::rt::TokioIo; use serde_json::Value; - use std; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::sync::{mpsc, Arc, Mutex}; use std::time::Duration; use std::{fs, thread}; - use tokio; + use tokio::net::TcpStream; use tokio::task::JoinSet; use tokio::time::timeout; @@ -626,7 +626,7 @@ mod tests { // Verify a query using the pending label #[tokio::test] async fn pending_success() { - let req = format!("/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING"); + let req = "/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING".to_string(); let (status, body) = run_request(&req).await; assert_eq!(status, StatusCode::OK); validate_response_extra("MyTest", DEFAULT_VERSION, vec!["AWSPENDING"], body); @@ -696,7 +696,7 @@ mod tests { #[tokio::test] async fn path_pending_success() { let req = "/v1/My/Test?versionStage=AWSPENDING"; - let (status, body) = run_request(&req).await; + let (status, body) = run_request(req).await; assert_eq!(status, StatusCode::OK); validate_response_extra("My/Test", DEFAULT_VERSION, vec!["AWSPENDING"], body); } diff --git a/aws_secretsmanager_agent/src/utils.rs b/aws_secretsmanager_agent/src/utils.rs index 5c914bd..d8a1c87 100644 --- a/aws_secretsmanager_agent/src/utils.rs +++ b/aws_secretsmanager_agent/src/utils.rs @@ -211,7 +211,7 @@ pub mod tests { // Used to inject env variable values for testing. Uses thread local data since // multi-threaded tests setting process wide env variables can collide. thread_local! { - static ENVVAR: RefCell>> = RefCell::new(None); + static ENVVAR: RefCell>> = const { RefCell::new(None) }; } pub fn set_test_var(key: &'static str, val: &'static str) { ENVVAR.set(Some(vec![(key, val)])); @@ -227,9 +227,9 @@ pub mod tests { return Err(VarError::NotPresent); } if let Some(varvec) = ENVVAR.with_borrow(|v| v.clone()) { - let found = varvec.iter().filter(|keyval| keyval.0 == key).next(); - if found != None { - return Ok(found.unwrap().1.to_string()); + let found = varvec.iter().find(|keyval| keyval.0 == key); + if let Some(found) = found { + return Ok(found.1.to_string()); } } else { // Return a default value if no value is injected. @@ -310,18 +310,14 @@ pub mod tests { file: Some(&tmpfile), }; set_test_var("", ""); - std::fs::write(&tmpfile, format!("ssrf_env_variables = [\"NOSUCHENV\"]")) - .expect("could not write"); + std::fs::write(&tmpfile, "ssrf_env_variables = [\"NOSUCHENV\"]").expect("could not write"); let cfg = Config::new(Some(&tmpfile)).expect("config failed"); - assert_eq!( - get_token(&cfg) - .err() - .unwrap() - .downcast_ref::() - .unwrap() - .eq(&VarError::NotPresent), - true - ); + assert!(get_token(&cfg) + .err() + .unwrap() + .downcast_ref::() + .unwrap() + .eq(&VarError::NotPresent)); } // Make sure the timeout functon returns the correct value. diff --git a/aws_secretsmanager_caching/src/lib.rs b/aws_secretsmanager_caching/src/lib.rs index c4fd5f3..75c3cce 100644 --- a/aws_secretsmanager_caching/src/lib.rs +++ b/aws_secretsmanager_caching/src/lib.rs @@ -15,7 +15,7 @@ pub mod secret_store; use aws_sdk_secretsmanager::Client as SecretsManagerClient; use secret_store::SecretStoreError; -use output::GetSecretValueOutputDef; +use output::{DescribeSecretOutputDef, GetSecretValueOutputDef}; use secret_store::{MemoryStore, SecretStore}; use std::{error::Error, num::NonZeroUsize, time::Duration}; use tokio::sync::RwLock; @@ -206,6 +206,52 @@ impl SecretsManagerCachingClient { Ok(false) } + + /// Retrieves the metadata of the secret + /// + /// # Arguments + /// + /// * `secret_id` - The ARN or name of the secret to retrieve. + async fn describe_secret( + &self, + secret_id: &str, + ) -> Result> { + let read_lock = self.store.read().await; + + match read_lock.describe_secret(secret_id) { + Ok(r) => Ok(r), + Err(SecretStoreError::ResourceNotFound | SecretStoreError::DescribeCacheExpired) => { + drop(read_lock); + Ok(self.refresh_describe(secret_id).await?) + } + Err(e) => Err(Box::new(e)), + } + } + + /// Refreshes the secret metadata through a DescribeSecret call to ASM + /// + /// # Arguments + /// * `secret_id` - The ARN or name of the secret to retrieve. + async fn refresh_describe( + &self, + secret_id: &str, + ) -> Result> { + let response = self + .asm_client + .describe_secret() + .secret_id(secret_id) + .send() + .await?; + + let result: DescribeSecretOutputDef = response.into(); + + self.store + .write() + .await + .write_describe_secret(secret_id.to_owned(), result.clone())?; + + Ok(result) + } } #[cfg(test)] @@ -484,9 +530,12 @@ mod tests { sleep(Duration::from_millis(10)).await; - match client.get_secret_value(secret_id, version_id, None).await { - Ok(_) => panic!("Expected failure"), - Err(_) => (), + if client + .get_secret_value(secret_id, version_id, None) + .await + .is_ok() + { + panic!("Expected failure") } } diff --git a/aws_secretsmanager_caching/src/output/describe_secret_output.rs b/aws_secretsmanager_caching/src/output/describe_secret_output.rs new file mode 100644 index 0000000..f560d7c --- /dev/null +++ b/aws_secretsmanager_caching/src/output/describe_secret_output.rs @@ -0,0 +1,224 @@ +use std::time::SystemTime; + +use aws_sdk_secretsmanager::{ + operation::describe_secret::DescribeSecretOutput, + types::{ReplicationStatusType, StatusType, Tag}, +}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, TimestampSecondsWithFrac}; + +/// Structure to store the secret details +#[serde_as] +#[derive(::std::clone::Clone, ::std::cmp::PartialEq, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "PascalCase")] +pub struct DescribeSecretOutputDef { + /// The ARN of the secret. + #[serde(rename(serialize = "ARN"))] + pub arn: Option, + + /// The name of the secret. + pub name: Option, + + /// The description of the secret. + pub description: Option, + + /// The key ID or alias ARN of the KMS key that Secrets Manager uses to encrypt the secret value. If the secret is encrypted with the Amazon Web Services managed key aws/secretsmanager, this field is omitted. Secrets created using the console use an KMS key ID. + pub kms_key_id: Option, + + /// Specifies whether automatic rotation is turned on for this secret. + /// To turn on rotation, use RotateSecret. To turn off rotation, use CancelRotateSecret. + pub rotation_enabled: Option, + + /// The ARN of the Lambda function that Secrets Manager invokes to rotate the secret. + pub rotation_lambda_arn: Option, + + // Todo: Add support for this; skipping as not in scope for Ragnarok + // pub rotation_rules: Option, + /// The last date and time that Secrets Manager rotated the secret. If the secret isn't configured for rotation, Secrets Manager returns null. + #[serde_as(as = "Option>")] + pub last_rotated_date: Option, + + /// The last date and time that this secret was modified in any way. + #[serde_as(as = "Option>")] + pub last_changed_date: Option, + + /// The date that the secret was last accessed in the Region. This field is omitted if the secret has never been retrieved in the Region. + #[serde_as(as = "Option>")] + pub last_accessed_date: Option, + + /// The date the secret is scheduled for deletion. If it is not scheduled for deletion, this field is omitted. When you delete a secret, Secrets Manager requires a recovery window of at least 7 days before deleting the secret. Some time after the deleted date, Secrets Manager deletes the secret, including all of its versions. + /// If a secret is scheduled for deletion, then its details, including the encrypted secret value, is not accessible. To cancel a scheduled deletion and restore access to the secret, use RestoreSecret. + #[serde_as(as = "Option>")] + pub deleted_date: Option, + + /// The next rotation is scheduled to occur on or before this date. If the secret isn't configured for rotation, Secrets Manager returns null. + #[serde_as(as = "Option>")] + pub next_rotation_date: Option, + + /// The list of tags attached to the secret. To add tags to a secret, use TagResource. To remove tags, use UntagResource. + pub tags: Option>, + + /// A list of the versions of the secret that have staging labels attached. Versions that don't have staging labels are considered deprecated and Secrets Manager can delete them. + /// Secrets Manager uses staging labels to indicate the status of a secret version during rotation. The three staging labels for rotation are: + ///
    + ///
  • AWSCURRENT, which indicates the current version of the secret.
  • + ///
  • AWSPENDING, which indicates the version of the secret that contains new secret information that will become the next current version when rotation finishes. During rotation, Secrets Manager creates an AWSPENDING version ID before creating the new secret version. To check if a secret version exists, call GetSecretValue.
  • + ///
  • AWSPREVIOUS, which indicates the previous current version of the secret. You can use this as the last known good version.
  • + ///
+ /// For more information about rotation and staging labels, see How rotation works. + pub version_ids_to_stages: Option<::std::collections::HashMap>>, + + /// The ID of the service that created this secret. For more information, see Secrets managed by other Amazon Web Services services. + pub owning_service: Option, + + /// The date the secret was created. + #[serde_as(as = "Option>")] + pub created_date: Option, + + // Todo: Add support for this; skipping as not in scope for Ragnarok + // pub replication_status: + // Option<::std::vec::Vec>, + /// The Region the secret is in. If a secret is replicated to other Regions, the replicas are listed in ReplicationStatus. + pub primary_region: Option, + ///

A list of the replicas of this secret and their status:

+ ///
    + ///
  • + ///

    Failed, which indicates that the replica was not created.

  • + ///
  • + ///

    InProgress, which indicates that Secrets Manager is in the process of creating the replica.

  • + ///
  • + ///

    InSync, which indicates that the replica was created.

  • + ///
+ pub replication_status: Option>, +} + +impl DescribeSecretOutputDef { + /// Converts DescribeSecretOutput to DescribeSecretOutputDef + pub fn new(describe: DescribeSecretOutput) -> Self { + DescribeSecretOutputDef { + arn: describe.arn, + name: describe.name, + description: describe.description, + kms_key_id: describe.kms_key_id, + rotation_enabled: describe.rotation_enabled, + rotation_lambda_arn: describe.rotation_lambda_arn, + last_rotated_date: describe.last_rotated_date.map(|i| i.try_into().unwrap()), + last_changed_date: describe.last_changed_date.map(|i| i.try_into().unwrap()), + last_accessed_date: describe.last_accessed_date.map(|i| i.try_into().unwrap()), + deleted_date: describe.deleted_date.map(|i| i.try_into().unwrap()), + next_rotation_date: describe.next_rotation_date.map(|i| i.try_into().unwrap()), + tags: describe + .tags + .map(|o| o.iter().map(|tag| tag.into()).collect()), + version_ids_to_stages: describe.version_ids_to_stages, + owning_service: describe.owning_service, + created_date: describe.created_date.map(|i| i.try_into().unwrap()), + replication_status: describe.replication_status.map(|replication_status| { + replication_status + .iter() + .map(|status| status.into()) + .collect() + }), + primary_region: describe.primary_region, + } + } +} + +/// Copy of the remote aws_sdk_secretsmanager::types::DateTime type. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TagDef { + /// The key identifier, or name, of the tag. + pub key: Option, + + /// The string value associated with the key of the tag. + pub value: Option, +} + +impl From<&Tag> for TagDef { + fn from(value: &Tag) -> Self { + TagDef { + key: value.key.clone(), + value: value.value.clone(), + } + } +} + +#[serde_as] +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +/// Replication status type +pub struct ReplicationStatusTypeDef { + ///

The Region where replication occurs.

+ pub region: Option, + ///

Can be an ARN, Key ID, or Alias.

+ pub kms_key_id: Option, + ///

The status can be InProgress, Failed, or InSync.

+ pub status: Option, + ///

Status message such as "Secret with this name already exists in this region".

+ pub status_message: Option, + ///

The date that the secret was last accessed in the Region. This field is omitted if the secret has never been retrieved in the Region.

+ pub last_accessed_date: Option, +} + +impl From for ReplicationStatusType { + fn from(value: ReplicationStatusTypeDef) -> Self { + ReplicationStatusType::builder() + .set_region(value.region) + .set_kms_key_id(value.kms_key_id) + .set_status(value.status.map(Into::into)) + .set_last_accessed_date(value.last_accessed_date.map(Into::into)) + .set_status_message(value.status_message) + .build() + } +} + +impl From<&ReplicationStatusType> for ReplicationStatusTypeDef { + fn from(value: &ReplicationStatusType) -> Self { + ReplicationStatusTypeDef { + region: value.region().map(String::from), + kms_key_id: value.kms_key_id().map(String::from), + status: value.status().map(Into::into), + status_message: value.status_message().map(String::from), + last_accessed_date: value.last_accessed_date().map(|i| (*i).try_into().unwrap()), + } + } +} + +#[serde_as] +#[derive(Clone, Eq, Ord, PartialEq, PartialOrd, Debug, Hash, Serialize, Deserialize)] +/// Status type +pub enum StatusTypeDef { + #[allow(missing_docs)] // documentation missing in model + Failed, + #[allow(missing_docs)] // documentation missing in model + InProgress, + #[allow(missing_docs)] // documentation missing in model + InSync, +} + +impl From for StatusType { + fn from(value: StatusTypeDef) -> Self { + match value { + StatusTypeDef::Failed => StatusType::Failed, + StatusTypeDef::InProgress => StatusType::InProgress, + StatusTypeDef::InSync => StatusType::InSync, + } + } +} + +impl From<&StatusType> for StatusTypeDef { + fn from(value: &StatusType) -> Self { + match value { + StatusType::Failed => StatusTypeDef::Failed, + StatusType::InProgress => StatusTypeDef::InProgress, + StatusType::InSync => StatusTypeDef::InSync, + _ => panic!("Invalid value for StatusTypeDef: {}", value), + } + } +} + +impl From for DescribeSecretOutputDef { + fn from(input: DescribeSecretOutput) -> Self { + Self::new(input) + } +} diff --git a/aws_secretsmanager_caching/src/output.rs b/aws_secretsmanager_caching/src/output/get_secret_value_output.rs similarity index 74% rename from aws_secretsmanager_caching/src/output.rs rename to aws_secretsmanager_caching/src/output/get_secret_value_output.rs index a8ecab4..7716a95 100644 --- a/aws_secretsmanager_caching/src/output.rs +++ b/aws_secretsmanager_caching/src/output/get_secret_value_output.rs @@ -1,7 +1,7 @@ use aws_sdk_secretsmanager::operation::get_secret_value::GetSecretValueOutput; use aws_smithy_types::base64; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_with::{serde_as, DeserializeAs, SerializeAs, TimestampSecondsWithFrac}; +use serde::{Deserialize, Serialize, Serializer}; +use serde_with::{serde_as, TimestampSecondsWithFrac}; use std::convert::TryFrom; use std::time::SystemTime; @@ -61,12 +61,6 @@ impl GetSecretValueOutputDef { } } -impl From for GetSecretValueOutputDef { - fn from(input: GetSecretValueOutput) -> Self { - Self::new(input) - } -} - /// Copy of the remote AWS SDK Blob type. #[serde_as] #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Deserialize)] @@ -96,40 +90,8 @@ impl Serialize for BlobDef { } } -/// Copy of the remote aws_smithy_types::DateTime type. -#[serde_as] -#[derive(Serialize, Deserialize, Debug)] -#[serde(remote = "::aws_smithy_types::DateTime")] -pub struct DateTimeDef { - #[serde(getter = "::aws_smithy_types::DateTime::secs")] - seconds: i64, - #[serde(getter = "::aws_smithy_types::DateTime::subsec_nanos")] - subsecond_nanos: u32, -} - -impl SerializeAs<::aws_smithy_types::DateTime> for DateTimeDef { - fn serialize_as( - source: &::aws_smithy_types::DateTime, - serializer: S, - ) -> Result - where - S: Serializer, - { - DateTimeDef::serialize(source, serializer) - } -} - -impl<'de> DeserializeAs<'de, ::aws_smithy_types::DateTime> for DateTimeDef { - fn deserialize_as(deserializer: D) -> Result<::aws_smithy_types::DateTime, D::Error> - where - D: Deserializer<'de>, - { - DateTimeDef::deserialize(deserializer) - } -} - -impl From for ::aws_smithy_types::DateTime { - fn from(def: DateTimeDef) -> ::aws_smithy_types::DateTime { - ::aws_smithy_types::DateTime::from_secs_and_nanos(def.seconds, def.subsecond_nanos) +impl From for GetSecretValueOutputDef { + fn from(input: GetSecretValueOutput) -> Self { + Self::new(input) } } diff --git a/aws_secretsmanager_caching/src/output/mod.rs b/aws_secretsmanager_caching/src/output/mod.rs new file mode 100644 index 0000000..cc52c7d --- /dev/null +++ b/aws_secretsmanager_caching/src/output/mod.rs @@ -0,0 +1,5 @@ +mod describe_secret_output; +mod get_secret_value_output; + +pub use describe_secret_output::DescribeSecretOutputDef; +pub use get_secret_value_output::GetSecretValueOutputDef; 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..ed36452 100644 --- a/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs +++ b/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs @@ -1,6 +1,6 @@ mod cache; -use crate::output::GetSecretValueOutputDef; +use crate::output::{DescribeSecretOutputDef, GetSecretValueOutputDef}; use self::cache::Cache; @@ -33,10 +33,26 @@ impl GSVValue { } } +#[derive(Debug, Clone)] +struct DescribeValue { + value: DescribeSecretOutputDef, + last_retrieved_at: Instant, +} + +impl DescribeValue { + fn new(value: DescribeSecretOutputDef) -> Self { + Self { + value, + last_retrieved_at: Instant::now(), + } + } +} + #[derive(Debug, Clone)] /// In-memory secret store using an time and space bound cache pub struct MemoryStore { gsv_cache: Cache, + describe_cache: Cache, ttl: Duration, } @@ -51,6 +67,7 @@ impl MemoryStore { pub fn new(max_size: NonZeroUsize, ttl: Duration) -> Self { Self { gsv_cache: Cache::new(max_size), + describe_cache: Cache::new(max_size), ttl, } } @@ -94,6 +111,30 @@ impl SecretStore for MemoryStore { Ok(()) } + + fn describe_secret<'a>( + &'a self, + secret_id: &'a str, + ) -> Result { + match self.describe_cache.get(secret_id) { + Some(describe) if describe.last_retrieved_at.elapsed() > self.ttl => { + Err(SecretStoreError::DescribeCacheExpired) + } + Some(describe) => Ok(describe.clone().value), + None => Err(SecretStoreError::ResourceNotFound), + } + } + + fn write_describe_secret( + &mut self, + secret_id: String, + data: DescribeSecretOutputDef, + ) -> Result<(), SecretStoreError> { + self.describe_cache + .insert(secret_id, DescribeValue::new(data)); + + Ok(()) + } } /// Write the secret value to the store diff --git a/aws_secretsmanager_caching/src/secret_store/mod.rs b/aws_secretsmanager_caching/src/secret_store/mod.rs index f566727..2593db3 100644 --- a/aws_secretsmanager_caching/src/secret_store/mod.rs +++ b/aws_secretsmanager_caching/src/secret_store/mod.rs @@ -4,7 +4,7 @@ pub use memory_store::MemoryStore; use serde::{Deserialize, Serialize}; use std::{error::Error, fmt::Debug}; -use crate::output::GetSecretValueOutputDef; +use crate::output::{DescribeSecretOutputDef, GetSecretValueOutputDef}; /// Response of the GetSecretValue API #[derive(Serialize, Deserialize, Debug, Clone)] @@ -29,6 +29,19 @@ pub trait SecretStore: Debug + Send + Sync { version_stage: Option, data: GetSecretValueOutputDef, ) -> Result<(), SecretStoreError>; + + /// Get secret metadata from the cache + fn describe_secret<'a>( + &'a self, + secret_id: &'a str, + ) -> Result; + + /// Write secret metadata to the cache + fn write_describe_secret( + &mut self, + secret_id: String, + data: DescribeSecretOutputDef, + ) -> Result<(), SecretStoreError>; } /// All possible error types @@ -42,6 +55,10 @@ pub enum SecretStoreError { #[error("cache expired")] CacheExpired(Box), + /// Describe Secret cache TTL expired + #[error("cache expired")] + DescribeCacheExpired, + /// An unexpected error occurred #[error("unhandled error {0:?}")] Unhandled(#[source] Box),