crowbar 0.4.10

Securily generates temporary AWS credentials through Identity Providers using SAML
Documentation
use crate::config::app::AppProfile;
use crate::utils::LevelFilter;
use anyhow::Result;
use clap::{crate_description, crate_version, Arg, ArgAction, ArgMatches, Command};

#[derive(Debug)]
pub struct CliConfig {
    pub force: bool,
    pub location: Option<String>,
    pub log_level: LevelFilter,
    pub action: CliAction,
}

#[derive(Debug)]
pub enum CliAction {
    Profiles {
        action: CliSubAction,
    },
    Exec {
        command: Vec<String>,
        profile: String,
    },
    Creds {
        profile: String,
        print: bool,
    },
}

#[derive(Debug)]
pub enum CliSubAction {
    Add { profile: AppProfile },
    Delete { profile_name: String },
    List,
}

fn get_matches() -> ArgMatches {
    Command::new("crowbar")
      .version(crate_version!())
      .about(crate_description!())
      .subcommand_required(true)
        .propagate_version(true)
            .subcommand_required(true)      .disable_help_subcommand(true)
      .arg(
          Arg::new("force")
              .short('f')
              .action(ArgAction::SetTrue)
              .long("force")
              .help("Forces re-entering of your Okta credentials"),
      )
      .arg(
          Arg::new("log-level")
              .short('l')
              .long("log-level")
              .value_name("LOG_LEVEL")
              .help("Set the log level")
              .value_parser(clap::builder::PossibleValuesParser::new(["info", "debug", "trace"]))
              .default_value("info")
      )
      .arg(
          Arg::new("location")
              .short('c')
              .long("config")
              .value_name("CONFIG")
              .help("The location of the configuration file"),
      )
      .subcommand(
          Command::new("profiles")
          .about("Add or delete profiles")
          .arg_required_else_help(true)
          .disable_help_subcommand(true)
        .subcommand(
              Command::new("add")
              .about("Add a profile")
              .arg(
                  Arg::new("provider")
                      .short('p')
                      .long("provider")
                      .value_name("PROVIDER")
                      .required(true)
                      .help("The name of the provider to use")
                      .value_parser(clap::builder::PossibleValuesParser::new(["okta","jumpcloud"]))
              )
              .arg(
                  Arg::new("username")
                      .short('u')
                      .long("username")
                      .value_name("USERNAME")
                      .required(true)
                      .help("The username to use for logging into your IdP"),
              )
              .arg(
                  Arg::new("url")
                      .long("url")
                      .value_name("URL")
                      .required(true)
                      .help("The URL used to log into AWS from your IdP"),
              )
              .arg(
                  Arg::new("role")
                      .long("r")
                      .value_name("ROLE")
                      .required(false)
                      .help("The AWS role to assume after a successful login (Optional)"),
              )
              .arg(
                  Arg::new("profile").required(true).help("The name of the profile"),
              ),
          )
          .subcommand(
              Command::new("list")
              .about("List all profiles")
          )
          .subcommand(
              Command::new("delete")
              .about("Delete a profile")
              .arg(
                  Arg::new("profile").required(true)
              ),
          )
      )
      .subcommand(
          Command::new("creds")
          .about("Exposed temporary credentials on the command line using the credential_process JSON layout")
          .arg(
              Arg::new("print")
              .short('p')
              .action(ArgAction::SetTrue)
              .long("print")
              .help("Print credentials to stdout"),
          )
          .arg(
              Arg::new("profile").required(true)
          ),
      )
      .subcommand(
        Command::new("exec")
        .about("Exposed temporary credentials on the command line by executing a child process with environment variables")
        .arg(
            Arg::new("profile").required(true)
        )
        .arg(
            Arg::new("command")
            .last(true)
            .action(ArgAction::Append)
        ),
    )
    .get_matches()
}

pub fn config() -> Result<CliConfig> {
    let matches = get_matches();
    let cli_action = select_action(&matches);
    let location = matches.get_one::<String>("location").map(|c| c.to_string());
    let log_level_from_matches = matches.get_one::<String>("log-level").unwrap();

    Ok(CliConfig {
        force: matches.get_flag("force"),
        location,
        log_level: select_log_level(log_level_from_matches),
        action: cli_action?,
    })
}

fn select_action(matches: &ArgMatches) -> Result<CliAction> {
    match matches.subcommand() {
        Some(("exec", m)) => {
            let parts = m
                .get_many::<String>("command")
                .unwrap()
                .map(|o| o.to_owned())
                .collect();
            Ok(CliAction::Exec {
                command: parts,
                profile: m.get_one::<String>("profile").unwrap().to_string(),
            })
        }
        Some(("creds", m)) => Ok(CliAction::Creds {
            print: m.get_flag("print"),
            profile: m.get_one::<String>("profile").unwrap().to_string(),
        }),
        Some(("profiles", action)) => Ok(CliAction::Profiles {
            action: match action.subcommand() {
                Some(("add", action)) => CliSubAction::Add {
                    profile: AppProfile::from(action),
                },
                Some(("delete", action)) => CliSubAction::Delete {
                    profile_name: action.get_one::<String>("profile").unwrap().to_string(),
                },
                Some(("list", _)) => CliSubAction::List,
                _ => unreachable!(),
            },
        }),
        _ => unreachable!(),
    }
}

fn select_log_level(selected_level: &str) -> LevelFilter {
    match selected_level {
        "trace" => LevelFilter::Trace,
        "debug" => LevelFilter::Debug,
        _ => LevelFilter::Info,
    }
}

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

    #[test]
    fn log_levels_as_expected() {
        assert_eq!(LevelFilter::Info, select_log_level("info"));
        assert_eq!(LevelFilter::Debug, select_log_level("debug"));
        assert_eq!(LevelFilter::Trace, select_log_level("trace"));
        assert_eq!(LevelFilter::Info, select_log_level("something"))
    }
}