libside 0.3.0

a library for building configuration management tools
Documentation
use serde::{de::DeserializeOwned, Serialize};
use std::{
    collections::{HashMap, HashSet},
    path::Path,
};

use crate::system::System;

pub mod keys;
pub mod password;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SecretId {
    package: String,
    name: String,
}

impl SecretId {
    pub fn new(package: String, name: String) -> SecretId {
        SecretId { package, name }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct InternalSecretId {
    id: SecretId,
    kind: String,
}

impl std::fmt::Display for InternalSecretId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}:{}", self.id.package, self.id.name, self.kind)
    }
}

pub struct SecretData(Vec<u8>);

pub struct Secrets {
    secrets: HashMap<InternalSecretId, SecretData>,
    new_secrets: HashSet<InternalSecretId>,
}

pub trait Secret: Serialize + DeserializeOwned + Clone {
    const KIND: &'static str;

    fn generate_new() -> Self;
}

impl Secrets {
    pub fn load<S: System>(path: &Path, system: &mut S) -> Result<Secrets, S::Error> {
        let mut result = Secrets {
            secrets: HashMap::new(),
            new_secrets: HashSet::new(),
        };

        for package_dir in system.read_dir(path)? {
            let package_dir = path.join(package_dir);

            for kind_dir in system.read_dir(&package_dir)? {
                let kind_dir = package_dir.join(&kind_dir);

                for entry_dir in system.read_dir(&kind_dir)? {
                    let entry_path = kind_dir.join(&entry_dir);

                    let package = package_dir
                        .file_name()
                        .unwrap()
                        .to_str()
                        .unwrap()
                        .to_string();
                    let kind = kind_dir.file_name().unwrap().to_str().unwrap().to_string();
                    let name = entry_path
                        .file_name()
                        .unwrap()
                        .to_str()
                        .unwrap()
                        .to_string();

                    let internal_id = InternalSecretId {
                        id: SecretId::new(package, name),
                        kind,
                    };

                    println!("  secret loaded: {}", internal_id);

                    result
                        .secrets
                        .insert(internal_id, SecretData(system.file_contents(&entry_path)?));
                }
            }
        }

        Ok(result)
    }

    pub fn save<S: System>(&mut self, path: &Path, system: &mut S) -> Result<(), S::Error> {
        for item in self.new_secrets.iter() {
            let dir = path.join(&item.id.package).join(&item.kind);

            system.make_dir_all(&dir)?;
            system.chmod(&dir, 0o700)?;

            let file = dir.join(&item.id.name);
            system.put_file_contents(&file, &self.secrets.get(item).unwrap().0)?;
            system.chmod(&file, 0o600)?;
        }

        self.new_secrets.clear();

        Ok(())
    }

    pub fn get_or_create<S: Secret + std::fmt::Debug>(
        &mut self,
        id: SecretId,
    ) -> Result<S, serde_json::Error> {
        let internal_id = InternalSecretId {
            id,
            kind: S::KIND.to_owned(),
        };

        if let Some(s) = self.secrets.get(&internal_id) {
            Ok(serde_json::from_slice(&s.0)?)
        } else {
            let new_secret = S::generate_new();
            self.secrets.insert(
                internal_id.clone(),
                SecretData(serde_json::to_vec(&new_secret.clone())?),
            );
            self.new_secrets.insert(internal_id.clone());

            println!("  secret generated: {} = {:?}", internal_id, new_secret);

            Ok(new_secret)
        }
    }
}