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
+38 -1
View File
@@ -96,6 +96,7 @@ gman aws sts get-caller-identity
- [GCP Secret Manager](#provider-gcp_secret_manager) - [GCP Secret Manager](#provider-gcp_secret_manager)
- [Azure Key Vault](#provider-azure_key_vault) - [Azure Key Vault](#provider-azure_key_vault)
- [Gopass](#provider-gopass) - [Gopass](#provider-gopass)
- [1Password](#provider-one_password)
- [Run Configurations](#run-configurations) - [Run Configurations](#run-configurations)
- [Specifying a Default Provider per Run Config](#specifying-a-default-provider-per-run-config) - [Specifying a Default Provider per Run Config](#specifying-a-default-provider-per-run-config)
- [Environment Variable Secret Injection](#environment-variable-secret-injection) - [Environment Variable Secret Injection](#environment-variable-secret-injection)
@@ -287,7 +288,7 @@ documented and added without breaking existing setups. The following table shows
| [`azure_key_vault`](https://azure.microsoft.com/en-us/products/key-vault/) | ✅ | [Azure Key Vault](#provider-azure_key_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) | | | [`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/) | ✅ | | | | [`gopass`](https://www.gopass.pw/) | ✅ | | |
| [`1password`](https://1password.com/) | 🕒 | | | | [`1password`](https://1password.com/) | | [1Password](#provider-one_password) | |
| [`bitwarden`](https://bitwarden.com/) | 🕒 | | | | [`bitwarden`](https://bitwarden.com/) | 🕒 | | |
| [`dashlane`](https://www.dashlane.com/) | 🕒 | | Waiting for CLI support for adding secrets | | [`dashlane`](https://www.dashlane.com/) | 🕒 | | Waiting for CLI support for adding secrets |
| [`lastpass`](https://www.lastpass.com/) | 🕒 | | | | [`lastpass`](https://www.lastpass.com/) | 🕒 | | |
@@ -450,6 +451,42 @@ Important notes:
- Secrets are managed using gopass's native commands; `gman` acts as a wrapper to interface with gopass. - Secrets are managed using gopass's native commands; `gman` acts as a wrapper to interface with gopass.
- Updates overwrite existing secrets - Updates overwrite existing secrets
- If no store is specified, the default gopass store is used and `gman sync` will sync with all configured stores. - If no store is specified, the default gopass store is used and `gman sync` will sync with all configured stores.
### Provider: `one_password`
The `one_password` provider uses the [1Password CLI (`op`)](https://developer.1password.com/docs/cli/) as the backing
storage location for secrets.
- Optional: `vault` (string) to specify which 1Password vault to use. If omitted, the default vault is used.
- Optional: `account` (string) to specify which 1Password account to use. Useful if you have multiple accounts. If
omitted, the default signed-in account is used.
Configuration example:
```yaml
default_provider: op
providers:
- name: op
type: one_password
vault: Production # Optional; if omitted, uses the default vault
account: my.1password.com # Optional; if omitted, uses the default account
```
Authentication:
- **Interactive**: Run `op signin` to sign in interactively.
- **Service Account**: Set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable for non-interactive/CI usage.
- **Desktop App Integration**: If the 1Password desktop app is installed and configured, the CLI can use biometric
authentication (Touch ID, Windows Hello, etc.).
Important notes:
- Ensure the 1Password CLI (`op`) is installed on your system. Install instructions are at
https://developer.1password.com/docs/cli/get-started/.
- Secrets are stored as 1Password Password items. The item title is the secret name and the `password` field holds the
secret value.
- **Deletions are permanent. Deleted items are not archived.**
- `add` creates a new Password item. If an item with the same title already exists in the vault, `op` will create a
duplicate. Use `update` to change an existing secret value.
- `list` returns the titles of all items in the configured vault.
## Run Configurations ## Run Configurations
Run configurations (or "profiles") tell `gman` how to inject secrets into a command. Three modes of secret injection are Run configurations (or "profiles") tell `gman` how to inject secrets into a command. Three modes of secret injection are
+4
View File
@@ -169,6 +169,10 @@ impl ProviderConfig {
debug!("Using Gopass provider"); debug!("Using Gopass provider");
provider_def 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; mod git_sync;
pub mod gopass; pub mod gopass;
pub mod local; pub mod local;
pub mod one_password;
use crate::providers::gopass::GopassProvider; use crate::providers::gopass::GopassProvider;
use crate::providers::local::LocalProvider; use crate::providers::local::LocalProvider;
use crate::providers::one_password::OnePasswordProvider;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use aws_secrets_manager::AwsSecretsManagerProvider; use aws_secrets_manager::AwsSecretsManagerProvider;
use azure_key_vault::AzureKeyVaultProvider; use azure_key_vault::AzureKeyVaultProvider;
@@ -76,6 +78,10 @@ pub enum SupportedProvider {
#[serde(flatten)] #[serde(flatten)]
provider_def: GopassProvider, provider_def: GopassProvider,
}, },
OnePassword {
#[serde(flatten)]
provider_def: OnePasswordProvider,
},
} }
impl Validate for SupportedProvider { impl Validate for SupportedProvider {
@@ -86,6 +92,7 @@ impl Validate for SupportedProvider {
SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(), SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(),
SupportedProvider::AzureKeyVault { provider_def } => provider_def.validate(), SupportedProvider::AzureKeyVault { provider_def } => provider_def.validate(),
SupportedProvider::Gopass { 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::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"),
SupportedProvider::AzureKeyVault { .. } => write!(f, "azure_key_vault"), SupportedProvider::AzureKeyVault { .. } => write!(f, "azure_key_vault"),
SupportedProvider::Gopass { .. } => write!(f, "gopass"), 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(())
}
}
+1
View File
@@ -3,4 +3,5 @@ mod azure_key_vault_tests;
mod gcp_secret_manager_tests; mod gcp_secret_manager_tests;
mod gopass_tests; mod gopass_tests;
mod local_tests; mod local_tests;
mod one_password_tests;
mod provider_tests; mod provider_tests;
+113
View File
@@ -0,0 +1,113 @@
use gman::config::{Config, ProviderConfig};
use gman::providers::{SecretProvider, SupportedProvider};
use pretty_assertions::{assert_eq, assert_str_eq};
use validator::Validate;
#[test]
fn test_one_password_supported_provider_display_and_validate_from_yaml() {
let yaml = r#"---
type: one_password
vault: Production
account: my.1password.com
"#;
let sp: SupportedProvider = serde_yaml::from_str(yaml).expect("valid supported provider yaml");
assert!(sp.validate().is_ok());
assert_eq!(sp.to_string(), "one_password");
}
#[test]
fn test_one_password_supported_provider_minimal_yaml() {
let yaml = r#"---
type: one_password
"#;
let sp: SupportedProvider = serde_yaml::from_str(yaml).expect("valid supported provider yaml");
assert!(sp.validate().is_ok());
assert_eq!(sp.to_string(), "one_password");
}
#[test]
fn test_one_password_supported_provider_vault_only() {
let yaml = r#"---
type: one_password
vault: Personal
"#;
let sp: SupportedProvider = serde_yaml::from_str(yaml).expect("valid supported provider yaml");
assert!(sp.validate().is_ok());
}
#[test]
fn test_one_password_supported_provider_account_only() {
let yaml = r#"---
type: one_password
account: team.1password.com
"#;
let sp: SupportedProvider = serde_yaml::from_str(yaml).expect("valid supported provider yaml");
assert!(sp.validate().is_ok());
}
#[test]
fn test_one_password_supported_provider_rejects_unknown_fields() {
let yaml = r#"---
type: one_password
vault: Production
unknown_field: bad
"#;
let result: Result<SupportedProvider, _> = serde_yaml::from_str(yaml);
assert!(result.is_err());
}
#[test]
fn test_provider_config_with_one_password_deserialize_and_extract() {
let yaml = r#"---
name: op
type: one_password
"#;
let pc: ProviderConfig = serde_yaml::from_str(yaml).expect("valid provider config yaml");
assert!(pc.validate().is_ok());
let mut pc_owned = pc.clone();
let provider: &mut dyn SecretProvider = pc_owned.extract_provider();
assert_str_eq!(provider.name(), "OnePasswordProvider");
let cfg_yaml = r#"---
default_provider: op
providers:
- name: op
type: one_password
vault: Production
account: my.1password.com
"#;
let cfg: Config = serde_yaml::from_str(cfg_yaml).expect("valid config yaml");
assert!(cfg.validate().is_ok());
let extracted = cfg
.extract_provider_config(None)
.expect("should find default provider");
assert_eq!(extracted.name.as_deref(), Some("op"));
}
#[test]
fn test_one_password_config_with_multiple_providers() {
let cfg_yaml = r#"---
default_provider: local
providers:
- name: local
type: local
- name: op
type: one_password
vault: Production
"#;
let cfg: Config = serde_yaml::from_str(cfg_yaml).expect("valid config yaml");
assert!(cfg.validate().is_ok());
let extracted = cfg
.extract_provider_config(Some("op".into()))
.expect("should find op provider");
assert_eq!(extracted.name.as_deref(), Some("op"));
}