1use crate::types;
4
5pub fn add_secret(
9 vault: &mut types::Vault,
10 murk: &mut types::Murk,
11 key: &str,
12 value: &str,
13 desc: Option<&str>,
14 scoped: bool,
15 tags: &[String],
16 identity: &age::x25519::Identity,
17) -> bool {
18 if scoped {
19 let pubkey = identity.to_public().to_string();
20 murk.scoped
21 .entry(key.into())
22 .or_default()
23 .insert(pubkey, value.into());
24 } else {
25 murk.values.insert(key.into(), value.into());
26 }
27
28 let is_new = !vault.schema.contains_key(key);
29
30 if let Some(entry) = vault.schema.get_mut(key) {
31 if let Some(d) = desc {
32 entry.description = d.into();
33 }
34 if !tags.is_empty() {
35 for t in tags {
36 if !entry.tags.contains(t) {
37 entry.tags.push(t.clone());
38 }
39 }
40 }
41 } else {
42 vault.schema.insert(
43 key.into(),
44 types::SchemaEntry {
45 description: desc.unwrap_or("").into(),
46 example: None,
47 tags: tags.to_vec(),
48 },
49 );
50 }
51
52 is_new && desc.is_none()
53}
54
55pub fn remove_secret(vault: &mut types::Vault, murk: &mut types::Murk, key: &str) {
57 murk.values.remove(key);
58 murk.scoped.remove(key);
59 vault.schema.remove(key);
60}
61
62pub fn get_secret<'a>(murk: &'a types::Murk, key: &str, pubkey: &str) -> Option<&'a str> {
64 if let Some(value) = murk.scoped.get(key).and_then(|m| m.get(pubkey)) {
65 return Some(value.as_str());
66 }
67 murk.values.get(key).map(String::as_str)
68}
69
70pub fn list_keys<'a>(vault: &'a types::Vault, tags: &[String]) -> Vec<&'a str> {
72 vault
73 .schema
74 .iter()
75 .filter(|(_, entry)| tags.is_empty() || entry.tags.iter().any(|t| tags.contains(t)))
76 .map(|(key, _)| key.as_str())
77 .collect()
78}
79
80pub fn import_secrets(
85 vault: &mut types::Vault,
86 murk: &mut types::Murk,
87 pairs: &[(String, String)],
88) -> Vec<String> {
89 let mut imported = Vec::new();
90 for (key, value) in pairs {
91 murk.values.insert(key.clone(), value.clone());
92
93 if !vault.schema.contains_key(key.as_str()) {
94 vault.schema.insert(
95 key.clone(),
96 types::SchemaEntry {
97 description: String::new(),
98 example: None,
99 tags: vec![],
100 },
101 );
102 }
103
104 imported.push(key.clone());
105 }
106 imported
107}
108
109pub fn describe_key(
111 vault: &mut types::Vault,
112 key: &str,
113 description: &str,
114 example: Option<&str>,
115 tags: &[String],
116) {
117 if let Some(entry) = vault.schema.get_mut(key) {
118 entry.description = description.into();
119 entry.example = example.map(Into::into);
120 if !tags.is_empty() {
121 entry.tags = tags.to_vec();
122 }
123 } else {
124 vault.schema.insert(
125 key.into(),
126 types::SchemaEntry {
127 description: description.into(),
128 example: example.map(Into::into),
129 tags: tags.to_vec(),
130 },
131 );
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::testutil::*;
139 use std::collections::HashMap;
140
141 #[test]
142 fn add_secret_shared() {
143 let (secret, _) = generate_keypair();
144 let identity = make_identity(&secret);
145 let mut vault = empty_vault();
146 let mut murk = empty_murk();
147
148 let needs_hint = add_secret(
149 &mut vault,
150 &mut murk,
151 "KEY",
152 "value",
153 None,
154 false,
155 &[],
156 &identity,
157 );
158
159 assert!(needs_hint);
160 assert_eq!(murk.values["KEY"], "value");
161 assert!(vault.schema.contains_key("KEY"));
162 assert!(vault.schema["KEY"].description.is_empty());
163 }
164
165 #[test]
166 fn add_secret_with_description() {
167 let (secret, _) = generate_keypair();
168 let identity = make_identity(&secret);
169 let mut vault = empty_vault();
170 let mut murk = empty_murk();
171
172 let needs_hint = add_secret(
173 &mut vault,
174 &mut murk,
175 "KEY",
176 "value",
177 Some("a desc"),
178 false,
179 &[],
180 &identity,
181 );
182
183 assert!(!needs_hint);
184 assert_eq!(vault.schema["KEY"].description, "a desc");
185 }
186
187 #[test]
188 fn add_secret_scoped() {
189 let (secret, pubkey) = generate_keypair();
190 let identity = make_identity(&secret);
191 let mut vault = empty_vault();
192 let mut murk = empty_murk();
193
194 add_secret(
195 &mut vault,
196 &mut murk,
197 "KEY",
198 "scoped_val",
199 None,
200 true,
201 &[],
202 &identity,
203 );
204
205 assert!(!murk.values.contains_key("KEY"));
206 assert_eq!(murk.scoped["KEY"][&pubkey], "scoped_val");
207 }
208
209 #[test]
210 fn add_secret_merges_tags() {
211 let (secret, _) = generate_keypair();
212 let identity = make_identity(&secret);
213 let mut vault = empty_vault();
214 let mut murk = empty_murk();
215
216 let tags1 = vec!["db".into()];
217 add_secret(
218 &mut vault, &mut murk, "KEY", "v1", None, false, &tags1, &identity,
219 );
220 assert_eq!(vault.schema["KEY"].tags, vec!["db"]);
221
222 let tags2 = vec!["backend".into()];
223 add_secret(
224 &mut vault, &mut murk, "KEY", "v2", None, false, &tags2, &identity,
225 );
226 assert_eq!(vault.schema["KEY"].tags, vec!["db", "backend"]);
227
228 let tags3 = vec!["db".into()];
230 add_secret(
231 &mut vault, &mut murk, "KEY", "v3", None, false, &tags3, &identity,
232 );
233 assert_eq!(vault.schema["KEY"].tags, vec!["db", "backend"]);
234 }
235
236 #[test]
237 fn add_secret_updates_existing_desc() {
238 let (secret, _) = generate_keypair();
239 let identity = make_identity(&secret);
240 let mut vault = empty_vault();
241 let mut murk = empty_murk();
242
243 add_secret(
244 &mut vault,
245 &mut murk,
246 "KEY",
247 "v1",
248 Some("old"),
249 false,
250 &[],
251 &identity,
252 );
253 add_secret(
254 &mut vault,
255 &mut murk,
256 "KEY",
257 "v2",
258 Some("new"),
259 false,
260 &[],
261 &identity,
262 );
263 assert_eq!(vault.schema["KEY"].description, "new");
264 }
265
266 #[test]
267 fn remove_secret_clears_all() {
268 let mut vault = empty_vault();
269 vault.schema.insert(
270 "KEY".into(),
271 types::SchemaEntry {
272 description: "desc".into(),
273 example: None,
274 tags: vec![],
275 },
276 );
277 let mut murk = empty_murk();
278 murk.values.insert("KEY".into(), "val".into());
279 let mut scoped = HashMap::new();
280 scoped.insert("age1pk".into(), "scoped_val".into());
281 murk.scoped.insert("KEY".into(), scoped);
282
283 remove_secret(&mut vault, &mut murk, "KEY");
284
285 assert!(!murk.values.contains_key("KEY"));
286 assert!(!murk.scoped.contains_key("KEY"));
287 assert!(!vault.schema.contains_key("KEY"));
288 }
289
290 #[test]
291 fn get_secret_shared_value() {
292 let mut murk = empty_murk();
293 murk.values.insert("KEY".into(), "shared_val".into());
294
295 assert_eq!(get_secret(&murk, "KEY", "age1pk"), Some("shared_val"));
296 }
297
298 #[test]
299 fn get_secret_scoped_overrides_shared() {
300 let mut murk = empty_murk();
301 murk.values.insert("KEY".into(), "shared_val".into());
302 let mut scoped = HashMap::new();
303 scoped.insert("age1pk".into(), "scoped_val".into());
304 murk.scoped.insert("KEY".into(), scoped);
305
306 assert_eq!(get_secret(&murk, "KEY", "age1pk"), Some("scoped_val"));
307 }
308
309 #[test]
310 fn get_secret_missing_returns_none() {
311 let murk = empty_murk();
312 assert_eq!(get_secret(&murk, "NONEXISTENT", "age1pk"), None);
313 }
314
315 #[test]
316 fn list_keys_no_filter() {
317 let mut vault = empty_vault();
318 vault.schema.insert(
319 "A".into(),
320 types::SchemaEntry {
321 description: String::new(),
322 example: None,
323 tags: vec![],
324 },
325 );
326 vault.schema.insert(
327 "B".into(),
328 types::SchemaEntry {
329 description: String::new(),
330 example: None,
331 tags: vec![],
332 },
333 );
334
335 let keys = list_keys(&vault, &[]);
336 assert_eq!(keys, vec!["A", "B"]);
337 }
338
339 #[test]
340 fn list_keys_with_tag_filter() {
341 let mut vault = empty_vault();
342 vault.schema.insert(
343 "A".into(),
344 types::SchemaEntry {
345 description: String::new(),
346 example: None,
347 tags: vec!["db".into()],
348 },
349 );
350 vault.schema.insert(
351 "B".into(),
352 types::SchemaEntry {
353 description: String::new(),
354 example: None,
355 tags: vec!["api".into()],
356 },
357 );
358 vault.schema.insert(
359 "C".into(),
360 types::SchemaEntry {
361 description: String::new(),
362 example: None,
363 tags: vec![],
364 },
365 );
366
367 let keys = list_keys(&vault, &["db".into()]);
368 assert_eq!(keys, vec!["A"]);
369 }
370
371 #[test]
372 fn list_keys_no_matches() {
373 let mut vault = empty_vault();
374 vault.schema.insert(
375 "A".into(),
376 types::SchemaEntry {
377 description: String::new(),
378 example: None,
379 tags: vec!["db".into()],
380 },
381 );
382
383 let keys = list_keys(&vault, &["nonexistent".into()]);
384 assert!(keys.is_empty());
385 }
386
387 #[test]
388 fn describe_key_creates_new() {
389 let mut vault = empty_vault();
390 describe_key(
391 &mut vault,
392 "KEY",
393 "a description",
394 Some("example"),
395 &["tag".into()],
396 );
397
398 assert_eq!(vault.schema["KEY"].description, "a description");
399 assert_eq!(vault.schema["KEY"].example.as_deref(), Some("example"));
400 assert_eq!(vault.schema["KEY"].tags, vec!["tag"]);
401 }
402
403 #[test]
404 fn describe_key_updates_existing() {
405 let mut vault = empty_vault();
406 vault.schema.insert(
407 "KEY".into(),
408 types::SchemaEntry {
409 description: "old".into(),
410 example: Some("old_ex".into()),
411 tags: vec!["old_tag".into()],
412 },
413 );
414
415 describe_key(&mut vault, "KEY", "new", None, &["new_tag".into()]);
416
417 assert_eq!(vault.schema["KEY"].description, "new");
418 assert_eq!(vault.schema["KEY"].example, None);
419 assert_eq!(vault.schema["KEY"].tags, vec!["new_tag"]);
420 }
421
422 #[test]
423 fn describe_key_preserves_tags_if_empty() {
424 let mut vault = empty_vault();
425 vault.schema.insert(
426 "KEY".into(),
427 types::SchemaEntry {
428 description: "old".into(),
429 example: None,
430 tags: vec!["keep".into()],
431 },
432 );
433
434 describe_key(&mut vault, "KEY", "new desc", None, &[]);
435
436 assert_eq!(vault.schema["KEY"].tags, vec!["keep"]);
437 }
438
439 #[test]
442 fn add_secret_overwrite_shared_with_scoped() {
443 let (secret, pubkey) = generate_keypair();
444 let identity = make_identity(&secret);
445 let mut vault = empty_vault();
446 let mut murk = empty_murk();
447
448 add_secret(
449 &mut vault,
450 &mut murk,
451 "KEY",
452 "shared_val",
453 None,
454 false,
455 &[],
456 &identity,
457 );
458 assert_eq!(murk.values["KEY"], "shared_val");
459
460 add_secret(
461 &mut vault,
462 &mut murk,
463 "KEY",
464 "scoped_val",
465 None,
466 true,
467 &[],
468 &identity,
469 );
470 assert_eq!(murk.values["KEY"], "shared_val");
472 assert_eq!(murk.scoped["KEY"][&pubkey], "scoped_val");
473 }
474
475 #[test]
476 fn add_secret_empty_value() {
477 let (secret, _) = generate_keypair();
478 let identity = make_identity(&secret);
479 let mut vault = empty_vault();
480 let mut murk = empty_murk();
481
482 add_secret(
483 &mut vault,
484 &mut murk,
485 "KEY",
486 "",
487 None,
488 false,
489 &[],
490 &identity,
491 );
492 assert_eq!(murk.values["KEY"], "");
493 }
494
495 #[test]
496 fn import_secrets_basic() {
497 let mut vault = empty_vault();
498 let mut murk = empty_murk();
499
500 let pairs = vec![
501 ("KEY1".into(), "val1".into()),
502 ("KEY2".into(), "val2".into()),
503 ];
504 let imported = import_secrets(&mut vault, &mut murk, &pairs);
505
506 assert_eq!(imported, vec!["KEY1", "KEY2"]);
507 assert_eq!(murk.values["KEY1"], "val1");
508 assert_eq!(murk.values["KEY2"], "val2");
509 assert!(vault.schema.contains_key("KEY1"));
510 assert!(vault.schema.contains_key("KEY2"));
511 }
512
513 #[test]
514 fn import_secrets_existing_schema_preserved() {
515 let mut vault = empty_vault();
516 vault.schema.insert(
517 "KEY1".into(),
518 types::SchemaEntry {
519 description: "existing desc".into(),
520 example: Some("ex".into()),
521 tags: vec!["tag".into()],
522 },
523 );
524 let mut murk = empty_murk();
525
526 let pairs = vec![("KEY1".into(), "new_val".into())];
527 import_secrets(&mut vault, &mut murk, &pairs);
528
529 assert_eq!(murk.values["KEY1"], "new_val");
530 assert_eq!(vault.schema["KEY1"].description, "existing desc");
531 }
532
533 #[test]
534 fn import_secrets_empty() {
535 let mut vault = empty_vault();
536 let mut murk = empty_murk();
537 let imported = import_secrets(&mut vault, &mut murk, &[]);
538 assert!(imported.is_empty());
539 }
540
541 #[test]
542 fn remove_secret_nonexistent() {
543 let mut vault = empty_vault();
544 let mut murk = empty_murk();
545
546 remove_secret(&mut vault, &mut murk, "NONEXISTENT");
548 }
549}