secret_store 0.1.1

A unified, async secret store interface for Azure Key Vault, AWS Secrets Manager, GCP Secret Manager, and generic HTTP endpoints
Documentation
//! Azure Key Vault SDK abstraction and real adapter.
//!
//! Separating this from [`super::store`] allows the real SDK calls to be
//! swapped out for a `MockAzureKvOps` in unit tests without touching any
//! network.

use async_trait::async_trait;
use azure_security_keyvault_secrets::SecretClient;

use crate::common::{Error, Result};

// ─────────────────────────────────────────────────────────────────────────────
// SDK abstraction trait
// ─────────────────────────────────────────────────────────────────────────────

/// Low-level Azure Key Vault operations.
///
/// The real implementation wraps [`SecretClient`]; unit tests inject a
/// `MockAzureKvOps` generated by `mockall`.
///
/// `display_name` is synchronous and returns an owned `String` so the store's
/// `Display` impl can delegate here without needing an `async` context.
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait AzureKvOps: Send + Sync {
    /// Minimal label used by `Display` (e.g. just the vault URL).
    fn display_name(&self) -> String;
    /// Verbose info used by `Debug` (vault URL, provider tag, etc.).
    fn debug_info(&self) -> String;

    async fn get(&self, name: &str) -> Result<String>;
    async fn set(&self, name: &str, value: &str) -> Result<()>;
    async fn delete(&self, name: &str) -> Result<()>;
    async fn list(&self, prefix: Option<String>) -> Result<Vec<String>>;
}

// ─────────────────────────────────────────────────────────────────────────────
// Real SDK adapter
// ─────────────────────────────────────────────────────────────────────────────

/// Wraps the real `azure_security_keyvault_secrets::SecretClient`.
pub(super) struct AzureSdkClient {
    pub client: SecretClient,
    pub vault_url: String,
}

#[async_trait]
impl AzureKvOps for AzureSdkClient {
    fn display_name(&self) -> String {
        self.vault_url.clone()
    }

    fn debug_info(&self) -> String {
        format!(
            "vault_url={vault_url}, provider=AzureKeyVault",
            vault_url = self.vault_url,
        )
    }

    async fn get(&self, name: &str) -> Result<String> {
        self.client
            .get_secret(name, None)
            .await
            .map_err(|e| map_azure_error(name, e))?
            .into_model()
            .map(|s| s.value.unwrap_or_default())
            .map_err(|e| map_azure_error(name, e))
    }

    async fn set(&self, name: &str, value: &str) -> Result<()> {
        use azure_security_keyvault_secrets::models::SetSecretParameters;

        let params = SetSecretParameters {
            value: Some(value.into()),
            ..Default::default()
        };
        self.client
            .set_secret(
                name,
                params.try_into().map_err(|e| map_azure_error(name, e))?,
                None,
            )
            .await
            .map(|_| ())
            .map_err(|e| map_azure_error(name, e))
    }

    async fn delete(&self, name: &str) -> Result<()> {
        self.client
            .delete_secret(name, None)
            .await
            .map(|_| ())
            .map_err(|e| map_azure_error(name, e))
    }

    async fn list(&self, prefix: Option<String>) -> Result<Vec<String>> {
        use azure_security_keyvault_secrets::ResourceExt;
        use futures_util::TryStreamExt;

        let mut pager = self
            .client
            .list_secret_properties(None)
            .map_err(|e| map_azure_error("list_secrets", e))?;

        let mut names = Vec::new();
        while let Some(item) = pager
            .try_next()
            .await
            .map_err(|e| map_azure_error("list_secrets", e))?
        {
            let rid = item
                .resource_id()
                .map_err(|e| map_azure_error("list_secrets", e))?;
            let name = rid.name.clone();
            if !name.is_empty() && prefix.as_deref().is_none_or(|p| name.starts_with(p)) {
                names.push(name);
            }
        }
        Ok(names)
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Error mapping
// ─────────────────────────────────────────────────────────────────────────────

/// Maps an `azure_core::Error` to our crate's [`Error`] by inspecting the HTTP
/// status code where available.
pub(super) fn map_azure_error(name: &str, e: azure_core::Error) -> Error {
    use azure_core::error::ErrorKind;
    use azure_core::http::StatusCode;

    match e.kind() {
        ErrorKind::HttpResponse { status, .. } => match *status {
            StatusCode::NotFound => Error::NotFound {
                name: name.to_owned(),
                source: Box::new(e),
            },
            StatusCode::Unauthorized => Error::Unauthenticated {
                source: Box::new(e),
            },
            StatusCode::Forbidden => Error::PermissionDenied {
                name: name.to_owned(),
                source: Box::new(e),
            },
            _ => Error::Generic {
                store: "AzureKeyVault",
                source: Box::new(e),
            },
        },
        _ => Error::Generic {
            store: "AzureKeyVault",
            source: Box::new(e),
        },
    }
}