use std::{convert::Infallible, ffi::OsString};
use snafu::prelude::*;
use crate::{
platform::MaybeSendSync,
secrets::{
DecodingError, Secret, SecretDecoder, SecretOutput, SecretString, encodings::StringEncoding,
},
};
#[derive(Debug, Clone)]
pub struct EnvVarSecret<Output = SecretString> {
value: Output,
}
impl<O> EnvVarSecret<O> {
pub fn new<E: SecretDecoder<Output = O>>(
var_name: impl Into<OsString>,
encoding: &E,
) -> Result<Self, EnvVarSecretError> {
let var_name = var_name.into();
let encoded_value = std::env::var(var_name.clone()).context(EnvAccessSnafu { var_name })?;
let value = encoding
.decode(encoded_value.as_bytes())
.context(DecodeSnafu)?;
Ok(Self { value })
}
}
impl EnvVarSecret<SecretString> {
pub fn string(var_name: impl Into<OsString>) -> Result<Self, EnvVarSecretError> {
Self::new(var_name, &StringEncoding)
}
}
impl<O: Clone + MaybeSendSync> Secret for EnvVarSecret<O> {
type Output = O;
type Error = Infallible;
async fn get_secret_value(&self) -> Result<SecretOutput<Self::Output>, Self::Error> {
Ok(SecretOutput {
value: self.value.clone(),
identity: None,
})
}
}
#[derive(Debug, Snafu)]
pub enum EnvVarSecretError {
#[snafu(display("Failed to read env variable '{}'", var_name.to_string_lossy()))]
EnvAccess {
var_name: OsString,
source: std::env::VarError,
},
#[snafu(display("Failed to decode secret"))]
Decode {
source: DecodingError,
},
}
impl crate::Error for EnvVarSecretError {
fn is_retryable(&self) -> bool {
false
}
}
#[cfg(feature = "fs")]
mod file_secret {
use std::path::PathBuf;
use snafu::prelude::*;
use crate::secrets::{
DecodingError, Secret, SecretDecoder, SecretOutput, encodings::StringEncoding,
};
#[derive(Debug, Clone)]
pub struct FileSecret<D: SecretDecoder = StringEncoding> {
path: PathBuf,
decoder: D,
}
impl<D: SecretDecoder> FileSecret<D> {
pub fn new(path: impl Into<PathBuf>, decoder: D) -> Self {
Self {
path: path.into(),
decoder,
}
}
}
impl FileSecret {
pub fn string(path: impl Into<PathBuf>) -> Self {
Self::new(path, StringEncoding)
}
}
impl<D: SecretDecoder> Secret for FileSecret<D> {
type Output = D::Output;
type Error = FileSecretError;
async fn get_secret_value(&self) -> Result<SecretOutput<Self::Output>, Self::Error> {
let bytes = tokio::fs::read(&self.path).await.context(ReadFileSnafu)?;
let value = self.decoder.decode(&bytes).context(FileDecodeSnafu)?;
Ok(SecretOutput {
value,
identity: None,
})
}
}
#[derive(Debug, Snafu)]
pub enum FileSecretError {
#[snafu(display("Failed to read secret file"))]
ReadFile {
source: std::io::Error,
},
#[snafu(display("Failed to decode secret file contents"))]
FileDecode {
source: DecodingError,
},
}
impl crate::Error for FileSecretError {
fn is_retryable(&self) -> bool {
match self {
Self::ReadFile { source } => matches!(
source.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
),
Self::FileDecode { .. } => false,
}
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
#[tokio::test]
async fn file_secret_trims_trailing_newline() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "my-secret").unwrap();
let secret = FileSecret::string(tmp.path());
let output = secret.get_secret_value().await.unwrap();
assert_eq!(output.value.expose_secret(), "my-secret");
}
}
}
#[cfg(feature = "fs")]
pub use file_secret::{FileSecret, FileSecretError};