use std::ffi::OsString;
use crate::{
error::{Error, ErrorKind},
platform::{MaybeSendBoxFuture, MaybeSendSync},
secrets::{
Secret, SecretBytes, SecretMap, SecretOutput, SecretString, encodings::StringEncoding,
},
};
#[derive(Debug, Clone)]
pub struct EnvVarSecret<Output = SecretString> {
value: Output,
}
impl<O> EnvVarSecret<O> {
pub fn new<M: SecretMap<In = SecretBytes, Out = O>>(
var_name: impl Into<OsString>,
encoding: &M,
) -> Result<Self, Error> {
let var_name = var_name.into();
let encoded_value = std::env::var(&var_name).map_err(|source| {
Error::new(ErrorKind::Config, source).with_context(format!(
"reading environment variable {}",
var_name.to_string_lossy()
))
})?;
let value = encoding
.apply(SecretBytes::new(encoded_value.into_bytes()))
.map_err(|err| {
err.with_context(format!(
"decoding environment variable {}",
var_name.to_string_lossy()
))
})?;
Ok(Self { value })
}
}
impl EnvVarSecret<SecretString> {
pub fn string(var_name: impl Into<OsString>) -> Result<Self, Error> {
Self::new(var_name, &StringEncoding)
}
}
impl<O: Clone + MaybeSendSync> Secret for EnvVarSecret<O> {
type Output = O;
fn get_secret_value(
&self,
) -> MaybeSendBoxFuture<'_, Result<SecretOutput<Self::Output>, Error>> {
Box::pin(async move {
Ok(SecretOutput {
value: self.value.clone(),
identity: None,
})
})
}
}
#[cfg(test)]
mod env_tests {
use super::*;
#[tokio::test]
async fn test_env_var_secret_missing() {
let result = EnvVarSecret::string("NON_EXISTENT_SECRET");
assert!(result.is_err());
}
}
#[derive(Debug, Clone)]
pub struct ProvidedSecret<Output = SecretString> {
value: Output,
}
impl<O> ProvidedSecret<O> {
pub fn new(value: O) -> Self {
Self { value }
}
}
impl<O: Clone + MaybeSendSync> Secret for ProvidedSecret<O> {
type Output = O;
fn get_secret_value(
&self,
) -> MaybeSendBoxFuture<'_, Result<SecretOutput<Self::Output>, Error>> {
Box::pin(async move {
Ok(SecretOutput {
value: self.value.clone(),
identity: None,
})
})
}
}
#[cfg(test)]
mod provided_tests {
use super::*;
#[tokio::test]
async fn provided_string_round_trips() {
let secret = ProvidedSecret::new(SecretString::new("already-fetched"));
let output = secret.get_secret_value().await.unwrap();
assert_eq!(output.value.expose_secret(), "already-fetched");
assert!(output.identity.is_none());
}
#[tokio::test]
async fn provided_bytes_round_trips() {
let secret = ProvidedSecret::new(SecretBytes::new(vec![0x2a; 4]));
let output = secret.get_secret_value().await.unwrap();
assert_eq!(output.value.expose_secret(), &[0x2a; 4]);
}
}
#[cfg(feature = "fs")]
mod file_secret {
use std::path::PathBuf;
use crate::{
error::{Error, ErrorKind},
platform::MaybeSendBoxFuture,
secrets::{
MappedSecret, Secret, SecretBytes, SecretMap, SecretOutput, encodings::StringEncoding,
},
};
#[derive(Debug, Clone)]
pub struct FileBytes {
path: PathBuf,
}
impl FileBytes {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
}
impl Secret for FileBytes {
type Output = SecretBytes;
fn get_secret_value(
&self,
) -> MaybeSendBoxFuture<'_, Result<SecretOutput<Self::Output>, Error>> {
Box::pin(async move {
let bytes = tokio::fs::read(&self.path).await.map_err(|source| {
let kind = match source.kind() {
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => {
ErrorKind::Transport { retryable: true }
}
_ => ErrorKind::Config,
};
Error::new(kind, source)
.with_context(format!("reading secret file {}", self.path.display()))
})?;
Ok(SecretOutput {
value: SecretBytes::new(bytes),
identity: None,
})
})
}
}
#[derive(Debug, Clone)]
pub struct FileSecret<M: SecretMap<In = SecretBytes> = StringEncoding> {
inner: MappedSecret<FileBytes, M>,
}
impl<M: SecretMap<In = SecretBytes>> FileSecret<M> {
pub fn new(path: impl Into<PathBuf>, map: M) -> Self {
let path = path.into();
let decode_context = format!("decoding secret file {}", path.display());
Self {
inner: MappedSecret::new(FileBytes::new(path), map).with_context(decode_context),
}
}
}
impl FileSecret {
pub fn string(path: impl Into<PathBuf>) -> Self {
Self::new(path, StringEncoding)
}
}
impl<M: SecretMap<In = SecretBytes>> Secret for FileSecret<M> {
type Output = M::Out;
fn get_secret_value(
&self,
) -> MaybeSendBoxFuture<'_, Result<SecretOutput<Self::Output>, Error>> {
self.inner.get_secret_value()
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
use crate::secrets::encodings::Base64Encoding;
#[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");
}
#[tokio::test]
async fn missing_file_is_config_error() {
let secret = FileSecret::string("/nonexistent/secret/path");
let err = secret.get_secret_value().await.unwrap_err();
assert_eq!(err.kind(), ErrorKind::Config);
assert!(!err.is_retryable());
}
#[tokio::test]
async fn decode_error_names_the_file() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
write!(tmp, "not valid base64 !!").unwrap();
let secret = FileSecret::new(tmp.path(), Base64Encoding);
let err = secret.get_secret_value().await.unwrap_err();
assert_eq!(err.kind(), ErrorKind::Config);
assert!(
err.to_string().contains(&tmp.path().display().to_string()),
"decode error should name the file: {err}"
);
}
#[tokio::test]
async fn file_bytes_mapped_composes() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
write!(tmp, "aGVs\nbG8=").unwrap();
let secret = FileBytes::new(tmp.path()).mapped(Base64Encoding);
let output = secret.get_secret_value().await.unwrap();
assert_eq!(output.value.expose_secret(), b"hello");
}
}
}
#[cfg(feature = "fs")]
pub use file_secret::{FileBytes, FileSecret};