use async_trait::async_trait;
use dialoguer::{Input, Password};
use secrecy::SecretString;
use std::process::Command;
use thiserror::Error;
use tracing::warn;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AuthHelperError {
#[error("error in user communication: {0}")]
Dialoguer(#[from] dialoguer::Error),
#[error("auth_helper is not supported in the non interactive mode")]
NotSupported,
#[error("error parsing the data as utf8: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("{0}")]
Other(String),
#[error("unknown error in the authentication helper")]
Unknown,
}
#[async_trait]
pub trait AuthHelper {
async fn get(
&mut self,
key: String,
connection_name: Option<String>,
) -> Result<String, AuthHelperError>;
async fn get_secret(
&mut self,
key: String,
connection_name: Option<String>,
) -> Result<SecretString, AuthHelperError>;
}
#[derive(Clone, Default)]
pub struct Dialoguer {
pub cloud_name: Option<String>,
}
#[async_trait]
impl AuthHelper for Dialoguer {
async fn get(
&mut self,
key: String,
connection_name: Option<String>,
) -> Result<String, AuthHelperError> {
let prompt = if let Some(connection) = &connection_name {
format!("Please provide the {key} for the cloud `{connection}`")
} else {
format!("Please provide the {key}")
};
Ok(Input::new().with_prompt(prompt).interact()?)
}
async fn get_secret(
&mut self,
key: String,
connection_name: Option<String>,
) -> Result<SecretString, AuthHelperError> {
let prompt = if let Some(connection) = &connection_name {
format!("Please provide the {key} for the cloud `{connection}`")
} else {
format!("Please provide the {key}")
};
let secret = Password::new().with_prompt(prompt).interact()?;
Ok(SecretString::from(secret))
}
}
#[derive(Clone, Default)]
pub struct Noop {}
#[async_trait]
impl AuthHelper for Noop {
async fn get(
&mut self,
_key: String,
_connection_name: Option<String>,
) -> Result<String, AuthHelperError> {
Err(AuthHelperError::NotSupported)
}
async fn get_secret(
&mut self,
_key: String,
_connection_name: Option<String>,
) -> Result<SecretString, AuthHelperError> {
Err(AuthHelperError::NotSupported)
}
}
#[derive(Clone, Debug)]
pub struct ExternalCmd {
pub command: String,
}
impl ExternalCmd {
pub fn new(command: String) -> Self {
Self { command }
}
}
#[async_trait]
impl AuthHelper for ExternalCmd {
async fn get(
&mut self,
key: String,
connection_name: Option<String>,
) -> Result<String, AuthHelperError> {
let mut command = Command::new(&self.command);
command.arg(key);
if let Some(connection) = &connection_name {
command.arg(connection);
}
match command.output() {
Ok(res) => {
if !res.stderr.is_empty() {
warn!(
"{:?} written to stderr during invocation: {:?}",
self.command,
String::from_utf8(res.stderr)?.trim_end_matches('\n')
);
}
let stdout: String = String::from_utf8(res.stdout)?;
Ok(stdout.trim_end_matches('\n').into())
}
Err(e) => {
return Err(AuthHelperError::Other(e.to_string()));
}
}
}
async fn get_secret(
&mut self,
key: String,
connection_name: Option<String>,
) -> Result<SecretString, AuthHelperError> {
let mut command = Command::new(&self.command);
command.arg(key);
if let Some(connection) = &connection_name {
command.arg(connection);
}
match command.output() {
Ok(res) => {
if !res.stderr.is_empty() {
warn!(
"{:?} written to stderr during invocation: {:?}",
self.command,
String::from_utf8(res.stderr)?.trim_end_matches('\n')
);
}
let stdout: String = String::from_utf8(res.stdout)?;
Ok(stdout.trim_end_matches('\n').into())
}
Err(e) => {
return Err(AuthHelperError::Other(e.to_string()));
}
}
}
}