Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/contexts/attribute_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ impl Reference {
}
}

/// Constructs a [Reference] from a literal top-level attribute name, escaping it per the
/// backwards-compatibility rules so a name beginning with '/' is not read as pointer syntax.
pub(crate) fn from_literal_name(name: &str) -> Self {
if !name.starts_with('/') {
return Self::new(name);
}
let mut escaped = name.replace('~', "~0").replace('/', "~1");
escaped.insert(0, '/');
Self::new(escaped)
}

/// Returns true if the reference is valid.
pub fn is_valid(&self) -> bool {
!matches!(&self.variant, Variant::Error(_))
Expand Down Expand Up @@ -303,12 +314,7 @@ impl From<AttributeName> for Reference {
/// string passed into the Reference constructor, not the original AttributeName. This
/// is desirable since data should be "upgraded" into the new format as it is encountered.
fn from(name: AttributeName) -> Self {
if !name.0.starts_with('/') {
return Self::new(name.0);
}
let mut escaped = name.0.replace('~', "~0").replace('/', "~1");
escaped.insert(0, '/');
Self::new(escaped)
Reference::from_literal_name(&name.0)
}
}

Expand Down
71 changes: 69 additions & 2 deletions src/contexts/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ impl ContextAttributes {
self.all_attributes_private || (self.redact_anonymous && context.anonymous);

for key in optional_attribute_names.iter() {
let reference = Reference::new(key);
let reference = Reference::from_literal_name(key);
if let Some(value) = context.get_value(&reference) {
// If redact_all is true, then there's no complex filtering or
// recursing to be done: all of these values are by definition private, so just add
Expand Down Expand Up @@ -867,9 +867,10 @@ mod tests {
use crate::{AttributeValue, ContextBuilder, MultiContextBuilder, Reference};
use maplit::hashmap;
use proptest::proptest;
use std::collections::HashSet;
use test_case::test_case;

use super::Kind;
use super::{ContextAttributes, Kind};

proptest! {
#[test]
Expand Down Expand Up @@ -1144,4 +1145,70 @@ mod tests {

assert!(context.is_err());
}

// Collects the strings in an event context's redactedAttributes as a set, since their order is
// not significant.
fn redacted_attributes(json: &serde_json::Value) -> HashSet<String> {
json["_meta"]["redactedAttributes"]
.as_array()
.expect("redactedAttributes should be an array")
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect()
}

#[test_case("/ssn", "/~1ssn"; "slash prefix is escaped")]
#[test_case("/a~b", "/~1a~0b"; "slash prefix with tilde escapes both")]
fn all_attributes_private_reports_escaped_references(name: &str, expected_ref: &str) {
let context = ContextBuilder::new("my-key")
.set_string(name, "secret")
.build()
.expect("Failed to build context");

let attributes = ContextAttributes::from_context(context, true, HashSet::new());
let json = serde_json::to_value(&attributes).expect("Failed to serialize");

assert_eq!(
redacted_attributes(&json),
HashSet::from([expected_ref.to_string()])
);
// The attribute value must not be leaked into the serialized output.
assert!(json.get(name).is_none());
}

#[test]
fn anonymous_redaction_reports_slash_prefixed_names_as_escaped_references() {
let context = ContextBuilder::new("my-key")
.anonymous(true)
.set_string("/ssn", "123-45-6789")
.build()
.expect("Failed to build context");

let attributes = ContextAttributes::from_context_with_anonymous_redaction(
context,
false,
HashSet::new(),
);
let json = serde_json::to_value(&attributes).expect("Failed to serialize");

assert_eq!(
redacted_attributes(&json),
HashSet::from(["/~1ssn".to_string()])
);
assert!(json.get("/ssn").is_none());
}

#[test]
fn slash_prefixed_attribute_is_serialized_under_its_literal_name_when_not_private() {
let context = ContextBuilder::new("my-key")
.set_string("/ssn", "123-45-6789")
.build()
.expect("Failed to build context");

let attributes = ContextAttributes::from_context(context, false, HashSet::new());
let json = serde_json::to_value(&attributes).expect("Failed to serialize");

assert_eq!(json["/ssn"], serde_json::json!("123-45-6789"));
assert!(json.get("_meta").is_none());
}
}
Loading