Skip to content

Commit c03c47d

Browse files
apollo_infra: add sensitive type (#9829)
1 parent e8c805c commit c03c47d

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

crates/apollo_config/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ pub mod converters;
8282
pub mod dumping;
8383
pub mod loading;
8484
pub mod presentation;
85+
pub mod secrets;
8586
pub mod validators;
8687

8788
/// The privacy level of a config parameter, that received as input from the configs.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//! A wrapper for values that are considered **sensitive** (e.g. secrets, tokens, URLs).
2+
//!
3+
//! `Sensitive<T>` keeps the inner value available while preventing accidental leakage through
4+
//! formatting, logging, and serialization:
5+
//!
6+
//! - Display/Debug/Serialize: returns a redacted default value, or a custom redaction via the
7+
//! provided `redactor`.
8+
//! - Deserialize: transparent, and deserializes exactly like `T`, ignoring the `redactor` field.
9+
10+
use core::fmt;
11+
12+
use serde::{Deserialize, Serialize, Serializer};
13+
14+
#[cfg(test)]
15+
#[path = "secrets_test.rs"]
16+
mod secrets_test;
17+
18+
const DEFAULT_REDACTION_OUTPUT: &str = "<<redacted>>";
19+
20+
type Redactor<T> = Box<dyn Fn(&T) -> String + Send + Sync + 'static>;
21+
22+
/// A wrapper for values that are considered **sensitive** (e.g. secrets, tokens, URLs).
23+
#[derive(Deserialize)]
24+
#[serde(transparent, bound(deserialize = "T: Deserialize<'de>"))]
25+
pub struct Sensitive<T> {
26+
inner: T,
27+
#[serde(skip)]
28+
redactor: Option<Redactor<T>>,
29+
}
30+
31+
impl<T> Sensitive<T> {
32+
/// Creates a new `Sensitive<T>` with no custom redactor.
33+
pub fn new(inner: T) -> Self {
34+
Self { inner, redactor: None }
35+
}
36+
37+
/// Attaches a custom redactor function to this `Sensitive` value.
38+
pub fn with_redactor<F>(mut self, redactor: F) -> Self
39+
where
40+
F: Fn(&T) -> String + Send + Sync + 'static,
41+
{
42+
self.redactor = Some(Box::new(redactor));
43+
self
44+
}
45+
46+
/// Consumes the wrapper and returns the inner sensitive value.
47+
pub fn into(self) -> T {
48+
self.inner
49+
}
50+
51+
// Returns the redacted string representation.
52+
fn redact(&self) -> String {
53+
match &self.redactor {
54+
Some(f) => f(&self.inner),
55+
None => DEFAULT_REDACTION_OUTPUT.to_string(),
56+
}
57+
}
58+
}
59+
60+
impl<T> AsRef<T> for Sensitive<T> {
61+
fn as_ref(&self) -> &T {
62+
&self.inner
63+
}
64+
}
65+
66+
impl<T> AsMut<T> for Sensitive<T> {
67+
fn as_mut(&mut self) -> &mut T {
68+
&mut self.inner
69+
}
70+
}
71+
72+
// Equality/ordering/hash only consider the inner value (ignore redactor)
73+
impl<T: PartialEq> PartialEq for Sensitive<T> {
74+
fn eq(&self, other: &Self) -> bool {
75+
self.inner.eq(&other.inner)
76+
}
77+
}
78+
impl<T: Eq> Eq for Sensitive<T> {}
79+
impl<T: PartialOrd> PartialOrd for Sensitive<T> {
80+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
81+
self.inner.partial_cmp(&other.inner)
82+
}
83+
}
84+
impl<T: Ord> Ord for Sensitive<T> {
85+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
86+
self.inner.cmp(&other.inner)
87+
}
88+
}
89+
impl<T: std::hash::Hash> std::hash::Hash for Sensitive<T> {
90+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
91+
self.inner.hash(state)
92+
}
93+
}
94+
impl<T> fmt::Debug for Sensitive<T> {
95+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96+
f.write_str(&self.redact())
97+
}
98+
}
99+
impl<T> fmt::Display for Sensitive<T> {
100+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101+
f.write_str(&self.redact())
102+
}
103+
}
104+
impl<T> Serialize for Sensitive<T> {
105+
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
106+
s.serialize_str(&self.redact())
107+
}
108+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use crate::secrets::{Sensitive, DEFAULT_REDACTION_OUTPUT};
2+
3+
#[test]
4+
fn test_default_redaction_output() {
5+
let sensitive = Sensitive::new("secret");
6+
assert_eq!(sensitive.redact(), DEFAULT_REDACTION_OUTPUT);
7+
}
8+
9+
#[test]
10+
fn test_custom_redaction_without_args() {
11+
let redactor = |_: &String| "censored".to_string();
12+
let sensitive = Sensitive::new("secret".to_string()).with_redactor(redactor);
13+
assert_eq!(sensitive.redact(), "censored");
14+
}
15+
16+
#[test]
17+
fn test_custom_redaction_with_args() {
18+
let redactor = |s: &String| s.chars().take(2).collect::<String>();
19+
let sensitive = Sensitive::new("abcdefgh".to_string()).with_redactor(redactor);
20+
assert_eq!(sensitive.redact(), "ab");
21+
}
22+
23+
#[test]
24+
fn test_debug_display_serialize() {
25+
let sensitive = Sensitive::new("secret");
26+
assert_eq!(format!("{:?}", sensitive), DEFAULT_REDACTION_OUTPUT);
27+
assert_eq!(format!("{}", sensitive), DEFAULT_REDACTION_OUTPUT);
28+
assert_eq!(
29+
serde_json::to_string(&sensitive).unwrap(),
30+
serde_json::to_string(&DEFAULT_REDACTION_OUTPUT).unwrap()
31+
);
32+
}
33+
34+
#[test]
35+
fn test_into() {
36+
let sensitive = Sensitive::new("secret");
37+
assert_eq!(sensitive.into(), "secret");
38+
}
39+
40+
#[test]
41+
fn test_ref() {
42+
let sensitive = Sensitive::new("secret");
43+
assert_eq!(sensitive.as_ref(), &"secret");
44+
}
45+
46+
#[test]
47+
fn test_mut() {
48+
let mut sensitive = Sensitive::new("secret".to_string());
49+
let inner = sensitive.as_mut();
50+
inner.push_str("123");
51+
assert_eq!(sensitive.into(), "secret123".to_string());
52+
}

0 commit comments

Comments
 (0)