From bf97f2261d4f8174c6d4578c6b7f26b56c5f7279 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 3 Jun 2026 08:30:47 -0600 Subject: [PATCH] feat: added round trip validation for vault providers to ensure permissions and authentication --- assets/functions/mcp.json | 2 +- src/config/mod.rs | 3 ++ src/vault/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++- src/vault/utils.rs | 11 ++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/assets/functions/mcp.json b/assets/functions/mcp.json index 71cb26e..b3e801b 100644 --- a/assets/functions/mcp.json +++ b/assets/functions/mcp.json @@ -18,6 +18,6 @@ "type": "stdio", "command": "uvx", "args": ["duckduckgo-mcp-server"] - }, + } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index c623479..70efb43 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -687,6 +687,9 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> { }, }; create_vault_password_file(&mut vault)?; + if provider_choice.is_some() { + vault.validate_round_trip()?; + } let client = Select::new("API Provider (required):", list_client_types()).prompt()?; diff --git a/src/vault/mod.rs b/src/vault/mod.rs index 272841e..9cebd75 100644 --- a/src/vault/mod.rs +++ b/src/vault/mod.rs @@ -8,7 +8,7 @@ pub use utils::prompt_provider_choice; use crate::cli::Cli; use crate::config::AppConfig; use crate::vault::utils::ensure_password_file_initialized; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; use fancy_regex::Regex; use gman::providers::SecretProvider; use gman::providers::SupportedProvider; @@ -157,6 +157,63 @@ impl Vault { Ok(secrets) } + pub fn auth_hint(&self) -> Option<&'static str> { + match &self.provider { + SupportedProvider::AwsSecretsManager { .. } => Some( + "Try `aws sso login` (for SSO setups) or `aws configure` (for static keys), then retry.", + ), + SupportedProvider::GcpSecretManager { .. } => Some( + "Try `gcloud auth application-default login`, then retry.", + ), + SupportedProvider::AzureKeyVault { .. } => Some( + "Try `az login`, then retry.", + ), + SupportedProvider::Gopass { .. } => Some( + "Make sure `gopass init` has been run and `gopass` is on your PATH.", + ), + SupportedProvider::OnePassword { .. } => Some( + "Try `op signin`, then retry.", + ), + SupportedProvider::Local { .. } => None, + } + } + + pub fn validate_round_trip(&self) -> Result<()> { + const PROBE_KEY: &str = "__coyote_setup_probe__"; + const PROBE_VALUE: &str = "ok"; + + let h = Handle::current(); + let result: Result<()> = tokio::task::block_in_place(|| { + h.block_on(async { + self.provider_ref() + .set_secret(PROBE_KEY, PROBE_VALUE) + .await + .with_context(|| "vault write probe failed")?; + let got = self + .provider_ref() + .get_secret(PROBE_KEY) + .await + .with_context(|| "vault read probe failed")?; + let _ = self.provider_ref().delete_secret(PROBE_KEY).await; + if got != PROBE_VALUE { + bail!("vault read probe returned an unexpected value"); + } + Ok(()) + }) + }); + + result.with_context(|| { + let base = "Vault validation failed. Check that your credentials have permission to create, read, and delete secrets in the configured backend."; + match self.auth_hint() { + Some(hint) => format!("{base}\n\nHint: {hint}"), + None => base.to_string(), + } + })?; + + println!("✓ Vault validation succeeded."); + Ok(()) + } + pub fn handle_vault_flags(cli: Cli, vault: &Vault) -> Result<()> { if let Some(secret_name) = cli.add_secret { vault.add_secret(&secret_name)?; diff --git a/src/vault/utils.rs b/src/vault/utils.rs index 5e0e84d..9445666 100644 --- a/src/vault/utils.rs +++ b/src/vault/utils.rs @@ -374,6 +374,17 @@ pub fn interpolate_secrets(content: &str, vault: &Vault) -> Result<(String, Vec< missing_secrets.push(name.to_string()); String::new() } + Some(SecretError::AuthFailed { .. }) => { + let base = format!( + "Failed to fetch secret '{name}' from vault: {e}" + ); + let msg = match vault.auth_hint() { + Some(hint) => format!("{base}\n\nHint: {hint}"), + None => base, + }; + fatal_error = Some(anyhow!("{msg}")); + String::new() + } _ => { fatal_error = Some(anyhow!( "Failed to fetch secret '{name}' from vault: {e}"