From 4042402d1bff13efbbf8054c3be6666ff182cba4 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 12 Sep 2025 19:28:15 -0600 Subject: [PATCH] feat: Azure Key Vault support --- Cargo.lock | 285 +++++++++++++++++++++++++++ Cargo.toml | 2 + src/bin/gman/main.rs | 6 + src/config.rs | 12 +- src/providers/aws_secrets_manager.rs | 2 +- src/providers/azure_key_vault.rs | 109 ++++++++++ src/providers/local.rs | 2 +- src/providers/mod.rs | 18 +- 8 files changed, 425 insertions(+), 11 deletions(-) create mode 100644 src/providers/azure_key_vault.rs diff --git a/Cargo.lock b/Cargo.lock index a12371e..59d59d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -595,6 +606,74 @@ dependencies = [ "tower-service", ] +[[package]] +name = "azure_core" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd9e026f749ac67e6d736ebcfa1ba36ab60ce3d6c446c67624a538f4e0667fa" +dependencies = [ + "async-lock", + "async-trait", + "azure_core_macros", + "bytes", + "futures", + "pin-project", + "rustc_version", + "serde", + "serde_json", + "tracing", + "typespec", + "typespec_client_core", +] + +[[package]] +name = "azure_core_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bce1a683e1a27013e64a1ff760700c7241275fe38787e578c3526f4ac569e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "tracing", + "typespec_client_core", +] + +[[package]] +name = "azure_identity" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5b2a19746da00d510787e406f07494a5b6e9b86f69871e3b72ef90d34631c77" +dependencies = [ + "async-lock", + "async-trait", + "azure_core", + "futures", + "pin-project", + "serde", + "time", + "tracing", + "typespec_client_core", + "url", +] + +[[package]] +name = "azure_security_keyvault_secrets" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad61be32356d8dadd7553620dd65b0e63db6b2d89f56e1ca766e34081c125f3" +dependencies = [ + "async-trait", + "azure_core", + "futures", + "rustc_version", + "serde", + "serde_json", + "time", + "tokio", + "typespec_client_core", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -899,6 +978,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "confy" version = "1.0.0" @@ -986,6 +1074,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.29.0" @@ -1228,6 +1322,27 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1265,6 +1380,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1454,6 +1584,8 @@ dependencies = [ "async-trait", "aws-config", "aws-sdk-secretsmanager", + "azure_identity", + "azure_security_keyvault_secrets", "backtrace", "base64 0.22.1", "chacha20poly1305", @@ -1741,6 +1873,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.7.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.16" @@ -2196,6 +2344,23 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -2273,12 +2438,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2312,6 +2515,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.4" @@ -2394,6 +2603,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plist" version = "1.7.4" @@ -2743,10 +2958,12 @@ dependencies = [ "http-body-util", "hyper 1.7.0", "hyper-rustls 0.27.7", + "hyper-tls", "hyper-util", "js-sys", "log", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "rustls 0.23.31", @@ -2757,6 +2974,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.2", "tokio-util", "tower", @@ -3384,6 +3602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde", @@ -3448,6 +3667,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -3662,6 +3891,56 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "typespec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2fffbed46125e0931e8f45618c3f6f0ffa2e0dc6d8b10a8de9f100b03138f33" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "typespec_client_core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96d81a432a1d2eb5cb3e9f813ff3811928e35f549bb5fa0a16abeffc66dec4c" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "dyn-clone", + "futures", + "getrandom 0.3.3", + "pin-project", + "rand", + "reqwest", + "serde", + "serde_json", + "time", + "tokio", + "tracing", + "typespec", + "typespec_macros", + "url", + "uuid", +] + +[[package]] +name = "typespec_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b032d7c2352fd8c2af91f942b914c52e315d3ea2b1bcad21a16cb94f72816bd" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "unarray" version = "0.1.4" @@ -3800,6 +4079,12 @@ dependencies = [ "syn", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 19b2388..3705c1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,8 @@ async-trait = "0.1.89" futures = "0.3.31" gcloud-sdk = { version = "0.28.1", features = ["google-cloud-secretmanager-v1"] } crc32c = "0.6.8" +azure_identity = "0.27.0" +azure_security_keyvault_secrets = "0.6.0" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/src/bin/gman/main.rs b/src/bin/gman/main.rs index e24661f..6dc9120 100644 --- a/src/bin/gman/main.rs +++ b/src/bin/gman/main.rs @@ -13,6 +13,7 @@ use std::panic::PanicHookInfo; use crate::cli::wrap_and_run_command; use std::panic; +use std::process::exit; mod cli; mod command; @@ -133,6 +134,11 @@ async fn main() -> Result<()> { println!("{}", get_config_file_path()?.display()); return Ok(()); } + if cli.command.is_none() { + Cli::command().print_help()?; + println!(); + exit(1); + } let config = load_config()?; let mut provider_config = config.extract_provider_config(cli.provider.clone())?; diff --git a/src/config.rs b/src/config.rs index afcdacf..1d3c7ce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -150,10 +150,14 @@ impl ProviderConfig { debug!("Using AWS Secrets Manager provider"); provider_def } - SupportedProvider::GcpSecretManager { provider_def } => { - debug!("Using GCP Secret Manager provider"); - provider_def - } + SupportedProvider::GcpSecretManager { provider_def } => { + debug!("Using GCP Secret Manager provider"); + provider_def + } + SupportedProvider::AzureKeyVault { provider_def } => { + debug!("Using Azure Key Vault provider"); + provider_def + } } } } diff --git a/src/providers/aws_secrets_manager.rs b/src/providers/aws_secrets_manager.rs index 66feeef..588d333 100644 --- a/src/providers/aws_secrets_manager.rs +++ b/src/providers/aws_secrets_manager.rs @@ -19,7 +19,7 @@ use validator::Validate; /// Example /// ```no_run /// use gman::providers::{SecretProvider, SupportedProvider}; -/// use gman::config::{Config, ProviderConfig}; +/// use gman::config::Config; /// use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider; /// /// let provider = AwsSecretsManagerProvider { diff --git a/src/providers/azure_key_vault.rs b/src/providers/azure_key_vault.rs new file mode 100644 index 0000000..07e60fa --- /dev/null +++ b/src/providers/azure_key_vault.rs @@ -0,0 +1,109 @@ +use crate::providers::SecretProvider; +use anyhow::{Context, Result}; +use azure_identity::DefaultAzureCredential; +use azure_security_keyvault_secrets::models::SetSecretParameters; +use azure_security_keyvault_secrets::{ResourceExt, SecretClient}; +use futures::TryStreamExt; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use validator::Validate; + +#[skip_serializing_none] +/// Configuration for Azure Key Vault provider +/// See [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) +/// for more information. +/// +/// This provider stores secrets in Azure Key Vault. It requires +/// a vault name to be specified. +/// +/// Example +/// ```no_run +/// use gman::providers::{SecretProvider, SupportedProvider}; +/// use gman::config::{Config, ProviderConfig}; +/// use gman::providers::azure_key_vault::AzureKeyVaultProvider; +/// +/// let provider = AzureKeyVaultProvider { +/// vault_name: Some("my-vault-name".to_string()), +/// }; +/// let _ = provider.set_secret("MY_SECRET", "value"); +#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct AzureKeyVaultProvider { + #[validate(required)] + pub vault_name: Option, +} + +#[async_trait::async_trait] +impl SecretProvider for AzureKeyVaultProvider { + fn name(&self) -> &'static str { + "AzureKeyVaultProvider" + } + + async fn get_secret(&self, key: &str) -> Result { + let body = self + .get_client()? + .get_secret(key, "", None) + .await? + .into_body() + .await?; + + body.value + .with_context(|| format!("Secret '{}' not found", key)) + } + + async fn set_secret(&self, key: &str, value: &str) -> Result<()> { + let params = SetSecretParameters { + value: Some(value.to_string()), + ..Default::default() + }; + + self.get_client()? + .set_secret(key, params.try_into()?, None) + .await? + .into_body() + .await?; + + Ok(()) + } + + async fn update_secret(&self, key: &str, value: &str) -> Result<()> { + self.set_secret(key, value).await + } + + async fn delete_secret(&self, key: &str) -> Result<()> { + self.get_client()?.delete_secret(key, None).await?; + + Ok(()) + } + + async fn list_secrets(&self) -> Result> { + let mut pager = self + .get_client()? + .list_secret_properties(None)? + .into_stream(); + let mut secrets = Vec::new(); + while let Some(props) = pager.try_next().await? { + let name = props.resource_id()?.name; + secrets.push(name); + } + + Ok(secrets) + } +} + +impl AzureKeyVaultProvider { + fn get_client(&self) -> Result { + let credential = DefaultAzureCredential::new()?; + let client = SecretClient::new( + format!( + "https://{}.vault.azure.net", + self.vault_name.as_ref().unwrap() + ) + .as_str(), + credential, + None, + )?; + + Ok(client) + } +} diff --git a/src/providers/local.rs b/src/providers/local.rs index 6ff70f7..298e799 100644 --- a/src/providers/local.rs +++ b/src/providers/local.rs @@ -37,7 +37,7 @@ use validator::Validate; /// ```no_run /// use gman::providers::local::LocalProvider; /// use gman::providers::{SecretProvider, SupportedProvider}; -/// use gman::config::{Config, ProviderConfig}; +/// use gman::config::Config; /// /// let provider = LocalProvider::default(); /// // Will prompt for a password when reading/writing secrets unless a diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 385dabc..9856421 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -3,17 +3,19 @@ //! Implementations provide storage/backends for secrets and a common //! interface used by the CLI. pub mod aws_secrets_manager; +pub mod azure_key_vault; pub mod gcp_secret_manager; mod git_sync; pub mod local; +use std::fmt; use crate::providers::local::LocalProvider; use anyhow::{Result, anyhow}; +use aws_secrets_manager::AwsSecretsManagerProvider; +use gcp_secret_manager::GcpSecretManagerProvider; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use validator::{Validate, ValidationErrors}; -use aws_secrets_manager::AwsSecretsManagerProvider; -use gcp_secret_manager::GcpSecretManagerProvider; /// A secret storage backend capable of CRUD, with optional /// update, listing, and sync support. @@ -59,6 +61,10 @@ pub enum SupportedProvider { #[serde(flatten)] provider_def: GcpSecretManagerProvider, }, + AzureKeyVault { + #[serde(flatten)] + provider_def: azure_key_vault::AzureKeyVaultProvider, + }, } impl Validate for SupportedProvider { @@ -66,7 +72,8 @@ impl Validate for SupportedProvider { match self { SupportedProvider::Local { provider_def } => provider_def.validate(), SupportedProvider::AwsSecretsManager { provider_def } => provider_def.validate(), - SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(), + SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(), + SupportedProvider::AzureKeyVault { provider_def } => provider_def.validate(), } } } @@ -80,11 +87,12 @@ impl Default for SupportedProvider { } impl Display for SupportedProvider { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { SupportedProvider::Local { .. } => write!(f, "local"), SupportedProvider::AwsSecretsManager { .. } => write!(f, "aws_secrets_manager"), - SupportedProvider::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"), + SupportedProvider::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"), + SupportedProvider::AzureKeyVault { .. } => write!(f, "azure_key_vault"), } } }