Skip to content

Commit

Permalink
feat: Redact anonymous attributes within feature events
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Jan 11, 2024
1 parent e2e8cb9 commit d37f52f
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 7 deletions.
1 change: 1 addition & 0 deletions contract-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ async fn status() -> impl Responder {
"context-type".to_string(),
"secure-mode-hash".to_string(),
"inline-context".to_string(),
"anonymous-redaction".to_string(),
],
})
}
Expand Down
8 changes: 6 additions & 2 deletions launchdarkly-server-sdk/src/events/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ impl EventDispatcher {
InputEvent::FeatureRequest(fre) => {
self.outbox.add_to_summary(&fre);

let inlined = fre.clone().into_inline(
let inlined = fre.clone().into_inline_with_anonymous_redaction(
self.events_configuration.all_attributes_private,
self.events_configuration.private_attributes.clone(),
);
Expand All @@ -207,7 +207,11 @@ impl EventDispatcher {
if let Some(debug_events_until_date) = fre.debug_events_until_date {
let time = u128::from(debug_events_until_date);
if time > now && time > self.last_known_time {
self.outbox.add_event(OutputEvent::Debug(inlined.clone()));
self.outbox
.add_event(OutputEvent::Debug(fre.clone().into_inline(
self.events_configuration.all_attributes_private,
self.events_configuration.private_attributes.clone(),
)));
}
}

Expand Down
175 changes: 170 additions & 5 deletions launchdarkly-server-sdk/src/events/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct BaseEvent {
// the right structure
inline: bool,
all_attribute_private: bool,
redact_anonymous: bool,
global_private_attributes: HashSet<Reference>,
}

Expand All @@ -29,11 +30,20 @@ impl Serialize for BaseEvent {
state.serialize_field("creationDate", &self.creation_date)?;

if self.inline {
let context_attribute = ContextAttributes::from_context(
self.context.clone(),
self.all_attribute_private,
self.global_private_attributes.clone(),
);
let context_attribute: ContextAttributes;
if self.redact_anonymous {
context_attribute = ContextAttributes::from_context_with_anonymous_redaction(
self.context.clone(),
self.all_attribute_private,
self.global_private_attributes.clone(),
);
} else {
context_attribute = ContextAttributes::from_context(
self.context.clone(),
self.all_attribute_private,
self.global_private_attributes.clone(),
);
}
state.serialize_field("context", &context_attribute)?;
} else {
state.serialize_field("contextKeys", &self.context.context_keys())?;
Expand All @@ -51,6 +61,7 @@ impl BaseEvent {
inline: false,
all_attribute_private: false,
global_private_attributes: HashSet::new(),
redact_anonymous: false,
}
}

Expand All @@ -66,6 +77,20 @@ impl BaseEvent {
..self
}
}

pub(crate) fn into_inline_with_anonymous_redaction(
self,
all_attribute_private: bool,
global_private_attributes: HashSet<Reference>,
) -> Self {
Self {
inline: true,
all_attribute_private,
global_private_attributes,
redact_anonymous: true,
..self
}
}
}

#[derive(Clone, Debug, PartialEq, Serialize)]
Expand Down Expand Up @@ -114,6 +139,20 @@ impl FeatureRequestEvent {
..self
}
}

pub(crate) fn into_inline_with_anonymous_redaction(
self,
all_attribute_private: bool,
global_private_attributes: HashSet<Reference>,
) -> Self {
Self {
base: self.base.into_inline_with_anonymous_redaction(
all_attribute_private,
global_private_attributes,
),
..self
}
}
}

#[derive(Clone, Debug, PartialEq, Serialize)]
Expand Down Expand Up @@ -720,6 +759,132 @@ mod tests {
}
}

#[test]
fn serializes_feature_request_event_with_anonymous_attribute_redaction() {
let flag = basic_flag("flag");
let default = FlagValue::from(false);
let context = ContextBuilder::new("alice")
.anonymous(true)
.set_value("foo", AttributeValue::Bool(true))
.build()
.expect("Failed to create context");
let fallthrough = Detail {
value: Some(FlagValue::from(false)),
variation_index: Some(1),
reason: Reason::Fallthrough {
in_experiment: false,
},
};

let event_factory = EventFactory::new(true);
let mut feature_request_event =
event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None);
// fix creation date so JSON is predictable
feature_request_event.base_mut().unwrap().creation_date = 1234;

if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
let output_event = OutputEvent::FeatureRequest(
feature_request_event.into_inline_with_anonymous_redaction(false, HashSet::new()),
);
let event_json = json!({
"kind": "feature",
"creationDate": 1234,
"context": {
"_meta": {
"redactedAttributes" : ["foo"]
},
"key": "alice",
"kind": "user",
"anonymous": true
},
"key": "flag",
"value": false,
"variation": 1,
"default": false,
"reason": {
"kind": "FALLTHROUGH"
},
"version": 42
});

assert_json_eq!(output_event, event_json);
}
}

#[test]
fn serializes_feature_request_event_with_anonymous_attribute_redaction_in_multikind_context() {
let flag = basic_flag("flag");
let default = FlagValue::from(false);
let user_context = ContextBuilder::new("alice")
.anonymous(true)
.set_value("foo", AttributeValue::Bool(true))
.build()
.expect("Failed to create user context");
let org_context = ContextBuilder::new("LaunchDarkly")
.kind("org")
.set_value("foo", AttributeValue::Bool(true))
.build()
.expect("Failed to create org context");
let multi_context = MultiContextBuilder::new()
.add_context(user_context)
.add_context(org_context)
.build()
.expect("Failed to create multi context");
let fallthrough = Detail {
value: Some(FlagValue::from(false)),
variation_index: Some(1),
reason: Reason::Fallthrough {
in_experiment: false,
},
};

let event_factory = EventFactory::new(true);
let mut feature_request_event = event_factory.new_eval_event(
&flag.key,
multi_context,
&flag,
fallthrough,
default,
None,
);
// fix creation date so JSON is predictable
feature_request_event.base_mut().unwrap().creation_date = 1234;

if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
let output_event = OutputEvent::FeatureRequest(
feature_request_event.into_inline_with_anonymous_redaction(false, HashSet::new()),
);
let event_json = json!({
"kind": "feature",
"creationDate": 1234,
"context": {
"kind": "multi",
"user": {
"_meta": {
"redactedAttributes" : ["foo"]
},
"key": "alice",
"anonymous": true
},
"org": {
"foo": true,
"key": "LaunchDarkly"
}
},
"key": "flag",
"value": false,
"variation": 1,
"default": false,
"reason": {
"kind": "FALLTHROUGH"
},
"version": 42
});

assert_json_eq!(output_event, event_json);
}
}

#[test]
fn serializes_feature_request_event_with_local_private_attribute() {
let flag = basic_flag("flag");
Expand Down

0 comments on commit d37f52f

Please sign in to comment.