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 tracing::debug!(resolver = %self.resolver, op_ref = ?self.op_ref, "Secret::resolve() called");
168 let registry = create_default_registry()?;
169 self.resolve_with_registry(®istry).await
170 }
171
172 pub async fn resolve_with_registry(&self, registry: &SecretRegistry) -> Result<String> {
177 let spec = self.to_spec();
178
179 registry
180 .resolve(&self.resolver, "secret", &spec)
181 .await
182 .map_err(|e| Error::configuration(format!("{e}")))
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use serde_json::json;
190
191 #[test]
196 fn test_exec_resolver_new() {
197 let resolver = ExecResolver {
198 command: "echo".to_string(),
199 args: vec!["hello".to_string()],
200 };
201
202 assert_eq!(resolver.command, "echo");
203 assert_eq!(resolver.args, vec!["hello"]);
204 }
205
206 #[test]
207 fn test_exec_resolver_serde_roundtrip() {
208 let resolver = ExecResolver {
209 command: "/usr/bin/vault".to_string(),
210 args: vec![
211 "read".to_string(),
212 "-field=value".to_string(),
213 "secret/data".to_string(),
214 ],
215 };
216
217 let json = serde_json::to_string(&resolver).unwrap();
218 let parsed: ExecResolver = serde_json::from_str(&json).unwrap();
219
220 assert_eq!(parsed.command, resolver.command);
221 assert_eq!(parsed.args, resolver.args);
222 }
223
224 #[test]
225 fn test_exec_resolver_clone() {
226 let resolver = ExecResolver {
227 command: "cmd".to_string(),
228 args: vec!["arg1".to_string()],
229 };
230
231 let cloned = resolver.clone();
232 assert_eq!(cloned.command, resolver.command);
233 assert_eq!(cloned.args, resolver.args);
234 }
235
236 #[test]
237 fn test_exec_resolver_eq() {
238 let r1 = ExecResolver {
239 command: "cmd".to_string(),
240 args: vec!["arg".to_string()],
241 };
242 let r2 = ExecResolver {
243 command: "cmd".to_string(),
244 args: vec!["arg".to_string()],
245 };
246 let r3 = ExecResolver {
247 command: "other".to_string(),
248 args: vec![],
249 };
250
251 assert_eq!(r1, r2);
252 assert_ne!(r1, r3);
253 }
254
255 #[test]
260 fn test_secret_new_exec() {
261 let secret = Secret::new("echo".to_string(), vec!["hello".to_string()]);
262
263 assert_eq!(secret.resolver, "exec");
264 assert_eq!(secret.command, "echo");
265 assert_eq!(secret.args, vec!["hello"]);
266 assert!(secret.op_ref.is_none());
267 assert!(secret.extra.is_empty());
268 }
269
270 #[test]
271 fn test_secret_onepassword() {
272 let secret = Secret::onepassword("op://vault/item/field");
273
274 assert_eq!(secret.resolver, "onepassword");
275 assert_eq!(secret.op_ref, Some("op://vault/item/field".to_string()));
276 assert!(secret.command.is_empty());
277 assert!(secret.args.is_empty());
278 }
279
280 #[test]
281 fn test_secret_onepassword_with_into() {
282 let secret = Secret::onepassword(String::from("op://my-vault/my-item/password"));
283
284 assert_eq!(secret.resolver, "onepassword");
285 assert_eq!(
286 secret.op_ref,
287 Some("op://my-vault/my-item/password".to_string())
288 );
289 }
290
291 #[test]
292 fn test_secret_with_extra() {
293 let mut extra = HashMap::new();
294 extra.insert("region".to_string(), json!("us-east-1"));
295 extra.insert("version".to_string(), json!(2));
296
297 let secret = Secret::with_extra(
298 "aws".to_string(),
299 vec!["secretsmanager".to_string(), "get-secret-value".to_string()],
300 extra.clone(),
301 );
302
303 assert_eq!(secret.resolver, "exec");
304 assert_eq!(secret.command, "aws");
305 assert_eq!(secret.extra, extra);
306 }
307
308 #[test]
313 fn test_secret_provider_exec() {
314 let secret = Secret::new("cmd".to_string(), vec![]);
315 assert_eq!(secret.provider(), "exec");
316 }
317
318 #[test]
319 fn test_secret_provider_onepassword() {
320 let secret = Secret::onepassword("op://vault/item/field");
321 assert_eq!(secret.provider(), "onepassword");
322 }
323
324 #[test]
325 fn test_secret_provider_custom() {
326 let secret = Secret {
327 resolver: "vault".to_string(),
328 command: String::new(),
329 args: Vec::new(),
330 op_ref: None,
331 extra: HashMap::new(),
332 };
333 assert_eq!(secret.provider(), "vault");
334 }
335
336 #[test]
341 fn test_secret_to_spec_onepassword() {
342 let secret = Secret::onepassword("op://vault/item/field");
343 let spec = secret.to_spec();
344
345 assert_eq!(spec.source, "op://vault/item/field");
346 }
347
348 #[test]
349 fn test_secret_to_spec_exec() {
350 let secret = Secret::new("echo".to_string(), vec!["hello".to_string()]);
351 let spec = secret.to_spec();
352
353 let source = &spec.source;
354 assert!(source.contains("echo"));
355 assert!(source.contains("hello"));
356 }
357
358 #[test]
359 fn test_secret_to_spec_onepassword_empty_ref() {
360 let secret = Secret {
361 resolver: "onepassword".to_string(),
362 command: String::new(),
363 args: Vec::new(),
364 op_ref: None, extra: HashMap::new(),
366 };
367 let spec = secret.to_spec();
368
369 assert_eq!(spec.source, "");
371 }
372
373 #[test]
374 fn test_secret_to_spec_other_resolver() {
375 let mut extra = HashMap::new();
376 extra.insert("path".to_string(), json!("secret/data/myapp"));
377
378 let secret = Secret {
379 resolver: "vault".to_string(),
380 command: String::new(),
381 args: Vec::new(),
382 op_ref: None,
383 extra,
384 };
385 let spec = secret.to_spec();
386
387 let source = &spec.source;
389 assert!(source.contains("vault"));
390 assert!(source.contains("path"));
391 }
392
393 #[test]
398 fn test_secret_serde_exec_roundtrip() {
399 let secret = Secret::new("echo".to_string(), vec!["test".to_string()]);
400 let json = serde_json::to_string(&secret).unwrap();
401 let parsed: Secret = serde_json::from_str(&json).unwrap();
402
403 assert_eq!(parsed.resolver, secret.resolver);
404 assert_eq!(parsed.command, secret.command);
405 assert_eq!(parsed.args, secret.args);
406 }
407
408 #[test]
409 fn test_secret_serde_onepassword_roundtrip() {
410 let secret = Secret::onepassword("op://vault/item/field");
411 let json = serde_json::to_string(&secret).unwrap();
412 let parsed: Secret = serde_json::from_str(&json).unwrap();
413
414 assert_eq!(parsed.resolver, secret.resolver);
415 assert_eq!(parsed.op_ref, secret.op_ref);
416 }
417
418 #[test]
419 fn test_secret_serde_from_json() {
420 let json = r#"{
421 "resolver": "exec",
422 "command": "vault",
423 "args": ["read", "-field=value", "secret/data"]
424 }"#;
425
426 let secret: Secret = serde_json::from_str(json).unwrap();
427 assert_eq!(secret.resolver, "exec");
428 assert_eq!(secret.command, "vault");
429 assert_eq!(secret.args.len(), 3);
430 }
431
432 #[test]
433 fn test_secret_serde_onepassword_ref_field() {
434 let json = r#"{
436 "resolver": "onepassword",
437 "ref": "op://vault/item/password"
438 }"#;
439
440 let secret: Secret = serde_json::from_str(json).unwrap();
441 assert_eq!(secret.resolver, "onepassword");
442 assert_eq!(secret.op_ref, Some("op://vault/item/password".to_string()));
443 }
444
445 #[test]
446 fn test_secret_serde_extra_fields() {
447 let json = r#"{
448 "resolver": "aws",
449 "command": "",
450 "secret_id": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp",
451 "region": "us-east-1"
452 }"#;
453
454 let secret: Secret = serde_json::from_str(json).unwrap();
455 assert_eq!(secret.resolver, "aws");
456 assert!(secret.extra.contains_key("secret_id"));
457 assert!(secret.extra.contains_key("region"));
458 }
459
460 #[test]
461 fn test_secret_serde_skip_empty_command() {
462 let secret = Secret::onepassword("op://vault/item/field");
463 let json = serde_json::to_string(&secret).unwrap();
464
465 assert!(!json.contains("\"command\":"));
467 }
468
469 #[test]
470 fn test_secret_serde_skip_empty_args() {
471 let secret = Secret::onepassword("op://vault/item/field");
472 let json = serde_json::to_string(&secret).unwrap();
473
474 assert!(!json.contains("\"args\":"));
476 }
477
478 #[test]
483 fn test_secret_clone() {
484 let secret = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
485 let cloned = secret.clone();
486
487 assert_eq!(cloned.resolver, secret.resolver);
488 assert_eq!(cloned.command, secret.command);
489 assert_eq!(cloned.args, secret.args);
490 }
491
492 #[test]
493 fn test_secret_eq() {
494 let s1 = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
495 let s2 = Secret::new("cmd".to_string(), vec!["arg".to_string()]);
496 let s3 = Secret::onepassword("op://v/i/f");
497
498 assert_eq!(s1, s2);
499 assert_ne!(s1, s3);
500 }
501
502 #[test]
503 fn test_secret_debug() {
504 let secret = Secret::new("echo".to_string(), vec!["test".to_string()]);
505 let debug = format!("{:?}", secret);
506
507 assert!(debug.contains("Secret"));
508 assert!(debug.contains("exec"));
509 assert!(debug.contains("echo"));
510 }
511}