secret-vault 1.19.0

Library provides a secure vault to store securely application secrets in memory from Google/AWS/K8S and environment variables
Documentation
use crate::errors::*;
use crate::*;
use async_trait::*;
use aws_sdk_secretsmanager::error::SdkError;
use aws_smithy_types_convert::date_time::DateTimeExt;
use rsb_derive::*;
use rvstruct::ValueStruct;
use secret_vault_value::SecretValue;
use std::collections::HashMap;
use tracing::*;

#[derive(Debug, Clone, Eq, PartialEq, Builder)]
pub struct AwsSecretManagerSourceOptions {
    pub account_id: String,
    pub region: Option<aws_sdk_secretsmanager::config::Region>,

    #[default = "false"]
    pub read_metadata: bool,
}

#[derive(Debug, Clone)]
pub struct AwsSecretManagerSource {
    client: aws_sdk_secretsmanager::Client,
    options: AwsSecretManagerSourceOptions,
}

impl AwsSecretManagerSource {
    pub async fn new(account_id: &str) -> SecretVaultResult<Self> {
        Self::with_options(AwsSecretManagerSourceOptions::new(account_id.to_string())).await
    }

    pub async fn with_options(options: AwsSecretManagerSourceOptions) -> SecretVaultResult<Self> {
        let shared_config = aws_config::load_from_env().await;
        let effective_region = options
            .region
            .clone()
            .or_else(|| shared_config.region().cloned())
            .ok_or_else(|| {
                SecretVaultError::InvalidParametersError(SecretVaultInvalidParametersError::new(
                    SecretVaultInvalidParametersPublicDetails::new(
                        "region".into(),
                        "AWS region must be specified or available in the AWS shared config".into(),
                    ),
                ))
            })?;

        let client = aws_sdk_secretsmanager::Client::new(&shared_config);
        Ok(AwsSecretManagerSource {
            client,
            options: options.with_region(effective_region),
        })
    }
}

#[async_trait]
impl SecretsSource for AwsSecretManagerSource {
    fn name(&self) -> String {
        "AmazonSecretManager".to_string()
    }

    async fn get_secrets(
        &self,
        references: &[SecretVaultRef],
    ) -> SecretVaultResult<HashMap<SecretVaultRef, Secret>> {
        let mut result_map: HashMap<SecretVaultRef, Secret> = HashMap::new();

        for secret_ref in references {
            let aws_secret_arn = format!(
                "arn:aws:secretsmanager:{}:{}:secret:{}",
                self.options.region.as_ref().unwrap(),
                self.options.account_id,
                secret_ref.key.secret_name.value()
            );

            match self
                .client
                .get_secret_value()
                .secret_id(aws_secret_arn.clone())
                .set_version_stage(
                    secret_ref
                        .key
                        .secret_version
                        .as_ref()
                        .map(|v| v.value().into()),
                )
                .send()
                .await
            {
                Ok(aws_secret) => {
                    let maybe_secret_value =
                        aws_secret.secret_string.map(SecretValue::from).or_else(|| {
                            aws_secret
                                .secret_binary
                                .map(|secret_binary| SecretValue::new(secret_binary.into_inner()))
                        });

                    if let Some(secret_value) = maybe_secret_value {
                        let mut metadata = SecretMetadata::create_from_ref(secret_ref);

                        let maybe_aws_secret = if self.options.read_metadata {
                            Some(
                                self.client
                                    .describe_secret()
                                    .secret_id(aws_secret_arn.clone())
                                    .send()
                                    .await?,
                            )
                        } else {
                            None
                        };

                        if let Some(aws_secret_desc) = maybe_aws_secret {
                            for tag in aws_secret_desc.tags() {
                                if let Some(tag_key) = tag.key() {
                                    metadata.add_label(
                                        SecretMetadataLabel::new(tag_key.to_string())
                                            .opt_value(tag.value().map(|s| s.to_string())),
                                    );
                                }
                            }

                            metadata.description =
                                aws_secret_desc.description().map(|s| s.to_string());
                            metadata.created_at = aws_secret_desc
                                .created_date()
                                .and_then(|d| d.to_chrono_utc().ok());
                            metadata.updated_at = aws_secret_desc
                                .last_changed_date()
                                .and_then(|d| d.to_chrono_utc().ok());
                        }

                        result_map.insert(secret_ref.clone(), Secret::new(secret_value, metadata));
                    } else if secret_ref.required {
                        return Err(SecretVaultError::DataNotFoundError(
                            SecretVaultDataNotFoundError::new(
                                SecretVaultErrorPublicGenericDetails::new("SECRET_PAYLOAD".into()),
                                format!(
                                    "Secret is required but payload is not found for {aws_secret_arn}"
                                ),
                            ),
                        ));
                    }
                }
                Err(SdkError::ServiceError(svc_err))
                    if svc_err.err().is_resource_not_found_exception() =>
                {
                    if secret_ref.required {
                        return Err(SecretVaultError::DataNotFoundError(
                                SecretVaultDataNotFoundError::new(
                                    SecretVaultErrorPublicGenericDetails::new("SECRET_NOT_FOUND".into()),
                                    format!(
                                        "Secret is required but not found in environment variables {:?}",
                                        secret_ref.key.secret_name
                                    ),
                                ),
                            ));
                    } else {
                        debug!("Secret or secret version {}/{:?} doesn't exist and since it is not required it is skipped",aws_secret_arn, &secret_ref.key.secret_version);
                    }
                }
                Err(err) => {
                    error!(
                        "Unable to read secret or secret version {}/{:?}: {}.",
                        aws_secret_arn, &secret_ref.key.secret_version, err
                    );
                    return Err(SecretVaultError::from(err));
                }
            }
        }

        Ok(result_map)
    }
}