crowbar 0.4.10

Securily generates temporary AWS credentials through Identity Providers using SAML
Documentation
use crate::providers::ProviderType;
use anyhow::{anyhow, Result};
use clap::ArgMatches;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::fmt;
use std::str::FromStr;
use url::Url;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct AppProfile {
    pub name: String,
    pub provider: ProviderType,
    pub username: String,
    pub url: String,
    pub role: Option<String>,
}

impl fmt::Display for AppProfile {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let identifier = match self.base_url() {
            Ok(url) => {
                let identifier = format!("{}-{}", url.as_str(), self.username);
                format!("{:x}", sha2::Sha256::digest(identifier.as_bytes()))
            }
            Err(_e) => return Err(std::fmt::Error),
        };

        write!(f, "{}", identifier)
    }
}

impl From<&ArgMatches> for AppProfile {
    fn from(action: &ArgMatches) -> AppProfile {
        AppProfile {
            name: action.get_one::<String>("profile").unwrap().to_string(),
            username: action.get_one::<String>("username").unwrap().to_string(),
            url: action.get_one::<String>("url").unwrap().to_string(),
            role: action.get_one::<String>("role").map(|r| r.to_string()),
            provider: ProviderType::from_str(action.get_one::<String>("provider").unwrap())
                .unwrap(),
        }
    }
}

impl AppProfile {
    pub fn request_url(&self) -> Result<Url> {
        let url = self.url.clone();
        match Url::parse(&url) {
            Ok(u) => Ok(u),
            Err(e) => Err(anyhow!("Cannot parse profile URL: {}", e)),
        }
    }

    pub fn base_url(&self) -> Result<Url> {
        let url = self.request_url()?;
        let base_url = &format!("{}://{}", url.scheme(), url.host().unwrap());
        match Url::from_str(base_url) {
            Ok(u) => Ok(u),
            Err(e) => Err(anyhow!("Unable to create base URL: {}", e)),
        }
    }

    pub fn is_profile(&self, profile: &str) -> bool {
        self.name == profile
    }
}

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

    #[test]
    fn returns_request_url() -> Result<()> {
        assert_eq!(
            Url::from_str("https://example.com/example/url")?,
            short_profile().request_url()?
        );
        Ok(())
    }

    #[test]
    fn returns_base_of_url_just_host() -> Result<()> {
        assert_eq!(
            Url::from_str("https://example.com")?,
            short_profile().base_url()?
        );
        Ok(())
    }

    #[test]
    fn returns_base_of_url_host_and_subdomain() -> Result<()> {
        assert_eq!(
            Url::from_str("https://subdomain.example.com")?,
            long_profile().base_url()?
        );
        Ok(())
    }

    #[test]
    fn validates_profile_name() {
        assert_eq!("profile", short_profile().name)
    }

    fn long_profile() -> AppProfile {
        toml::from_str(
            r#"
            name = "profile"
            provider = "okta"
            url = "https://subdomain.example.com/example/url"
            username = "username"
            role = "role"
        "#
            .trim_start(),
        )
        .unwrap()
    }

    fn short_profile() -> AppProfile {
        toml::from_str(
            r#"
            name = "profile"
            provider = "okta"
            url = "https://example.com/example/url"
            username = "username"
        "#
            .trim_start(),
        )
        .unwrap()
    }
}