chuoku 0.1.0

A lib to store/restore aws cli credential
Documentation
mod collection;
mod export;
pub use self::collection::ProfileMap;
use crate::error::Error::*;
use crate::error::*;
use chrono::{DateTime, Duration, FixedOffset, Local, Utc};
use prettytable::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, PartialEq)]
pub struct AccessKey {
    pub access_key_id: String,
    pub secret_access_key: String,
    pub mfa_device: Option<String>,
}

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Credential {
    pub access_key_id: String,
    pub expiration: DateTime<Utc>,
    pub secret_access_key: String,
    pub session_token: String,
}

pub enum ProfileType {
    AssumeRole(String),
    SessionWithMFA,
    Keys,
    None,
}

impl Credential {
    pub fn new(cred: &rusoto_sts::Credentials) -> Result<Self> {
        let expiration = DateTime::<FixedOffset>::parse_from_rfc3339(&cred.expiration)
            .map(|dt| dt.with_timezone(&Utc))
            .map_err(|e| AwsResponseFormatError(format!("{:?}", e)))?;
        Ok(Self {
            access_key_id: cred.access_key_id.to_owned(),
            expiration,
            secret_access_key: cred.secret_access_key.to_owned(),
            session_token: cred.session_token.to_owned(),
        })
    }

    pub fn life(&self) -> Duration {
        self.expiration.signed_duration_since(Utc::now())
    }

    pub fn local_expired_at_str(&self) -> String {
        format!("{}", self.expiration.with_timezone(&Local).to_string())
    }
}

#[derive(Debug, Default, PartialEq)]
pub struct AssumedRole {
    pub role_arn: String,
    pub mfa_serial: String,
    pub source_profile: String,
}

#[derive(Debug, PartialEq)]
pub enum Access {
    AccessKey(AccessKey),
    AssumeRole(AssumedRole),
}

#[derive(Debug, Default, PartialEq)]
pub struct Profile {
    pub order: usize,
    pub profile_name: String,
    pub region: Option<String>,
    pub access: Option<Access>,
    pub credential: Option<Credential>,
}

impl Profile {
    pub fn print_table(profile_map: &ProfileMap) {
        let mut profiles = profile_map.profiles();
        profiles.sort_by(|a, b| a.order.cmp(&b.order));

        let mut table = Table::new();
        table.set_titles(row!["id", "profile", "region", "type", "credential"]);
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        for profile in profiles.iter() {
            table.add_row(row![
                profile.order + 1,
                profile.profile_name,
                profile.region_str(),
                profile.profile_type_str(),
                profile.credential_str(),
            ]);
        }
        table.printstd();
    }

    pub fn export(&self) -> Result<String> {
        match &self.credential {
            Some(cred) => {
                if cred.life().gt(&chrono::Duration::seconds(0)) {
                    Ok(export::rc(
                        vec![
                            ("AWS_ACCESS_KEY_ID", &cred.access_key_id),
                            ("AWS_SECRET_ACCESS_KEY", &cred.secret_access_key),
                            ("AWS_SESSION_TOKEN", &cred.session_token),
                        ],
                        vec!["AWS_PROFILE"],
                        vec![&format!(
                            "set access_key_id, secret_access_key, session_token to env for profile '{}'",
                            self.profile_name
                        )],
                    ))
                } else {
                    Err(SessionExpiredError(cred.local_expired_at_str()))
                }
            }
            None => match self.profile_type() {
                ProfileType::Keys | ProfileType::None => Ok(export::rc(
                    vec![("AWS_PROFILE", &self.profile_name)],
                    vec![
                        "AWS_ACCESS_KEY_ID",
                        "AWS_SECRET_ACCESS_KEY",
                        "AWS_SESSION_TOKEN",
                    ],
                    vec![&format!(
                        "set AWS_PROFILE for profile '{}'",
                        self.profile_name
                    )],
                )),
                ProfileType::AssumeRole(_) | ProfileType::SessionWithMFA => {
                    Err(ProfileNotForExport(format!(
                        "Need to sign in before export profile {}",
                        self.profile_name
                    )))
                }
            },
        }
    }

    fn profile_type_str(&self) -> String {
        match &self.profile_type() {
            ProfileType::AssumeRole(assumed_role) => format!("Assume role from {}", assumed_role),
            ProfileType::SessionWithMFA => "Access key with mfa device".to_string(),
            ProfileType::Keys => "Access key".to_string(),
            ProfileType::None => "".to_string(),
        }
    }

    pub fn profile_type(&self) -> ProfileType {
        match &self.access {
            Some(Access::AssumeRole(assumed_role)) => {
                ProfileType::AssumeRole(assumed_role.source_profile.to_string())
            }
            Some(Access::AccessKey(access_key)) => match access_key.mfa_device {
                Some(_) => ProfileType::SessionWithMFA,
                None => ProfileType::Keys,
            },
            None => ProfileType::None,
        }
    }
    fn credential_str(&self) -> String {
        match &self.credential {
            Some(cred) => {
                let life = cred.life();
                if life.gt(&chrono::Duration::seconds(0)) {
                    format!(
                        "{}{}{}",
                        duration_to_string(life.num_hours(), "hour", "hours"),
                        duration_to_string(life.num_minutes() % 60, "minute", "minutes"),
                        duration_to_string(life.num_seconds() % 60, "second", "seconds")
                    )
                } else {
                    format!("expired at {}", cred.local_expired_at_str())
                }
            }
            None => "-".to_string(),
        }
    }

    fn region_str(&self) -> String {
        self.region
            .as_ref()
            .unwrap_or(&"none".to_string())
            .to_string()
    }
}

fn duration_to_string(num: i64, singular: &str, plural: &str) -> String {
    if num <= 0 {
        "".to_string()
    } else if num == 1 {
        format!(" 1 {}", singular)
    } else {
        format!(" {} {}", num, plural)
    }
}