From 4d07a8611348c8d94303c835e0743198fe226e1a Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Tue, 9 Jan 2024 11:00:59 -0500 Subject: [PATCH 1/2] feat: Redact anonymous attributes within feature events --- contract-tests/service.rb | 1 + lib/ldclient-rb/events.rb | 10 ++++++---- lib/ldclient-rb/impl/context_filter.rb | 22 +++++++++++++--------- spec/events_spec.rb | 10 ++++++---- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/contract-tests/service.rb b/contract-tests/service.rb index d7d58720..2442a1c9 100644 --- a/contract-tests/service.rb +++ b/contract-tests/service.rb @@ -37,6 +37,7 @@ 'event-sampling', 'context-comparison', 'inline-context', + 'anonymous-redaction', ], }.to_json end diff --git a/lib/ldclient-rb/events.rb b/lib/ldclient-rb/events.rb index f0ea7fd5..d1dd0242 100644 --- a/lib/ldclient-rb/events.rb +++ b/lib/ldclient-rb/events.rb @@ -481,12 +481,14 @@ def make_output_events(events, summary) key: event.key, value: event.value, } + out[:default] = event.default unless event.default.nil? out[:variation] = event.variation unless event.variation.nil? out[:version] = event.version unless event.version.nil? out[:prereqOf] = event.prereq_of unless event.prereq_of.nil? - out[:context] = @context_filter.filter(event.context) + out[:context] = @context_filter.filter(event.context, true) out[:reason] = event.reason unless event.reason.nil? + out when LaunchDarkly::Impl::MigrationOpEvent @@ -552,7 +554,7 @@ def make_output_events(events, summary) kind: IDENTIFY_KIND, creationDate: event.timestamp, key: event.context.fully_qualified_key, - context: @context_filter.filter(event.context), + context: @context_filter.filter(event.context, false), } when LaunchDarkly::Impl::CustomEvent @@ -570,7 +572,7 @@ def make_output_events(events, summary) { kind: INDEX_KIND, creationDate: event.timestamp, - context: @context_filter.filter(event.context), + context: @context_filter.filter(event.context, false), } when LaunchDarkly::Impl::DebugEvent @@ -579,7 +581,7 @@ def make_output_events(events, summary) kind: DEBUG_KIND, creationDate: original.timestamp, key: original.key, - context: @context_filter.filter(original.context), + context: @context_filter.filter(original.context, false), value: original.value, } out[:default] = original.default unless original.default.nil? diff --git a/lib/ldclient-rb/impl/context_filter.rb b/lib/ldclient-rb/impl/context_filter.rb index 8ed0c19f..467bd671 100644 --- a/lib/ldclient-rb/impl/context_filter.rb +++ b/lib/ldclient-rb/impl/context_filter.rb @@ -20,17 +20,18 @@ def initialize(all_attributes_private, private_attributes) # redaction applied. # # @param context [LaunchDarkly::LDContext] + # @param redact_anonymous [Boolean] # @return [Hash] # - def filter(context) - return filter_single_context(context, true) unless context.multi_kind? + def filter(context, redact_anonymous) + return filter_single_context(context, true, redact_anonymous) unless context.multi_kind? filtered = {kind: 'multi'} (0...context.individual_context_count).each do |i| c = context.individual_context(i) next if c.nil? - filtered[c.kind] = filter_single_context(c, false) + filtered[c.kind] = filter_single_context(c, false, redact_anonymous) end filtered @@ -43,22 +44,24 @@ def filter(context) # @param include_kind [Boolean] # @return [Hash] # - private def filter_single_context(context, include_kind) + private def filter_single_context(context, include_kind, redact_anonymous) filtered = {key: context.key} filtered[:kind] = context.kind if include_kind - filtered[:anonymous] = true if context.get_value(:anonymous) + + anonymous = context.get_value(:anonymous) + filtered[:anonymous] = true if anonymous redacted = [] private_attributes = @private_attributes.concat(context.private_attributes) name = context.get_value(:name) - if !name.nil? && !check_whole_attribute_private(:name, private_attributes, redacted) + if !name.nil? && !check_whole_attribute_private(:name, private_attributes, redacted, anonymous && redact_anonymous) filtered[:name] = name end context.get_custom_attribute_names.each do |attribute| - unless check_whole_attribute_private(attribute, private_attributes, redacted) + unless check_whole_attribute_private(attribute, private_attributes, redacted, anonymous && redact_anonymous) value = context.get_value(attribute) filtered[attribute] = redact_json_value(nil, attribute, value, private_attributes, redacted) end @@ -75,10 +78,11 @@ def filter(context) # @param attribute [Symbol] # @param private_attributes [Array] # @param redacted [Array] + # @param redact_all [Boolean] # @return [Boolean] # - private def check_whole_attribute_private(attribute, private_attributes, redacted) - if @all_attributes_private + private def check_whole_attribute_private(attribute, private_attributes, redacted, redact_all) + if @all_attributes_private || redact_all redacted << attribute return true end diff --git a/spec/events_spec.rb b/spec/events_spec.rb index 1cdf84ae..007c9c19 100644 --- a/spec/events_spec.rb +++ b/spec/events_spec.rb @@ -601,7 +601,7 @@ def index_event(config, context, timestamp = starting_timestamp) out = { kind: "index", creationDate: timestamp, - context: context_filter.filter(context), + context: context_filter.filter(context, false), } JSON.parse(out.to_json, symbolize_names: true) end @@ -618,7 +618,7 @@ def identify_event(config, context, timestamp = starting_timestamp) kind: "identify", creationDate: timestamp, key: context.fully_qualified_key, - context: context_filter.filter(context), + context: context_filter.filter(context, false), } JSON.parse(out.to_json, symbolize_names: true) end @@ -634,10 +634,12 @@ def identify_event(config, context, timestamp = starting_timestamp) # def feature_event(config, flag, context, variation, value, timestamp = starting_timestamp) context_filter = Impl::ContextFilter.new(config.all_attributes_private, config.private_attributes) + redacted_context = context_filter.filter(context, true) + out = { kind: 'feature', creationDate: timestamp, - context: context_filter.filter(context), + context: redacted_context, key: flag[:key], variation: variation, version: flag[:version], @@ -691,7 +693,7 @@ def debug_event(config, flag, context, variation, value, timestamp = starting_ti variation: variation, version: flag[:version], value: value, - context: context_filter.filter(context), + context: context_filter.filter(context, false), } JSON.parse(out.to_json, symbolize_names: true) end From 77ebb8280afe881d1c3bc1aa47eb2f3176a4da6d Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 10 Jan 2024 11:05:57 -0500 Subject: [PATCH 2/2] Add filter_redact_anonymous --- lib/ldclient-rb/events.rb | 8 ++++---- lib/ldclient-rb/impl/context_filter.rb | 21 +++++++++++++++++++-- spec/events_spec.rb | 8 ++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/ldclient-rb/events.rb b/lib/ldclient-rb/events.rb index d1dd0242..4800163b 100644 --- a/lib/ldclient-rb/events.rb +++ b/lib/ldclient-rb/events.rb @@ -486,7 +486,7 @@ def make_output_events(events, summary) out[:variation] = event.variation unless event.variation.nil? out[:version] = event.version unless event.version.nil? out[:prereqOf] = event.prereq_of unless event.prereq_of.nil? - out[:context] = @context_filter.filter(event.context, true) + out[:context] = @context_filter.filter_redact_anonymous(event.context) out[:reason] = event.reason unless event.reason.nil? out @@ -554,7 +554,7 @@ def make_output_events(events, summary) kind: IDENTIFY_KIND, creationDate: event.timestamp, key: event.context.fully_qualified_key, - context: @context_filter.filter(event.context, false), + context: @context_filter.filter(event.context), } when LaunchDarkly::Impl::CustomEvent @@ -572,7 +572,7 @@ def make_output_events(events, summary) { kind: INDEX_KIND, creationDate: event.timestamp, - context: @context_filter.filter(event.context, false), + context: @context_filter.filter(event.context), } when LaunchDarkly::Impl::DebugEvent @@ -581,7 +581,7 @@ def make_output_events(events, summary) kind: DEBUG_KIND, creationDate: original.timestamp, key: original.key, - context: @context_filter.filter(original.context, false), + context: @context_filter.filter(original.context), value: original.value, } out[:default] = original.default unless original.default.nil? diff --git a/lib/ldclient-rb/impl/context_filter.rb b/lib/ldclient-rb/impl/context_filter.rb index 467bd671..bec00400 100644 --- a/lib/ldclient-rb/impl/context_filter.rb +++ b/lib/ldclient-rb/impl/context_filter.rb @@ -20,10 +20,27 @@ def initialize(all_attributes_private, private_attributes) # redaction applied. # # @param context [LaunchDarkly::LDContext] - # @param redact_anonymous [Boolean] # @return [Hash] # - def filter(context, redact_anonymous) + def filter(context) + internal_filter(context, false) + end + + # + # Return a hash representation of the provided context with attribute + # redaction applied. + # + # If a context is anonyomous, all attributes will be redacted except + # for key, kind, and anonymous. + # + # @param context [LaunchDarkly::LDContext] + # @return [Hash] + # + def filter_redact_anonymous(context) + internal_filter(context, true) + end + + private def internal_filter(context, redact_anonymous) return filter_single_context(context, true, redact_anonymous) unless context.multi_kind? filtered = {kind: 'multi'} diff --git a/spec/events_spec.rb b/spec/events_spec.rb index 007c9c19..f10100fd 100644 --- a/spec/events_spec.rb +++ b/spec/events_spec.rb @@ -601,7 +601,7 @@ def index_event(config, context, timestamp = starting_timestamp) out = { kind: "index", creationDate: timestamp, - context: context_filter.filter(context, false), + context: context_filter.filter(context), } JSON.parse(out.to_json, symbolize_names: true) end @@ -618,7 +618,7 @@ def identify_event(config, context, timestamp = starting_timestamp) kind: "identify", creationDate: timestamp, key: context.fully_qualified_key, - context: context_filter.filter(context, false), + context: context_filter.filter(context), } JSON.parse(out.to_json, symbolize_names: true) end @@ -634,7 +634,7 @@ def identify_event(config, context, timestamp = starting_timestamp) # def feature_event(config, flag, context, variation, value, timestamp = starting_timestamp) context_filter = Impl::ContextFilter.new(config.all_attributes_private, config.private_attributes) - redacted_context = context_filter.filter(context, true) + redacted_context = context_filter.filter_redact_anonymous(context) out = { kind: 'feature', @@ -693,7 +693,7 @@ def debug_event(config, flag, context, variation, value, timestamp = starting_ti variation: variation, version: flag[:version], value: value, - context: context_filter.filter(context, false), + context: context_filter.filter(context), } JSON.parse(out.to_json, symbolize_names: true) end