Skip to content
Merged
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
8 changes: 4 additions & 4 deletions lib/ldclient-rb/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Config
# @option opts [Float] :poll_interval (30) See {#poll_interval}.
# @option opts [Boolean] :stream (true) See {#stream?}.
# @option opts [Boolean] all_attributes_private (false) See {#all_attributes_private}.
# @option opts [Array] :private_attributes See {#private_attributes}.
# @option opts [Enumerable<String>] :private_attributes See {#private_attributes}.
# @option opts [Boolean] :send_events (true) See {#send_events}.
# @option opts [Integer] :context_keys_capacity (1000) See {#context_keys_capacity}.
# @option opts [Float] :context_keys_flush_interval (300) See {#context_keys_flush_interval}.
Expand Down Expand Up @@ -69,7 +69,7 @@ def initialize(opts = {})
@offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > Config.default_poll_interval ? opts[:poll_interval] : Config.default_poll_interval
@all_attributes_private = opts[:all_attributes_private] || false
@private_attributes = opts[:private_attributes] || []
@private_attributes = Set.new(opts[:private_attributes] || []).freeze
@send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events
@compress_events = opts.has_key?(:compress_events) ? opts[:compress_events] : Config.default_compress_events
@context_keys_capacity = opts[:context_keys_capacity] || Config.default_context_keys_capacity
Expand Down Expand Up @@ -255,7 +255,7 @@ def offline?
attr_reader :all_attributes_private

#
# A list of context attribute names that should always be considered private. This means that the
# A set of context attribute names that should always be considered private. This means that the
# attribute values will not be sent to LaunchDarkly in analytics events and will not appear on
# the LaunchDarkly dashboard.
#
Expand All @@ -268,7 +268,7 @@ def offline?
#
# @see https://docs.launchdarkly.com/sdk/features/user-context-config#using-private-attributes
#
# @return [Array<String>]
# @return [Set<String>]
# @see #all_attributes_private
#
attr_reader :private_attributes
Expand Down
19 changes: 8 additions & 11 deletions lib/ldclient-rb/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class LDContext
private_constant :ERR_KIND_MULTI_DUPLICATES
ERR_CUSTOM_NON_HASH = 'context custom must be a hash'
private_constant :ERR_CUSTOM_NON_HASH
ERR_PRIVATE_NON_ARRAY = 'context private attributes must be an array'
ERR_PRIVATE_NON_ARRAY = 'context private attributes must be an array or a set'

