mod utils; use std::path::PathBuf; pub use utils::create_vault_password_file; pub use utils::interpolate_secrets; use crate::cli::Cli; use crate::config::AppConfig; use crate::vault::utils::ensure_password_file_initialized; use anyhow::{Context, Result, anyhow}; use fancy_regex::Regex; use gman::providers::SecretProvider; use gman::providers::SupportedProvider; use gman::providers::local::LocalProvider; use inquire::{Password, PasswordDisplayMode, required}; use std::sync::{Arc, LazyLock}; use tokio::runtime::Handle; pub static SECRET_RE: LazyLock = LazyLock::new(|| Regex::new(r"\{\{(.+)}}").unwrap()); #[derive(Debug, Default, Clone)] pub struct Vault { pub(crate) provider: SupportedProvider, } pub type GlobalVault = Arc; impl Vault { pub fn init_bare() -> Self { let vault_password_file = AppConfig::default().vault_password_file(); let local_provider = LocalProvider { password_file: Some(vault_password_file), git_branch: None, ..LocalProvider::default() }; Self { provider: SupportedProvider::Local { provider_def: local_provider, }, } } pub fn init(config: &AppConfig) -> Self { let mut provider = match &config.secrets_provider { Some(p) => p.clone(), None => SupportedProvider::Local { provider_def: LocalProvider { password_file: Some(config.vault_password_file()), ..LocalProvider::default() }, }, }; if let SupportedProvider::Local { provider_def } = &mut provider { ensure_password_file_initialized(provider_def) .expect("Failed to initialize password file"); } Self { provider } } pub fn local_password_file(&self) -> Result { match &self.provider { SupportedProvider::Local { provider_def } => provider_def .password_file .clone() .with_context(|| "A password file is required for the local provider"), _ => Err(anyhow!( "password_file is only available for the local provider" )), } } fn provider_ref(&self) -> &dyn SecretProvider { match &self.provider { SupportedProvider::Local { provider_def } => provider_def, SupportedProvider::AwsSecretsManager { provider_def } => provider_def, SupportedProvider::GcpSecretManager { provider_def } => provider_def, SupportedProvider::AzureKeyVault { provider_def } => provider_def, SupportedProvider::Gopass { provider_def } => provider_def, SupportedProvider::OnePassword { provider_def } => provider_def, } } pub fn add_secret(&self, secret_name: &str) -> Result<()> { let secret_value = Password::new("Enter the secret value:") .with_validator(required!()) .with_display_mode(PasswordDisplayMode::Masked) .prompt() .with_context(|| "unable to read secret from input")?; let h = Handle::current(); tokio::task::block_in_place(|| { h.block_on(self.provider_ref().set_secret(secret_name, &secret_value)) })?; println!("✓ Secret '{secret_name}' added to the vault."); Ok(()) } pub fn get_secret(&self, secret_name: &str, display_output: bool) -> Result { let h = Handle::current(); let secret = tokio::task::block_in_place(|| { h.block_on(self.provider_ref().get_secret(secret_name)) })?; if display_output { println!("{}", secret); } Ok(secret) } pub fn update_secret(&self, secret_name: &str) -> Result<()> { let secret_value = Password::new("Enter the secret value:") .with_validator(required!()) .with_display_mode(PasswordDisplayMode::Masked) .prompt() .with_context(|| "unable to read secret from input")?; let h = Handle::current(); tokio::task::block_in_place(|| { h.block_on( self.provider_ref() .update_secret(secret_name, &secret_value), ) })?; println!("✓ Secret '{secret_name}' updated in the vault."); Ok(()) } pub fn delete_secret(&self, secret_name: &str) -> Result<()> { let h = Handle::current(); tokio::task::block_in_place(|| h.block_on(self.provider_ref().delete_secret(secret_name)))?; println!("✓ Secret '{secret_name}' deleted from the vault."); Ok(()) } pub fn list_secrets(&self, display_output: bool) -> Result> { let h = Handle::current(); let secrets = tokio::task::block_in_place(|| h.block_on(self.provider_ref().list_secrets()))?; if display_output { if secrets.is_empty() { println!("The vault is empty."); } else { for key in &secrets { println!("{}", key); } } } Ok(secrets) } pub fn handle_vault_flags(cli: Cli, vault: &Vault) -> Result<()> { if let Some(secret_name) = cli.add_secret { vault.add_secret(&secret_name)?; } if let Some(secret_name) = cli.get_secret { vault.get_secret(&secret_name, true)?; } if let Some(secret_name) = cli.update_secret { vault.update_secret(&secret_name)?; } if let Some(secret_name) = cli.delete_secret { vault.delete_secret(&secret_name)?; } if cli.list_secrets { vault.list_secrets(true)?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn secret_re_matches_double_braces() { let captures = SECRET_RE.captures("{{MY_SECRET}}").unwrap().unwrap(); assert_eq!(&captures[1], "MY_SECRET"); } #[test] fn secret_re_matches_with_surrounding_text() { let text = "key={{API_KEY}} here"; let captures = SECRET_RE.captures(text).unwrap().unwrap(); assert_eq!(&captures[1], "API_KEY"); } #[test] fn secret_re_no_match_single_braces() { let result = SECRET_RE.captures("{NOT_SECRET}").unwrap(); assert!(result.is_none()); } #[test] fn secret_re_no_match_plain_text() { let result = SECRET_RE.captures("just plain text").unwrap(); assert!(result.is_none()); } #[test] fn secret_re_matches_with_spaces() { let captures = SECRET_RE.captures("{{ SPACED }}").unwrap().unwrap(); assert_eq!(&captures[1], " SPACED "); } #[test] fn vault_default_creates_instance() { let vault = Vault::default(); assert!(vault.local_password_file().is_err()); } }