use std::collections::HashSet;
use std::fmt;
use serde::de::{Deserialize, Deserializer, IgnoredAny};
use serde::ser::{Serialize, Serializer};
use serde_json::Value;
use crate::id::EffectName;
pub const REDACTED: &str = "[REDACTED]";
#[derive(Clone, Default, PartialEq, Eq)]
pub struct Secret<T>(Option<T>);
impl<T> Secret<T> {
pub const fn new(value: T) -> Self {
Self(Some(value))
}
pub const fn expose(&self) -> Option<&T> {
self.0.as_ref()
}
pub fn into_inner(self) -> Option<T> {
self.0
}
pub const fn is_redacted(&self) -> bool {
self.0.is_none()
}
}
impl<T> From<T> for Secret<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
impl<T> fmt::Display for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
impl<T> Serialize for Secret<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(REDACTED)
}
}
impl<'de, T> Deserialize<'de> for Secret<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
IgnoredAny::deserialize(deserializer)?;
Ok(Self(None))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Field {
Input,
Output,
AuditPayload,
ErrorMessage,
}
pub trait Redactor: Send + Sync + 'static {
fn redact(&self, field: Field, effect: &EffectName, value: &mut Value);
}
impl<F> Redactor for F
where
F: Fn(Field, &EffectName, &mut Value) + Send + Sync + 'static,
{
fn redact(&self, field: Field, effect: &EffectName, value: &mut Value) {
self(field, effect, value);
}
}
#[derive(Clone, Debug, Default)]
pub struct RedactKeys {
keys: HashSet<String>,
}
impl RedactKeys {
pub fn new<K: AsRef<str>>(keys: impl IntoIterator<Item = K>) -> Self {
Self {
keys: keys
.into_iter()
.map(|k| k.as_ref().to_ascii_lowercase())
.collect(),
}
}
fn mask(&self, value: &mut Value) {
match value {
Value::Object(map) => {
for (key, value) in map.iter_mut() {
if self.keys.contains(&key.to_ascii_lowercase()) {
*value = Value::String(REDACTED.into());
} else {
self.mask(value);
}
}
}
Value::Array(items) => items.iter_mut().for_each(|item| self.mask(item)),
_ => {}
}
}
}
impl Redactor for RedactKeys {
fn redact(&self, _field: Field, _effect: &EffectName, value: &mut Value) {
self.mask(value);
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn a_secret_never_serializes_its_value_and_reads_back_redacted() {
let secret = Secret::new("tok_live_x".to_string());
let stored = serde_json::to_value(&secret).unwrap();
assert_eq!(stored, json!(REDACTED));
let back: Secret<String> = serde_json::from_value(stored).unwrap();
assert!(back.is_redacted());
assert_eq!(back.expose(), None);
let anything: Secret<u64> = serde_json::from_value(json!({ "x": 1 })).unwrap();
assert!(
anything.is_redacted(),
"any stored shape reads back redacted"
);
}
#[test]
fn a_closure_is_a_redactor() {
let redactor = |field: Field, _: &EffectName, value: &mut Value| {
if field == Field::ErrorMessage {
*value = json!("hidden");
}
};
let mut message = json!("token sk_live_1 rejected");
redactor.redact(
Field::ErrorMessage,
&EffectName::new("x").unwrap(),
&mut message,
);
assert_eq!(message, json!("hidden"));
}
}