# @return [String, nil] Returns the key for this context
attr_reader :key
Expand All @@ -66,7 +66,7 @@ class LDContext
# @param name [String, nil]
# @param anonymous [Boolean, nil]
# @param attributes [Hash, nil]
# @param private_attributes [Array<String>, nil]
# @param private_attributes [Array<String>, Set<String>, nil]
# @param error [String, nil]
# @param contexts [Array<LDContext>, nil]
#
Expand All @@ -82,6 +82,7 @@ def initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attr
reference = Reference.create(attribute)
@private_attributes.add(reference) if reference.error.nil?
end
@private_attributes.freeze
@error = error
@contexts = contexts
@is_multi = !contexts.nil?
Expand All @@ -91,12 +92,9 @@ def initialize(key, fully_qualified_key, kind, name = nil, anonymous = nil, attr
protected attr_reader :name, :anonymous, :attributes

#
# @return [Array<Reference>] Returns the private attributes associated with this LDContext
# @return [Set<Reference>] Returns the private attributes associated with this LDContext. The set is frozen.
#
def private_attributes
# TODO(sc-227265): Return a set instead of an array.
@private_attributes.to_a
end
attr_reader :private_attributes

#
# @return [Boolean] Is this LDContext a multi-kind context?
Expand Down Expand Up @@ -353,8 +351,7 @@ def ==(other)
return false unless self.anonymous == other.anonymous
return false unless self.attributes == other.attributes

# TODO(sc-227265): Calling .to_set is unnecessary once private_attributes are sets.
return false unless self.private_attributes.to_set == other.private_attributes.to_set
return false unless self.private_attributes == other.private_attributes

true
end
Expand Down Expand Up @@ -413,7 +410,7 @@ def to_h

hash[:name] = name unless name.nil?
hash[:anonymous] = anonymous if anonymous
hash[:_meta] = {privateAttributes: private_attributes} unless private_attributes.empty?
hash[:_meta] = {privateAttributes: private_attributes.to_a} unless private_attributes.empty?

hash
end
Expand Down Expand Up @@ -570,7 +567,7 @@ def self.create_multi(contexts)

meta = data.fetch(:_meta, {})
private_attributes = meta[:privateAttributes]
if private_attributes && !private_attributes.is_a?(Array)
if private_attributes && !private_attributes.is_a?(Array) && !private_attributes.is_a?(Set)
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
return create_invalid_context(ERR_PRIVATE_NON_ARRAY)
end

Expand Down
10 changes: 5 additions & 5 deletions lib/ldclient-rb/impl/context_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ module Impl
class ContextFilter
#
# @param all_attributes_private [Boolean]
# @param private_attributes [Array<String>]
# @param private_attributes [Enumerable<String>]
# @param logger [Logger, nil]
#
def initialize(all_attributes_private, private_attributes, logger = nil)
@all_attributes_private = all_attributes_private
@logger = logger
@non_symbol_name_logged = Concurrent::AtomicBoolean.new(false)

@private_attributes = []
@private_attributes = Set.new
private_attributes.each do |attribute|
reference = LaunchDarkly::Reference.create(attribute)
@private_attributes << reference if reference.error.nil?
Expand Down Expand Up @@ -75,7 +75,7 @@ def filter_redact_anonymous(context)
filtered[:anonymous] = true if anonymous

redacted = []
private_attributes = @private_attributes + context.private_attributes
private_attributes = @private_attributes | context.private_attributes

name = context.get_value(:name)
if !name.nil? && !check_whole_attribute_private(Reference.create_literal(:name), private_attributes, redacted, anonymous && redact_anonymous)
Expand Down Expand Up @@ -103,7 +103,7 @@ def filter_redact_anonymous(context)
# Check if an entire attribute should be redacted.
#
# @param attribute [Reference]
# @param private_attributes [Array<Reference>]
# @param private_attributes [Set<Reference>]
# @param redacted [Array<Symbol>]
# @param redact_all [Boolean]
# @return [Boolean]
Expand All @@ -130,7 +130,7 @@ def filter_redact_anonymous(context)
# @param parent_path [Array<String>, nil]
# @param name [String]
# @param value [any]
# @param private_attributes [Array<Reference>]
# @param private_attributes [Set<Reference>]
# @param redacted [Array<Symbol>]
# @param visited [Array<Hash>]
# @return [any]
Expand Down
6 changes: 5 additions & 1 deletion lib/ldclient-rb/reference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,14 @@ def initialize(raw_path, components = [], error = nil)
# validation fails, {#error} will return a non-nil error and any SDK method
# that takes this Reference as a parameter will consider it invalid.
#
# @param value [String, Symbol]
# If the value is a Reference, this method returns the same Reference.
#
# @param value [String, Symbol, Reference]
# @return [Reference]
#
def self.create(value)
return value if value.is_a?(Reference)

unless value.is_a?(String) || value.is_a?(Symbol)
return new(value, [], ERR_EMPTY)
end
Expand Down
11 changes: 11 additions & 0 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,16 @@ module LaunchDarkly
expect(subject.new(omit_anonymous_contexts: true).omit_anonymous_contexts).to eq true
end
end
describe ".private_attributes" do
it "defaults to an empty set" do
expect(subject.new.private_attributes).to eq Set.new
end
it "converts an array to a set" do
expect(subject.new(private_attributes: ["name", "email", "name"]).private_attributes).to eq Set["name", "email"]
end
it "is frozen" do
expect(subject.new(private_attributes: ["name"]).private_attributes).to be_frozen
end
end
end
end
26 changes: 26 additions & 0 deletions spec/context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ module LaunchDarkly
expect(subject.create(context).valid?).to be false
end

it "accepts privateAttributes as a set" do
context = subject.create({ key: "user-key", kind: "user", _meta: { privateAttributes: Set["email"] } })
expect(context.valid?).to be true
expect(context.private_attributes).to eq(Set[LaunchDarkly::Reference.create("email")])
end

it "accepts privateAttributes from another context" do
original = subject.create({ key: "user-key", kind: "user", _meta: { privateAttributes: ["email", "/address/city"] } })
context = subject.create({ key: "user-key", kind: "user", _meta: { privateAttributes: original.private_attributes } })
expect(context.valid?).to be true
expect(context.private_attributes).to eq(original.private_attributes)
end

it "creates the correct fully qualified key" do
expect(subject.create({ key: "user-key", kind: "user" }).fully_qualified_key).to eq("user-key")
expect(subject.create({ key: "org-key", kind: "org" }).fully_qualified_key).to eq("org:org-key")
Expand Down Expand Up @@ -394,6 +407,19 @@ module LaunchDarkly
end
end

describe "private attributes" do
it "returns a frozen set of references" do
context = subject.create({ key: 'context-key', kind: 'user', _meta: { privateAttributes: ['name', 'address/city', 'name'] } })
expect(context.private_attributes).to be_a(Set)
expect(context.private_attributes).to be_frozen
expect(context.private_attributes).to eq(Set[LaunchDarkly::Reference.create('name'), LaunchDarkly::Reference.create('address/city')])
end

it "returns an empty set when none are given" do
expect(subject.create({ key: 'context-key', kind: 'user' }).private_attributes).to eq(Set.new)
end
end

describe "equality comparisons" do
it "wrong types are not equal" do
context = subject.create({ key: 'context-key', kind: 'user' })
Expand Down
5 changes: 5 additions & 0 deletions spec/reference_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ module LaunchDarkly
end
end

it "returns the same reference when given a reference" do
reference = Reference.create("/a/b")
expect(Reference.create(reference)).to be(reference)
end

describe "creating literal references" do
it "can create valid references" do
[
Expand Down
Loading