aws_smithy_http_server/instrumentation/sensitivity/uri/
mod.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Wrappers around [`Uri`] and it's constituents to allow for sensitivity.
7
8mod label;
9mod query;
10
11use std::fmt::{Debug, Display, Error, Formatter};
12
13use http::Uri;
14
15pub use label::*;
16pub use query::*;
17
18use crate::instrumentation::{MakeDisplay, MakeFmt, MakeIdentity};
19
20/// A wrapper around [`&Uri`](Uri) which modifies the behavior of [`Display`]. Specific parts of the [`Uri`] as are
21/// marked as sensitive using the methods provided.
22///
23/// The [`Display`] implementation will respect the `unredacted-logging` flag.
24#[allow(missing_debug_implementations)]
25pub struct SensitiveUri<'a, P, Q> {
26    uri: &'a Uri,
27    make_path: P,
28    make_query: Q,
29}
30
31impl<'a> SensitiveUri<'a, MakeIdentity, MakeIdentity> {
32    /// Constructs a new [`SensitiveUri`] with nothing marked as sensitive.
33    pub fn new(uri: &'a Uri) -> Self {
34        Self {
35            uri,
36            make_path: MakeIdentity,
37            make_query: MakeIdentity,
38        }
39    }
40}
41
42impl<'a, P, Q> SensitiveUri<'a, P, Q> {
43    pub(crate) fn make_path<M>(self, make_path: M) -> SensitiveUri<'a, M, Q> {
44        SensitiveUri {
45            uri: self.uri,
46            make_path,
47            make_query: self.make_query,
48        }
49    }
50
51    pub(crate) fn make_query<M>(self, make_query: M) -> SensitiveUri<'a, P, M> {
52        SensitiveUri {
53            uri: self.uri,
54            make_path: self.make_path,
55            make_query,
56        }
57    }
58
59    /// Marks path segments as sensitive by providing predicate over the segment index.
60    ///
61    /// See [`Label`] for more info.
62    pub fn label<F>(self, label_marker: F, greedy_label: Option<GreedyLabel>) -> SensitiveUri<'a, MakeLabel<F>, Q> {
63        self.make_path(MakeLabel {
64            label_marker,
65            greedy_label,
66        })
67    }
68
69    /// Marks specific query string values as sensitive by supplying a predicate over the query string keys.
70    ///
71    /// See [`Query`] for more info.
72    pub fn query<F>(self, marker: F) -> SensitiveUri<'a, P, MakeQuery<F>> {
73        self.make_query(MakeQuery(marker))
74    }
75}
76
77impl<'a, P, Q> Display for SensitiveUri<'a, P, Q>
78where
79    P: MakeDisplay<&'a str>,
80    Q: MakeDisplay<&'a str>,
81{
82    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
83        if let Some(scheme) = self.uri.scheme() {
84            write!(f, "{scheme}://")?;
85        }
86
87        if let Some(authority) = self.uri.authority() {
88            write!(f, "{authority}")?;
89        }
90
91        let path = self.uri.path();
92        let path = self.make_path.make_display(path);
93        write!(f, "{path}")?;
94
95        if let Some(query) = self.uri.query() {
96            let query = self.make_query.make_display(query);
97            write!(f, "?{query}")?;
98        }
99
100        Ok(())
101    }
102}
103
104/// A [`MakeFmt`] producing [`SensitiveUri`].
105#[derive(Clone)]
106pub struct MakeUri<P, Q> {
107    pub(crate) make_path: P,
108    pub(crate) make_query: Q,
109}
110
111impl<P, Q> Debug for MakeUri<P, Q> {
112    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
113        f.debug_struct("MakeUri").finish_non_exhaustive()
114    }
115}
116
117impl<'a, P, Q> MakeFmt<&'a http::Uri> for MakeUri<P, Q>
118where
119    Q: Clone,
120    P: Clone,
121{
122    type Target = SensitiveUri<'a, P, Q>;
123
124    fn make(&self, source: &'a http::Uri) -> Self::Target {
125        SensitiveUri::new(source)
126            .make_query(self.make_query.clone())
127            .make_path(self.make_path.clone())
128    }
129}
130
131impl Default for MakeUri<MakeIdentity, MakeIdentity> {
132    fn default() -> Self {
133        Self {
134            make_path: MakeIdentity,
135            make_query: MakeIdentity,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use http::Uri;
143
144    use super::{QueryMarker, SensitiveUri};
145
146    // https://www.w3.org/2004/04/uri-rel-test.html
147    // NOTE: http::Uri's `Display` implementation trims the fragment, we mirror this behavior
148    pub const EXAMPLES: [&str; 19] = [
149        "g:h",
150        "http://a/b/c/g",
151        "http://a/b/c/g/",
152        "http://a/g",
153        "http://g",
154        "http://a/b/c/d;p?y",
155        "http://a/b/c/g?y",
156        "http://a/b/c/d;p?q#s",
157        "http://a/b/c/g#s",
158        "http://a/b/c/g?y#s",
159        "http://a/b/c/;x",
160        "http://a/b/c/g;x",
161        "http://a/b/c/g;x?y#s",
162        "http://a/b/c/d;p?q",
163        "http://a/b/c/",
164        "http://a/b/c/",
165        "http://a/b/",
166        "http://a/b/g",
167        "http://a/",
168    ];
169
170    pub const QUERY_STRING_EXAMPLES: [&str; 11] = [
171        "http://a/b/c/g?&",
172        "http://a/b/c/g?x",
173        "http://a/b/c/g?x&y",
174        "http://a/b/c/g?x&y&",
175        "http://a/b/c/g?x&y&z",
176        "http://a/b/c/g?x=y&x=z",
177        "http://a/b/c/g?x=y&z",
178        "http://a/b/c/g?x=y&",
179        "http://a/b/c/g?x=y&y=z",
180        "http://a/b/c/g?&x=z",
181        "http://a/b/c/g?x&x=y",
182    ];
183
184    #[test]
185    fn path_mark_none() {
186        let originals = EXAMPLES.into_iter().map(Uri::from_static);
187        for original in originals {
188            let output = SensitiveUri::new(&original).to_string();
189            assert_eq!(output, original.to_string());
190        }
191    }
192
193    #[cfg(not(feature = "unredacted-logging"))]
194    const FIRST_PATH_EXAMPLES: [&str; 19] = [
195        "g:h",
196        "http://a/{redacted}/c/g",
197        "http://a/{redacted}/c/g/",
198        "http://a/{redacted}",
199        "http://g/{redacted}",
200        "http://a/{redacted}/c/d;p?y",
201        "http://a/{redacted}/c/g?y",
202        "http://a/{redacted}/c/d;p?q#s",
203        "http://a/{redacted}/c/g#s",
204        "http://a/{redacted}/c/g?y#s",
205        "http://a/{redacted}/c/;x",
206        "http://a/{redacted}/c/g;x",
207        "http://a/{redacted}/c/g;x?y#s",
208        "http://a/{redacted}/c/d;p?q",
209        "http://a/{redacted}/c/",
210        "http://a/{redacted}/c/",
211        "http://a/{redacted}/",
212        "http://a/{redacted}/g",
213        "http://a/{redacted}",
214    ];
215    #[cfg(feature = "unredacted-logging")]
216    const FIRST_PATH_EXAMPLES: [&str; 19] = EXAMPLES;
217
218    #[test]
219    fn path_mark_first_segment() {
220        let originals = EXAMPLES.into_iter().map(Uri::from_static);
221        let expecteds = FIRST_PATH_EXAMPLES.into_iter().map(Uri::from_static);
222        for (original, expected) in originals.zip(expecteds) {
223            let output = SensitiveUri::new(&original).label(|x| x == 0, None).to_string();
224            assert_eq!(output, expected.to_string(), "original = {original}");
225        }
226    }
227
228    #[cfg(not(feature = "unredacted-logging"))]
229    const LAST_PATH_EXAMPLES: [&str; 19] = [
230        "g:h",
231        "http://a/b/c/{redacted}",
232        "http://a/b/c/g/{redacted}",
233        "http://a/{redacted}",
234        "http://g/{redacted}",
235        "http://a/b/c/{redacted}?y",
236        "http://a/b/c/{redacted}?y",
237        "http://a/b/c/{redacted}?q#s",
238        "http://a/b/c/{redacted}#s",
239        "http://a/b/c/{redacted}?y#s",
240        "http://a/b/c/{redacted}",
241        "http://a/b/c/{redacted}",
242        "http://a/b/c/{redacted}?y#s",
243        "http://a/b/c/{redacted}?q",
244        "http://a/b/c/{redacted}",
245        "http://a/b/c/{redacted}",
246        "http://a/b/{redacted}",
247        "http://a/b/{redacted}",
248        "http://a/{redacted}",
249    ];
250    #[cfg(feature = "unredacted-logging")]
251    const LAST_PATH_EXAMPLES: [&str; 19] = EXAMPLES;
252
253    #[test]
254    fn path_mark_last_segment() {
255        let originals = EXAMPLES.into_iter().map(Uri::from_static);
256        let expecteds = LAST_PATH_EXAMPLES.into_iter().map(Uri::from_static);
257        for (original, expected) in originals.zip(expecteds) {
258            let path_len = original.path().split('/').skip(1).count();
259            let output = SensitiveUri::new(&original)
260                .label(|x| x + 1 == path_len, None)
261                .to_string();
262            assert_eq!(output, expected.to_string(), "original = {original}");
263        }
264    }
265
266    #[cfg(not(feature = "unredacted-logging"))]
267    pub const ALL_KEYS_QUERY_STRING_EXAMPLES: [&str; 11] = [
268        "http://a/b/c/g?&",
269        "http://a/b/c/g?x",
270        "http://a/b/c/g?x&y",
271        "http://a/b/c/g?x&y&",
272        "http://a/b/c/g?x&y&z",
273        "http://a/b/c/g?{redacted}=y&{redacted}=z",
274        "http://a/b/c/g?{redacted}=y&z",
275        "http://a/b/c/g?{redacted}=y&",
276        "http://a/b/c/g?{redacted}=y&{redacted}=z",
277        "http://a/b/c/g?&{redacted}=z",
278        "http://a/b/c/g?x&{redacted}=y",
279    ];
280    #[cfg(feature = "unredacted-logging")]
281    pub const ALL_KEYS_QUERY_STRING_EXAMPLES: [&str; 11] = QUERY_STRING_EXAMPLES;
282
283    #[test]
284    fn query_mark_all_keys() {
285        let originals = QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
286        let expecteds = ALL_KEYS_QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
287        for (original, expected) in originals.zip(expecteds) {
288            let output = SensitiveUri::new(&original)
289                .query(|_| QueryMarker {
290                    key: true,
291                    value: false,
292                })
293                .to_string();
294            assert_eq!(output, expected.to_string(), "original = {original}");
295        }
296    }
297
298    #[cfg(not(feature = "unredacted-logging"))]
299    pub const ALL_VALUES_QUERY_STRING_EXAMPLES: [&str; 11] = [
300        "http://a/b/c/g?&",
301        "http://a/b/c/g?x",
302        "http://a/b/c/g?x&y",
303        "http://a/b/c/g?x&y&",
304        "http://a/b/c/g?x&y&z",
305        "http://a/b/c/g?x={redacted}&x={redacted}",
306        "http://a/b/c/g?x={redacted}&z",
307        "http://a/b/c/g?x={redacted}&",
308        "http://a/b/c/g?x={redacted}&y={redacted}",
309        "http://a/b/c/g?&x={redacted}",
310        "http://a/b/c/g?x&x={redacted}",
311    ];
312    #[cfg(feature = "unredacted-logging")]
313    pub const ALL_VALUES_QUERY_STRING_EXAMPLES: [&str; 11] = QUERY_STRING_EXAMPLES;
314
315    #[test]
316    fn query_mark_all_values() {
317        let originals = QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
318        let expecteds = ALL_VALUES_QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
319        for (original, expected) in originals.zip(expecteds) {
320            let output = SensitiveUri::new(&original)
321                .query(|_| QueryMarker {
322                    key: false,
323                    value: true,
324                })
325                .to_string();
326            assert_eq!(output, expected.to_string(), "original = {original}");
327        }
328    }
329
330    #[cfg(not(feature = "unredacted-logging"))]
331    pub const ALL_PAIRS_QUERY_STRING_EXAMPLES: [&str; 11] = [
332        "http://a/b/c/g?&",
333        "http://a/b/c/g?x",
334        "http://a/b/c/g?x&y",
335        "http://a/b/c/g?x&y&",
336        "http://a/b/c/g?x&y&z",
337        "http://a/b/c/g?{redacted}={redacted}&{redacted}={redacted}",
338        "http://a/b/c/g?{redacted}={redacted}&z",
339        "http://a/b/c/g?{redacted}={redacted}&",
340        "http://a/b/c/g?{redacted}={redacted}&{redacted}={redacted}",
341        "http://a/b/c/g?&{redacted}={redacted}",
342        "http://a/b/c/g?x&{redacted}={redacted}",
343    ];
344    #[cfg(feature = "unredacted-logging")]
345    pub const ALL_PAIRS_QUERY_STRING_EXAMPLES: [&str; 11] = QUERY_STRING_EXAMPLES;
346
347    #[test]
348    fn query_mark_all_pairs() {
349        let originals = QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
350        let expecteds = ALL_PAIRS_QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
351        for (original, expected) in originals.zip(expecteds) {
352            let output = SensitiveUri::new(&original)
353                .query(|_| QueryMarker { key: true, value: true })
354                .to_string();
355            assert_eq!(output, expected.to_string(), "original = {original}");
356        }
357    }
358
359    #[cfg(not(feature = "unredacted-logging"))]
360    pub const X_QUERY_STRING_EXAMPLES: [&str; 11] = [
361        "http://a/b/c/g?&",
362        "http://a/b/c/g?x",
363        "http://a/b/c/g?x&y",
364        "http://a/b/c/g?x&y&",
365        "http://a/b/c/g?x&y&z",
366        "http://a/b/c/g?x={redacted}&x={redacted}",
367        "http://a/b/c/g?x={redacted}&z",
368        "http://a/b/c/g?x={redacted}&",
369        "http://a/b/c/g?x={redacted}&y=z",
370        "http://a/b/c/g?&x={redacted}",
371        "http://a/b/c/g?x&x={redacted}",
372    ];
373    #[cfg(feature = "unredacted-logging")]
374    pub const X_QUERY_STRING_EXAMPLES: [&str; 11] = QUERY_STRING_EXAMPLES;
375
376    #[test]
377    fn query_mark_x() {
378        let originals = QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
379        let expecteds = X_QUERY_STRING_EXAMPLES.into_iter().map(Uri::from_static);
380        for (original, expected) in originals.zip(expecteds) {
381            let output = SensitiveUri::new(&original)
382                .query(|key| QueryMarker {
383                    key: false,
384                    value: key == "x",
385                })
386                .to_string();
387            assert_eq!(output, expected.to_string(), "original = {original}");
388        }
389    }
390}