diff --git a/contract-tests/src/main.rs b/contract-tests/src/main.rs index 5da2617..65ca265 100644 --- a/contract-tests/src/main.rs +++ b/contract-tests/src/main.rs @@ -101,6 +101,8 @@ async fn status() -> impl Responder { "service-endpoints".to_string(), "context-type".to_string(), "secure-mode-hash".to_string(), + "inline-context".to_string(), + "anonymous-redaction".to_string(), ], }) } diff --git a/launchdarkly-server-sdk/Cargo.toml b/launchdarkly-server-sdk/Cargo.toml index b2972b1..062d3f6 100644 --- a/launchdarkly-server-sdk/Cargo.toml +++ b/launchdarkly-server-sdk/Cargo.toml @@ -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" diff --git a/launchdarkly-server-sdk/src/events/dispatcher.rs b/launchdarkly-server-sdk/src/events/dispatcher.rs index e4efd5d..34de9e6 100644 --- a/launchdarkly-server-sdk/src/events/dispatcher.rs +++ b/launchdarkly-server-sdk/src/events/dispatcher.rs @@ -187,6 +187,11 @@ impl EventDispatcher { InputEvent::FeatureRequest(fre) => { self.outbox.add_to_summary(&fre); + let inlined = fre.clone().into_inline_with_anonymous_redaction( + self.events_configuration.all_attributes_private, + self.events_configuration.private_attributes.clone(), + ); + if self.notice_context(&fre.base.context) { self.outbox.add_event(OutputEvent::Index(fre.to_index_event( self.events_configuration.all_attributes_private, @@ -202,16 +207,16 @@ 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 { - let event = fre.clone().into_inline( - self.events_configuration.all_attributes_private, - self.events_configuration.private_attributes.clone(), - ); - self.outbox.add_event(OutputEvent::Debug(event)); + self.outbox + .add_event(OutputEvent::Debug(fre.clone().into_inline( + self.events_configuration.all_attributes_private, + self.events_configuration.private_attributes.clone(), + ))); } } if fre.track_events { - self.outbox.add_event(OutputEvent::FeatureRequest(fre)); + self.outbox.add_event(OutputEvent::FeatureRequest(inlined)); } } InputEvent::Identify(identify) => { diff --git a/launchdarkly-server-sdk/src/events/event.rs b/launchdarkly-server-sdk/src/events/event.rs index ea505d0..19442de 100644 --- a/launchdarkly-server-sdk/src/events/event.rs +++ b/launchdarkly-server-sdk/src/events/event.rs @@ -17,6 +17,7 @@ pub struct BaseEvent { // the right structure inline: bool, all_attribute_private: bool, + redact_anonymous: bool, global_private_attributes: HashSet, } @@ -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())?; @@ -51,6 +60,7 @@ impl BaseEvent { inline: false, all_attribute_private: false, global_private_attributes: HashSet::new(), + redact_anonymous: false, } } @@ -66,6 +76,20 @@ impl BaseEvent { ..self } } + + pub(crate) fn into_inline_with_anonymous_redaction( + self, + all_attribute_private: bool, + global_private_attributes: HashSet, + ) -> Self { + Self { + inline: true, + all_attribute_private, + global_private_attributes, + redact_anonymous: true, + ..self + } + } } #[derive(Clone, Debug, PartialEq, Serialize)] @@ -114,6 +138,20 @@ impl FeatureRequestEvent { ..self } } + + pub(crate) fn into_inline_with_anonymous_redaction( + self, + all_attribute_private: bool, + global_private_attributes: HashSet, + ) -> Self { + Self { + base: self.base.into_inline_with_anonymous_redaction( + all_attribute_private, + global_private_attributes, + ), + ..self + } + } } #[derive(Clone, Debug, PartialEq, Serialize)] @@ -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");