mithril_common/entities/
config_secret.rs1use std::fmt;
2use std::hash::Hash;
3use std::str::FromStr;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7pub struct ConfigSecret<T>(T);
22
23impl<T> ConfigSecret<T> {
24 pub fn new(value: T) -> Self {
26 Self(value)
27 }
28
29 pub fn expose_secret(&self) -> &T {
33 &self.0
34 }
35
36 pub fn into_inner(self) -> T {
38 self.0
39 }
40}
41
42impl<T> fmt::Debug for ConfigSecret<T> {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str("[REDACTED]")
45 }
46}
47
48impl<T> fmt::Display for ConfigSecret<T> {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.write_str("[REDACTED]")
51 }
52}
53
54impl<T: Clone> Clone for ConfigSecret<T> {
55 fn clone(&self) -> Self {
56 Self(self.0.clone())
57 }
58}
59
60impl<T: Default> Default for ConfigSecret<T> {
61 fn default() -> Self {
62 Self(T::default())
63 }
64}
65
66impl<T> From<T> for ConfigSecret<T> {
67 fn from(value: T) -> Self {
68 Self::new(value)
69 }
70}
71
72impl<T: FromStr> FromStr for ConfigSecret<T> {
73 type Err = T::Err;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 T::from_str(s).map(Self::new)
77 }
78}
79
80impl<T: PartialEq> PartialEq for ConfigSecret<T> {
81 fn eq(&self, other: &Self) -> bool {
82 self.0.eq(&other.0)
83 }
84}
85
86impl<T: Eq> Eq for ConfigSecret<T> {}
87
88impl<T: Hash> Hash for ConfigSecret<T> {
89 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
90 self.0.hash(state);
91 }
92}
93
94impl<T: Serialize> Serialize for ConfigSecret<T> {
95 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96 where
97 S: Serializer,
98 {
99 serializer.serialize_str("[REDACTED]")
100 }
101}
102
103impl<'de, T: Deserialize<'de>> Deserialize<'de> for ConfigSecret<T> {
104 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105 where
106 D: Deserializer<'de>,
107 {
108 T::deserialize(deserializer).map(ConfigSecret::new)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use crate::test::TestLogger;
115
116 use super::*;
117
118 #[test]
119 fn test_debug_redacted_in_display_and_debug() {
120 let secret = ConfigSecret::new("secret");
121 assert_eq!(format!("{secret}"), "[REDACTED]");
122 assert_eq!(format!("{secret:#}"), "[REDACTED]");
123 assert_eq!(format!("{secret:?}"), "[REDACTED]");
124 assert_eq!(format!("{secret:#?}"), "[REDACTED]");
125 }
126
127 #[test]
128 fn test_redacted_in_slog() {
129 let (logger, inspector) = TestLogger::memory();
130 let secret = ConfigSecret::new("0123456789ABCD");
131
132 slog::info!(
133 logger,
134 "log: {secret}, log debug: {secret:?}, log alternate: {secret:#}, log alternate debug: {secret:#?}"
135 );
136 slog::info!(logger, "log in keys";
137 "secret" => %secret, "debug" => ?secret, "alternate" => #%secret, "alternate_debug" => #?secret
138 );
139
140 assert!(!inspector.contains_log(secret.expose_secret()));
141 }
142
143 #[test]
144 fn test_from_str() {
145 let secret: ConfigSecret<String> = "my-secret".parse().unwrap();
146 assert_eq!(secret.expose_secret(), "my-secret");
147 }
148
149 #[test]
150 fn test_serde_serialization() {
151 let secret = ConfigSecret::new("secret");
152 let serialized = serde_json::to_string(&secret).unwrap();
153 assert_eq!(serialized, r#""[REDACTED]""#);
154 }
155
156 #[test]
157 fn test_serde_deserialization() {
158 let secret: ConfigSecret<String> = serde_json::from_str(r#""secret""#).unwrap();
159 assert_eq!(secret, ConfigSecret::new("secret".to_string()));
160
161 #[derive(Deserialize, PartialEq, Debug)]
162 struct Mixed {
163 secret: ConfigSecret<String>,
164 non_secret: String,
165 }
166 let mixed_struct: Mixed =
167 serde_json::from_str(r#"{ "secret": "secret", "non_secret": "non-secret" }"#).unwrap();
168 assert_eq!(
169 mixed_struct,
170 Mixed {
171 secret: ConfigSecret::new("secret".to_string()),
172 non_secret: "non-secret".to_string(),
173 }
174 );
175 }
176}