use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
pub use cuenv_secrets::{
BatchResolver, ResolvedSecrets, SaltConfig, SecretError, SecretRegistry, SecretResolver,
SecretSpec, compute_secret_fingerprint,
};
pub use cuenv_secrets::resolvers::{EnvSecretResolver, ExecSecretResolver};
#[cfg(feature = "1password")]
pub use cuenv_1password::secrets::{OnePasswordConfig, OnePasswordResolver};
#[allow(clippy::unnecessary_wraps)]
pub fn create_default_registry() -> Result<SecretRegistry> {
let mut registry = SecretRegistry::new();
registry.register(Arc::new(EnvSecretResolver::new()));
registry.register(Arc::new(ExecSecretResolver::new()));
#[cfg(feature = "1password")]
{
let op_resolver = OnePasswordResolver::new().map_err(|e| {
Error::configuration(format!("Failed to initialize 1Password resolver: {e}"))
})?;
registry.register(Arc::new(op_resolver));
}
Ok(registry)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExecResolver {
pub command: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Secret {
pub resolver: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
pub op_ref: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
impl Secret {
#[must_use]
pub fn new(command: String, args: Vec<String>) -> Self {
Secret {
resolver: "exec".to_string(),
command,
args,
op_ref: None,
extra: HashMap::new(),
}
}
#[must_use]
pub fn onepassword(reference: impl Into<String>) -> Self {
Secret {
resolver: "onepassword".to_string(),
command: String::new(),
args: Vec::new(),
op_ref: Some(reference.into()),
extra: HashMap::new(),
}
}
#[must_use]
pub fn with_extra(command: String, args: Vec<String>, extra: HashMap<String, Value>) -> Self {
Secret {
resolver: "exec".to_string(),
command,
args,
op_ref: None,
extra,
}
}
#[must_use]
pub fn provider(&self) -> &str {
&self.resolver
}
#[must_use]
pub fn to_spec(&self) -> SecretSpec {
let source = match self.resolver.as_str() {
"onepassword" => self.op_ref.clone().unwrap_or_default(),
"exec" => serde_json::json!({
"command": self.command,
"args": self.args
})
.to_string(),
_ => serde_json::to_string(self).unwrap_or_default(),
};
SecretSpec::new(source)
}
pub async fn resolve(&self) -> Result<String> {
tracing::debug!(resolver = %self.resolver, op_ref = ?self.op_ref, "Secret::resolve() called");
let registry = create_default_registry()?;
self.resolve_with_registry(®istry).await
}
pub async fn resolve_with_registry(&self, registry: &SecretRegistry) -> Result<String> {
let spec = self.to_spec();
registry
.resolve(&self.resolver, "secret", &spec)
.await
.map_err(|e| Error::configuration(format!("{e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_exec_resolver_new() {
let resolver = ExecResolver {
command: "echo".to_string(),
args: vec!["hello".to_string()],
};
assert_eq!(resolver.command, "echo");
assert_eq!(resolver.args, vec!["hello"]);
}
#[test]
fn test_exec_resolver_serde_roundtrip() {
let resolver = ExecResolver {
command: "/usr/bin/vault".to_string(),
args: vec![
"read".to_string(),
"-field=value".to_string(),
"secret/data".to_string(),
],
};
let json = serde_json::to_string(&resolver).unwrap();
let parsed: ExecResolver = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.command, resolver.command);
assert_eq!(parsed.args, resolver.args);
}
#[test]
fn test_exec_resolver_clone() {
let resolver = ExecResolver {
command: "cmd".to_string(),
args: vec!["arg1".to_string()],
};
let cloned = resolver.clone();
assert_eq!(cloned.command, resolver.command);
assert_eq!(cloned.args, resolver.args);
}
#[test]
fn test_exec_resolver_eq() {
let r1 = ExecResolver {
command: "cmd".to_string(),
args: vec!["arg".to_string()],
};
let r2 = ExecResolver {
command: "cmd".to_string(),
args: vec!["arg".to_string()],
};
let r3 = ExecResolver {
command: "other".to_string(),
args: vec![],
};
assert_eq!(r1, r2);
assert_ne!(r1, r3);
}
#[test]
fn test_secret_new_exec() {
let secret = Secret::new("echo".to_string(), vec!["hello".to_string()]);
assert_eq!(secret.resolver, "exec");
assert_eq!(secret.command, "echo");
assert_eq!(secret.args, vec!["hello"]);
assert!(secret.op_ref.is_none());
assert!(secret.extra.is_empty());
}
#[test]
fn test_secret_onepassword() {
let secret = Secret::onepassword("op://vault/item/field");
assert_eq!(secret.resolver, "onepassword");
assert_eq!(secret.op_ref, Some("op://vault/item/field".to_string()));
assert!(secret.command.is_empty());
assert!(secret.args.is_empty());
}
#[test]
fn test_secret_onepassword_with_into() {
let secret = Secret::onepassword(String::from("op://my-vault/my-item/password"));
assert_eq!(secret.resolver, "onepassword");
assert_eq!(
secret.op_ref,
Some("op://my-vault/my-item/password".to_string())
);
}
#[test]
fn test_secret_with_extra() {
let mut extra = HashMap::new();
extra.insert("region".to_string(), json!("us-east-1"));
extra.insert("version".to_string(), json!(2));
let secret = Secret::with_extra(
"aws".to_string(),
vec!["secretsmanager".to_string(), "get-secret-value".to_string()],
extra.clone(),
);
assert_eq!(secret.resolver, "exec");
assert_eq!(secret.command, "aws");
assert_eq!(secret.extra, extra);
}
#[test]
fn test_secret_provider_exec() {
let secret = Secret::new("cmd".to_string(), vec![]);
assert_eq!(secret.provider(), "exec");
}
#[test]
fn test_secret_provider_onepassword() {
let secret = Secret::onepassword("op://vault/item/field");
assert_eq!(secret.provider(), "onepassword");
}
#[test]
fn test_secret_provider_custom() {
let secret = Secret {
resolver: "vault".to_string(),
command: String::new(),
args: Vec::new(),
op_ref: None,
extra: HashMap::new(),
};
assert_eq!(secret.provider(), "vault");
}
#[test]
fn test_secret_to_spec_onepassword() {
let secret = Secret::onepassword("op://vault/item/field");
let spec = secret.to_spec();
assert_eq!(spec.source, "op://vault/item/field");
}
#[test]
fn test_secret_to_spec_exec() {
let secret = Secret::new("echo".to_string(), vec!["hello".to_string()]);
let spec = secret.to_spec();
let source = &spec.source;
assert!(source.contains("echo"));
assert!(source.contains("hello"));
}
#[test]
fn test_secret_to_spec_onepassword_empty_ref() {
let secret = Secret {
resolver: "onepassword".to_string(),
command: String::new(),
args: Vec::new(),
op_ref: None, extra: HashMap::new(),
};
let spec = secret.to_spec();
assert_eq!(spec.source, "");
}
#[test]
fn test_secret_to_spec_other_resolver() {
let mut extra = HashMap::new();
extra.insert("path".to_string(), json!("secret/data/myapp"));
let secret = Secret {
resolver: "vault".to_string(),
command: String::new(),
args: Vec::new(),
op_ref: None,
extra,
};
let spec = secret.to_spec();
let source = &spec.source;
assert!(source.contains("vault"));
assert!(source.contains("path"));
}
#[test]
fn test_secret_serde_exec_roundtrip() {
let secret = Secret::new("echo".to_string(), vec!["test".to_string()]);
let json = serde_json::to_string(&secret).unwrap();
let parsed: Secret = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.resolver, secret.resolver);
assert_eq!(parsed.command, secret.command);
assert_eq!(parsed.args, secret.args);
}
#[test]
fn test_secret_serde_onepassword_roundtrip() {
let secret = Secret::onepassword("op://vault/item/field");
let json = serde_json::to_string(&secret).unwrap();
let parsed: Secret = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.resolver, secret.resolver);
assert_eq!(parsed.op_ref, secret.op_ref);
}
#[test]
fn test_secret_serde_from_json() {
let json = r#"{
"resolver": "exec",
"command": "vault",
"args": ["read", "-field=value", "secret/data"]
}"#;
let secret: Secret = serde_json::from_str(json).unwrap();
assert_eq!(secret.resolver, "exec");
assert_eq!(secret.command, "vault");
assert_eq!(secret.args.len(), 3);
}
#[test]
fn test_secret_serde_onepassword_ref_field() {
let json = r#"{
"resolver": "onepassword",
"ref": "op://vault/item/password"
}"#;
let secret: Secret = serde_json::from_str(json).unwrap();
assert_eq!(secret.resolver, "onepassword");
assert_eq!(secret.op_ref, Some("op://vault/item/password".to_string()));
}
#[test]
fn test_secret_serde_extra_fields() {
let json = r#"{
"resolver": "aws",
"command": "",
"secret_id": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp",
"region": "us-east-1"
}"#;
let secret: Secret = serde_json::from_str(json).unwrap();
assert_eq!(secret.resolver, "aws");
assert!(secret.extra.contains_key("secret_id"));
assert!(secret.extra.contains_key("region"));
}
#[test]
fn test_secret_serde_skip_empty_command() {
let secret = Secret::onepassword("op://vault/item/field");
let json = serde_json::to_string(&secret).unwrap();
assert!(!json.contains("\"command\":"));
}
#[test]
fn test_secret_serde_skip_empty_args() {
let secret = Secret::onepassword("op://vault/item/field");
let json = serde_json::to_string(&secret).unwrap();
assert!(!json.contains("\"args\":"));
}
#[test]
fn test_secret_clone() {
let secret = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
let cloned = secret.clone();
assert_eq!(cloned.resolver, secret.resolver);
assert_eq!(cloned.command, secret.command);
assert_eq!(cloned.args, secret.args);
}
#[test]
fn test_secret_eq() {
let s1 = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
let s2 = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
let s3 = Secret::onepassword("op://v/i/f");
assert_eq!(s1, s2);
assert_ne!(s1, s3);
}
#[test]
fn test_secret_debug() {
let secret = Secret::new("echo".to_string(), vec!["test".to_string()]);
let debug = format!("{:?}", secret);
assert!(debug.contains("Secret"));
assert!(debug.contains("exec"));
assert!(debug.contains("echo"));
}
}