use async_trait::async_trait;
use azure_security_keyvault_secrets::SecretClient;
use crate::common::{Error, Result};
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait AzureKvOps: Send + Sync {
fn display_name(&self) -> String;
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>>;
}
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)
}
}
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),
},
}
}