apollo_redaction/lib.rs
1//! Redaction utility for sensitive data in error messages, debug logs, etc.
2//!
3//! # Usage
4//! Wrap values in [`Redacted`] to prevent them from being printed directly.
5//!
6//! ```rust
7//! use apollo_redaction::Redacted;
8//! use apollo_redaction::strategy::SimpleRedactor;
9//!
10//! let secret = Redacted::<_, SimpleRedactor>::new("hunter2");
11//! assert_eq!(secret.to_string(), "[REDACTED]");
12//! assert_eq!(secret.unredact().to_string(), "hunter2");
13//! ```
14//!
15//! ## Deserialization
16//!
17//! Often, you won't create [`Redacted`] instances manually. Instead, you might deserialize them
18//! from a configuration structure:
19//!
20//! ```rust
21//! use apollo_redaction::Redacted;
22//!
23//! #[derive(Debug, serde::Deserialize)]
24//! struct RedisConfig {
25//! username: Redacted<String>,
26//! password: Redacted<String>,
27//! }
28//!
29//! let redis_config: RedisConfig = serde_json::from_str(r#"
30//! {
31//! "username": "admin",
32//! "password": "topsecret"
33//! }
34//! "#).unwrap();
35//!
36//! // If you did eg. `dbg!(redis_config)`...
37//! assert_eq!(
38//! format!("{redis_config:?}"),
39//! r#"RedisConfig { username: [REDACTED], password: [REDACTED] }"#,
40//! );
41//! ```
42//!
43//! ## Serialization
44//!
45//! Values wrapped in a [`Redacted`] cannot be serialized with [serde]. This is to prevent
46//! accidentally serializing either the redacted or the unredacted form when you don't expect it.
47//! You can use the `#[serde(serialize_with)]` attribute to serialize a [`Redacted`] field.
48//!
49//! apollo-redaction comes with serialization helpers:
50//! - [`ser::unredacted`] - serializes the unredacted representation
51//! - [`ser::display_redacted`] - serializes the redacted [`Display`] representation
52//!
53//! # Strategies
54//!
55//! A strategy must be provided as a type parameter. For example:
56//!
57//! ```rust
58//! use apollo_redaction::Redacted;
59//! use apollo_redaction::strategy::UrlHostRedactor;
60//! use apollo_redaction::strategy::EmailRedactor;
61//!
62//! // when deserializing a field...
63//! struct TelemetryConfig {
64//! url: Redacted<String, UrlHostRedactor>,
65//! }
66//! // when constructing a value manually...
67//! let email = Redacted::<_, EmailRedactor>::new("myemail@address.com");
68//! ```
69//!
70//! | Strategy | Sample input | Sample output |
71//! |----------|--------------|---------------|
72//! | [`SimpleRedactor`] | any | `[REDACTED]` |
73//! | [`FullRedactor`] | `secret` | `******` |
74//! | [`PasswordRedactor`] | any | `********` |
75//! | [`IpRedactor`] | `192.168.1.100` | `192.168.*.*` |
76//! | [`UrlHostRedactor`] | `https://username:passwd@mytelemetryhost.com/v1/traces` | `****/v1/traces` |
77//! | [`UrlPasswordRedactor`] | `https://alice:s3cr3t@proxy.example.com/` | `https://alice:***@proxy.example.com/` |
78//! | [`EmailRedactor`] | `janedoe@gmail.com` | `j***e@gmail.com` |
79//! | [`First4Redactor`] | `sk_live_abc123` | `sk_l****` |
80//! | [`Last4Redactor`] | `4242424242424242` | `************4242` |
81//! | [`CardRedactor`] | `4242-4242-4242-4242` | `****-****-****-4242` |
82//! | [`FixedRedactor<N>`] | `password123` (N=4) | `pass*******` |
83//!
84//! [`IpRedactor`]: crate::strategy::IpRedactor
85//! [`UrlHostRedactor`]: crate::strategy::UrlHostRedactor
86//! [`UrlPasswordRedactor`]: crate::strategy::UrlPasswordRedactor
87//! [`EmailRedactor`]: crate::strategy::EmailRedactor
88//! [`FullRedactor`]: crate::strategy::FullRedactor
89//! [`PasswordRedactor`]: crate::strategy::PasswordRedactor
90//! [`First4Redactor`]: crate::strategy::First4Redactor
91//! [`Last4Redactor`]: crate::strategy::Last4Redactor
92//! [`CardRedactor`]: crate::strategy::CardRedactor
93//! [`FixedRedactor<N>`]: crate::strategy::FixedRedactor
94//!
95//! # Feature flags
96//! - `schemars` enables a transparent [`schemars::JsonSchema`] implementation for `Redacted<T>` when `T`
97//! implements `schemars::JsonSchema`.
98
99use crate::strategy::SimpleRedactor;
100use std::fmt;
101use std::fmt::Debug;
102use std::fmt::Display;
103use std::str::FromStr;
104
105pub mod ser;
106pub mod strategy;
107
108/// A redaction strategy tells apollo-redaction how to redact values for `Display` and `Debug`.
109///
110/// The trait is generic over the value being redacted. If you know your redaction strategy is just
111/// going to be used for a single type, you can write:
112///
113/// ```rust,ignore
114/// use url::Url;
115/// use apollo_redaction::RedactionStrategy;
116///
117/// #[derive(Default, Clone)]
118/// struct UrlRedactor;
119/// impl RedactionStrategy<Url> for UrlRedactor {
120/// // ...
121/// }
122/// ```
123///
124/// ## Bounds and deserialization
125/// It's strongly recommended that all your redaction strategies derive [`Default`] and [`Clone`].
126/// Without [`Default`], a redacted value cannot be deserialized with [serde], making it
127/// significantly less useful.
128pub trait RedactionStrategy<T> {
129 /// How to redact the value for the [`Debug`] (`{:?}`) implementation. By default, this
130 /// delegates to [`RedactionStrategy::display`].
131 fn debug(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 self.display(value, f)
133 }
134
135 /// How to redact the value for the [`Display`] (`{}`) implementation.
136 fn display(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result;
137}
138
139/// A wrapper type for redacting values, so the actual values are not accidentally printed out.
140///
141/// A redacted value comes with a redaction strategy. The strategy provides the [`Display`] and
142/// [`Debug`] implementations for the inner type.
143///
144/// To access the redacted value proper, you must use the [`Redacted::unredact`] method.
145#[derive(Default, Clone)]
146pub struct Redacted<T, R = SimpleRedactor> {
147 value: T,
148 strategy: R,
149}
150
151impl<T, R: Default> Redacted<T, R> {
152 /// Create a new redacted value with a [`Default`] redaction strategy.
153 ///
154 /// ```rust
155 /// use apollo_redaction::Redacted;
156 /// use apollo_redaction::strategy::SimpleRedactor;
157 ///
158 /// let redacted = Redacted::<_, SimpleRedactor>::new("password for all the company's secrets");
159 /// ```
160 pub fn new(value: T) -> Self {
161 Self::from(value)
162 }
163}
164
165impl<T, R> Redacted<T, R> {
166 /// Create a new redacted value with a redaction strategy.
167 pub fn with_strategy(value: T, strategy: R) -> Self {
168 Self { value, strategy }
169 }
170}
171
172impl<T, R: Default> From<T> for Redacted<T, R> {
173 /// Wrap a value in [`Redacted`] with a [`Default`] redaction strategy.
174 ///
175 /// ```rust
176 /// use apollo_redaction::Redacted;
177 /// use apollo_redaction::strategy::SimpleRedactor;
178 ///
179 /// let redacted = Redacted::<_, SimpleRedactor>::from("password for all the company's secrets");
180 /// ```
181 fn from(value: T) -> Self {
182 Self::with_strategy(value, Default::default())
183 }
184}
185
186impl<T: FromStr, R: Default> FromStr for Redacted<T, R> {
187 type Err = T::Err;
188
189 /// Parse a value from a string with a [`Default`] redaction strategy.
190 ///
191 /// ```rust
192 /// use apollo_redaction::Redacted;
193 /// use apollo_redaction::strategy::SimpleRedactor;
194 /// use url::Url;
195 ///
196 /// let redacted: Redacted<Url, SimpleRedactor> = "https://apollographql.com".parse().expect("it's valid");
197 /// ```
198 fn from_str(s: &str) -> Result<Self, Self::Err> {
199 T::from_str(s).map(Self::from)
200 }
201}
202
203impl<'de, T: serde::Deserialize<'de>, R: Default> serde::Deserialize<'de> for Redacted<T, R> {
204 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
205 where
206 D: serde::Deserializer<'de>,
207 {
208 T::deserialize(deserializer)
209 .map(Self::new)
210 // We don't want to leak the actual value in the error message, so we return a generic error.
211 .map_err(|_| {
212 let type_name = std::any::type_name::<T>();
213 let short_name = type_name.rsplit("::").next().unwrap_or(type_name);
214 serde::de::Error::custom(format!("failed to parse '{short_name}'"))
215 })
216 }
217}
218
219impl<T, R> Redacted<T, R> {
220 /// Access the underlying value.
221 pub fn unredact(&self) -> &T {
222 &self.value
223 }
224
225 /// Access the underlying value.
226 pub fn unredact_mut(&mut self) -> &mut T {
227 &mut self.value
228 }
229}
230
231impl<T, R: RedactionStrategy<T>> Display for Redacted<T, R> {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 self.strategy.display(&self.value, f)
234 }
235}
236
237impl<T, R: RedactionStrategy<T>> Debug for Redacted<T, R> {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 self.strategy.debug(&self.value, f)
240 }
241}
242
243#[cfg(feature = "schemars")]
244impl<T, R> schemars::JsonSchema for Redacted<T, R>
245where
246 T: schemars::JsonSchema,
247{
248 fn schema_id() -> std::borrow::Cow<'static, str> {
249 T::schema_id()
250 }
251
252 fn schema_name() -> std::borrow::Cow<'static, str> {
253 T::schema_name()
254 }
255
256 fn inline_schema() -> bool {
257 T::inline_schema()
258 }
259
260 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
261 T::json_schema(generator)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn unredact() {
271 let redacted = Redacted::<_, SimpleRedactor>::new(1234);
272 assert_eq!(*redacted.unredact(), 1234);
273 let mut redacted = redacted;
274 assert_eq!(*redacted.unredact_mut(), 1234);
275
276 *redacted.unredact_mut() = 1235;
277 assert_eq!(*redacted.unredact(), 1235);
278 }
279
280 #[test]
281 fn deserialize_error_does_not_leak_value() {
282 // URLs often contain credentials (usernames/passwords). When deserializing fails,
283 // the url crate's error message would normally include the malformed URL, potentially
284 // leaking these credentials. Redacted<Url> prevents this.
285
286 // Without redaction this would leak an error "relative URL without a base: \"not-a-url-with-secret-api-key-12345\" at line 1 column 37"
287 let redacted_err =
288 serde_json::from_str::<Redacted<url::Url>>(r#""not-a-url-with-secret-api-key-12345""#)
289 .unwrap_err()
290 .to_string();
291 assert_eq!(redacted_err, "failed to parse 'Url'");
292 assert!(
293 !redacted_err.contains("secret-api-key-12345"),
294 "Redacted<Url> error must not contain the secret"
295 );
296 }
297}