use std::path::PathBuf;
use zeph_core::config::Config;
use zeph_core::vault::{AgeVaultError, AgeVaultProvider};
use crate::cli::VaultCommand;
pub(crate) fn default_vault_dir() -> PathBuf {
zeph_core::vault::default_vault_dir()
}
pub(crate) fn handle_vault_command(
cmd: VaultCommand,
config: &Config,
key_path: Option<&std::path::Path>,
vault_path: Option<&std::path::Path>,
) -> anyhow::Result<()> {
let dir = default_vault_dir();
let vault_args = crate::bootstrap::parse_vault_args(config, None, key_path, vault_path)
.map_err(|e| anyhow::anyhow!(e))?;
let key_path_owned = vault_args
.key_path
.map_or_else(|| dir.join("vault-key.txt"), PathBuf::from);
let vault_path_owned = vault_args
.vault_path
.map_or_else(|| dir.join("secrets.age"), PathBuf::from);
match cmd {
VaultCommand::Init { force } => {
AgeVaultProvider::init_vault_at(&key_path_owned, &vault_path_owned, force).map_err(
|e| match e {
AgeVaultError::VaultAlreadyExists(path) => anyhow::anyhow!(
"vault already exists at {}. Re-run with --force to overwrite it.",
path.display()
),
other => anyhow::anyhow!("vault init failed: {other}"),
},
)?;
}
VaultCommand::Set { key, value, force } => {
let mut provider = AgeVaultProvider::load(&key_path_owned, &vault_path_owned)
.map_err(|e| anyhow::anyhow!("failed to load vault: {e}"))?;
provider
.set_secret_mut(key.clone(), value, force)
.map_err(|e| match e {
AgeVaultError::AlreadyExists(_) => anyhow::anyhow!(
"key '{key}' already exists in the vault. Re-run with --force to \
overwrite it."
),
other => anyhow::anyhow!("failed to set secret: {other}"),
})?;
provider
.save()
.map_err(|e| anyhow::anyhow!("failed to save vault: {e}"))?;
}
VaultCommand::Get { key } => {
let provider = AgeVaultProvider::load(&key_path_owned, &vault_path_owned)
.map_err(|e| anyhow::anyhow!("failed to load vault: {e}"))?;
if let Some(val) = provider.get(&key) {
println!("{val}"); } else {
anyhow::bail!("key not found: {key}");
}
}
VaultCommand::List => {
let provider = AgeVaultProvider::load(&key_path_owned, &vault_path_owned)
.map_err(|e| anyhow::anyhow!("failed to load vault: {e}"))?;
for key in provider.list_keys() {
println!("{key}");
}
}
VaultCommand::Rm { key } => {
let mut provider = AgeVaultProvider::load(&key_path_owned, &vault_path_owned)
.map_err(|e| anyhow::anyhow!("failed to load vault: {e}"))?;
if !provider.remove_secret_mut(&key) {
anyhow::bail!("key not found: {key}");
}
provider
.save()
.map_err(|e| anyhow::anyhow!("failed to save vault: {e}"))?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[allow(unsafe_code)]
#[test]
#[serial]
fn default_vault_dir_xdg_config_home() {
unsafe {
std::env::set_var("XDG_CONFIG_HOME", "/tmp/xdg-test");
}
let dir = default_vault_dir();
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
}
assert_eq!(dir, PathBuf::from("/tmp/xdg-test/zeph"));
}
#[allow(unsafe_code)]
#[test]
#[serial]
fn default_vault_dir_appdata() {
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
std::env::set_var("APPDATA", "/tmp/appdata-test");
}
let dir = default_vault_dir();
unsafe {
std::env::remove_var("APPDATA");
}
assert_eq!(dir, PathBuf::from("/tmp/appdata-test/zeph"));
}
#[allow(unsafe_code)]
#[test]
#[serial]
fn default_vault_dir_home_fallback() {
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
std::env::remove_var("APPDATA");
std::env::set_var("HOME", "/tmp/home-test");
}
let dir = default_vault_dir();
unsafe {
std::env::remove_var("HOME");
}
assert_eq!(dir, PathBuf::from("/tmp/home-test/.config/zeph"));
}
#[test]
fn handle_vault_command_set_get_list_rm() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
zeph_core::vault::AgeVaultProvider::init_vault(dir.path()).unwrap();
handle_vault_command(
VaultCommand::Set {
key: "FOO".into(),
value: "bar".into(),
force: false,
},
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
handle_vault_command(
VaultCommand::List,
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
handle_vault_command(
VaultCommand::Get { key: "FOO".into() },
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
handle_vault_command(
VaultCommand::Rm { key: "FOO".into() },
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
}
#[test]
fn handle_vault_command_init_respects_path_override() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
handle_vault_command(
VaultCommand::Init { force: false },
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
assert!(
key_path.exists(),
"vault key must be written to the override path"
);
assert!(
vault_path.exists(),
"vault file must be written to the override path"
);
}
#[test]
fn handle_vault_command_init_existing_vault_without_force_errors() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
handle_vault_command(
VaultCommand::Init { force: false },
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
let original_key = std::fs::read(&key_path).unwrap();
let err = handle_vault_command(
VaultCommand::Init { force: false },
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap_err();
assert!(err.to_string().contains("--force"));
assert_eq!(std::fs::read(&key_path).unwrap(), original_key);
}
#[test]
fn handle_vault_command_init_existing_vault_with_force_overwrites() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
handle_vault_command(
VaultCommand::Init { force: false },
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
let original_key = std::fs::read(&key_path).unwrap();
handle_vault_command(
VaultCommand::Init { force: true },
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
assert_ne!(
std::fs::read(&key_path).unwrap(),
original_key,
"--force must regenerate the keypair"
);
}
#[test]
fn handle_vault_command_get_missing_key_errors() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
zeph_core::vault::AgeVaultProvider::init_vault(dir.path()).unwrap();
let err = handle_vault_command(
VaultCommand::Get {
key: "NONEXISTENT".into(),
},
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap_err();
assert!(err.to_string().contains("key not found"));
}
#[test]
fn handle_vault_command_set_existing_key_without_force_errors() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
zeph_core::vault::AgeVaultProvider::init_vault(dir.path()).unwrap();
handle_vault_command(
VaultCommand::Set {
key: "FOO".into(),
value: "original".into(),
force: false,
},
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
let err = handle_vault_command(
VaultCommand::Set {
key: "FOO".into(),
value: "clobbered".into(),
force: false,
},
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap_err();
assert!(err.to_string().contains("--force"));
let provider = zeph_core::vault::AgeVaultProvider::load(&key_path, &vault_path).unwrap();
assert_eq!(provider.get("FOO"), Some("original"));
}
#[test]
fn handle_vault_command_set_existing_key_with_force_overwrites() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
zeph_core::vault::AgeVaultProvider::init_vault(dir.path()).unwrap();
handle_vault_command(
VaultCommand::Set {
key: "FOO".into(),
value: "original".into(),
force: false,
},
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
handle_vault_command(
VaultCommand::Set {
key: "FOO".into(),
value: "updated".into(),
force: true,
},
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap();
let provider = zeph_core::vault::AgeVaultProvider::load(&key_path, &vault_path).unwrap();
assert_eq!(provider.get("FOO"), Some("updated"));
}
#[test]
fn handle_vault_command_rm_missing_key_errors() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
zeph_core::vault::AgeVaultProvider::init_vault(dir.path()).unwrap();
let err = handle_vault_command(
VaultCommand::Rm {
key: "NONEXISTENT".into(),
},
&Config::default(),
Some(&key_path),
Some(&vault_path),
)
.unwrap_err();
assert!(err.to_string().contains("key not found"));
}
#[allow(unsafe_code)]
#[test]
#[serial]
fn handle_vault_command_respects_env_vars_without_cli_override() {
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("vault-key.txt");
let vault_path = dir.path().join("secrets.age");
zeph_core::vault::AgeVaultProvider::init_vault(dir.path()).unwrap();
unsafe {
std::env::set_var("ZEPH_VAULT_KEY", &key_path);
std::env::set_var("ZEPH_VAULT_PATH", &vault_path);
}
let result = handle_vault_command(
VaultCommand::Set {
key: "FOO".into(),
value: "bar".into(),
force: false,
},
&Config::default(),
None,
None,
);
unsafe {
std::env::remove_var("ZEPH_VAULT_KEY");
std::env::remove_var("ZEPH_VAULT_PATH");
}
result.unwrap();
let provider = zeph_core::vault::AgeVaultProvider::load(&key_path, &vault_path).unwrap();
assert_eq!(provider.get("FOO"), Some("bar"));
}
}