feat: added round trip validation for vault providers to ensure permissions and authentication
This commit is contained in:
@@ -18,6 +18,6 @@
|
|||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["duckduckgo-mcp-server"]
|
"args": ["duckduckgo-mcp-server"]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,6 +687,9 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
create_vault_password_file(&mut vault)?;
|
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()?;
|
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::cli::Cli;
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::vault::utils::ensure_password_file_initialized;
|
use crate::vault::utils::ensure_password_file_initialized;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
use gman::providers::SecretProvider;
|
use gman::providers::SecretProvider;
|
||||||
use gman::providers::SupportedProvider;
|
use gman::providers::SupportedProvider;
|
||||||
@@ -157,6 +157,63 @@ impl Vault {
|
|||||||
Ok(secrets)
|
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<()> {
|
pub fn handle_vault_flags(cli: Cli, vault: &Vault) -> Result<()> {
|
||||||
if let Some(secret_name) = cli.add_secret {
|
if let Some(secret_name) = cli.add_secret {
|
||||||
vault.add_secret(&secret_name)?;
|
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());
|
missing_secrets.push(name.to_string());
|
||||||
String::new()
|
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!(
|
fatal_error = Some(anyhow!(
|
||||||
"Failed to fetch secret '{name}' from vault: {e}"
|
"Failed to fetch secret '{name}' from vault: {e}"
|
||||||
|
|||||||
Reference in New Issue
Block a user