From 9b054a6bede78d1dd07510743646559804bb4ebb Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:36:02 -0700 Subject: [PATCH] feat: Redact bearer tokens in insta snapshots (#325) Also, implement `From` for various `Subject` enum variants. --- src/middleware/http/auth/jwt/mod.rs | 130 ++++++++++++++++++ ...wt__tests__subject_from_string@case_1.snap | 21 +++ ...wt__tests__subject_from_string@case_2.snap | 7 + ...wt__tests__subject_from_string@case_3.snap | 7 + ...wt__tests__subject_from_string@case_4.snap | 7 + ...ts__subject_from_u16@subject_from_u16.snap | 7 + ...ts__subject_from_u32@subject_from_u32.snap | 7 + ...ts__subject_from_u64@subject_from_u64.snap | 7 + ...ests__subject_from_u8@subject_from_u8.snap | 7 + ...p__auth__jwt__tests__subject_from_uri.snap | 21 +++ ...__subject_from_uuid@subject_from_uuid.snap | 7 + src/testing/snapshot.rs | 29 ++++ ..._snapshot__tests__bearer_token@case_1.snap | 5 + ..._snapshot__tests__bearer_token@case_2.snap | 5 + ..._snapshot__tests__bearer_token@case_3.snap | 5 + ..._snapshot__tests__bearer_token@case_4.snap | 5 + ..._snapshot__tests__bearer_token@case_5.snap | 5 + 17 files changed, 282 insertions(+) create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_1.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_2.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_3.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_4.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u16@subject_from_u16.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u32@subject_from_u32.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u64@subject_from_u64.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u8@subject_from_u8.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uri.snap create mode 100644 src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uuid@subject_from_uuid.snap create mode 100644 src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_1.snap create mode 100644 src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_2.snap create mode 100644 src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_3.snap create mode 100644 src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_4.snap create mode 100644 src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_5.snap diff --git a/src/middleware/http/auth/jwt/mod.rs b/src/middleware/http/auth/jwt/mod.rs index d81c050a..735c3ea6 100644 --- a/src/middleware/http/auth/jwt/mod.rs +++ b/src/middleware/http/auth/jwt/mod.rs @@ -156,6 +156,70 @@ pub enum Subject { String(String), } +impl From for Subject { + fn from(value: Uuid) -> Self { + Subject::Uuid(value) + } +} + +impl From for Subject { + fn from(value: u8) -> Self { + Subject::Int(value as u64) + } +} + +impl From for Subject { + fn from(value: u16) -> Self { + Subject::Int(value as u64) + } +} + +impl From for Subject { + fn from(value: u32) -> Self { + Subject::Int(value as u64) + } +} + +impl From for Subject { + fn from(value: u64) -> Self { + Subject::Int(value) + } +} + +impl From for Subject { + fn from(value: Url) -> Self { + Subject::Uri(value) + } +} + +impl From for Subject { + fn from(value: String) -> Self { + if let Ok(value) = value.parse::() { + value.into() + } else if let Ok(value) = value.parse::() { + value.into() + } else if let Ok(value) = value.parse::() { + value.into() + } else { + Subject::String(value) + } + } +} + +impl From<&str> for Subject { + fn from(value: &str) -> Self { + if let Ok(value) = value.parse::() { + value.into() + } else if let Ok(value) = value.parse::() { + value.into() + } else if let Ok(value) = value.parse::() { + value.into() + } else { + Subject::String(value.to_string()) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -200,6 +264,13 @@ mod tests { ); } + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn subject_from_uri() { + let subject: Subject = Url::from_str("https://example.com").unwrap().into(); + assert_debug_snapshot!(subject); + } + #[test] #[cfg_attr(coverage_nightly, coverage(off))] fn deserialize_subject_as_uuid() { @@ -208,6 +279,15 @@ mod tests { assert_eq!(value.inner, Subject::Uuid(uuid)); } + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn subject_from_uuid() { + let _case = case(); + + let subject: Subject = Uuid::new_v4().into(); + assert_debug_snapshot!(subject); + } + #[test] #[cfg_attr(coverage_nightly, coverage(off))] fn deserialize_subject_as_int() { @@ -216,6 +296,42 @@ mod tests { assert_eq!(value.inner, Subject::Int(num)); } + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn subject_from_u8() { + let _case = case(); + + let subject: Subject = 12u8.into(); + assert_debug_snapshot!(subject); + } + + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn subject_from_u16() { + let _case = case(); + + let subject: Subject = 1234u16.into(); + assert_debug_snapshot!(subject); + } + + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn subject_from_u32() { + let _case = case(); + + let subject: Subject = 1234u32.into(); + assert_debug_snapshot!(subject); + } + + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn subject_from_u64() { + let _case = case(); + + let subject: Subject = 1234u64.into(); + assert_debug_snapshot!(subject); + } + #[test] #[cfg_attr(coverage_nightly, coverage(off))] fn serialize_subject_int_as_string() { @@ -233,4 +349,18 @@ mod tests { let value: Wrapper = from_str(r#"{"inner": "invalid-uri"}"#).unwrap(); assert_eq!(value.inner, Subject::String("invalid-uri".to_string())); } + + #[rstest] + #[case("http://example.com".to_string())] + #[case(Uuid::new_v4().to_string())] + #[case("1234".to_string())] + #[case("foo".to_string())] + #[cfg_attr(coverage_nightly, coverage(off))] + fn subject_from_string(_case: TestCase, #[case] value: String) { + let subject_from_str: Subject = value.as_str().into(); + let subject: Subject = value.into(); + + assert_eq!(subject, subject_from_str); + assert_debug_snapshot!(subject); + } } diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_1.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_1.snap new file mode 100644 index 00000000..08603b9f --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_1.snap @@ -0,0 +1,21 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Uri( + Url { + scheme: "http", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_2.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_2.snap new file mode 100644 index 00000000..6669aafc --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_2.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Uuid( + [uuid], +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_3.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_3.snap new file mode 100644 index 00000000..dab7c445 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_3.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Int( + 1234, +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_4.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_4.snap new file mode 100644 index 00000000..4cfb226d --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_string@case_4.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +String( + "foo", +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u16@subject_from_u16.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u16@subject_from_u16.snap new file mode 100644 index 00000000..dab7c445 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u16@subject_from_u16.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Int( + 1234, +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u32@subject_from_u32.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u32@subject_from_u32.snap new file mode 100644 index 00000000..dab7c445 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u32@subject_from_u32.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Int( + 1234, +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u64@subject_from_u64.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u64@subject_from_u64.snap new file mode 100644 index 00000000..dab7c445 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u64@subject_from_u64.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Int( + 1234, +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u8@subject_from_u8.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u8@subject_from_u8.snap new file mode 100644 index 00000000..0bbdc7c3 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_u8@subject_from_u8.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Int( + 12, +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uri.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uri.snap new file mode 100644 index 00000000..8b8204a8 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uri.snap @@ -0,0 +1,21 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Uri( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, +) diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uuid@subject_from_uuid.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uuid@subject_from_uuid.snap new file mode 100644 index 00000000..6669aafc --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__subject_from_uuid@subject_from_uuid.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: subject +--- +Uuid( + [uuid], +) diff --git a/src/testing/snapshot.rs b/src/testing/snapshot.rs index 6c2c9155..76ddb2ba 100644 --- a/src/testing/snapshot.rs +++ b/src/testing/snapshot.rs @@ -7,6 +7,8 @@ use itertools::Itertools; use std::thread::current; use typed_builder::TypedBuilder; +const BEARER_TOKEN_REGEX: &str = r"Bearer [\w\.-]+"; + /// Configure which settings to apply on the snapshot [Settings]. /// /// When built, a [TestCase] is returned. @@ -80,6 +82,12 @@ pub struct TestCaseConfig { #[builder(default = true)] pub redact_uuid: bool, + /// Whether to redact auth tokens from snapshots. This is useful for tests involving + /// dynamically created auth tokens that will be different on every test run, or involve real + /// auth tokens that you don't want leaked in your source code. + #[builder(default = true)] + pub redact_auth_tokens: bool, + /// Whether to automatically bind the [Settings] to the current scope. If `true`, the settings /// will be automatically applied for the test in which the [TestCase] was built. If `false`, /// the settings will only be applied after manually calling [Settings::bind_to_scope], or @@ -165,6 +173,9 @@ impl From for TestCase { if value.redact_uuid { snapshot_redact_uuid(&mut settings); } + if value.redact_auth_tokens { + snapshot_redact_bearer_tokens(&mut settings); + } let _settings_guard = if value.bind_scope { Some(settings.bind_to_scope()) @@ -196,6 +207,13 @@ pub fn snapshot_redact_uuid(settings: &mut Settings) -> &mut Settings { settings } +/// Redact instances of UUIDs in snapshots. Applies a filter on the [Settings] to replace +/// sub-strings matching [UUID_REGEX] with `[uuid]`. +pub fn snapshot_redact_bearer_tokens(settings: &mut Settings) -> &mut Settings { + settings.add_filter(BEARER_TOKEN_REGEX, "Sensitive"); + settings +} + /// Extract the last segment of the current thread name to use as the test case description. /// /// See: @@ -286,6 +304,17 @@ mod tests { assert_snapshot!(format!("Foo '{uuid}' bar")); } + #[rstest] + #[case("Bearer 1234")] + #[case("Bearer access-token")] + #[case("Bearer some.jwt.token")] + #[case("Bearer foo-bar.baz-1234")] + #[case("Bearer token;")] + #[cfg_attr(coverage_nightly, coverage(off))] + fn bearer_token(_case: TestCase, #[case] token: &str) { + assert_snapshot!(format!("Foo {token} bar")); + } + #[rstest] #[case("")] #[case("foo")] diff --git a/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_1.snap b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_1.snap new file mode 100644 index 00000000..5f17bbf7 --- /dev/null +++ b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_1.snap @@ -0,0 +1,5 @@ +--- +source: src/testing/snapshot.rs +expression: "format!(\"Foo {token} bar\")" +--- +Foo Sensitive bar diff --git a/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_2.snap b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_2.snap new file mode 100644 index 00000000..5f17bbf7 --- /dev/null +++ b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_2.snap @@ -0,0 +1,5 @@ +--- +source: src/testing/snapshot.rs +expression: "format!(\"Foo {token} bar\")" +--- +Foo Sensitive bar diff --git a/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_3.snap b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_3.snap new file mode 100644 index 00000000..5f17bbf7 --- /dev/null +++ b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_3.snap @@ -0,0 +1,5 @@ +--- +source: src/testing/snapshot.rs +expression: "format!(\"Foo {token} bar\")" +--- +Foo Sensitive bar diff --git a/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_4.snap b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_4.snap new file mode 100644 index 00000000..5f17bbf7 --- /dev/null +++ b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_4.snap @@ -0,0 +1,5 @@ +--- +source: src/testing/snapshot.rs +expression: "format!(\"Foo {token} bar\")" +--- +Foo Sensitive bar diff --git a/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_5.snap b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_5.snap new file mode 100644 index 00000000..230282cc --- /dev/null +++ b/src/testing/snapshots/roadster__testing__snapshot__tests__bearer_token@case_5.snap @@ -0,0 +1,5 @@ +--- +source: src/testing/snapshot.rs +expression: "format!(\"Foo {token} bar\")" +--- +Foo Sensitive; bar