Skip to content

Commit

Permalink
feat: Redact anonymous attributes within feature events (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Mar 15, 2024
1 parent b31b5e7 commit 66e2e54
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 8 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
2 changes: 1 addition & 1 deletion launchdarkly-server-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ lazy_static = "1.4.0"
log = "0.4.14"
lru = { version = "0.12.0", default-features = false }
ring = "0.17.5"
launchdarkly-server-sdk-evaluation = "1.1.1"
launchdarkly-server-sdk-evaluation = "1.2.0"
serde = { version = "1.0.132", features = ["derive"] }
serde_json = { version = "1.0.73", features = ["float_roundtrip"] }
thiserror = "1.0"
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
174 changes: 169 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,19 @@ 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 {
ContextAttributes::from_context_with_anonymous_redaction(
self.context.clone(),
self.all_attribute_private,
self.global_private_attributes.clone(),
)
} else {
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 +60,7 @@ impl BaseEvent {
inline: false,
all_attribute_private: false,
global_private_attributes: HashSet::new(),
redact_anonymous: false,
}
}

Expand All @@ -66,6 +76,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 +138,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 +758,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 66e2e54

Please sign in to comment.