1use crate::{Error, Result};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15use std::sync::Arc;
16
17pub use cuenv_secrets::{
19 BatchResolver, ResolvedSecrets, SaltConfig, SecretError, SecretRegistry, SecretResolver,
20 SecretSpec, compute_secret_fingerprint,
21};
22
23pub use cuenv_secrets::resolvers::{EnvSecretResolver, ExecSecretResolver};
25
26#[cfg(feature = "1password")]
28pub use cuenv_1password::secrets::{OnePasswordConfig, OnePasswordResolver};
29
30#[allow(clippy::unnecessary_wraps)]
41pub fn create_default_registry() -> Result<SecretRegistry> {
42 let mut registry = SecretRegistry::new();
43
44 registry.register(Arc::new(EnvSecretResolver::new()));
46 registry.register(Arc::new(ExecSecretResolver::new()));
47
48 #[cfg(feature = "1password")]
50 {
51 let op_resolver = OnePasswordResolver::new().map_err(|e| {
52 Error::configuration(format!("Failed to initialize 1Password resolver: {e}"))
53 })?;
54 registry.register(Arc::new(op_resolver));
55 }
56
57 Ok(registry)
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct ExecResolver {
63 pub command: String,
65
66 pub args: Vec<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80pub struct Secret {
81 pub resolver: String,
83
84 #[serde(default, skip_serializing_if = "String::is_empty")]
86 pub command: String,
87
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub args: Vec<String>,
91
92 #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
94 pub op_ref: Option<String>,
95
96 #[serde(flatten)]
98 pub extra: HashMap<String, Value>,
99}
100
101impl Secret {
102 #[must_use]
104 pub fn new(command: String, args: Vec<String>) -> Self {
105 Secret {
106 resolver: "exec".to_string(),
107 command,
108 args,
109 op_ref: None,
110 extra: HashMap::new(),
111 }
112 }
113
114 #[must_use]
116 pub fn onepassword(reference: impl Into<String>) -> Self {
117 Secret {
118 resolver: "onepassword".to_string(),
119 command: String::new(),
120 args: Vec::new(),
121 op_ref: Some(reference.into()),
122 extra: HashMap::new(),
123 }
124 }
125
126 #[must_use]
128 pub fn with_extra(command: String, args: Vec<String>, extra: HashMap<String, Value>) -> Self {
129 Secret {
130 resolver: "exec".to_string(),
131 command,
132 args,
133 op_ref: None,
134 extra,
135 }
136 }
137
138 #[must_use]
140 pub fn provider(&self) -> &str {
141 &self.resolver
142 }
143
144 #[must_use]
146 pub fn to_spec(&self) -> SecretSpec {
147 let source = match self.resolver.as_str() {
148 "onepassword" => self.op_ref.clone().unwrap_or_default(),
149 "exec" => serde_json::json!({
150 "command": self.command,
151 "args": self.args
152 })
153 .to_string(),
154 _ => serde_json::to_string(self).unwrap_or_default(),
156 };
157 SecretSpec::new(source)
158 }
159
160 pub async fn resolve(&self) -> Result<String> {
167 let registry = create_default_registry()?;
168 self.resolve_with_registry(®istry).await
169 }
170
171 pub async fn resolve_with_registry(&self, registry: &SecretRegistry) -> Result<String> {
176 let spec = self.to_spec();
177
178 registry
179 .resolve(&self.resolver, "secret", &spec)
180 .await
181 .map_err(|e| Error::configuration(format!("{e}")))
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use serde_json::json;
189
190 #[test]
195 fn test_exec_resolver_new() {
196 let resolver = ExecResolver {
197 command: "echo".to_string(),
198 args: vec!["hello".to_string()],
199 };
200
201 assert_eq!(resolver.command, "echo");
202 assert_eq!(resolver.args, vec!["hello"]);
203 }
204
205 #[test]
206 fn test_exec_resolver_serde_roundtrip() {
207 let resolver = ExecResolver {
208 command: "/usr/bin/vault".to_string(),
209 args: vec![
210 "read".to_string(),
211 "-field=value".to_string(),
212 "secret/data".to_string(),
213 ],
214 };
215
216 let json = serde_json::to_string(&resolver).unwrap();
217 let parsed: ExecResolver = serde_json::from_str(&json).unwrap();
218
219 assert_eq!(parsed.command, resolver.command);
220 assert_eq!(parsed.args, resolver.args);
221 }
222
223 #[test]
224 fn test_exec_resolver_clone() {
225 let resolver = ExecResolver {
226 command: "cmd".to_string(),
227 args: vec!["arg1".to_string()],
228 };
229
230 let cloned = resolver.clone();
231 assert_eq!(cloned.command, resolver.command);
232 assert_eq!(cloned.args, resolver.args);
233 }
234
235 #[test]
236 fn test_exec_resolver_eq() {
237 let r1 = ExecResolver {
238 command: "cmd".to_string(),
239 args: vec!["arg".to_string()],
240 };
241 let r2 = ExecResolver {
242 command: "cmd".to_string(),
243 args: vec!["arg".to_string()],
244 };
245 let r3 = ExecResolver {
246 command: "other".to_string(),
247 args: vec![],
248 };
249
250 assert_eq!(r1, r2);
251 assert_ne!(r1, r3);
252 }
253
254 #[test]
259 fn test_secret_new_exec() {
260 let secret = Secret::new("echo".to_string(), vec!["hello".to_string()]);
261
262 assert_eq!(secret.resolver, "exec");
263 assert_eq!(secret.command, "echo");
264 assert_eq!(secret.args, vec!["hello"]);
265 assert!(secret.op_ref.is_none());
266 assert!(secret.extra.is_empty());
267 }
268
269 #[test]
270 fn test_secret_onepassword() {
271 let secret = Secret::onepassword("op://vault/item/field");
272
273 assert_eq!(secret.resolver, "onepassword");
274 assert_eq!(secret.op_ref, Some("op://vault/item/field".to_string()));
275 assert!(secret.command.is_empty());
276 assert!(secret.args.is_empty());
277 }
278
279 #[test]
280 fn test_secret_onepassword_with_into() {
281 let secret = Secret::onepassword(String::from("op://my-vault/my-item/password"));
282
283 assert_eq!(secret.resolver, "onepassword");
284 assert_eq!(
285 secret.op_ref,
286 Some("op://my-vault/my-item/password".to_string())
287 );
288 }
289
290 #[test]
291 fn test_secret_with_extra() {
292 let mut extra = HashMap::new();
293 extra.insert("region".to_string(), json!("us-east-1"));
294 extra.insert("version".to_string(), json!(2));
295
296 let secret = Secret::with_extra(
297 "aws".to_string(),
298 vec!["secretsmanager".to_string(), "get-secret-value".to_string()],
299 extra.clone(),
300 );
301
302 assert_eq!(secret.resolver, "exec");
303 assert_eq!(secret.command, "aws");
304 assert_eq!(secret.extra, extra);
305 }
306
307 #[test]
312 fn test_secret_provider_exec() {
313 let secret = Secret::new("cmd".to_string(), vec![]);
314 assert_eq!(secret.provider(), "exec");
315 }
316
317 #[test]
318 fn test_secret_provider_onepassword() {
319 let secret = Secret::onepassword("op://vault/item/field");
320 assert_eq!(secret.provider(), "onepassword");
321 }
322
323 #[test]
324 fn test_secret_provider_custom() {
325 let secret = Secret {
326 resolver: "vault".to_string(),
327 command: String::new(),
328 args: Vec::new(),
329 op_ref: None,
330 extra: HashMap::new(),
331 };
332 assert_eq!(secret.provider(), "vault");
333 }
334
335 #[test]
340 fn test_secret_to_spec_onepassword() {
341 let secret = Secret::onepassword("op://vault/item/field");
342 let spec = secret.to_spec();
343
344 assert_eq!(spec.source, "op://vault/item/field");
345 }
346
347 #[test]
348 fn test_secret_to_spec_exec() {
349 let secret = Secret::new("echo".to_string(), vec!["hello".to_string()]);
350 let spec = secret.to_spec();
351
352 let source = &spec.source;
353 assert!(source.contains("echo"));
354 assert!(source.contains("hello"));
355 }
356
357 #[test]
358 fn test_secret_to_spec_onepassword_empty_ref() {
359 let secret = Secret {
360 resolver: "onepassword".to_string(),
361 command: String::new(),
362 args: Vec::new(),
363 op_ref: None, extra: HashMap::new(),
365 };
366 let spec = secret.to_spec();
367
368 assert_eq!(spec.source, "");
370 }
371
372 #[test]
373 fn test_secret_to_spec_other_resolver() {
374 let mut extra = HashMap::new();
375 extra.insert("path".to_string(), json!("secret/data/myapp"));
376
377 let secret = Secret {
378 resolver: "vault".to_string(),
379 command: String::new(),
380 args: Vec::new(),
381 op_ref: None,
382 extra,
383 };
384 let spec = secret.to_spec();
385
386 let source = &spec.source;
388 assert!(source.contains("vault"));
389 assert!(source.contains("path"));
390 }
391
392 #[test]
397 fn test_secret_serde_exec_roundtrip() {
398 let secret = Secret::new("echo".to_string(), vec!["test".to_string()]);
399 let json = serde_json::to_string(&secret).unwrap();
400 let parsed: Secret = serde_json::from_str(&json).unwrap();
401
402 assert_eq!(parsed.resolver, secret.resolver);
403 assert_eq!(parsed.command, secret.command);
404 assert_eq!(parsed.args, secret.args);
405 }
406
407 #[test]
408 fn test_secret_serde_onepassword_roundtrip() {
409 let secret = Secret::onepassword("op://vault/item/field");
410 let json = serde_json::to_string(&secret).unwrap();
411 let parsed: Secret = serde_json::from_str(&json).unwrap();
412
413 assert_eq!(parsed.resolver, secret.resolver);
414 assert_eq!(parsed.op_ref, secret.op_ref);
415 }
416
417 #[test]
418 fn test_secret_serde_from_json() {
419 let json = r#"{
420 "resolver": "exec",
421 "command": "vault",
422 "args": ["read", "-field=value", "secret/data"]
423 }"#;
424
425 let secret: Secret = serde_json::from_str(json).unwrap();
426 assert_eq!(secret.resolver, "exec");
427 assert_eq!(secret.command, "vault");
428 assert_eq!(secret.args.len(), 3);
429 }
430
431 #[test]
432 fn test_secret_serde_onepassword_ref_field() {
433 let json = r#"{
435 "resolver": "onepassword",
436 "ref": "op://vault/item/password"
437 }"#;
438
439 let secret: Secret = serde_json::from_str(json).unwrap();
440 assert_eq!(secret.resolver, "onepassword");
441 assert_eq!(secret.op_ref, Some("op://vault/item/password".to_string()));
442 }
443
444 #[test]
445 fn test_secret_serde_extra_fields() {
446 let json = r#"{
447 "resolver": "aws",
448 "command": "",
449 "secret_id": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp",
450 "region": "us-east-1"
451 }"#;
452
453 let secret: Secret = serde_json::from_str(json).unwrap();
454 assert_eq!(secret.resolver, "aws");
455 assert!(secret.extra.contains_key("secret_id"));
456 assert!(secret.extra.contains_key("region"));
457 }
458
459 #[test]
460 fn test_secret_serde_skip_empty_command() {
461 let secret = Secret::onepassword("op://vault/item/field");
462 let json = serde_json::to_string(&secret).unwrap();
463
464 assert!(!json.contains("\"command\":"));
466 }
467
468 #[test]
469 fn test_secret_serde_skip_empty_args() {
470 let secret = Secret::onepassword("op://vault/item/field");
471 let json = serde_json::to_string(&secret).unwrap();
472
473 assert!(!json.contains("\"args\":"));
475 }
476
477 #[test]
482 fn test_secret_clone() {
483 let secret = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
484 let cloned = secret.clone();
485
486 assert_eq!(cloned.resolver, secret.resolver);
487 assert_eq!(cloned.command, secret.command);
488 assert_eq!(cloned.args, secret.args);
489 }
490
491 #[test]
492 fn test_secret_eq() {
493 let s1 = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
494 let s2 = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
495 let s3 = Secret::onepassword("op://v/i/f");
496
497 assert_eq!(s1, s2);
498 assert_ne!(s1, s3);
499 }
500
501 #[test]
502 fn test_secret_debug() {
503 let secret = Secret::new("echo".to_string(), vec!["test".to_string()]);
504 let debug = format!("{:?}", secret);
505
506 assert!(debug.contains("Secret"));
507 assert!(debug.contains("exec"));
508 assert!(debug.contains("echo"));
509 }
510}