1use std::collections::{BTreeMap, HashMap};
4
5use crate::types;
6
7pub fn resolve_secrets(
10 vault: &types::Vault,
11 murk: &types::Murk,
12 pubkey: &str,
13 tags: &[String],
14) -> BTreeMap<String, String> {
15 let mut values = murk.values.clone();
16
17 for (key, scoped_map) in &murk.scoped {
19 if let Some(value) = scoped_map.get(pubkey) {
20 values.insert(key.clone(), value.clone());
21 }
22 }
23
24 let allowed_keys: Option<std::collections::HashSet<&str>> = if tags.is_empty() {
26 None
27 } else {
28 Some(
29 vault
30 .schema
31 .iter()
32 .filter(|(_, e)| e.tags.iter().any(|t| tags.contains(t)))
33 .map(|(k, _)| k.as_str())
34 .collect(),
35 )
36 };
37
38 let mut result = BTreeMap::new();
39 for (k, v) in &values {
40 if let Some(ref allowed) = allowed_keys {
41 if !allowed.contains(k.as_str()) {
42 continue;
43 }
44 }
45 result.insert(k.clone(), v.clone());
46 }
47 result
48}
49
50pub fn export_secrets(
53 vault: &types::Vault,
54 murk: &types::Murk,
55 pubkey: &str,
56 tags: &[String],
57) -> BTreeMap<String, String> {
58 resolve_secrets(vault, murk, pubkey, tags)
59 .into_iter()
60 .map(|(k, v)| (k, v.replace('\'', "'\\''")))
61 .collect()
62}
63
64#[derive(Debug, PartialEq, Eq)]
66pub enum DiffKind {
67 Added,
68 Removed,
69 Changed,
70}
71
72#[derive(Debug)]
74pub struct DiffEntry {
75 pub key: String,
76 pub kind: DiffKind,
77 pub old_value: Option<String>,
78 pub new_value: Option<String>,
79}
80
81pub fn diff_secrets(
83 old: &HashMap<String, String>,
84 new: &HashMap<String, String>,
85) -> Vec<DiffEntry> {
86 let mut all_keys: Vec<&str> = old
87 .keys()
88 .chain(new.keys())
89 .map(String::as_str)
90 .collect::<std::collections::HashSet<_>>()
91 .into_iter()
92 .collect();
93 all_keys.sort_unstable();
94
95 let mut entries = Vec::new();
96 for key in all_keys {
97 match (old.get(key), new.get(key)) {
98 (None, Some(v)) => entries.push(DiffEntry {
99 key: key.into(),
100 kind: DiffKind::Added,
101 old_value: None,
102 new_value: Some(v.clone()),
103 }),
104 (Some(v), None) => entries.push(DiffEntry {
105 key: key.into(),
106 kind: DiffKind::Removed,
107 old_value: Some(v.clone()),
108 new_value: None,
109 }),
110 (Some(old_v), Some(new_v)) if old_v != new_v => entries.push(DiffEntry {
111 key: key.into(),
112 kind: DiffKind::Changed,
113 old_value: Some(old_v.clone()),
114 new_value: Some(new_v.clone()),
115 }),
116 _ => {}
117 }
118 }
119 entries
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::testutil::*;
126 use crate::types;
127
128 #[test]
129 fn export_secrets_basic() {
130 let mut vault = empty_vault();
131 vault.schema.insert(
132 "FOO".into(),
133 types::SchemaEntry {
134 description: String::new(),
135 example: None,
136 tags: vec![],
137 },
138 );
139
140 let mut murk = empty_murk();
141 murk.values.insert("FOO".into(), "bar".into());
142
143 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
144 assert_eq!(exports.len(), 1);
145 assert_eq!(exports["FOO"], "bar");
146 }
147
148 #[test]
149 fn export_secrets_scoped_override() {
150 let mut vault = empty_vault();
151 vault.schema.insert(
152 "KEY".into(),
153 types::SchemaEntry {
154 description: String::new(),
155 example: None,
156 tags: vec![],
157 },
158 );
159
160 let mut murk = empty_murk();
161 murk.values.insert("KEY".into(), "shared".into());
162 let mut scoped = HashMap::new();
163 scoped.insert("age1pk".into(), "override".into());
164 murk.scoped.insert("KEY".into(), scoped);
165
166 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
167 assert_eq!(exports["KEY"], "override");
168 }
169
170 #[test]
171 fn export_secrets_tag_filter() {
172 let mut vault = empty_vault();
173 vault.schema.insert(
174 "A".into(),
175 types::SchemaEntry {
176 description: String::new(),
177 example: None,
178 tags: vec!["db".into()],
179 },
180 );
181 vault.schema.insert(
182 "B".into(),
183 types::SchemaEntry {
184 description: String::new(),
185 example: None,
186 tags: vec!["api".into()],
187 },
188 );
189
190 let mut murk = empty_murk();
191 murk.values.insert("A".into(), "val_a".into());
192 murk.values.insert("B".into(), "val_b".into());
193
194 let exports = export_secrets(&vault, &murk, "age1pk", &["db".into()]);
195 assert_eq!(exports.len(), 1);
196 assert_eq!(exports["A"], "val_a");
197 }
198
199 #[test]
200 fn export_secrets_shell_escaping() {
201 let mut vault = empty_vault();
202 vault.schema.insert(
203 "KEY".into(),
204 types::SchemaEntry {
205 description: String::new(),
206 example: None,
207 tags: vec![],
208 },
209 );
210
211 let mut murk = empty_murk();
212 murk.values.insert("KEY".into(), "it's a test".into());
213
214 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
215 assert_eq!(exports["KEY"], "it'\\''s a test");
216 }
217
218 #[test]
219 fn diff_secrets_no_changes() {
220 let old = HashMap::from([("K".into(), "V".into())]);
221 let new = old.clone();
222 assert!(diff_secrets(&old, &new).is_empty());
223 }
224
225 #[test]
226 fn diff_secrets_added() {
227 let old = HashMap::new();
228 let new = HashMap::from([("KEY".into(), "val".into())]);
229 let entries = diff_secrets(&old, &new);
230 assert_eq!(entries.len(), 1);
231 assert_eq!(entries[0].kind, DiffKind::Added);
232 assert_eq!(entries[0].key, "KEY");
233 assert_eq!(entries[0].new_value.as_deref(), Some("val"));
234 }
235
236 #[test]
237 fn diff_secrets_removed() {
238 let old = HashMap::from([("KEY".into(), "val".into())]);
239 let new = HashMap::new();
240 let entries = diff_secrets(&old, &new);
241 assert_eq!(entries.len(), 1);
242 assert_eq!(entries[0].kind, DiffKind::Removed);
243 assert_eq!(entries[0].old_value.as_deref(), Some("val"));
244 }
245
246 #[test]
247 fn diff_secrets_changed() {
248 let old = HashMap::from([("KEY".into(), "old_val".into())]);
249 let new = HashMap::from([("KEY".into(), "new_val".into())]);
250 let entries = diff_secrets(&old, &new);
251 assert_eq!(entries.len(), 1);
252 assert_eq!(entries[0].kind, DiffKind::Changed);
253 assert_eq!(entries[0].old_value.as_deref(), Some("old_val"));
254 assert_eq!(entries[0].new_value.as_deref(), Some("new_val"));
255 }
256
257 #[test]
258 fn diff_secrets_mixed() {
259 let old = HashMap::from([
260 ("KEEP".into(), "same".into()),
261 ("REMOVE".into(), "gone".into()),
262 ("CHANGE".into(), "old".into()),
263 ]);
264 let new = HashMap::from([
265 ("KEEP".into(), "same".into()),
266 ("ADD".into(), "new".into()),
267 ("CHANGE".into(), "new".into()),
268 ]);
269 let entries = diff_secrets(&old, &new);
270 assert_eq!(entries.len(), 3);
271
272 let kinds: Vec<&DiffKind> = entries.iter().map(|e| &e.kind).collect();
273 assert!(kinds.contains(&&DiffKind::Added));
274 assert!(kinds.contains(&&DiffKind::Removed));
275 assert!(kinds.contains(&&DiffKind::Changed));
276 }
277
278 #[test]
279 fn diff_secrets_sorted_by_key() {
280 let old = HashMap::new();
281 let new = HashMap::from([
282 ("Z".into(), "z".into()),
283 ("A".into(), "a".into()),
284 ("M".into(), "m".into()),
285 ]);
286 let entries = diff_secrets(&old, &new);
287 let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
288 assert_eq!(keys, vec!["A", "M", "Z"]);
289 }
290
291 #[test]
294 fn resolve_secrets_basic() {
295 let mut vault = empty_vault();
296 vault.schema.insert(
297 "FOO".into(),
298 types::SchemaEntry {
299 description: String::new(),
300 example: None,
301 tags: vec![],
302 },
303 );
304
305 let mut murk = empty_murk();
306 murk.values.insert("FOO".into(), "bar".into());
307
308 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
309 assert_eq!(resolved.len(), 1);
310 assert_eq!(resolved["FOO"], "bar");
311 }
312
313 #[test]
314 fn resolve_secrets_no_escaping() {
315 let mut vault = empty_vault();
316 vault.schema.insert(
317 "KEY".into(),
318 types::SchemaEntry {
319 description: String::new(),
320 example: None,
321 tags: vec![],
322 },
323 );
324
325 let mut murk = empty_murk();
326 murk.values.insert("KEY".into(), "it's a test".into());
327
328 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
329 assert_eq!(resolved["KEY"], "it's a test");
330 }
331
332 #[test]
333 fn resolve_secrets_scoped_override() {
334 let mut vault = empty_vault();
335 vault.schema.insert(
336 "KEY".into(),
337 types::SchemaEntry {
338 description: String::new(),
339 example: None,
340 tags: vec![],
341 },
342 );
343
344 let mut murk = empty_murk();
345 murk.values.insert("KEY".into(), "shared".into());
346 let mut scoped = HashMap::new();
347 scoped.insert("age1pk".into(), "override".into());
348 murk.scoped.insert("KEY".into(), scoped);
349
350 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
351 assert_eq!(resolved["KEY"], "override");
352 }
353
354 #[test]
355 fn resolve_secrets_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 },
364 );
365 vault.schema.insert(
366 "B".into(),
367 types::SchemaEntry {
368 description: String::new(),
369 example: None,
370 tags: vec!["api".into()],
371 },
372 );
373
374 let mut murk = empty_murk();
375 murk.values.insert("A".into(), "val_a".into());
376 murk.values.insert("B".into(), "val_b".into());
377
378 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
379 assert_eq!(resolved.len(), 1);
380 assert_eq!(resolved["A"], "val_a");
381 }
382
383 #[test]
386 fn export_secrets_empty_vault() {
387 let vault = empty_vault();
388 let murk = empty_murk();
389 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
390 assert!(exports.is_empty());
391 }
392
393 #[test]
394 fn diff_secrets_both_empty() {
395 let old = HashMap::new();
396 let new = HashMap::new();
397 assert!(diff_secrets(&old, &new).is_empty());
398 }
399}