diff --git a/lib/mongo.rb b/lib/mongo.rb index ed93036ce0..1be61d7b46 100644 --- a/lib/mongo.rb +++ b/lib/mongo.rb @@ -15,6 +15,7 @@ require 'forwardable' require 'bson' require 'openssl' +require 'mongo/options' require 'mongo/loggable' require 'mongo/monitoring' require 'mongo/logger' @@ -32,7 +33,6 @@ require 'mongo/dbref' require 'mongo/grid' require 'mongo/index' -require 'mongo/options' require 'mongo/protocol' require 'mongo/server' require 'mongo/server_selector' diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index 7e9d787b28..3c0fcb4543 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -153,7 +153,7 @@ def hash # logs at the default 250 characters. # # @since 2.0.0 - def initialize(addresses_or_uri, options = {}) + def initialize(addresses_or_uri, options = Options::Redacted.new) @monitoring = Monitoring.new(options) if addresses_or_uri.is_a?(::String) create_from_uri(addresses_or_uri, options) @@ -185,7 +185,7 @@ def inspect # # @since 2.0.0 def read_preference - @read_preference ||= ServerSelector.get((options[:read] || {}).merge(options)) + @read_preference ||= ServerSelector.get(Options::Redacted.new(options[:read] || {}).merge(options)) end # Use the database with the provided name. This will switch the current @@ -215,9 +215,9 @@ def use(name) # @return [ Mongo::Client ] A new client instance. # # @since 2.0.0 - def with(new_options = {}) + def with(new_options = Options::Redacted.new) clone.tap do |client| - opts = new_options || {} + opts = Options::Redacted.new(new_options) || Options::Redacted.new client.options.update(opts) Database.create(client) # We can't use the same cluster if some options that would affect it @@ -291,15 +291,15 @@ def list_databases private - def create_from_addresses(addresses, opts = {}) - @options = Database::DEFAULT_OPTIONS.merge(opts).freeze + def create_from_addresses(addresses, opts = Options::Redacted.new) + @options = Options::Redacted.new(Database::DEFAULT_OPTIONS.merge(opts)).freeze @cluster = Cluster.new(addresses, @monitoring, options) @database = Database.new(self, options[:database], options) end - def create_from_uri(connection_string, opts = {}) + def create_from_uri(connection_string, opts = Options::Redacted.new) uri = URI.new(connection_string, opts) - @options = Database::DEFAULT_OPTIONS.merge(uri.client_options.merge(opts)).freeze + @options = Options::Redacted.new(Database::DEFAULT_OPTIONS.merge(uri.client_options.merge(opts))).freeze @cluster = Cluster.new(uri.servers, @monitoring, options) @database = Database.new(self, options[:database], options) end @@ -314,7 +314,7 @@ def initialize_copy(original) def cluster_modifying?(new_options) cluster_options = new_options.reject do |name| - CRUD_OPTIONS.include?(name) + CRUD_OPTIONS.include?(name.to_sym) end cluster_options.any? do |name, value| options[name] != value diff --git a/lib/mongo/cluster.rb b/lib/mongo/cluster.rb index f54aa97c09..9d600e4dab 100644 --- a/lib/mongo/cluster.rb +++ b/lib/mongo/cluster.rb @@ -88,7 +88,7 @@ def add(host) # @param [ Hash ] options The options. # # @since 2.0.0 - def initialize(seeds, monitoring, options = {}) + def initialize(seeds, monitoring, options = Options::Redacted.new) @addresses = [] @servers = [] @monitoring = monitoring @@ -125,7 +125,7 @@ def inspect # # @since 2.0.0 def next_primary - ServerSelector.get({ mode: :primary }.merge(options)).select_server(self) + ServerSelector.get(ServerSelector::PRIMARY.merge(options)).select_server(self) end # Elect a primary server from the description that has just changed to a diff --git a/lib/mongo/collection/view/readable.rb b/lib/mongo/collection/view/readable.rb index 3ca9f9bbcf..b0424ef75d 100644 --- a/lib/mongo/collection/view/readable.rb +++ b/lib/mongo/collection/view/readable.rb @@ -298,7 +298,8 @@ def projection(document = nil) # @since 2.0.0 def read(value = nil) return default_read if value.nil? - configure(:read, value.is_a?(Hash) ? ServerSelector.get(value) : value) + selector = value.is_a?(Hash) ? ServerSelector.get(client.options.merge(value)) : value + configure(:read, selector) end # Set whether to return only the indexed field or fields. diff --git a/lib/mongo/database.rb b/lib/mongo/database.rb index ea6744c67c..012b3fcb79 100644 --- a/lib/mongo/database.rb +++ b/lib/mongo/database.rb @@ -36,7 +36,7 @@ class Database # The default database options. # # @since 2.0.0 - DEFAULT_OPTIONS = { :database => ADMIN }.freeze + DEFAULT_OPTIONS = Options::Redacted.new(:database => ADMIN).freeze # Database name field constant. # @@ -148,7 +148,7 @@ def collections # # @return [ Hash ] The result of the command execution. def command(operation, opts = {}) - preference = opts[:read] ? ServerSelector.get(opts[:read].merge(options)) : read_preference + preference = opts[:read] ? ServerSelector.get(client.options.merge(opts[:read])) : read_preference server = preference.select_server(cluster) Operation::Command.new({ :selector => operation, diff --git a/lib/mongo/grid/fs_bucket.rb b/lib/mongo/grid/fs_bucket.rb index 1a9a5384ca..aeb24e4849 100644 --- a/lib/mongo/grid/fs_bucket.rb +++ b/lib/mongo/grid/fs_bucket.rb @@ -19,6 +19,7 @@ module Grid # # @since 2.0.0 class FSBucket + extend Forwardable # The default root prefix. # @@ -55,6 +56,12 @@ class FSBucket # @since 2.1.0 attr_reader :options + # Get client from the database. + # + # @since 2.1.0 + def_delegators :database, + :client + # Find files collection documents matching a given selector. # # @example Find files collection documents by a filename. @@ -395,7 +402,7 @@ def upload_from_stream(filename, io, opts = {}) # @since 2.1.0 def read_preference @read_preference ||= @options[:read] ? - ServerSelector.get((@options[:read] || {}).merge(database.options)) : + ServerSelector.get(Options::Redacted.new((@options[:read] || {}).merge(client.options))) : database.read_preference end diff --git a/lib/mongo/grid/stream/read.rb b/lib/mongo/grid/stream/read.rb index 279a6edeec..20865f5101 100644 --- a/lib/mongo/grid/stream/read.rb +++ b/lib/mongo/grid/stream/read.rb @@ -134,7 +134,7 @@ def closed? # @since 2.1.0 def read_preference @read_preference ||= @options[:read] ? - ServerSelector.get((@options[:read] || {}).merge(fs.options)) : + ServerSelector.get(Options::Redacted.new((@options[:read] || {}).merge(fs.options))) : fs.read_preference end diff --git a/lib/mongo/options.rb b/lib/mongo/options.rb index 37479cda49..7562654c20 100644 --- a/lib/mongo/options.rb +++ b/lib/mongo/options.rb @@ -13,3 +13,4 @@ # limitations under the License. require 'mongo/options/mapper' +require 'mongo/options/redacted' diff --git a/lib/mongo/options/redacted.rb b/lib/mongo/options/redacted.rb new file mode 100644 index 0000000000..1a557eb2f9 --- /dev/null +++ b/lib/mongo/options/redacted.rb @@ -0,0 +1,156 @@ +# Copyright (C) 2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + module Options + + # Class for wrapping options that could be sensitive. + # When printed, the sensitive values will be redacted. + # + # @since 2.1.0 + class Redacted < BSON::Document + + # The options whose values will be redacted. + # + # @since 2.1.0 + SENSITIVE_OPTIONS = [ :password, + :pwd ].freeze + + # The replacement string used in place of the value for sensitive keys. + # + # @since 2.1.0 + STRING_REPLACEMENT = ''.freeze + + # Get a string representation of the options. + # + # @return [ String ] The string representation of the options. + # + # @since 2.1.0 + def inspect + redacted_string(:inspect) + end + + # Get a string representation of the options. + # + # @return [ String ] The string representation of the options. + # + # @since 2.1.0 + def to_s + redacted_string(:to_s) + end + + # Whether these options contain a given key. + # + # @example Determine if the options contain a given key. + # options.has_key?(:name) + # + # @param [ String, Symbol ] key The key to check for existence. + # + # @return [ true, false ] If the options contain the given key. + # + # @since 2.1.0 + def has_key?(key) + super(convert_key(key)) + end + alias_method :key?, :has_key? + + # Returns a new options object consisting of pairs for which the block returns false. + # + # @example Get a new options object with pairs for which the block returns false. + # new_options = options.reject { |k, v| k == 'database' } + # + # @yieldparam [ String, Object ] The key as a string and its value. + # + # @return [ Options::Redacted ] A new options object. + # + # @since 2.1.0 + def reject(&block) + new_options = dup + new_options.reject!(&block) || new_options + end + + # Only keeps pairs for which the block returns false. + # + # @example Remove pairs from this object for which the block returns true. + # options.reject! { |k, v| k == 'database' } + # + # @yieldparam [ String, Object ] The key as a string and its value. + # + # @return [ Options::Redacted, nil ] This object or nil if no changes were made. + # + # @since 2.1.0 + def reject! + if block_given? + n_keys = keys.size + keys.each do |key| + delete(key) if yield(key, self[key]) + end + n_keys == keys.size ? nil : self + else + to_enum + end + end + + # Returns a new options object consisting of pairs for which the block returns true. + # + # @example Get a new options object with pairs for which the block returns true. + # ssl_options = options.select { |k, v| k =~ /ssl/ } + # + # @yieldparam [ String, Object ] The key as a string and its value. + # + # @return [ Options::Redacted ] A new options object. + # + # @since 2.1.0 + def select(&block) + new_options = dup + new_options.select!(&block) || new_options + end + + # Only keeps pairs for which the block returns true. + # + # @example Remove pairs from this object for which the block does not return true. + # options.select! { |k, v| k =~ /ssl/ } + # + # @yieldparam [ String, Object ] The key as a string and its value. + # + # @return [ Options::Redacted, nil ] This object or nil if no changes were made. + # + # @since 2.1.0 + def select! + if block_given? + n_keys = keys.size + keys.each do |key| + delete(key) unless yield(key, self[key]) + end + n_keys == keys.size ? nil : self + else + to_enum + end + end + + private + + def redacted_string(method) + '{' + reduce([]) do |list, (k, v)| + list << "#{k.send(method)}=>#{redact(k, v, method)}" + end.join(', ') + '}' + end + + def redact(k, v, method) + return STRING_REPLACEMENT if SENSITIVE_OPTIONS.include?(k.to_sym) + v.send(method) + end + end + end +end diff --git a/lib/mongo/server/connection.rb b/lib/mongo/server/connection.rb index 58e8bb9b84..008006ca6d 100644 --- a/lib/mongo/server/connection.rb +++ b/lib/mongo/server/connection.rb @@ -183,7 +183,7 @@ def deliver(messages) def setup_authentication! if options[:user] default_mechanism = @server.features.scram_sha_1_enabled? ? :scram : :mongodb_cr - user = Auth::User.new({ :auth_mech => default_mechanism }.merge(options)) + user = Auth::User.new(Options::Redacted.new(:auth_mech => default_mechanism).merge(options)) @authenticator = Auth.get(user) end end diff --git a/lib/mongo/server_selector.rb b/lib/mongo/server_selector.rb index c393014bde..921af8f221 100644 --- a/lib/mongo/server_selector.rb +++ b/lib/mongo/server_selector.rb @@ -38,6 +38,11 @@ module ServerSelector # @since 2.0.0 SERVER_SELECTION_TIMEOUT = 30.freeze + # Primary read preference. + # + # @since 2.1.0 + PRIMARY = Options::Redacted.new(mode: :primary).freeze + # Hash lookup for the selector classes based off the symbols # provided in configuration. # diff --git a/lib/mongo/server_selector/selectable.rb b/lib/mongo/server_selector/selectable.rb index 9518da1152..eee877ae6d 100644 --- a/lib/mongo/server_selector/selectable.rb +++ b/lib/mongo/server_selector/selectable.rb @@ -61,10 +61,10 @@ def ==(other) # # @since 2.0.0 def initialize(options = {}) + @options = (options || {}).freeze tag_sets = options[:tag_sets] || [] validate_tag_sets!(tag_sets) - @tag_sets = tag_sets - @options = options + @tag_sets = tag_sets.freeze end # Select a server from eligible candidates. diff --git a/lib/mongo/uri.rb b/lib/mongo/uri.rb index 5f69d2b1af..d456d90101 100644 --- a/lib/mongo/uri.rb +++ b/lib/mongo/uri.rb @@ -268,7 +268,7 @@ def split_creds_hosts(string) def parse_db_opts!(string) auth_db, d, uri_opts = string.partition(URI_OPTS_DELIM) - @uri_options = parse_uri_options!(uri_opts) + @uri_options = Options::Redacted.new(parse_uri_options!(uri_opts)) @database = parse_database!(auth_db) end diff --git a/spec/mongo/client_spec.rb b/spec/mongo/client_spec.rb index 6db81847f8..405af9e464 100644 --- a/spec/mongo/client_spec.rb +++ b/spec/mongo/client_spec.rb @@ -167,16 +167,20 @@ ['127.0.0.1:27017'], :read => { :mode => :primary }, :local_threshold_ms => 10, - :server_selection_timeout_ms => 10000, + :server_selection_timeout => 10000, :database => TEST_DB ) end + let(:options) do + Mongo::Options::Redacted.new(:read => { :mode => :primary }, + :local_threshold_ms => 10, + :server_selection_timeout => 10000, + :database => TEST_DB) + end + let(:expected) do - [client.cluster, { :read => { :mode => :primary }, - :local_threshold_ms => 10, - :server_selection_timeout_ms => 10000, - :database => TEST_DB }].hash + [client.cluster, options].hash end it 'returns a hash of the cluster and options' do @@ -199,6 +203,23 @@ " { :mode => :primary }, + :database => TEST_DB, + :password => 'some_password', + :user => 'emily' + ) + end + + it 'does not print out sensitive data' do + expect(client.inspect).not_to match('some_password') + end + end end describe '#initialize' do @@ -291,8 +312,12 @@ described_class.new(uri) end + let(:expected_options) do + Mongo::Options::Redacted.new(:write => { :w => 3 }, :database => 'testdb') + end + it 'sets the options' do - expect(client.options).to eq(:write => { :w => 3 }, :database => 'testdb') + expect(client.options).to eq(expected_options) end end @@ -306,8 +331,12 @@ described_class.new(uri, :write => { :w => 3 }) end + let(:expected_options) do + Mongo::Options::Redacted.new(:write => { :w => 3 }, :database => 'testdb') + end + it 'sets the options' do - expect(client.options).to eq(:write => { :w => 3 }, :database => 'testdb') + expect(client.options).to eq(expected_options) end end @@ -321,8 +350,12 @@ described_class.new(uri, :write => { :w => 4 }) end + let(:expected_options) do + Mongo::Options::Redacted.new(:write => { :w => 4 }, :database => 'testdb') + end + it 'allows explicit options to take preference' do - expect(client.options).to eq(:write => { :w => 4 }, :database => 'testdb') + expect(client.options).to eq(expected_options) end end @@ -348,7 +381,8 @@ let(:client) do described_class.new(['127.0.0.1:27017'], :database => TEST_DB, - :read => mode) + :read => mode, + :server_selection_timeout => 2) end let(:preference) do @@ -366,7 +400,7 @@ end it 'passes the options to the read preference' do - expect(preference.options[:database]).to eq(TEST_DB) + expect(preference.options[:server_selection_timeout]).to eq(2) end end @@ -424,6 +458,33 @@ expect(preference).to be_a(Mongo::ServerSelector::Primary) end end + + context 'when the read preference is printed' do + + let(:client) do + described_class.new([ DEFAULT_ADDRESS ], options) + end + + let(:options) do + { user: 'Emily', password: 'sensitive_data', server_selection_timeout: 0.1 } + end + + before do + allow(client.database.cluster).to receive(:single?).and_return(false) + end + + let(:error) do + begin + client.database.command(ping: 1) + rescue => e + e + end + end + + it 'redacts sensitive client options' do + expect(error.message).not_to match(options[:password]) + end + end end describe '#use' do @@ -497,20 +558,28 @@ client.with(:read => { :mode => :primary }) end + let(:new_options) do + Mongo::Options::Redacted.new(:read => { :mode => :primary }, + :write => { :w => 1 }, + :database => TEST_DB) + end + + let(:original_options) do + Mongo::Options::Redacted.new(:read => { :mode => :secondary }, + :write => { :w => 1 }, + :database => TEST_DB) + end + it 'returns a new client' do expect(new_client).not_to equal(client) end it 'replaces the existing options' do - expect(new_client.options).to eq({ - :read => { :mode => :primary }, :write => { :w => 1 }, :database => TEST_DB - }) + expect(new_client.options).to eq(new_options) end it 'does not modify the original client' do - expect(client.options).to eq({ - :read => { :mode => :secondary }, :write => { :w => 1 }, :database => TEST_DB - }) + expect(client.options).to eq(original_options) end it 'keeps the same cluster' do @@ -579,7 +648,7 @@ end it 'returns a acknowledged write concern' do - expect(concern.get_last_error).to eq(:getlasterror => 1, :j => true) + expect(concern.get_last_error).to eq(:getlasterror => 1, 'j' => true) end end @@ -658,4 +727,19 @@ expect(client.reconnect).to be(true) end end + + describe '#dup' do + + let(:client) do + described_class.new( + ['127.0.0.1:27017'], + :read => { :mode => :primary }, + :database => TEST_DB + ) + end + + it 'creates a client with Redacted options' do + expect(client.dup.options).to be_a(Mongo::Options::Redacted) + end + end end diff --git a/spec/mongo/grid/fs_bucket_spec.rb b/spec/mongo/grid/fs_bucket_spec.rb index 21ee4fbe98..c7492da5b7 100644 --- a/spec/mongo/grid/fs_bucket_spec.rb +++ b/spec/mongo/grid/fs_bucket_spec.rb @@ -55,7 +55,7 @@ end let(:read_pref) do - Mongo::ServerSelector.get(options[:read].merge(authorized_client.options)) + Mongo::ServerSelector.get(Mongo::Options::Redacted.new(options[:read].merge(authorized_client.options))) end it 'sets the read preference' do diff --git a/spec/mongo/grid/stream/write_spec.rb b/spec/mongo/grid/stream/write_spec.rb index d8de33de43..633b56904c 100644 --- a/spec/mongo/grid/stream/write_spec.rb +++ b/spec/mongo/grid/stream/write_spec.rb @@ -87,14 +87,14 @@ context 'when chunks are inserted' do it 'uses that write concern' do - expect(stream.send(:chunks_collection).write_concern.options).to eq(expected) + expect(stream.send(:chunks_collection).write_concern.options[:w]).to eq(expected[:w]) end end context 'when a files document is inserted' do it 'uses that write concern' do - expect(stream.send(:files_collection).write_concern.options).to eq(expected) + expect(stream.send(:files_collection).write_concern.options[:w]).to eq(expected[:w]) end end end diff --git a/spec/mongo/options/redacted_spec.rb b/spec/mongo/options/redacted_spec.rb new file mode 100644 index 0000000000..0cfed16c34 --- /dev/null +++ b/spec/mongo/options/redacted_spec.rb @@ -0,0 +1,350 @@ +require 'spec_helper' + +describe Mongo::Options::Redacted do + + let(:options) do + described_class.new(original_opts) + end + + describe '#to_s' do + + context 'when the hash contains a sensitive key' do + + let(:original_opts) do + { password: 'sensitive_data' } + end + + it 'replaces the value with the redacted string' do + expect(options.to_s).not_to match(original_opts[:password]) + end + + it 'replaces the value with the redacted string' do + expect(options.to_s).to match(Mongo::Options::Redacted::STRING_REPLACEMENT) + end + end + + context 'when the hash does not contain a sensitive key' do + + let(:original_opts) do + { user: 'emily' } + end + + it 'prints all the values' do + expect(options.to_s).to match(original_opts[:user]) + end + end + end + + describe '#inspect' do + + context 'when the hash contains a sensitive key' do + + let(:original_opts) do + { password: 'sensitive_data' } + end + + it 'replaces the value with the redacted string' do + expect(options.inspect).not_to match(original_opts[:password]) + end + + it 'replaces the value with the redacted string' do + expect(options.inspect).to match(Mongo::Options::Redacted::STRING_REPLACEMENT) + end + end + + context 'when the hash does not contain a sensitive key' do + + let(:original_opts) do + { name: 'some_name' } + end + + it 'does not replace the value with the redacted string' do + expect(options.inspect).to match(original_opts[:name]) + end + + it 'does not replace the value with the redacted string' do + expect(options.inspect).not_to match(Mongo::Options::Redacted::STRING_REPLACEMENT) + end + end + end + + describe '#has_key?' do + + context 'when the original key is a String' do + + let(:original_opts) do + { 'name' => 'Emily' } + end + + context 'when the method argument is a String' do + + it 'returns true' do + expect(options.has_key?('name')).to be(true) + end + end + + context 'when method argument is a Symbol' do + + it 'returns true' do + expect(options.has_key?(:name)).to be(true) + end + end + end + + context 'when the original key is a Symbol' do + + let(:original_opts) do + { name: 'Emily' } + end + + context 'when the method argument is a String' do + + it 'returns true' do + expect(options.has_key?('name')).to be(true) + end + end + + context 'when method argument is a Symbol' do + + it 'returns true' do + expect(options.has_key?(:name)).to be(true) + end + end + end + + context 'when the hash does not contain the key' do + + let(:original_opts) do + { other: 'Emily' } + end + + context 'when the method argument is a String' do + + it 'returns false' do + expect(options.has_key?('name')).to be(false) + end + end + + context 'when method argument is a Symbol' do + + it 'returns false' do + expect(options.has_key?(:name)).to be(false) + end + end + end + end + + describe '#reject' do + + let(:options) do + described_class.new(a: 1, b: 2, c: 3) + end + + context 'when no block is provided' do + + it 'returns an enumerable' do + expect(options.reject).to be_a(Enumerator) + end + end + + context 'when a block is provided' do + + context 'when the block evaluates to true for some pairs' do + + let(:result) do + options.reject { |k,v| k == 'a' } + end + + it 'returns an object consisting of only the remaining pairs' do + expect(result).to eq(described_class.new(b: 2, c: 3)) + end + + it 'returns a new object' do + expect(result).not_to be(options) + end + end + + context 'when the block does not evaluate to true for any pairs' do + + let(:result) do + options.reject { |k,v| k == 'd' } + end + + it 'returns an object with all pairs intact' do + expect(result).to eq(described_class.new(a: 1, b: 2, c: 3)) + end + + it 'returns a new object' do + expect(result).not_to be(options) + end + end + end + end + + describe '#reject!' do + + let(:options) do + described_class.new(a: 1, b: 2, c: 3) + end + + context 'when no block is provided' do + + it 'returns an enumerable' do + expect(options.reject).to be_a(Enumerator) + end + end + + context 'when a block is provided' do + + context 'when the block evaluates to true for some pairs' do + + let(:result) do + options.reject! { |k,v| k == 'a' } + end + + it 'returns an object consisting of only the remaining pairs' do + expect(result).to eq(described_class.new(b: 2, c: 3)) + end + + it 'returns the same object' do + expect(result).to be(options) + end + end + + context 'when the block does not evaluate to true for any pairs' do + + let(:result) do + options.reject! { |k,v| k == 'd' } + end + + it 'returns nil' do + expect(result).to be(nil) + end + end + end + end + + describe '#select' do + + let(:options) do + described_class.new(a: 1, b: 2, c: 3) + end + + context 'when no block is provided' do + + it 'returns an enumerable' do + expect(options.reject).to be_a(Enumerator) + end + end + + context 'when a block is provided' do + + context 'when the block evaluates to true for some pairs' do + + let(:result) do + options.select { |k,v| k == 'a' } + end + + it 'returns an object consisting of those pairs' do + expect(result).to eq(described_class.new(a: 1)) + end + + it 'returns a new object' do + expect(result).not_to be(options) + end + end + + context 'when the block does not evaluate to true for any pairs' do + + let(:result) do + options.select { |k,v| k == 'd' } + end + + it 'returns an object with no pairs' do + expect(result).to eq(described_class.new) + end + + it 'returns a new object' do + expect(result).not_to be(options) + end + end + + context 'when the object is unchanged' do + + let(:options) do + described_class.new(a: 1, b: 2, c: 3) + end + + let(:result) do + options.select { |k,v| ['a', 'b', 'c'].include?(k) } + end + + it 'returns a new object' do + expect(result).to eq(described_class.new(a: 1, b: 2, c: 3)) + end + end + end + end + + describe '#select!' do + + let(:options) do + described_class.new(a: 1, b: 2, c: 3) + end + + context 'when no block is provided' do + + it 'returns an enumerable' do + expect(options.reject).to be_a(Enumerator) + end + end + + context 'when a block is provided' do + + context 'when the block evaluates to true for some pairs' do + + let(:result) do + options.select! { |k,v| k == 'a' } + end + + it 'returns an object consisting of those pairs' do + expect(result).to eq(described_class.new(a: 1)) + end + + it 'returns the same object' do + expect(result).to be(options) + end + end + + context 'when the block does not evaluate to true for any pairs' do + + let(:result) do + options.select! { |k,v| k == 'd' } + end + + it 'returns an object with no pairs' do + expect(result).to eq(described_class.new) + end + + it 'returns the same object' do + expect(result).to be(options) + end + end + + context 'when the object is unchanged' do + + let(:options) do + described_class.new(a: 1, b: 2, c: 3) + end + + let(:result) do + options.select! { |k,v| ['a', 'b', 'c'].include?(k) } + end + + it 'returns nil' do + expect(result).to be(nil) + end + end + end + end +end \ No newline at end of file diff --git a/spec/mongo/server_selector/nearest_spec.rb b/spec/mongo/server_selector/nearest_spec.rb index ee6daed3c3..26c4d2b4ec 100644 --- a/spec/mongo/server_selector/nearest_spec.rb +++ b/spec/mongo/server_selector/nearest_spec.rb @@ -11,6 +11,7 @@ end it_behaves_like 'a server selector accepting tag sets' + it_behaves_like 'a server selector with sensitive data in its options' describe '#to_mongos' do diff --git a/spec/mongo/server_selector/primary_preferred_spec.rb b/spec/mongo/server_selector/primary_preferred_spec.rb index 1fd9f2ab56..be25b7f3f6 100644 --- a/spec/mongo/server_selector/primary_preferred_spec.rb +++ b/spec/mongo/server_selector/primary_preferred_spec.rb @@ -11,6 +11,7 @@ end it_behaves_like 'a server selector accepting tag sets' + it_behaves_like 'a server selector with sensitive data in its options' describe '#to_mongos' do diff --git a/spec/mongo/server_selector/primary_spec.rb b/spec/mongo/server_selector/primary_spec.rb index 6a37ad6025..810f0a0a1d 100644 --- a/spec/mongo/server_selector/primary_spec.rb +++ b/spec/mongo/server_selector/primary_spec.rb @@ -9,6 +9,7 @@ it_behaves_like 'a server selector mode' do let(:slave_ok) { false } end + it_behaves_like 'a server selector with sensitive data in its options' describe '#tag_sets' do diff --git a/spec/mongo/server_selector/secondary_preferred_spec.rb b/spec/mongo/server_selector/secondary_preferred_spec.rb index a0888f07a8..117427adb6 100644 --- a/spec/mongo/server_selector/secondary_preferred_spec.rb +++ b/spec/mongo/server_selector/secondary_preferred_spec.rb @@ -9,6 +9,7 @@ it_behaves_like 'a server selector mode' do let(:slave_ok) { true } end + it_behaves_like 'a server selector with sensitive data in its options' it_behaves_like 'a server selector accepting tag sets' diff --git a/spec/mongo/server_selector/secondary_spec.rb b/spec/mongo/server_selector/secondary_spec.rb index 55f36ae70e..e53edf277d 100644 --- a/spec/mongo/server_selector/secondary_spec.rb +++ b/spec/mongo/server_selector/secondary_spec.rb @@ -9,6 +9,7 @@ it_behaves_like 'a server selector mode' do let(:slave_ok) { true } end + it_behaves_like 'a server selector with sensitive data in its options' it_behaves_like 'a server selector accepting tag sets' diff --git a/spec/mongo/uri_spec.rb b/spec/mongo/uri_spec.rb index 8b3c784cd1..46c61014cf 100644 --- a/spec/mongo/uri_spec.rb +++ b/spec/mongo/uri_spec.rb @@ -365,7 +365,7 @@ context 'numerical w value' do let(:options) { 'w=1' } - let(:concern) { { :w => 1 } } + let(:concern) { Mongo::Options::Redacted.new(:w => 1)} it 'sets the write concern options' do expect(uri.uri_options[:write]).to eq(concern) @@ -374,7 +374,7 @@ context 'w=majority' do let(:options) { 'w=majority' } - let(:concern) { { :w => :majority } } + let(:concern) { Mongo::Options::Redacted.new(:w => :majority) } it 'sets the write concern options' do expect(uri.uri_options[:write]).to eq(concern) @@ -383,7 +383,7 @@ context 'journal' do let(:options) { 'journal=true' } - let(:concern) { { :j => true } } + let(:concern) { Mongo::Options::Redacted.new(:j => true) } it 'sets the write concern options' do expect(uri.uri_options[:write]).to eq(concern) @@ -392,7 +392,7 @@ context 'fsync' do let(:options) { 'fsync=true' } - let(:concern) { { :fsync => true } } + let(:concern) { Mongo::Options::Redacted.new(:fsync => true) } it 'sets the write concern options' do expect(uri.uri_options[:write]).to eq(concern) @@ -402,7 +402,7 @@ context 'wtimeoutMS' do let(:timeout) { 1234 } let(:options) { "w=2&wtimeoutMS=#{timeout}" } - let(:concern) { { :w => 2, :timeout => timeout } } + let(:concern) { Mongo::Options::Redacted.new(:w => 2, :timeout => timeout) } it 'sets the write concern options' do expect(uri.uri_options[:write]).to eq(concern) @@ -415,7 +415,7 @@ context 'primary' do let(:mode) { 'primary' } - let(:read) { { :mode => :primary } } + let(:read) { Mongo::Options::Redacted.new(:mode => :primary) } it 'sets the read preference' do expect(uri.uri_options[:read]).to eq(read) @@ -424,7 +424,7 @@ context 'primaryPreferred' do let(:mode) { 'primaryPreferred' } - let(:read) { { :mode => :primary_preferred } } + let(:read) { Mongo::Options::Redacted.new(:mode => :primary_preferred) } it 'sets the read preference' do expect(uri.uri_options[:read]).to eq(read) @@ -433,7 +433,7 @@ context 'secondary' do let(:mode) { 'secondary' } - let(:read) { { :mode => :secondary } } + let(:read) { Mongo::Options::Redacted.new(:mode => :secondary) } it 'sets the read preference' do expect(uri.uri_options[:read]).to eq(read) @@ -442,7 +442,7 @@ context 'secondaryPreferred' do let(:mode) { 'secondaryPreferred' } - let(:read) { { :mode => :secondary_preferred } } + let(:read) { Mongo::Options::Redacted.new(:mode => :secondary_preferred) } it 'sets the read preference' do expect(uri.uri_options[:read]).to eq(read) @@ -451,7 +451,7 @@ context 'nearest' do let(:mode) { 'nearest' } - let(:read) { { :mode => :nearest } } + let(:read) { Mongo::Options::Redacted.new(:mode => :nearest) } it 'sets the read preference' do expect(uri.uri_options[:read]).to eq(read) @@ -467,7 +467,7 @@ end let(:read) do - { :tag_sets => [{ :dc => 'ny', :rack => '1' }] } + Mongo::Options::Redacted.new(:tag_sets => [{ 'dc' => 'ny', 'rack' => '1' }]) end it 'sets the read preference tag set' do @@ -481,7 +481,7 @@ end let(:read) do - { :tag_sets => [{ :dc => 'ny' }, { :dc => 'bos' }] } + Mongo::Options::Redacted.new(:tag_sets => [{ 'dc' => 'ny' }, { 'dc' => 'bos' }]) end it 'sets the read preference tag sets' do @@ -536,7 +536,7 @@ context 'regular db' do let(:source) { 'foo' } - let(:auth) { { :source => 'foo' } } + let(:auth) { Mongo::Options::Redacted.new(:source => 'foo') } it 'sets the auth source to the database' do expect(uri.uri_options[:auth]).to eq(auth) @@ -545,7 +545,7 @@ context '$external' do let(:source) { '$external' } - let(:auth) { { :source => :external } } + let(:auth) { Mongo::Options::Redacted.new(:source => :external) } it 'sets the auth source to :external' do expect(uri.uri_options[:auth]).to eq(auth) @@ -562,7 +562,7 @@ let(:service_name) { 'foo' } let(:auth) do - { auth_mech_properties: { service_name: service_name } } + Mongo::Options::Redacted.new(auth_mech_properties: { service_name: service_name }) end it 'sets the auth mechanism properties' do @@ -577,7 +577,7 @@ let(:canonicalize_host_name) { 'true' } let(:auth) do - { auth_mech_properties: { canonicalize_host_name: true } } + Mongo::Options::Redacted.new(auth_mech_properties: { canonicalize_host_name: true }) end it 'sets the auth mechanism properties' do @@ -592,7 +592,7 @@ let(:service_realm) { 'dumdum' } let(:auth) do - { auth_mech_properties: { service_realm: service_realm } } + Mongo::Options::Redacted.new(auth_mech_properties: { service_realm: service_realm }) end it 'sets the auth mechanism properties' do @@ -612,9 +612,9 @@ let(:service_realm) { 'dumdum' } let(:auth) do - { auth_mech_properties: { service_name: service_name, - canonicalize_host_name: true, - service_realm: service_realm } } + Mongo::Options::Redacted.new(auth_mech_properties: { service_name: service_name, + canonicalize_host_name: true, + service_realm: service_realm }) end it 'sets the auth mechanism properties' do diff --git a/spec/support/shared/server_selector.rb b/spec/support/shared/server_selector.rb index a625cf6a2c..d2e4ea8ced 100644 --- a/spec/support/shared/server_selector.rb +++ b/spec/support/shared/server_selector.rb @@ -32,7 +32,8 @@ def server(mode, options = {}) end let(:primary) { server(:primary) } let(:secondary) { server(:secondary) } - let(:selector) { described_class.new(:mode => name, :tag_sets => tag_sets) } + let(:options) { { :mode => name, :tag_sets => tag_sets } } + let(:selector) { described_class.new(options) } before(:all) do module Mongo @@ -157,3 +158,20 @@ class Server end end end + +shared_examples 'a server selector with sensitive data in its options' do + + describe '#inspect' do + + context 'when there is sensitive data in the options' do + + let(:options) do + Mongo::Options::Redacted.new(:mode => name, :password => 'sensitive_data') + end + + it 'does not print out sensitive data' do + expect(selector.inspect).not_to match(options[:password]) + end + end + end +end