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