Skip to content

Commit

Permalink
feat: Redact bearer tokens in insta snapshots (#325)
Browse files Browse the repository at this point in the history
Also, implement `From` for various `Subject` enum variants.
  • Loading branch information
spencewenski authored Aug 11, 2024
1 parent 9ede96c commit 9b054a6
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 0 deletions.
130 changes: 130 additions & 0 deletions src/middleware/http/auth/jwt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,70 @@ pub enum Subject {
String(String),
}

impl From<Uuid> for Subject {
fn from(value: Uuid) -> Self {
Subject::Uuid(value)
}
}

impl From<u8> for Subject {
fn from(value: u8) -> Self {
Subject::Int(value as u64)
}
}

impl From<u16> for Subject {
fn from(value: u16) -> Self {
Subject::Int(value as u64)
}
}

impl From<u32> for Subject {
fn from(value: u32) -> Self {
Subject::Int(value as u64)
}
}

impl From<u64> for Subject {
fn from(value: u64) -> Self {
Subject::Int(value)
}
}

impl From<Url> for Subject {
fn from(value: Url) -> Self {
Subject::Uri(value)
}
}

impl From<String> for Subject {
fn from(value: String) -> Self {
if let Ok(value) = value.parse::<Url>() {
value.into()
} else if let Ok(value) = value.parse::<Uuid>() {
value.into()
} else if let Ok(value) = value.parse::<u64>() {
value.into()
} else {
Subject::String(value)
}
}
}

impl From<&str> for Subject {
fn from(value: &str) -> Self {
if let Ok(value) = value.parse::<Url>() {
value.into()
} else if let Ok(value) = value.parse::<Uuid>() {
value.into()
} else if let Ok(value) = value.parse::<u64>() {
value.into()
} else {
Subject::String(value.to_string())
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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() {
Expand All @@ -233,4 +349,18 @@ mod tests {
let value: Wrapper<Subject> = 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);
}
}
Original file line number Diff line number Diff line change
@@ -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,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
Uuid(
[uuid],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
Int(
1234,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
String(
"foo",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
Int(
1234,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
Int(
1234,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
Int(
1234,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
Int(
12,
)
Original file line number Diff line number Diff line change
@@ -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,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: src/middleware/http/auth/jwt/mod.rs
expression: subject
---
Uuid(
[uuid],
)
29 changes: 29 additions & 0 deletions src/testing/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -165,6 +173,9 @@ impl From<TestCaseConfig> 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())
Expand Down Expand Up @@ -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: <https://github.com/adriangb/pgpq/blob/b0b0f8c77c862c0483d81571e76f3a2b746136fc/pgpq/src/lib.rs#L649-L669>
Expand Down Expand Up @@ -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")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: src/testing/snapshot.rs
expression: "format!(\"Foo {token} bar\")"
---
Foo Sensitive bar
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: src/testing/snapshot.rs
expression: "format!(\"Foo {token} bar\")"
---
Foo Sensitive bar
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: src/testing/snapshot.rs
expression: "format!(\"Foo {token} bar\")"
---
Foo Sensitive bar
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: src/testing/snapshot.rs
expression: "format!(\"Foo {token} bar\")"
---
Foo Sensitive bar
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: src/testing/snapshot.rs
expression: "format!(\"Foo {token} bar\")"
---
Foo Sensitive; bar

0 comments on commit 9b054a6

Please sign in to comment.