awsmfa 0.3.2

The automation tool for Multi-Factor Authentication (MFA) process to use awscli.
Documentation
use crate::Result;

use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Deserialize, Serialize, Default)]
pub struct MfaConfig {
    secrets: Vec<Secret>,
}

impl fmt::Display for MfaConfig {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.secrets.is_empty() {
            writeln!(
                f,
                "There are no secret keys in ~/.aws/awsmfa.yml. Use set command to register your first secret key."
            )
        } else {
            for s in self.secrets.iter() {
                writeln!(f, "{s}")?;
            }
            write!(f, "")
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone)]
struct Secret {
    profile: String,
    value: String,
}

impl fmt::Display for Secret {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "[profile {}]", self.profile)?;
        writeln!(f, "secret\t: {}", self.value)
    }
}

impl MfaConfig {
    pub fn new() -> Result<Self> {
        let path = Self::path()?;
        Self::load(path.as_path())
    }

    pub fn set(self, profile: &str, value: &str) -> Self {
        let mut secrets = self.remove(profile).secrets;

        secrets.push(Secret {
            profile: profile.into(),
            value: value.into(),
        });

        Self { secrets }
    }

    pub fn remove(self, profile: &str) -> Self {
        let secrets: Vec<Secret> = self
            .secrets
            .into_iter()
            .filter(|s| s.profile != profile)
            .collect();

        Self { secrets }
    }

    pub fn save(&self) -> Result<()> {
        let path = Self::path()?;
        self.write(path.as_path())
    }

    pub fn get_secret(&self, profile: &str) -> Result<String> {
        self.secrets
            .iter()
            .find_map(|s| {
                if s.profile == profile {
                    Some(s.value.to_string())
                } else {
                    None
                }
            })
            .ok_or(anyhow!("Not found mfa device for profile: {}", profile))
    }

    fn load(path: &Path) -> Result<Self> {
        if path.exists() {
            let config = std::fs::read_to_string(path)
                .map_err(|e| anyhow!("{}: {}", e, path.to_str().unwrap()))?;
            serde_yaml::from_str(&config).map_err(anyhow::Error::new)
        } else {
            Ok(Self::default())
        }
    }

    fn write(&self, path: &Path) -> Result<()> {
        let file = fs::File::create(path).map_err(anyhow::Error::new)?;
        serde_yaml::to_writer(file, self).map_err(anyhow::Error::new)
    }

    fn path() -> Result<PathBuf> {
        dirs::home_dir()
            .ok_or(anyhow!("Failed to get home directory."))
            .map(|p| p.join(".aws/awsmfa.yml"))
    }
}

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

    #[test]
    fn it_reads_config() {
        let path = Path::new("mock/test.yml");
        let config = MfaConfig::load(path);
        assert!(config.is_ok());

        let config = config.unwrap();
        assert_eq!(config.secrets.len(), 1);

        let secret = config.secrets.first().unwrap();
        assert_eq!(secret.profile, "test");
        assert_eq!(secret.value, "somesecret");
    }

    #[test]
    fn it_gets_secret() {
        let path = Path::new("mock/test.yml");
        let config = MfaConfig::load(path).unwrap();
        let secret = config.get_secret("test");
        assert!(secret.is_ok());
        assert_eq!(secret.unwrap(), "somesecret");
    }

    #[test]
    fn it_init_config_when_notfound() {
        let path = Path::new("mock/notfound.yml");
        let config = MfaConfig::load(path);
        assert!(config.is_ok());

        let config = config.unwrap();
        assert_eq!(config.secrets.len(), 0);
    }

    #[test]
    fn it_sets_new_device() {
        let path = Path::new("mock/test.yml");
        let config = MfaConfig::load(path)
            .unwrap()
            .set("new_profile", "new_secret");

        assert_eq!(config.secrets.len(), 2);

        let secret = config.secrets.iter().find(|d| d.profile == "new_profile");
        assert!(secret.is_some());

        let secret = secret.unwrap();
        assert_eq!(secret.profile, "new_profile");
        assert_eq!(secret.value, "new_secret");
    }

    #[test]
    fn it_writes_contents() {
        let path = Path::new("mock/write_test.yml");
        MfaConfig::load(Path::new("mock/test.yml"))
            .unwrap()
            .set("write_profile", "write_secret")
            .write(path)
            .unwrap();

        let config = MfaConfig::load(path).unwrap();
        assert_eq!(config.secrets.len(), 2);
    }
}