sheesy_vault/
base.rs

1use crate::error::{IOMode, VaultError};
2use crate::spec::WriteMode;
3use crate::util::{strip_ext, write_at, FingerprintUserId, ResetCWD};
4use failure::{err_msg, Error, ResultExt};
5use glob::glob;
6use gpgme;
7use serde_yaml;
8use std::collections::HashSet;
9use std::fs::create_dir_all;
10use std::fs::File;
11use std::io;
12use std::io::{stdin, BufRead, BufReader, Read, Write};
13use std::iter::once;
14use std::path::{Path, PathBuf};
15use std::str::FromStr;
16
17pub const GPG_GLOB: &str = "**/*.gpg";
18pub fn recipients_default() -> PathBuf {
19    PathBuf::from(".gpg-id")
20}
21
22pub fn secrets_default() -> PathBuf {
23    PathBuf::from(".")
24}
25
26#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash)]
27pub enum VaultKind {
28    Leader,
29    Partition,
30}
31
32impl Default for VaultKind {
33    fn default() -> Self {
34        VaultKind::Leader
35    }
36}
37
38#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash, Ord, PartialOrd)]
39#[serde(rename_all = "kebab-case")]
40pub enum TrustModel {
41    GpgWebOfTrust,
42    Always,
43}
44
45impl Default for TrustModel {
46    fn default() -> Self {
47        TrustModel::GpgWebOfTrust
48    }
49}
50
51impl FromStr for TrustModel {
52    type Err = String;
53
54    fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
55        Ok(match s {
56            "web-of-trust" => TrustModel::GpgWebOfTrust,
57            "always" => TrustModel::Always,
58            _ => return Err(format!("Unknown trust model: '{}'", s)),
59        })
60    }
61}
62
63#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash)]
64pub struct Vault {
65    pub name: Option<String>,
66    #[serde(skip)]
67    pub kind: VaultKind,
68    #[serde(skip)]
69    pub index: usize,
70    #[serde(skip)]
71    pub partitions: Vec<Vault>,
72    #[serde(skip)]
73    pub resolved_at: PathBuf,
74    #[serde(skip)]
75    pub vault_path: Option<PathBuf>,
76    #[serde(default)]
77    pub auto_import: Option<bool>,
78    #[serde(default)]
79    pub trust_model: Option<TrustModel>,
80    #[serde(default = "secrets_default")]
81    pub secrets: PathBuf,
82    pub gpg_keys: Option<PathBuf>,
83    #[serde(default = "recipients_default")]
84    pub recipients: PathBuf,
85}
86
87impl Default for Vault {
88    fn default() -> Self {
89        Vault {
90            kind: VaultKind::default(),
91            index: 0,
92            partitions: Default::default(),
93            trust_model: Default::default(),
94            auto_import: Some(true),
95            vault_path: None,
96            name: None,
97            secrets: secrets_default(),
98            resolved_at: secrets_default(),
99            gpg_keys: None,
100            recipients: recipients_default(),
101        }
102    }
103}
104
105impl Vault {
106    pub fn from_file(path: &Path) -> Result<Vec<Vault>, Error> {
107        let path_is_stdin = path == Path::new("-");
108        let reader: Box<dyn Read> = if path_is_stdin {
109            Box::new(stdin())
110        } else {
111            if !path.exists() {
112                if let Some(recipients_path) = path.parent().map(|p| p.join(recipients_default())) {
113                    if recipients_path.is_file() {
114                        let mut vault = Vault {
115                            name: None,
116                            kind: VaultKind::Leader,
117                            index: 0,
118                            partitions: Vec::new(),
119                            resolved_at: path.to_owned(),
120                            vault_path: None,
121                            secrets: PathBuf::from("."),
122                            gpg_keys: None,
123                            recipients: recipients_default(),
124                            auto_import: Some(false),
125                            trust_model: Some(TrustModel::GpgWebOfTrust),
126                        };
127                        vault = vault.set_resolved_at(
128                            &recipients_path
129                                .parent()
130                                .expect("parent dir for recipient path which was joined before")
131                                .join("sy-vault.yml"),
132                        )?;
133                        return Ok(vec![vault]);
134                    }
135                }
136            }
137            Box::new(File::open(path).map_err(|cause| VaultError::from_io_err(cause, path, &IOMode::Read))?)
138        };
139        let vaults: Vec<_> = split_documents(reader)?
140            .iter()
141            .enumerate()
142            .map(|(index, s)| {
143                serde_yaml::from_str(s)
144                    .map_err(|cause| VaultError::Deserialization {
145                        cause,
146                        path: path.to_owned(),
147                    })
148                    .map_err(Into::into)
149                    .and_then(|v: Vault| match v.set_resolved_at(path) {
150                        Ok(mut v) => {
151                            v.index = index;
152                            Ok(v)
153                        }
154                        err => err,
155                    })
156            })
157            .collect::<Result<_, _>>()?;
158        if !vaults.is_empty() {
159            vaults[0].validate()?;
160        }
161        Ok(vaults)
162    }
163
164    pub fn set_resolved_at(mut self, vault_file: &Path) -> Result<Self, Error> {
165        self.resolved_at = normalize(
166            vault_file
167                .parent()
168                .ok_or_else(|| format_err!("The vault file path '{}' is invalid.", vault_file.display()))?,
169        );
170        self.vault_path = Some(vault_file.to_owned());
171        Ok(self)
172    }
173
174    pub fn validate(&self) -> Result<(), Error> {
175        if self.partitions.is_empty() {
176            return Ok(());
177        }
178        {
179            let all_secrets_paths: Vec<_> = self
180                .partitions
181                .iter()
182                .map(|v| v.secrets_path())
183                .chain(once(self.secrets_path()))
184                .map(|mut p| {
185                    if p.is_relative() {
186                        p = Path::new(".").join(p);
187                    }
188                    p
189                })
190                .collect();
191            for (sp, dp) in iproduct!(
192                all_secrets_paths.iter().enumerate(),
193                all_secrets_paths.iter().enumerate()
194            )
195            .filter_map(|((si, s), (di, d))| if si == di { None } else { Some((s, d)) })
196            {
197                if sp.starts_with(&dp) {
198                    bail!(
199                        "Partition at '{}' is contained in another partitions resources directory at '{}'",
200                        sp.display(),
201                        dp.display()
202                    );
203                }
204            }
205        }
206        {
207            let mut seen: HashSet<_> = Default::default();
208            for path in self
209                .partitions
210                .iter()
211                .map(|v| v.recipients_path())
212                .chain(once(self.recipients_path()))
213            {
214                if seen.contains(&path) {
215                    bail!(
216                        "Recipients path '{}' is already used, but must be unique across all partitions",
217                        path.display()
218                    );
219                }
220                seen.insert(path);
221            }
222        }
223
224        Ok(())
225    }
226
227    pub fn to_file(&self, path: &Path, mode: WriteMode) -> Result<(), VaultError> {
228        if let WriteMode::RefuseOverwrite = mode {
229            if path.exists() {
230                return Err(VaultError::ConfigurationFileExists(path.to_owned()));
231            }
232        }
233        self.validate().map_err(VaultError::Validation)?;
234
235        match self.kind {
236            VaultKind::Partition => return Err(VaultError::PartitionUnsupported),
237            VaultKind::Leader => {
238                let mut file = write_at(path).map_err(|cause| VaultError::from_io_err(cause, path, &IOMode::Write))?;
239                let all_vaults = self.all_in_order();
240                for vault in &all_vaults {
241                    serde_yaml::to_writer(&mut file, vault)
242                        .map_err(|cause| VaultError::Serialization {
243                            cause,
244                            path: path.to_owned(),
245                        })
246                        .and_then(|_| {
247                            writeln!(file).map_err(|cause| VaultError::from_io_err(cause, path, &IOMode::Write))
248                        })?;
249                }
250            }
251        }
252        Ok(())
253    }
254
255    pub fn absolute_path(&self, path: &Path) -> PathBuf {
256        normalize(&self.resolved_at.join(path))
257    }
258
259    pub fn secrets_path(&self) -> PathBuf {
260        normalize(&self.absolute_path(&self.secrets))
261    }
262    pub fn url(&self) -> String {
263        format!(
264            "syv://{}{}",
265            self.name
266                .as_ref()
267                .map(|s| format!("{}@", s))
268                .unwrap_or_else(String::new),
269            self.secrets_path().display()
270        )
271    }
272
273    pub fn print_resources(&self, w: &mut dyn Write) -> Result<(), Error> {
274        let has_multiple_partitions = !self.partitions.is_empty();
275        for partition in once(self).chain(self.partitions.iter()) {
276            writeln!(w, "{}", partition.url())?;
277            let dir = partition.secrets_path();
278            if !dir.is_dir() {
279                continue;
280            }
281            let _change_cwd = ResetCWD::from_path(&dir)?;
282            for entry in glob(GPG_GLOB).expect("valid pattern").filter_map(Result::ok) {
283                if has_multiple_partitions {
284                    writeln!(w, "{}", dir.join(strip_ext(&entry)).display())?;
285                } else {
286                    writeln!(w, "{}", strip_ext(&entry).display())?;
287                }
288            }
289        }
290        Ok(())
291    }
292
293    pub fn write_recipients_list(&self, recipients: &mut Vec<String>) -> Result<PathBuf, Error> {
294        recipients.sort();
295        recipients.dedup();
296
297        let recipients_path = self.recipients_path();
298        if let Some(recipients_parent_dir) = recipients_path.parent() {
299            if !recipients_parent_dir.is_dir() {
300                create_dir_all(recipients_parent_dir).context(format!(
301                    "Failed to create directory leading to recipients file at '{}'",
302                    recipients_path.display()
303                ))?;
304            }
305        }
306        let mut writer = write_at(&recipients_path).context(format!(
307            "Failed to open recipients at '{}' file for writing",
308            recipients_path.display()
309        ))?;
310        for recipient in recipients {
311            writeln!(&mut writer, "{}", recipient).context(format!(
312                "Failed to write recipient '{}' to file at '{}'",
313                recipient,
314                recipients_path.display()
315            ))?
316        }
317        Ok(recipients_path)
318    }
319
320    pub fn recipients_path(&self) -> PathBuf {
321        self.absolute_path(&self.recipients)
322    }
323
324    pub fn recipients_list(&self) -> Result<Vec<String>, Error> {
325        let recipients_file_path = self.recipients_path();
326        let rfile = File::open(&recipients_file_path).map(BufReader::new).context(format!(
327            "Could not open recipients file at '{}' for reading",
328            recipients_file_path.display()
329        ))?;
330        Ok(rfile.lines().collect::<Result<_, _>>().context(format!(
331            "Could not read all recipients from file at '{}'",
332            recipients_file_path.display()
333        ))?)
334    }
335
336    pub fn keys_by_ids(
337        &self,
338        ctx: &mut gpgme::Context,
339        ids: &[String],
340        type_of_ids_for_errors: &str,
341        gpg_keys_dir: Option<&Path>,
342        output: &mut dyn io::Write,
343    ) -> Result<Vec<gpgme::Key>, Error> {
344        ctx.find_keys(ids)
345            .context(format!("Could not iterate keys for given {}s", type_of_ids_for_errors))?;
346        let (keys, missing): (Vec<gpgme::Key>, Vec<String>) = ids.iter().map(|id| (ctx.get_key(id), id)).fold(
347            (Vec::new(), Vec::new()),
348            |(mut keys, mut missing), (r, id)| {
349                match r {
350                    Ok(k) => keys.push(k),
351                    Err(_) => missing.push(id.to_owned()),
352                };
353                (keys, missing)
354            },
355        );
356        if keys.len() == ids.len() {
357            assert_eq!(missing.len(), 0);
358            return Ok(keys);
359        }
360        let diff: isize = ids.len() as isize - keys.len() as isize;
361        let mut msg = vec![if diff > 0 {
362            if let Some(dir) = gpg_keys_dir {
363                self.import_keys(ctx, dir, &missing, output)
364                    .context("Could not auto-import all required keys")?;
365                return self.keys_by_ids(ctx, ids, type_of_ids_for_errors, None, output);
366            }
367
368            let mut msg = format!(
369                "Didn't find the key for {} {}(s) in the gpg database.{}",
370                diff,
371                type_of_ids_for_errors,
372                match self.gpg_keys.as_ref() {
373                    Some(dir) => format!(
374                        " This might mean it wasn't imported yet from the '{}' directory.",
375                        self.absolute_path(dir).display()
376                    ),
377                    None => String::new(),
378                }
379            );
380            msg.push_str(&format!(
381                "\nThe following {}(s) could not be found in the gpg key database:",
382                type_of_ids_for_errors
383            ));
384            for fpr in missing {
385                msg.push_str("\n");
386                let key_path_info = match self.gpg_keys.as_ref() {
387                    Some(dir) => {
388                        let key_path = self.absolute_path(dir).join(&fpr);
389                        format!(
390                            "{}'{}'",
391                            if key_path.is_file() {
392                                "Import key-file using 'gpg --import "
393                            } else {
394                                "Key-file does not exist at "
395                            },
396                            key_path.display()
397                        )
398                    }
399                    None => "No GPG keys directory".into(),
400                };
401                msg.push_str(&format!("{} ({})", &fpr, key_path_info));
402            }
403            msg
404        } else {
405            format!(
406                "Found {} additional keys to encrypt for, which may indicate an unusual \
407                 {}s specification in the recipients file at '{}'",
408                diff,
409                type_of_ids_for_errors,
410                self.recipients_path().display()
411            )
412        }];
413        if !keys.is_empty() {
414            msg.push(format!("All {}s found in gpg database:", type_of_ids_for_errors));
415            msg.extend(keys.iter().map(|k| format!("{}", FingerprintUserId(k))));
416        }
417        Err(err_msg(msg.join("\n")))
418    }
419
420    pub fn recipient_keys(
421        &self,
422        ctx: &mut gpgme::Context,
423        gpg_keys_dir: Option<&Path>,
424        output: &mut dyn io::Write,
425    ) -> Result<Vec<gpgme::Key>, Error> {
426        let recipients_fprs = self.recipients_list()?;
427        if recipients_fprs.is_empty() {
428            return Err(format_err!(
429                "No recipients found in recipients file at '{}'.",
430                self.recipients.display()
431            ));
432        }
433        self.keys_by_ids(ctx, &recipients_fprs, "recipient", gpg_keys_dir, output)
434    }
435
436    fn vault_path_for_display(&self) -> String {
437        self.vault_path
438            .as_ref()
439            .map(|p| p.to_string_lossy().into_owned())
440            .unwrap_or_else(|| String::from("<unknown>"))
441    }
442
443    /// TODO: change this to be similar to `find_trust_model()`
444    /// as it's OK to let partitions override the master vault settings
445    pub fn find_gpg_keys_dir(&self) -> Result<PathBuf, Error> {
446        once(self.gpg_keys_dir())
447            .chain(self.partitions.iter().map(|p| p.gpg_keys_dir()))
448            .filter_map(Result::ok)
449            .next()
450            .ok_or_else(|| {
451                format_err!(
452                    "The vault at '{}' does not have a gpg_keys directory configured.",
453                    self.vault_path_for_display()
454                )
455            })
456    }
457
458    pub fn gpg_keys_dir_for_auto_import(&self, partition: &Vault) -> Option<PathBuf> {
459        let auto_import = partition.auto_import.or(self.auto_import).unwrap_or(false);
460        if auto_import {
461            self.find_gpg_keys_dir().ok()
462        } else {
463            None
464        }
465    }
466
467    pub fn gpg_keys_dir(&self) -> Result<PathBuf, Error> {
468        self.gpg_keys.as_ref().map(|p| self.absolute_path(p)).ok_or_else(|| {
469            format_err!(
470                "The vault at '{}' does not have a gpg_keys directory configured.",
471                self.vault_path_for_display()
472            )
473        })
474    }
475}
476
477pub trait VaultExt {
478    fn select(self, vault_id: &str) -> Result<Vault, Error>;
479}
480
481impl VaultExt for Vec<Vault> {
482    fn select(mut self, selector: &str) -> Result<Vault, Error> {
483        let leader_index = Vault::partition_index(selector, self.iter(), None)?;
484        for (_, vault) in self.iter_mut().enumerate().filter(|&(vid, _)| vid != leader_index) {
485            vault.kind = VaultKind::Partition;
486        }
487
488        let mut vault = self[leader_index].clone();
489        vault.kind = VaultKind::Leader;
490
491        self.retain(|v| match v.kind {
492            VaultKind::Partition => true,
493            VaultKind::Leader => false,
494        });
495
496        vault.partitions = self;
497        Ok(vault)
498    }
499}
500
501fn normalize(p: &Path) -> PathBuf {
502    use std::path::Component;
503    let mut p = p.components().fold(PathBuf::new(), |mut p, c| {
504        match c {
505            Component::CurDir => {}
506            _ => p.push(c.as_os_str()),
507        }
508        p
509    });
510    if p.components().count() == 0 {
511        p = PathBuf::from(".");
512    }
513    p
514}
515
516fn split_documents<R: Read>(mut r: R) -> Result<Vec<String>, Error> {
517    use yaml_rust::{YamlEmitter, YamlLoader};
518
519    let mut buf = String::new();
520    r.read_to_string(&mut buf)?;
521
522    let docs = YamlLoader::load_from_str(&buf).context("YAML deserialization failed")?;
523    Ok(docs
524        .iter()
525        .map(|d| {
526            let mut out_str = String::new();
527            {
528                let mut emitter = YamlEmitter::new(&mut out_str);
529                emitter.dump(d).expect("dumping a valid yaml into a string to work");
530            }
531            out_str
532        })
533        .collect())
534}
535
536#[cfg(test)]
537mod tests_vault_ext {
538    use super::*;
539
540    #[test]
541    fn it_selects_by_name() {
542        let vault = Vault {
543            name: Some("foo".into()),
544            ..Default::default()
545        };
546        let v = vec![vault.clone()];
547        assert_eq!(v.select("foo").unwrap(), vault)
548    }
549
550    #[test]
551    fn it_selects_by_secrets_dir() {
552        let vault = Vault {
553            secrets: PathBuf::from("../dir"),
554            ..Default::default()
555        };
556        let v = vec![vault.clone()];
557        assert_eq!(v.select("../dir").unwrap(), vault)
558    }
559
560    #[test]
561    fn it_selects_by_index() {
562        let v = vec![Vault::default()];
563        assert!(v.select("0").is_ok())
564    }
565
566    #[test]
567    fn it_errors_if_name_is_unknown() {
568        let v = Vec::<Vault>::new();
569        assert_eq!(
570            format!("{}", v.select("foo").unwrap_err()),
571            "No partition matched the given selector 'foo'"
572        )
573    }
574    #[test]
575    fn it_errors_if_index_is_out_of_bounds() {
576        let v = Vec::<Vault>::new();
577        assert_eq!(
578            format!("{}", v.select("0").unwrap_err()),
579            "Could not find partition with index 0"
580        )
581    }
582}
583
584#[cfg(test)]
585mod tests_utils {
586    use super::*;
587
588    #[test]
589    fn it_will_always_remove_current_dirs_including_the_first_one() {
590        assert_eq!(format!("{}", normalize(Path::new("./././a")).display()), "a")
591    }
592    #[test]
593    fn it_does_not_alter_parent_dirs() {
594        assert_eq!(format!("{}", normalize(Path::new("./../.././a")).display()), "../../a")
595    }
596}
597
598#[cfg(test)]
599mod tests_vault {
600    use super::*;
601
602    #[test]
603    fn it_print_the_name_in_the_url_if_there_is_none() {
604        let mut v = Vault::default();
605        v.name = Some("name".into());
606        assert_eq!(v.url(), "syv://name@.")
607    }
608
609    #[test]
610    fn it_does_not_print_the_name_in_the_url_if_there_is_none() {
611        let v = Vault::default();
612        assert_eq!(v.url(), "syv://.")
613    }
614}