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 gcloud_sdk::google::cloud::secretmanager::v1::secret_manager_service_client::SecretManagerServiceClient;
use gcloud_sdk::*;
use rsb_derive::*;
use rvstruct::ValueStruct;
use std::collections::HashMap;

use crate::errors::*;
use crate::secrets_source::SecretsSource;
use crate::*;
use tracing::*;

use crate::prost_chrono::chrono_time_from_prost;
use async_trait::*;
use gcloud_sdk::google::cloud::secretmanager::v1::{AccessSecretVersionRequest, GetSecretRequest};

#[derive(Debug, Clone, Eq, PartialEq, Builder)]
pub struct GcpSecretManagerSourceOptions {
    pub google_project_id: String,

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

pub struct GcpSecretManagerSource {
    secret_manager_client: GoogleApi<SecretManagerServiceClient<GoogleAuthMiddleware>>,
    options: GcpSecretManagerSourceOptions,
}

impl GcpSecretManagerSource {
    pub async fn new(google_project_id: &str) -> SecretVaultResult<Self> {
        Self::with_options(GcpSecretManagerSourceOptions::new(
            google_project_id.to_string(),
        ))
        .await
    }

    pub async fn with_options(options: GcpSecretManagerSourceOptions) -> SecretVaultResult<Self> {
        let client: GoogleApi<SecretManagerServiceClient<GoogleAuthMiddleware>> =
            GoogleApi::from_function(
                SecretManagerServiceClient::new,
                "https://secretmanager.googleapis.com",
                None,
            )
            .await
            .map_err(SecretVaultError::from)?;

        Ok(Self {
            secret_manager_client: client,
            options,
        })
    }
}

#[async_trait]
impl SecretsSource for GcpSecretManagerSource {
    fn name(&self) -> String {
        "GoogleSecretManager".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 gcp_secret_version = secret_ref
                .key
                .secret_version
                .as_ref()
                .map(|v| v.value().clone())
                .unwrap_or_else(|| "latest".to_string());

            let gcp_secret_version_path = format!(
                "projects/{}/secrets/{}/versions/{}",
                self.options.google_project_id,
                secret_ref.key.secret_name.value(),
                &gcp_secret_version
            );

            trace!("Reading GCP secret: {}", gcp_secret_version_path);
            let get_secret_response = self
                .secret_manager_client
                .get()
                .access_secret_version(tonic::Request::new(AccessSecretVersionRequest {
                    name: gcp_secret_version_path.clone(),
                    ..Default::default()
                }))
                .await
                .map_err(SecretVaultError::from);

            match get_secret_response {
                Ok(response) => {
                    let secret_response = response.into_inner();
                    if let Some(payload) = secret_response.payload {
                        let maybe_gcp_secret = if self.options.read_metadata {
                            let gcp_secret_path = format!(
                                "projects/{}/secrets/{}",
                                self.options.google_project_id,
                                secret_ref.key.secret_name.value()
                            );

                            Some(
                                self.secret_manager_client
                                    .get()
                                    .get_secret(tonic::Request::new(GetSecretRequest {
                                        name: gcp_secret_path.clone(),
                                    }))
                                    .await
                                    .map_err(SecretVaultError::from)?
                                    .into_inner(),
                            )
                        } else {
                            None
                        };

                        let mut metadata = SecretMetadata::create_from_ref(secret_ref)
                            .with_version(gcp_secret_version.into());

                        if let Some(gcp_secret) = maybe_gcp_secret {
                            if let Some(expiration) = gcp_secret.expiration {
                                metadata.expiration(from_google_expiration(expiration)?);
                            }

                            for (k, v) in gcp_secret.labels {
                                metadata.add_label(SecretMetadataLabel::new(k).with_value(v));
                            }

                            for (k, v) in gcp_secret.annotations {
                                metadata
                                    .add_annotation(SecretMetadataAnnotation::new(k).with_value(v));
                            }

                            metadata.created_at =
                                gcp_secret.create_time.and_then(chrono_time_from_prost);
                        }

                        result_map.insert(secret_ref.clone(), Secret::new(payload.data, 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 {gcp_secret_version_path}"
                                ),
                            ),
                        ));
                    }
                }
                Err(err) => match err {
                    SecretVaultError::DataNotFoundError(_) if !secret_ref.required => {
                        debug!("Secret or secret version {gcp_secret_version_path} doesn't exist and since it is not required it is skipped");
                    }
                    _ => {
                        error!(
                            "Unable to read secret or secret version {gcp_secret_version_path}: {err}."
                        );
                        return Err(err);
                    }
                },
            }
        }
        Ok(result_map)
    }
}

fn from_google_expiration(
    gcp_expiration: gcloud_sdk::google::cloud::secretmanager::v1::secret::Expiration,
) -> SecretVaultResult<SecretExpiration> {
    match gcp_expiration {
        gcloud_sdk::google::cloud::secretmanager::v1::secret::Expiration::ExpireTime(ts) => {
            if let Some(dt) = crate::prost_chrono::chrono_time_from_prost(ts) {
                Ok(SecretExpiration::ExpireTime(dt))
            } else {
                Err(SecretVaultError::InvalidParametersError(
                    SecretVaultInvalidParametersError::new(
                        SecretVaultInvalidParametersPublicDetails::new(
                            "expiration".into(),
                            "Secret expire time conversion error".into(),
                        ),
                    ),
                ))
            }
        }
        gcloud_sdk::google::cloud::secretmanager::v1::secret::Expiration::Ttl(ts) => Ok(
            SecretExpiration::Ttl(crate::prost_chrono::chrono_duration_from_prost(ts)),
        ),
    }
}