feat: gopass support
This commit is contained in:
@@ -262,6 +262,7 @@ documented and added without breaking existing setups. The following table shows
|
||||
| [`hashicorp_vault`](https://www.hashicorp.com/en/products/vault) | 🕒 | | |
|
||||
| [`azure_key_vault`](https://azure.microsoft.com/en-us/products/key-vault/) | ✅ | [Azure Key Vault](#provider-azure_key_vault) | |
|
||||
| [`gcp_secret_manager`](https://cloud.google.com/security/products/secret-manager?hl=en) | ✅ | [GCP Secret Manager](#provider-gcp_secret_manager) | |
|
||||
| [`gopass`](https://www.gopass.pw/) | ✅ | | |
|
||||
| [`1password`](https://1password.com/) | 🕒 | | |
|
||||
| [`bitwarden`](https://bitwarden.com/) | 🕒 | | |
|
||||
| [`dashlane`](https://www.dashlane.com/) | 🕒 | | Waiting for CLI support for adding secrets |
|
||||
@@ -405,6 +406,26 @@ Important notes:
|
||||
- Ensure your identity has the necessary Key Vault permissions (RBAC such as `Key Vault Secrets User`/`Administrator`,
|
||||
or appropriate access policies) for get/set/list/delete.
|
||||
|
||||
### Provider: `gopass`
|
||||
The `gopass` provider uses [gopass](https://www.gopass.pw/) as the backing storage location for secrets.
|
||||
|
||||
- Optional: `store` (string) to specify a particular gopass store if you have multiple.
|
||||
|
||||
Configuration example:
|
||||
|
||||
```yaml
|
||||
default_provider: gopass
|
||||
providers:
|
||||
- name: gopass
|
||||
type: gopass
|
||||
store: my-store # Optional; if omitted, uses the default configured gopass store
|
||||
```
|
||||
|
||||
Important notes:
|
||||
- Ensure `gopass` is installed and initialized on your system.
|
||||
- Secrets are managed using gopass's native commands; `gman` acts as a wrapper to interface with gopass.
|
||||
- Updates overwrite existing secrets
|
||||
- If no store is specified, the default gopass store is used and `gman sync` will sync with all configured stores.
|
||||
## Run Configurations
|
||||
|
||||
Run configurations (or "profiles") tell `gman` how to inject secrets into a command. Three modes of secret injection are
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ pub async fn wrap_and_run_command(
|
||||
.find(|c| c.name.as_deref() == Some(run_config_profile_name))
|
||||
});
|
||||
if let Some(run_cfg) = run_config_opt {
|
||||
let mut provider_config = config.extract_provider_config(provider.or(run_cfg.provider.clone()))?;
|
||||
let mut provider_config =
|
||||
config.extract_provider_config(provider.or(run_cfg.provider.clone()))?;
|
||||
let secrets_provider = provider_config.extract_provider();
|
||||
let secrets_result_futures = run_cfg
|
||||
.secrets
|
||||
|
||||
@@ -162,6 +162,10 @@ impl ProviderConfig {
|
||||
debug!("Using Azure Key Vault provider");
|
||||
provider_def
|
||||
}
|
||||
SupportedProvider::Gopass { provider_def } => {
|
||||
debug!("Using Gopass provider");
|
||||
provider_def
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
use crate::providers::{ENV_PATH, SecretProvider};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::io::{Read, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use validator::Validate;
|
||||
|
||||
#[skip_serializing_none]
|
||||
/// Gopass-based secret provider
|
||||
/// See [Gopass](https://gopass.pw/) for more information.
|
||||
///
|
||||
/// You must already have gopass installed and configured on your system.
|
||||
///
|
||||
/// This provider stores secrets in a gopass store. It requires
|
||||
/// an optional store name to be specified. If no store name is
|
||||
/// specified, the default store will be used.
|
||||
///
|
||||
/// Example
|
||||
/// ```no_run
|
||||
/// use gman::providers::local::GopassProvider;
|
||||
/// use gman::providers::{SecretProvider, SupportedProvider};
|
||||
/// use gman::config::Config;
|
||||
///
|
||||
/// let provider = GopassProvider::default();
|
||||
/// let _ = provider.set_secret("MY_SECRET", "value");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GopassProvider {
|
||||
pub store: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SecretProvider for GopassProvider {
|
||||
fn name(&self) -> &'static str {
|
||||
"GopassProvider"
|
||||
}
|
||||
|
||||
async fn get_secret(&self, key: &str) -> Result<String> {
|
||||
ensure_gopass_installed()?;
|
||||
|
||||
let mut child = Command::new("gopass")
|
||||
.args(["show", "-yfon", key])
|
||||
.env("PATH", ENV_PATH.as_ref().expect("No ENV_PATH set"))
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.context("Failed to spawn gopass command")?;
|
||||
|
||||
let mut output = String::new();
|
||||
child
|
||||
.stdout
|
||||
.as_mut()
|
||||
.expect("Failed to open gopass stdout")
|
||||
.read_to_string(&mut output)
|
||||
.context("Failed to read gopass output")?;
|
||||
|
||||
let status = child.wait().context("Failed to wait on gopass process")?;
|
||||
if !status.success() {
|
||||
return Err(anyhow!("gopass command failed with status: {}", status));
|
||||
}
|
||||
|
||||
Ok(output.trim_end_matches(&['\r', '\n'][..]).to_string())
|
||||
}
|
||||
|
||||
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
ensure_gopass_installed()?;
|
||||
|
||||
let mut child = Command::new("gopass")
|
||||
.args(["insert", "-f", key])
|
||||
.env("PATH", ENV_PATH.as_ref().expect("No ENV_PATH set"))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.context("Failed to spawn gopass command")?;
|
||||
|
||||
{
|
||||
let stdin = child.stdin.as_mut().expect("Failed to open gopass stdin");
|
||||
stdin
|
||||
.write_all(value.as_bytes())
|
||||
.context("Failed to write to gopass stdin")?;
|
||||
}
|
||||
|
||||
let status = child.wait().context("Failed to wait on gopass process")?;
|
||||
if !status.success() {
|
||||
return Err(anyhow!("gopass command failed with status: {}", status));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
ensure_gopass_installed()?;
|
||||
|
||||
self.set_secret(key, value).await
|
||||
}
|
||||
|
||||
async fn delete_secret(&self, key: &str) -> Result<()> {
|
||||
ensure_gopass_installed()?;
|
||||
|
||||
let mut child = Command::new("gopass")
|
||||
.args(["rm", "-f", key])
|
||||
.env("PATH", ENV_PATH.as_ref().expect("No ENV_PATH set"))
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.context("Failed to spawn gopass command")?;
|
||||
|
||||
let status = child.wait().context("Failed to wait on gopass process")?;
|
||||
if !status.success() {
|
||||
return Err(anyhow!("gopass command failed with status: {}", status));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_secrets(&self) -> Result<Vec<String>> {
|
||||
ensure_gopass_installed()?;
|
||||
|
||||
let mut child = Command::new("gopass")
|
||||
.args(["ls", "-f"])
|
||||
.env("PATH", ENV_PATH.as_ref().expect("No ENV_PATH set"))
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.context("Failed to spawn gopass command")?;
|
||||
|
||||
let mut output = String::new();
|
||||
child
|
||||
.stdout
|
||||
.as_mut()
|
||||
.expect("Failed to open gopass stdout")
|
||||
.read_to_string(&mut output)
|
||||
.context("Failed to read gopass output")?;
|
||||
|
||||
let status = child.wait().context("Failed to wait on gopass process")?;
|
||||
if !status.success() {
|
||||
return Err(anyhow!("gopass command failed with status: {}", status));
|
||||
}
|
||||
|
||||
let secrets: Vec<String> = output
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect();
|
||||
|
||||
Ok(secrets)
|
||||
}
|
||||
|
||||
async fn sync(&mut self) -> Result<()> {
|
||||
ensure_gopass_installed()?;
|
||||
let mut child = Command::new("gopass");
|
||||
child.arg("sync");
|
||||
|
||||
if let Some(store) = &self.store {
|
||||
child.args(["-s", store]);
|
||||
}
|
||||
|
||||
let status = child
|
||||
.env("PATH", ENV_PATH.as_ref().expect("No ENV_PATH set"))
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.context("Failed to spawn gopass command")?
|
||||
.wait()
|
||||
.context("Failed to wait on gopass process")?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(anyhow!("gopass command failed with status: {}", status));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_gopass_installed() -> Result<()> {
|
||||
if which::which("gopass").is_err() {
|
||||
Err(anyhow!(
|
||||
"Gopass is not installed or not found in PATH. Please install Gopass from https://gopass.pw/"
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -6,17 +6,24 @@ pub mod aws_secrets_manager;
|
||||
pub mod azure_key_vault;
|
||||
pub mod gcp_secret_manager;
|
||||
mod git_sync;
|
||||
mod gopass;
|
||||
pub mod local;
|
||||
|
||||
use crate::providers::gopass::GopassProvider;
|
||||
use crate::providers::local::LocalProvider;
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use aws_secrets_manager::AwsSecretsManagerProvider;
|
||||
use azure_key_vault::AzureKeyVaultProvider;
|
||||
use gcp_secret_manager::GcpSecretManagerProvider;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::{env, fmt};
|
||||
use validator::{Validate, ValidationErrors};
|
||||
|
||||
pub(in crate::providers) static ENV_PATH: Lazy<Result<String>> =
|
||||
Lazy::new(|| env::var("PATH").context("No PATH environment variable"));
|
||||
|
||||
/// A secret storage backend capable of CRUD, with optional
|
||||
/// update, listing, and sync support.
|
||||
#[async_trait::async_trait]
|
||||
@@ -63,7 +70,11 @@ pub enum SupportedProvider {
|
||||
},
|
||||
AzureKeyVault {
|
||||
#[serde(flatten)]
|
||||
provider_def: azure_key_vault::AzureKeyVaultProvider,
|
||||
provider_def: AzureKeyVaultProvider,
|
||||
},
|
||||
Gopass {
|
||||
#[serde(flatten)]
|
||||
provider_def: GopassProvider,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -74,6 +85,7 @@ impl Validate for SupportedProvider {
|
||||
SupportedProvider::AwsSecretsManager { provider_def } => provider_def.validate(),
|
||||
SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(),
|
||||
SupportedProvider::AzureKeyVault { provider_def } => provider_def.validate(),
|
||||
SupportedProvider::Gopass { provider_def } => provider_def.validate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +105,7 @@ impl Display for SupportedProvider {
|
||||
SupportedProvider::AwsSecretsManager { .. } => write!(f, "aws_secrets_manager"),
|
||||
SupportedProvider::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"),
|
||||
SupportedProvider::AzureKeyVault { .. } => write!(f, "azure_key_vault"),
|
||||
SupportedProvider::Gopass { .. } => write!(f, "gopass"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user