rops 0.1.7

SOPS-like library in pure Rust
Documentation
use std::{env::VarError, fmt::Debug, path::PathBuf};

use crate::*;

const ROPS_APPLICATION_NAME: &str = "rops";

pub trait Integration: Sized {
    const NAME: &'static str;
    type KeyId: AppendIntegrationKey<Self>;
    type PrivateKey;
    type Config: IntegrationConfig<Self>;

    fn private_key_env_var_name() -> String {
        format!("ROPS_{}", Self::NAME.to_uppercase())
    }

    fn private_key_file_path_override_env_var_name() -> String {
        format!("ROPS_{}_KEY_FILE", Self::NAME.to_uppercase())
    }

    fn private_keys_from_env() -> IntegrationResult<Vec<Self::PrivateKey>> {
        match std::env::var(Self::private_key_env_var_name()) {
            Ok(found_string) => found_string.split(',').map(Self::parse_private_key).collect(),
            Err(env_var_error) => match env_var_error {
                VarError::NotPresent => Ok(Vec::default()),
                VarError::NotUnicode(os_str) => Err(IntegrationError::EnvVarNotUnicode(os_str)),
            },
        }
    }

    fn private_keys_from_default_key_file() -> IntegrationResult<Vec<Self::PrivateKey>> {
        let integration_key_file = match std::env::var_os(Self::private_key_file_path_override_env_var_name()) {
            Some(os_string) => PathBuf::from(os_string),
            None => directories::BaseDirs::new()
                .ok_or(IntegrationError::NoHomeDir)?
                .config_local_dir()
                .join(ROPS_APPLICATION_NAME)
                .join(format!("{}_keys", Self::NAME)),
        };

        match integration_key_file.exists() {
            true => std::fs::read_to_string(integration_key_file)?
                .lines()
                .map(|line| line.trim())
                .filter(|line| !line.is_empty())
                .map(Self::parse_private_key)
                .collect(),
            false => Ok(Vec::new()),
        }
    }

    fn retrieve_private_keys() -> IntegrationResult<Vec<Self::PrivateKey>> {
        let mut private_key_strings = Vec::<Self::PrivateKey>::new();

        private_key_strings.append(&mut Self::private_keys_from_env()?);
        private_key_strings.append(&mut Self::private_keys_from_default_key_file()?);

        Ok(private_key_strings)
    }

    fn parse_key_id(key_id_str: &str) -> IntegrationResult<Self::KeyId>;

    fn parse_private_key(private_key_str: impl AsRef<str>) -> IntegrationResult<Self::PrivateKey>;

    fn encrypt_data_key(key_id: &Self::KeyId, data_key: &DataKey) -> IntegrationResult<String>;

    fn decrypt_data_key(key_id: &Self::KeyId, encrypted_data_key: &str) -> IntegrationResult<Option<DataKey>>;

    fn select_metadata_units(integration_metadata: &mut IntegrationMetadata) -> &mut IntegrationMetadataUnits<Self>;
}

pub trait IntegrationConfig<I: Integration>: Debug + PartialEq {
    const INCLUDE_DATA_KEY_CREATED_AT: bool;

    fn new(key_id: I::KeyId) -> Self;

    fn key_id(&self) -> &I::KeyId;
}

#[cfg(test)]
use stub_integration::StubIntegration;
#[cfg(test)]
mod stub_integration {
    use super::*;

    pub struct StubIntegration;

    impl Integration for StubIntegration {
        const NAME: &'static str = "stub";
        type KeyId = String;
        type PrivateKey = String;
        type Config = StubIntegrationConfig;

        fn parse_key_id(_key_id_str: &str) -> IntegrationResult<Self::KeyId> {
            unimplemented!()
        }

        fn parse_private_key(private_key_str: impl AsRef<str>) -> IntegrationResult<Self::PrivateKey> {
            Ok(private_key_str.as_ref().to_string())
        }

        fn encrypt_data_key(_key_id: &Self::KeyId, _data_key: &DataKey) -> IntegrationResult<String> {
            unimplemented!()
        }

        fn decrypt_data_key(_key_id: &Self::KeyId, _encrypted_data_key: &str) -> IntegrationResult<Option<DataKey>> {
            unimplemented!()
        }

        fn select_metadata_units(_integration_metadata: &mut IntegrationMetadata) -> &mut IntegrationMetadataUnits<Self> {
            unimplemented!()
        }
    }

    impl AppendIntegrationKey<StubIntegration> for String {
        fn append_to_metadata_builder(self, _integration_metadata_builder: &mut IntegrationMetadataBuilder) {
            unimplemented!()
        }
    }

    #[derive(Debug, PartialEq, Eq, Hash)]
    pub struct StubIntegrationConfig(String);

    impl IntegrationConfig<StubIntegration> for StubIntegrationConfig {
        const INCLUDE_DATA_KEY_CREATED_AT: bool = false;

        fn new(key_id: <StubIntegration as Integration>::KeyId) -> Self {
            Self(key_id)
        }

        fn key_id(&self) -> &<StubIntegration as Integration>::KeyId {
            &self.0
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const STUB_KEYS: &[&str] = &["key1", "key2"];

    #[test]
    fn gets_private_key_env_var_name() {
        assert_eq!("ROPS_STUB", StubIntegration::private_key_env_var_name())
    }

    #[test]
    fn gets_private_key_file_path_override_env_var_name() {
        assert_eq!("ROPS_STUB_KEY_FILE", StubIntegration::private_key_file_path_override_env_var_name())
    }

    #[test]
    fn gets_private_key_from_env() {
        let var_name = StubIntegration::private_key_env_var_name();
        std::env::set_var(&var_name, "key1,key2");

        assert_eq!(STUB_KEYS, StubIntegration::private_keys_from_env().unwrap().as_slice());

        std::env::remove_var(&var_name);
    }

    const STUB_KEY_FILE_CONTENTS: &str = "
        key1
        key2
    ";

    #[test]
    #[serial_test::serial]
    fn gets_private_key_from_default_key_file() {
        let rops_config_dir = directories::BaseDirs::new().unwrap().config_local_dir().join(ROPS_APPLICATION_NAME);

        let created_config_dir = match rops_config_dir.is_dir() {
            true => false,
            false => {
                std::fs::create_dir_all(&rops_config_dir).unwrap();
                true
            }
        };

        let stub_keys_path = rops_config_dir.join(format!("{}_keys", StubIntegration::NAME));

        std::fs::write(&stub_keys_path, STUB_KEY_FILE_CONTENTS).unwrap();

        assert_eq!(STUB_KEYS, StubIntegration::private_keys_from_default_key_file().unwrap().as_slice());

        match created_config_dir {
            true => std::fs::remove_dir_all(rops_config_dir).unwrap(),
            false => std::fs::remove_file(stub_keys_path).unwrap(),
        }
    }

    #[test]
    #[serial_test::serial]
    fn gets_private_key_from_default_key_file_with_location_override() {
        let temp_dir = tempfile::tempdir().unwrap();

        let key_file_override_path = temp_dir.path().join("keys_override");

        std::fs::write(&key_file_override_path, STUB_KEY_FILE_CONTENTS).unwrap();

        let override_var_name = StubIntegration::private_key_file_path_override_env_var_name();
        std::env::set_var(&override_var_name, key_file_override_path.as_os_str());

        assert_eq!(STUB_KEYS, StubIntegration::private_keys_from_default_key_file().unwrap());

        std::env::remove_var(override_var_name)
    }
}