chuoku 0.1.2

A lib to store/restore aws cli credential
Documentation
use crate::error::Error::*;
use crate::error::*;
use crate::file;
use crate::file::credentials::restore_credentials;
use crate::profile::*;
use ini::ini::{Ini, Properties};
use std::collections::HashMap;

pub type PropertyMap = HashMap<String, (usize, HashMap<String, String>)>;

pub fn read_aws_config() -> Result<ProfileMap> {
    let mut props = HashMap::new();

    let conf = Ini::load_from_file(file::aws_config_file()?)
        .map_err(|e| ConfigFileError(format!("{:?}", e)))?;
    read_config(&conf, &mut props)?;

    let cred = Ini::load_from_file(file::aws_credential_file()?)
        .map_err(|e| CredentialFileError(format!("{:?}", e)))?;
    read_credentials(&cred, &mut props)?;

    if let Ok(ngydv) = Ini::load_from_file(file::ngydv_config_file()?)  {
        read_ngydv(&ngydv, &mut props)?;
    }

    let mut profiles = ProfileMap::new();
    for (name, prop) in props.iter() {
        profiles.insert(name, create_profile(name, prop));
    }

    restore_credentials(&mut profiles)?;

    Ok(profiles)
}

fn create_profile(name: &str, prop: &(usize, HashMap<String, String>)) -> Profile {
    let (order, prop) = prop;
    let mut profile = Profile {
        profile_name: name.to_owned(),
        order: *order,
        ..Profile::default()
    };

    if let Some(region) = prop.get("region") {
        profile.region = Some(region.to_string());
    }

    if let (Some(access_key_id), Some(secret_access_key)) = (
        prop.get("aws_access_key_id"),
        prop.get("aws_secret_access_key"),
    ) {
        profile.access = Some(Access::AccessKey(AccessKey {
            access_key_id: access_key_id.to_string(),
            secret_access_key: secret_access_key.to_string(),
            mfa_device: prop.get("mfa_device").map(|s| s.to_string()),
        }));
    }

    if let (Some(role_arn), Some(mfa_serial), Some(source_profile)) = (
        prop.get("role_arn"),
        prop.get("mfa_serial"),
        prop.get("source_profile"),
    ) {
        profile.access = Some(Access::AssumeRole(AssumedRole {
            role_arn: role_arn.to_string(),
            mfa_serial: mfa_serial.to_string(),
            source_profile: source_profile.to_string(),
        }))
    }

    profile
}

fn read_config(conf: &Ini, props: &mut PropertyMap) -> Result<()> {
    for (section_key, properties) in conf {
        let section = section_key.ok_or(ConfigFileError("section name not found".to_string()))?;
        let profile_name = profile_name(section).ok_or(ConfigFileError(format!(
            "section header line {} is empty.",
            section
        )))?;

        props.insert(profile_name, (props.len(), properties_to_vec(properties)));
    }
    Ok(())
}

fn read_credentials(cred: &Ini, props: &mut PropertyMap) -> Result<()> {
    for (section_key, prop) in cred {
        let section =
            section_key.ok_or(CredentialFileError("section name not found".to_string()))?;
        let profile_name = profile_name(section).ok_or(CredentialFileError(format!(
            "section header line {} is empty.",
            section
        )))?;

        merge_props(&profile_name, prop, props);
    }
    Ok(())
}

fn read_ngydv(cred: &Ini, props: &mut PropertyMap) -> Result<()> {
    for (section_key, prop) in cred {
        let section = section_key.ok_or(NgydvConfigError("section name not found".to_string()))?;
        let profile_name = profile_name(section).ok_or(NgydvConfigError(format!(
            "section header line {} is empty.",
            section
        )))?;
        merge_props(&profile_name, prop, props);
    }
    Ok(())
}

fn merge_props(profile_name: &str, prop: &Properties, props: &mut PropertyMap) {
    let prop_map = properties_to_vec(prop);
    match props.get_mut(profile_name) {
        Some((_, map)) => map.extend(prop_map),
        None => {
            props.insert(profile_name.to_owned(), (props.len(), prop_map));
        }
    }
}

fn properties_to_vec(properties: &Properties) -> HashMap<String, String> {
    properties
        .iter()
        .map(|(k, v)| (k.to_owned(), v.to_owned()))
        .collect()
}

