use std::convert::Infallible;
use std::convert::TryFrom;
use std::env;
use std::fs;
use std::str::FromStr;
use camino::Utf8PathBuf;
use secrecy::Secret;
use crate::error::LoadError;
#[derive(Debug, Clone)]
pub enum SecretLoader {
Env(String),
File(Utf8PathBuf),
Plain(Secret<String>),
}
impl SecretLoader {
pub fn new<S: AsRef<str>>(val: S) -> Self {
val.as_ref().parse().unwrap()
}
pub fn into_secret(self) -> Result<Secret<String>, LoadError> {
let secret = match self {
Self::Env(env_var) => env::var(env_var)?.parse().expect("Infallible"),
Self::File(path) => fs::read_to_string(path)?.parse().expect("Infallible"),
Self::Plain(secret) => secret,
};
Ok(secret)
}
pub fn is_env(&self) -> bool {
matches!(self, Self::Env(_))
}
pub fn is_file(&self) -> bool {
matches!(self, Self::File(_))
}
pub fn is_plain(&self) -> bool {
matches!(self, Self::Plain(_))
}
}
impl FromStr for SecretLoader {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cred = match s {
val if val.starts_with("env:") => Self::Env(val[4..].to_owned()),
val if val.starts_with("file:") => Self::File(val[5..].parse()?),
val => Self::Plain(val.parse()?),
};
Ok(cred)
}
}
impl From<String> for SecretLoader {
fn from(s: String) -> Self {
s.parse().expect("Infallible")
}
}
impl TryFrom<SecretLoader> for Secret<String> {
type Error = LoadError;
fn try_from(value: SecretLoader) -> Result<Self, Self::Error> {
value.into_secret()
}
}
#[cfg(test)]
mod tests {
use std::convert::TryInto;
use std::env;
use std::io::Write;
use secrecy::ExposeSecret;
use serial_test::serial;
use tempfile::NamedTempFile;
use super::*;
fn setup_env(value: Option<&str>) {
match value {
Some(value) => env::set_var("SECRET", value),
None => env::remove_var("SECRET"),
}
}
fn env_is_set() -> bool {
env::var("SECRET").is_ok()
}
#[test]
fn parse_env() {
let cred = "env:SECRET".parse().unwrap();
match cred {
SecretLoader::Env(env_var) => {
assert_eq!(env_var, "SECRET");
}
_ => panic!("Wrong loader type"),
}
}
#[test]
fn parse_file() {
let cred = "file:/home/user/.secrets".parse().unwrap();
match cred {
SecretLoader::File(path) => {
assert_eq!(path, "/home/user/.secrets");
}
_ => panic!("Wrong loader type"),
}
}
#[test]
fn parse_plain() {
let cred = "plaincredentialstorageisbad".parse().unwrap();
match cred {
SecretLoader::Plain(secret) => {
assert_eq!(secret.expose_secret(), "plaincredentialstorageisbad");
}
_ => panic!("Wrong loader type"),
}
}
#[test]
#[serial(Env)]
fn secret_from_env_present() {
let cred: SecretLoader = "env:SECRET".parse().unwrap();
setup_env(Some("superenvsecret"));
assert!(env_is_set());
let secret: Secret<String> = cred.try_into().unwrap();
assert_eq!(secret.expose_secret(), "superenvsecret");
}
#[test]
#[serial(Env)]
fn secret_from_env_missing() {
let cred: SecretLoader = "env:SECRET".parse().unwrap();
setup_env(None);
assert!(!env_is_set());
let secret: Result<Secret<String>, _> = cred.try_into();
assert!(matches!(secret.unwrap_err(), LoadError::Env(_)));
}
#[test]
fn secret_from_file_present() {
let mut tempfile = NamedTempFile::new().unwrap();
write!(tempfile, "superfilesecret").unwrap();
let tempfile = tempfile.into_temp_path();
let cred: SecretLoader = format!("file:{}", tempfile.display()).parse().unwrap();
let secret: Secret<String> = cred.try_into().unwrap();
assert_eq!(secret.expose_secret(), "superfilesecret");
tempfile.close().unwrap();
}
#[test]
fn secret_from_file_missing() {
let cred: SecretLoader = "file:/does/not/exist".parse().unwrap();
let secret: Result<Secret<String>, _> = cred.try_into();
assert!(matches!(secret.unwrap_err(), LoadError::Io(_)));
}
#[test]
fn secret_from_plain() {
let cred: SecretLoader = "plaincredentialstorageisbad".parse().unwrap();
let secret: Secret<String> = cred.try_into().unwrap();
assert_eq!(secret.expose_secret(), "plaincredentialstorageisbad");
}
}