#![cfg_attr(not(feature = "keyvault"), allow(dead_code))]
#[cfg(feature = "keyvault")]
pub use client::KeyVaultClient;
#[cfg(feature = "keyvault")]
pub use client::{CredentialConfig, CredentialType};
#[cfg(feature = "keyvault")]
mod client {
use anyhow::{Context, Result};
use azure_core::credentials::{Secret as CoreSecret, TokenCredential};
use azure_identity::{ClientSecretCredential, DeveloperToolsCredential};
use azure_security_keyvault_secrets::{SecretClient, models::Secret as KeyVaultSecret};
use std::env;
use std::sync::Arc;
use tracing::{debug, warn};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CredentialType {
ClientSecret,
DeveloperTools,
}
#[derive(Debug, Clone)]
pub struct CredentialConfig {
pub tenant_id: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub disable_managed_identity: bool,
}
impl CredentialConfig {
pub fn from_env() -> Self {
let tenant_id = env::var("AZURE_TENANT_ID").ok();
let client_id = env::var("AZURE_CLIENT_ID").ok();
let client_secret = env::var("AZURE_CLIENT_SECRET").ok();
let disable_managed_identity =
env::var("AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
Self {
tenant_id,
client_id,
client_secret,
disable_managed_identity,
}
}
pub fn resolve_credential_type(&self) -> Result<CredentialType> {
if self.tenant_id.is_some() && self.client_id.is_some() && self.client_secret.is_some()
{
Ok(CredentialType::ClientSecret)
} else if self.disable_managed_identity {
Err(anyhow::anyhow!(
"KeyVault enabled but no env credentials provided and managed identity disabled"
))
} else {
Ok(CredentialType::DeveloperTools)
}
}
pub fn has_client_secret_credentials(&self) -> bool {
self.tenant_id.is_some() && self.client_id.is_some() && self.client_secret.is_some()
}
pub fn has_partial_credentials(&self) -> bool {
let count = [
self.tenant_id.is_some(),
self.client_id.is_some(),
self.client_secret.is_some(),
]
.iter()
.filter(|&&v| v)
.count();
count > 0 && count < 3
}
}
pub struct KeyVaultClient {
client: SecretClient,
}
impl std::fmt::Debug for KeyVaultClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeyVaultClient")
.field("client", &"<SecretClient>")
.finish()
}
}
impl KeyVaultClient {
pub fn new(vault_url: &str) -> Result<Self> {
Self::with_config(vault_url, CredentialConfig::from_env())
}
pub fn with_config(vault_url: &str, config: CredentialConfig) -> Result<Self> {
if vault_url.trim().is_empty() {
return Err(anyhow::anyhow!("KeyVault URL cannot be empty"));
}
let credential = Self::create_credential(&config)?;
let client = SecretClient::new(vault_url, credential, None)?;
Ok(Self { client })
}
fn create_credential(config: &CredentialConfig) -> Result<Arc<dyn TokenCredential>> {
match config.resolve_credential_type()? {
CredentialType::ClientSecret => {
let tenant_id = config.tenant_id.as_ref().unwrap();
let client_id = config.client_id.clone().unwrap();
let client_secret = config.client_secret.clone().unwrap();
debug!(target: "keyvault", "using ClientSecretCredential from env");
Ok(ClientSecretCredential::new(
tenant_id,
client_id,
CoreSecret::new(client_secret),
None,
)?)
}
CredentialType::DeveloperTools => {
debug!(
target: "keyvault",
"using DeveloperToolsCredential (env creds not present)"
);
Ok(DeveloperToolsCredential::new(None)?)
}
}
}
pub async fn fetch_secret(&self, name: &str) -> Result<Option<String>> {
if name.trim().is_empty() {
return Ok(None);
}
let resp = self
.client
.get_secret(name, None)
.await
.with_context(|| format!("failed to fetch secret '{name}' from KeyVault"))?;
let body = resp.into_body();
let secret: KeyVaultSecret = body
.json()
.with_context(|| format!("invalid secret payload for '{name}'"))?;
if let Some(value) = secret.value {
debug!(target: "keyvault", secret_name = name, "fetched secret from KeyVault");
Ok(Some(value))
} else {
warn!(target: "keyvault", secret_name = name, "secret exists but has no value");
Ok(None)
}
}
pub async fn fetch_secrets(&self, names: &[&str]) -> Result<Vec<Option<String>>> {
use futures::future::try_join_all;
let futures = names.iter().map(|name| self.fetch_secret(name));
try_join_all(futures).await
}
}
}
#[cfg(all(test, feature = "keyvault"))]
mod tests;
#[cfg(not(feature = "keyvault"))]
pub struct KeyVaultClient;
#[cfg(not(feature = "keyvault"))]
impl KeyVaultClient {
pub fn new(_vault_url: &str) -> anyhow::Result<Self> {
Err(anyhow::anyhow!(
"KeyVault feature not enabled. Rebuild with --features keyvault"
))
}
}