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 describe_key(
82 vault: &mut types::Vault,
83 key: &str,
84 description: &str,
85 example: Option<&str>,
86 tags: &[String],
87) {
88 if let Some(entry) = vault.schema.get_mut(key) {
89 entry.description = description.into();
90 entry.example = example.map(Into::into);
91 if !tags.is_empty() {
92 entry.tags = tags.to_vec();
93 }
94 } else {
95 vault.schema.insert(
96 key.into(),
97 types::SchemaEntry {
98 description: description.into(),
99 example: example.map(Into::into),
100 tags: tags.to_vec(),
101 },
102 );
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::testutil::*;
110 use std::collections::HashMap;
111
112 #[test]
113 fn add_secret_shared() {
114 let (secret, _) = generate_keypair();
115 let identity = make_identity(&secret);
116 let mut vault = empty_vault();
117 let mut murk = empty_murk();
118
119 let needs_hint = add_secret(
120 &mut vault,
121 &mut murk,
122 "KEY",
123 "value",
124 None,
125 false,
126 &[],
127 &identity,
128 );
129
130 assert!(needs_hint);
131 assert_eq!(murk.values["KEY"], "value");
132 assert!(vault.schema.contains_key("KEY"));
133 assert!(vault.schema["KEY"].description.is_empty());
134 }
135
136 #[test]
137 fn add_secret_with_description() {
138 let (secret, _) = generate_keypair();
139 let identity = make_identity(&secret);
140 let mut vault = empty_vault();
141 let mut murk = empty_murk();
142
143 let needs_hint = add_secret(
144 &mut vault,
145 &mut murk,
146 "KEY",
147 "value",
148 Some("a desc"),
149 false,
150 &[],
151 &identity,
152 );
153
154 assert!(!needs_hint);
155 assert_eq!(vault.schema["KEY"].description, "a desc");
156 }
157
158 #[test]
159 fn add_secret_scoped() {
160 let (secret, pubkey) = generate_keypair();
161 let identity = make_identity(&secret);
162 let mut vault = empty_vault();
163 let mut murk = empty_murk();
164
165 add_secret(
166 &mut vault,
167 &mut murk,
168 "KEY",
169 "scoped_val",
170 None,
171 true,
172 &[],
173 &identity,
174 );
175
176 assert!(!murk.values.contains_key("KEY"));
177 assert_eq!(murk.scoped["KEY"][&pubkey], "scoped_val");
178 }
179
180 #[test]
181 fn add_secret_merges_tags() {
182 let (secret, _) = generate_keypair();
183 let identity = make_identity(&secret);
184 let mut vault = empty_vault();
185 let mut murk = empty_murk();
186
187 let tags1 = vec!["db".into()];
188 add_secret(
189 &mut vault, &mut murk, "KEY", "v1", None, false, &tags1, &identity,
190 );
191 assert_eq!(vault.schema["KEY"].tags, vec!["db"]);
192
193 let tags2 = vec!["backend".into()];
194 add_secret(
195 &mut vault, &mut murk, "KEY", "v2", None, false, &tags2, &identity,
196 );
197 assert_eq!(vault.schema["KEY"].tags, vec!["db", "backend"]);
198
199 let tags3 = vec!["db".into()];
201 add_secret(
202 &mut vault, &mut murk, "KEY", "v3", None, false, &tags3, &identity,
203 );
204 assert_eq!(vault.schema["KEY"].tags, vec!["db", "backend"]);
205 }
206
207 #[test]
208 fn add_secret_updates_existing_desc() {
209 let (secret, _) = generate_keypair();
210 let identity = make_identity(&secret);
211 let mut vault = empty_vault();
212 let mut murk = empty_murk();
213
214 add_secret(
215 &mut vault,
216 &mut murk,
217 "KEY",
218 "v1",
219 Some("old"),
220 false,
221 &[],
222 &identity,
223 );
224 add_secret(
225 &mut vault,
226 &mut murk,
227 "KEY",
228 "v2",
229 Some("new"),
230 false,
231 &[],
232 &identity,
233 );
234 assert_eq!(vault.schema["KEY"].description, "new");
235 }
236
237 #[test]
238 fn remove_secret_clears_all() {
239 let mut vault = empty_vault();
240 vault.schema.insert(
241 "KEY".into(),
242 types::SchemaEntry {
243 description: "desc".into(),
244 example: None,
245 tags: vec![],
246 },
247 );
248 let mut murk = empty_murk();
249 murk.values.insert("KEY".into(), "val".into());
250 let mut scoped = HashMap::new();
251 scoped.insert("age1pk".into(), "scoped_val".into());
252 murk.scoped.insert("KEY".into(), scoped);
253
254 remove_secret(&mut vault, &mut murk, "KEY");
255
256 assert!(!murk.values.contains_key("KEY"));
257 assert!(!murk.scoped.contains_key("KEY"));
258 assert!(!vault.schema.contains_key("KEY"));
259 }
260
261 #[test]
262 fn get_secret_shared_value() {
263 let mut murk = empty_murk();
264 murk.values.insert("KEY".into(), "shared_val".into());
265
266 assert_eq!(get_secret(&murk, "KEY", "age1pk"), Some("shared_val"));
267 }
268
269 #[test]
270 fn get_secret_scoped_overrides_shared() {
271 let mut murk = empty_murk();
272 murk.values.insert("KEY".into(), "shared_val".into());
273 let mut scoped = HashMap::new();
274 scoped.insert("age1pk".into(), "scoped_val".into());
275 murk.scoped.insert("KEY".into(), scoped);
276
277 assert_eq!(get_secret(&murk, "KEY", "age1pk"), Some("scoped_val"));
278 }
279
280 #[test]
281 fn get_secret_missing_returns_none() {
282 let murk = empty_murk();
283 assert_eq!(get_secret(&murk, "NONEXISTENT", "age1pk"), None);
284 }
285
286 #[test]
287 fn list_keys_no_filter() {
288 let mut vault = empty_vault();
289 vault.schema.insert(
290 "A".into(),
291 types::SchemaEntry {
292 description: String::new(),
293 example: None,
294 tags: vec![],
295 },
296 );
297 vault.schema.insert(
298 "B".into(),
299 types::SchemaEntry {
300 description: String::new(),
301 example: None,
302 tags: vec![],
303 },
304 );
305
306 let keys = list_keys(&vault, &[]);
307 assert_eq!(keys, vec!["A", "B"]);
308 }
309
310 #[test]
311 fn list_keys_with_tag_filter() {
312 let mut vault = empty_vault();
313 vault.schema.insert(
314 "A".into(),
315 types::SchemaEntry {
316 description: String::new(),
317 example: None,
318 tags: vec!["db".into()],
319 },
320 );
321 vault.schema.insert(
322 "B".into(),
323 types::SchemaEntry {
324 description: String::new(),
325 example: None,
326 tags: vec!["api".into()],
327 },
328 );
329 vault.schema.insert(
330 "C".into(),
331 types::SchemaEntry {
332 description: String::new(),
333 example: None,
334 tags: vec![],
335 },
336 );
337
338 let keys = list_keys(&vault, &["db".into()]);
339 assert_eq!(keys, vec!["A"]);
340 }
341
342 #[test]
343 fn list_keys_no_matches() {
344 let mut vault = empty_vault();
345 vault.schema.insert(
346 "A".into(),
347 types::SchemaEntry {
348 description: String::new(),
349 example: None,
350 tags: vec!["db".into()],
351 },
352 );
353
354 let keys = list_keys(&vault, &["nonexistent".into()]);
355 assert!(keys.is_empty());
356 }
357
358 #[test]
359 fn describe_key_creates_new() {
360 let mut vault = empty_vault();
361 describe_key(
362 &mut vault,
363 "KEY",
364 "a description",
365 Some("example"),
366 &["tag".into()],
367 );
368
369 assert_eq!(vault.schema["KEY"].description, "a description");
370 assert_eq!(vault.schema["KEY"].example.as_deref(), Some("example"));
371 assert_eq!(vault.schema["KEY"].tags, vec!["tag"]);
372 }
373
374 #[test]
375 fn describe_key_updates_existing() {
376 let mut vault = empty_vault();
377 vault.schema.insert(
378 "KEY".into(),
379 types::SchemaEntry {
380 description: "old".into(),
381 example: Some("old_ex".into()),
382 tags: vec!["old_tag".into()],
383 },
384 );
385
386 describe_key(&mut vault, "KEY", "new", None, &["new_tag".into()]);
387
388 assert_eq!(vault.schema["KEY"].description, "new");
389 assert_eq!(vault.schema["KEY"].example, None);
390 assert_eq!(vault.schema["KEY"].tags, vec!["new_tag"]);
391 }
392
393 #[test]
394 fn describe_key_preserves_tags_if_empty() {
395 let mut vault = empty_vault();
396 vault.schema.insert(
397 "KEY".into(),
398 types::SchemaEntry {
399 description: "old".into(),
400 example: None,
401 tags: vec!["keep".into()],
402 },
403 );
404
405 describe_key(&mut vault, "KEY", "new desc", None, &[]);
406
407 assert_eq!(vault.schema["KEY"].tags, vec!["keep"]);
408 }
409
410 #[test]
413 fn add_secret_overwrite_shared_with_scoped() {
414 let (secret, pubkey) = generate_keypair();
415 let identity = make_identity(&secret);
416 let mut vault = empty_vault();
417 let mut murk = empty_murk();
418
419 add_secret(
420 &mut vault,
421 &mut murk,
422 "KEY",
423 "shared_val",
424 None,
425 false,
426 &[],
427 &identity,
428 );
429 assert_eq!(murk.values["KEY"], "shared_val");
430
431 add_secret(
432 &mut vault,
433 &mut murk,
434 "KEY",
435 "scoped_val",
436 None,
437 true,
438 &[],
439 &identity,
440 );
441 assert_eq!(murk.values["KEY"], "shared_val");
443 assert_eq!(murk.scoped["KEY"][&pubkey], "scoped_val");
444 }
445
446 #[test]
447 fn add_secret_empty_value() {
448 let (secret, _) = generate_keypair();
449 let identity = make_identity(&secret);
450 let mut vault = empty_vault();
451 let mut murk = empty_murk();
452
453 add_secret(
454 &mut vault,
455 &mut murk,
456 "KEY",
457 "",
458 None,
459 false,
460 &[],
461 &identity,
462 );
463 assert_eq!(murk.values["KEY"], "");
464 }
465
466 #[test]
467 fn remove_secret_nonexistent() {
468 let mut vault = empty_vault();
469 let mut murk = empty_murk();
470
471 remove_secret(&mut vault, &mut murk, "NONEXISTENT");
473 }
474}