fn profile_name(section_key: &str) -> Option<String> {
    let profile_name = section_key.replacen("profile ", "", 1).trim().to_owned();

    if profile_name.is_empty() || profile_name == "profile" {
        None
    } else {
        Some(profile_name)
    }
}

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

    const KEY_ID: &str = "XXXXXXXXXXXXXXXXXXXX";
    const SECRET_KEY: &str = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

    fn profile_text() -> String {
        r#"
[profile default]
output = yaml
region = ap-northeast-1
"#
        .to_string()
    }

    fn credential_text() -> String {
        format!(
            r#"
[default]
aws_access_key_id = {}
aws_secret_access_key = {}
"#,
            KEY_ID, SECRET_KEY
        )
        .to_string()
    }

    fn add_prop(expected: &mut PropertyMap, profile_name: &str, kvs: Vec<(&str, &str)>) {
        let mut values = HashMap::new();
        for (k, v) in kvs {
            values.insert(k.to_string(), v.to_string());
        }
        expected.insert(profile_name.to_string(), (0, values));
    }

    mod create_profile {
        use super::*;

        #[test]
        fn create_profile_with_credential_and_config() {
            let mut props = HashMap::new();

            let ini = Ini::load_from_str(&profile_text()).unwrap();
            read_config(&ini, &mut props).unwrap();
            let ini = Ini::load_from_str(&credential_text()).unwrap();
            read_credentials(&ini, &mut props).unwrap();

            let expected = Profile {
                order: 0,
                profile_name: "default".to_string(),
                region: Some("ap-northeast-1".to_string()),
                access: Some(Access::AccessKey(AccessKey {
                    access_key_id: KEY_ID.to_string(),
                    secret_access_key: SECRET_KEY.to_string(),
                    mfa_device: None,
                })),
                credential: None,
            };
            assert_eq!(
                create_profile("default", &props.get("default").unwrap()),
                expected
            );
        }
    }

    mod read_credentials {
        use super::*;

        #[test]
        fn read_credentials_only() {
            let mut props = HashMap::new();

            let ini = Ini::load_from_str(&credential_text()).unwrap();
            assert!(read_credentials(&ini, &mut props).is_ok());
            let mut expected = HashMap::new();
            add_prop(
                &mut expected,
                &"default",
                vec![
                    ("aws_access_key_id", KEY_ID),
                    ("aws_secret_access_key", SECRET_KEY),
                ],
            );
            assert_eq!(props, expected);
        }

        #[test]
        fn read_credential_with_config() {
            let mut props = HashMap::new();

            let ini = Ini::load_from_str(&profile_text()).unwrap();
            assert!(read_config(&ini, &mut props).is_ok());

            let ini = Ini::load_from_str(&credential_text()).unwrap();
            assert!(read_credentials(&ini, &mut props).is_ok());

            let mut expected = HashMap::new();
            add_prop(
                &mut expected,
                &"default",
                vec![
                    ("aws_access_key_id", KEY_ID),
                    ("aws_secret_access_key", SECRET_KEY),
                    ("output", "yaml"),
                    ("region", "ap-northeast-1"),
                ],
            );
            assert_eq!(props, expected);
        }

        fn read_credential_error(str: &str) {
            let mut props = HashMap::new();
            let ini = Ini::load_from_str(str).unwrap();
            assert!(read_credentials(&ini, &mut props).is_err());
        }

        #[test]
        fn read_credential_empty_profile_header() {
            read_credential_error(
                r#"
[]
aws_access_key_id = AAAAAAAAA
aws_secret_access_key = aaaaaaaaaaaaaaaaaa
"#,
            );
        }
    }

    mod read_config {
        use super::*;

        #[test]
        fn read_config_ok() {
            let mut props = HashMap::new();
            let ini = Ini::load_from_str(&profile_text()).unwrap();
            assert!(read_config(&ini, &mut props).is_ok());
            let mut expected = HashMap::new();
            add_prop(
                &mut expected,
                "default",
                vec![("output", "yaml"), ("region", "ap-northeast-1")],
            );
            assert_eq!(props, expected);
        }

        fn read_config_error(str: &str) {
            let mut props = HashMap::new();
            let ini = Ini::load_from_str(str).unwrap();
            assert!(read_config(&ini, &mut props).is_err());
        }

        #[test]
        fn read_config_profile_name() {
            read_config_error(
                r#"
    [profile ]
    output = yaml
    region = ap-northeast-1
"#,
            );
        }

        #[test]
        fn read_config_empty_profile_header() {
            read_config_error(
                r#"
    [ ]
    output = yaml
    region = ap-northeast-1
"#,
            );
        }
    }
}