feat: added round trip validation for vault providers to ensure permissions and authentication
This commit is contained in:
@@ -18,6 +18,6 @@
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": ["duckduckgo-mcp-server"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
+58
-1
@@ -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)?;
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user