use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use crate::{
error::{BindError, UnboundError, UrnParseError},
source::SourceRegistry,
urn::Urn,
};
pub trait SecretValue: Sized {
fn type_name() -> &'static str;
fn from_bytes(bytes: Vec<u8>, urn: &str) -> Result<Self, BindError>;
fn masked_size(&self) -> String;
}
impl SecretValue for String {
fn type_name() -> &'static str {
"string"
}
fn from_bytes(bytes: Vec<u8>, urn: &str) -> Result<Self, BindError> {
String::from_utf8(bytes).map_err(|e| BindError::TypeConversion {
urn: urn.to_owned(),
detail: e.to_string(),
})
}
fn masked_size(&self) -> String {
self.chars().count().to_string()
}
}
impl SecretValue for Vec<u8> {
fn type_name() -> &'static str {
"bytes"
}
fn from_bytes(bytes: Vec<u8>, _urn: &str) -> Result<Self, BindError> {
Ok(bytes)
}
fn masked_size(&self) -> String {
self.len().to_string()
}
}
impl SecretValue for serde_json::Value {
fn type_name() -> &'static str {
"json"
}
fn from_bytes(bytes: Vec<u8>, urn: &str) -> Result<Self, BindError> {
serde_json::from_slice(&bytes).map_err(|e| BindError::TypeConversion {
urn: urn.to_owned(),
detail: e.to_string(),
})
}
fn masked_size(&self) -> String {
self.to_string().len().to_string()
}
}
pub struct Secret<T: SecretValue> {
urn: Urn,
value: Option<T>,
}
impl<T: SecretValue> Secret<T> {
pub fn new(urn_str: &str) -> Result<Self, UrnParseError> {
Ok(Self {
urn: urn_str.parse()?,
value: None,
})
}
pub fn urn(&self) -> &Urn {
&self.urn
}
pub fn value(&self) -> Result<&T, UnboundError> {
self.value.as_ref().ok_or_else(|| UnboundError {
urn: self.urn.to_string(),
})
}
pub fn masked_value(&self) -> String {
match &self.value {
None => format!("{} [UNBOUND]", self.urn),
Some(v) => format!("{} [{}:{}]", self.urn, T::type_name(), v.masked_size()),
}
}
pub fn bind(&mut self, registry: &SourceRegistry) -> Result<(), BindError> {
let urn_str = self.urn.to_string();
let source =
registry
.get(&self.urn.source_id)
.ok_or_else(|| BindError::SourceNotFound {
source_id: self.urn.source_id.clone(),
})?;
let bytes = source.get(&self.urn.name).map_err(|e| {
use crate::error::SourceError;
match e {
SourceError::NotFound { name } => BindError::NameNotFound {
source_id: self.urn.source_id.clone(),
name,
},
other => BindError::Source {
urn: urn_str.clone(),
source: other,
},
}
})?;
self.value = Some(T::from_bytes(bytes, &urn_str)?);
Ok(())
}
}
impl<T: SecretValue> fmt::Display for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.masked_value())
}
}
impl<T: SecretValue> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Secret({})", self.masked_value())
}
}
impl<T: SecretValue> Serialize for Secret<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.masked_value())
}
}
impl<'de, T: SecretValue> Deserialize<'de> for Secret<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
let urn = s.parse::<Urn>().map_err(de::Error::custom)?;
Ok(Self { urn, value: None })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::SourceRegistry;
#[test]
fn unbound_masked_value() {
let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
assert_eq!(s.masked_value(), "urn:secrets-rs:env:MY_KEY [UNBOUND]");
}
#[test]
fn display_shows_masked_value() {
let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
assert_eq!(s.to_string(), "urn:secrets-rs:env:MY_KEY [UNBOUND]");
}
#[test]
fn debug_shows_masked_value() {
let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
assert_eq!(
format!("{s:?}"),
"Secret(urn:secrets-rs:env:MY_KEY [UNBOUND])"
);
}
#[test]
fn value_before_bind_is_error() {
let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
assert!(s.value().is_err());
}
#[test]
fn bound_masked_value_includes_type_and_length() {
unsafe { std::env::set_var("SECRET_TEST_MASKED", "hello") };
let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_TEST_MASKED").unwrap();
let registry = SourceRegistry::new();
s.bind(®istry).unwrap();
assert_eq!(
s.masked_value(),
"urn:secrets-rs:env:SECRET_TEST_MASKED [string:5]"
);
unsafe { std::env::remove_var("SECRET_TEST_MASKED") };
}
#[test]
fn value_after_bind_returns_correct_value() {
unsafe { std::env::set_var("SECRET_TEST_VALUE", "s3cr3t") };
let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_TEST_VALUE").unwrap();
let registry = SourceRegistry::new();
s.bind(®istry).unwrap();
assert_eq!(s.value().unwrap(), "s3cr3t");
unsafe { std::env::remove_var("SECRET_TEST_VALUE") };
}
#[test]
fn serialize_produces_masked_string() {
let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, r#""urn:secrets-rs:env:MY_KEY [UNBOUND]""#);
}
#[test]
fn deserialize_valid_urn_produces_unbound_secret() {
let s: Secret<String> = serde_json::from_str(r#""urn:secrets-rs:env:MY_KEY""#).unwrap();
assert_eq!(s.urn().to_string(), "urn:secrets-rs:env:MY_KEY");
assert!(s.value().is_err());
}
#[test]
fn deserialize_non_urn_string_errors() {
let result = serde_json::from_str::<Secret<String>>(r#""not-a-urn""#);
assert!(result.is_err());
}
#[test]
fn urn_returns_source_id_and_name() {
let s: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
assert_eq!(s.urn().source_id, "env");
assert_eq!(s.urn().name, "MY_KEY");
}
#[test]
fn urn_is_unchanged_after_bind() {
unsafe { std::env::set_var("SECRET_URN_BIND_TEST", "value") };
let mut s: Secret<String> = Secret::new("urn:secrets-rs:env:SECRET_URN_BIND_TEST").unwrap();
let urn_before = s.urn().to_string();
let registry = SourceRegistry::new();
s.bind(®istry).unwrap();
assert_eq!(s.urn().to_string(), urn_before);
unsafe { std::env::remove_var("SECRET_URN_BIND_TEST") };
}
#[test]
fn urn_can_be_used_to_construct_second_unbound_secret() {
let original: Secret<String> = Secret::new("urn:secrets-rs:env:MY_KEY").unwrap();
let copy: Secret<String> = Secret::new(&original.urn().to_string()).unwrap();
assert_eq!(original.urn(), copy.urn());
assert!(copy.value().is_err());
}
}