feat: Added 1password support
Check / stable / fmt (push) Has been cancelled
Check / beta / clippy (push) Has been cancelled
Check / stable / clippy (push) Has been cancelled
Check / nightly / doc (push) Has been cancelled
Check / 1.89.0 / check (push) Has been cancelled
Test Suite / ubuntu / beta (push) Has been cancelled
Test Suite / ubuntu / stable (push) Has been cancelled
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
Test Suite / ubuntu / stable / coverage (push) Has been cancelled

This commit is contained in:
2026-03-09 16:33:57 -06:00
parent e8e0bd02e9
commit 66b950991c
6 changed files with 363 additions and 1 deletions
+4
View File
@@ -169,6 +169,10 @@ impl ProviderConfig {
debug!("Using Gopass provider");
provider_def
}
SupportedProvider::OnePassword { provider_def } => {
debug!("Using 1Password provider");
provider_def
}
}
}
}
+8
View File
@@ -8,9 +8,11 @@ pub mod gcp_secret_manager;
mod git_sync;
pub mod gopass;
pub mod local;
pub mod one_password;
use crate::providers::gopass::GopassProvider;
use crate::providers::local::LocalProvider;
use crate::providers::one_password::OnePasswordProvider;
use anyhow::{Context, Result, anyhow};
use aws_secrets_manager::AwsSecretsManagerProvider;
use azure_key_vault::AzureKeyVaultProvider;
@@ -76,6 +78,10 @@ pub enum SupportedProvider {
#[serde(flatten)]
provider_def: GopassProvider,
},
OnePassword {
#[serde(flatten)]
provider_def: OnePasswordProvider,
},
}
impl Validate for SupportedProvider {
@@ -86,6 +92,7 @@ impl Validate for SupportedProvider {
SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(),
SupportedProvider::AzureKeyVault { provider_def } => provider_def.validate(),
SupportedProvider::Gopass { provider_def } => provider_def.validate(),
SupportedProvider::OnePassword { provider_def } => provider_def.validate(),
}
}
}
@@ -106,6 +113,7 @@ impl Display for SupportedProvider {
SupportedProvider::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"),
SupportedProvider::AzureKeyVault { .. } => write!(f, "azure_key_vault"),
SupportedProvider::Gopass { .. } => write!(f, "gopass"),
SupportedProvider::OnePassword { .. } => write!(f, "one_password"),
}
}
}
+199
View File
@@ -0,0 +1,199 @@
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;
use std::process::{Command, Stdio};
use validator::Validate;
#[skip_serializing_none]
/// 1Password-based secret provider.
/// See [1Password CLI](https://developer.1password.com/docs/cli/) for more
/// information.
///
/// You must already have the 1Password CLI (`op`) installed and configured
/// on your system.
///
/// This provider stores secrets as 1Password Password items. It requires
/// an optional vault name and an optional account identifier to be specified.
/// If no vault is specified, the user's default vault is used. If no account
/// is specified, the default signed-in account is used.
///
/// Example
/// ```no_run
/// use gman::providers::one_password::OnePasswordProvider;
/// use gman::providers::{SecretProvider, SupportedProvider};
/// use gman::config::Config;
///
/// let provider = OnePasswordProvider::default();
/// let _ = provider.set_secret("MY_SECRET", "value");
/// ```
#[derive(Debug, Default, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct OnePasswordProvider {
pub vault: Option<String>,
pub account: Option<String>,
}
impl OnePasswordProvider {
fn base_command(&self) -> Command {
let mut cmd = Command::new("op");
cmd.env("PATH", ENV_PATH.as_ref().expect("No ENV_PATH set"));
if let Some(account) = &self.account {
cmd.args(["--account", account]);
}
cmd
}
fn vault_args(&self) -> Vec<&str> {
match &self.vault {
Some(vault) => vec!["--vault", vault],
None => vec![],
}
}
}
#[async_trait::async_trait]
impl SecretProvider for OnePasswordProvider {
fn name(&self) -> &'static str {
"OnePasswordProvider"
}
async fn get_secret(&self, key: &str) -> Result<String> {
ensure_op_installed()?;
let mut cmd = self.base_command();
cmd.args(["item", "get", key, "--fields", "password", "--reveal"]);
cmd.args(self.vault_args());
cmd.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().context("Failed to spawn op command")?;
let mut output = String::new();
child
.stdout
.as_mut()
.expect("Failed to open op stdout")
.read_to_string(&mut output)
.context("Failed to read op output")?;
let status = child.wait().context("Failed to wait on op process")?;
if !status.success() {
return Err(anyhow!("op 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_op_installed()?;
let mut cmd = self.base_command();
cmd.args(["item", "create", "--category", "password", "--title", key]);
cmd.args(self.vault_args());
cmd.arg(format!("password={}", value));
cmd.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().context("Failed to spawn op command")?;
let status = child.wait().context("Failed to wait on op process")?;
if !status.success() {
return Err(anyhow!("op command failed with status: {}", status));
}
Ok(())
}
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
ensure_op_installed()?;
let mut cmd = self.base_command();
cmd.args(["item", "edit", key]);
cmd.args(self.vault_args());
cmd.arg(format!("password={}", value));
cmd.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().context("Failed to spawn op command")?;
let status = child.wait().context("Failed to wait on op process")?;
if !status.success() {
return Err(anyhow!("op command failed with status: {}", status));
}
Ok(())
}
async fn delete_secret(&self, key: &str) -> Result<()> {
ensure_op_installed()?;
let mut cmd = self.base_command();
cmd.args(["item", "delete", key]);
cmd.args(self.vault_args());
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().context("Failed to spawn op command")?;
let status = child.wait().context("Failed to wait on op process")?;
if !status.success() {
return Err(anyhow!("op command failed with status: {}", status));
}
Ok(())
}
async fn list_secrets(&self) -> Result<Vec<String>> {
ensure_op_installed()?;
let mut cmd = self.base_command();
cmd.args(["item", "list", "--format", "json"]);
cmd.args(self.vault_args());
cmd.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().context("Failed to spawn op command")?;
let mut output = String::new();
child
.stdout
.as_mut()
.expect("Failed to open op stdout")
.read_to_string(&mut output)
.context("Failed to read op output")?;
let status = child.wait().context("Failed to wait on op process")?;
if !status.success() {
return Err(anyhow!("op command failed with status: {}", status));
}
let items: Vec<serde_json::Value> =
serde_json::from_str(&output).context("Failed to parse op item list JSON output")?;
let secrets: Vec<String> = items
.iter()
.filter_map(|item| item.get("title").and_then(|t| t.as_str()))
.map(|s| s.to_string())
.collect();
Ok(secrets)
}
}
fn ensure_op_installed() -> Result<()> {
if which::which("op").is_err() {
Err(anyhow!(
"1Password CLI (op) is not installed or not found in PATH. \
Please install it from https://developer.1password.com/docs/cli/get-started/"
))
} else {
Ok(())
}
}