mongo 1.11.1 → 1.12.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/VERSION +1 -1
  5. data/lib/mongo/collection.rb +8 -3
  6. data/lib/mongo/collection_writer.rb +1 -1
  7. data/lib/mongo/connection/socket/unix_socket.rb +1 -1
  8. data/lib/mongo/cursor.rb +8 -1
  9. data/lib/mongo/db.rb +61 -33
  10. data/lib/mongo/exception.rb +57 -0
  11. data/lib/mongo/functional.rb +1 -0
  12. data/lib/mongo/functional/authentication.rb +138 -11
  13. data/lib/mongo/functional/read_preference.rb +31 -22
  14. data/lib/mongo/functional/scram.rb +555 -0
  15. data/lib/mongo/functional/uri_parser.rb +107 -79
  16. data/lib/mongo/mongo_client.rb +19 -24
  17. data/lib/mongo/mongo_replica_set_client.rb +2 -1
  18. data/test/functional/authentication_test.rb +3 -0
  19. data/test/functional/client_test.rb +33 -0
  20. data/test/functional/collection_test.rb +29 -19
  21. data/test/functional/db_api_test.rb +16 -1
  22. data/test/functional/pool_test.rb +7 -6
  23. data/test/functional/uri_test.rb +111 -7
  24. data/test/helpers/test_unit.rb +17 -3
  25. data/test/replica_set/client_test.rb +31 -0
  26. data/test/replica_set/insert_test.rb +49 -32
  27. data/test/replica_set/pinning_test.rb +50 -0
  28. data/test/replica_set/query_test.rb +1 -1
  29. data/test/replica_set/replication_ack_test.rb +3 -3
  30. data/test/shared/authentication/basic_auth_shared.rb +14 -1
  31. data/test/shared/authentication/gssapi_shared.rb +13 -8
  32. data/test/shared/authentication/scram_shared.rb +92 -0
  33. data/test/tools/mongo_config.rb +18 -6
  34. data/test/unit/client_test.rb +40 -6
  35. data/test/unit/connection_test.rb +15 -5
  36. data/test/unit/db_test.rb +1 -1
  37. data/test/unit/read_pref_test.rb +291 -0
  38. metadata +9 -6
  39. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 047647bfd88def86157a8e4e25df514da21a1364
4
- data.tar.gz: 7fd48a7694323f7e4759b94f12058b5e34a6bba4
3
+ metadata.gz: 0c376366d59154151291d12a0a859a6db8850416
4
+ data.tar.gz: 7d92d814592073a3f2548fea9b2960e241c98502
5
5
  SHA512:
6
- metadata.gz: 3b360776478001d2b41342764c13dd209954c0bbcf73c5b87dc4f2169d3e42bd27c6650514ca281a1146c33d1d2624edf653cf8ddd51e3ff8f9e37c9bf4505d3
7
- data.tar.gz: e4a9d033eb058e0424ecae35599a27be0a7c68e56b2a0557dcbc42a331e940a0c42c73f0b1c1eeb3b76d977fe63a84df5b7310bc001feeb2f8441518e163b2eb
6
+ metadata.gz: dc0c717e7741c0871d80af3b6f65be1bd8908a2e0364c6bf17106056f5d71ea4677f410a4cacf0a6ec85181486a2e3eb5392a1b09472c0d728ac9c3dcaecd027
7
+ data.tar.gz: fd7c88744f849a3cdec1a1e514a2028d4391615028dceb3c48edb0fbe5b6f87a1bceae1745cbc96ad2c0f96f0380897d6f1a9668d4a620adb9d52b0b32c67fe5
Binary file
data.tar.gz.sig CHANGED
Binary file
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.11.1
1
+ 1.12.0.rc0
@@ -717,11 +717,13 @@ module Mongo
717
717
 
718
718
  if result.key?('cursor')
719
719
  cursor_info = result['cursor']
720
+ pinned_pool = @connection.pinned_pool
721
+ pinned_pool = pinned_pool[:pool] if pinned_pool.respond_to?(:keys)
720
722
 
721
723
  seed = {
722
724
  :cursor_id => cursor_info['id'],
723
725
  :first_batch => cursor_info['firstBatch'],
724
- :pool => @connection.pinned_pool
726
+ :pool => pinned_pool
725
727
  }
726
728
 
727
729
  return Cursor.new(self, seed.merge!(opts))
@@ -887,10 +889,13 @@ module Mongo
887
889
  result = @db.command(cmd, command_options(opts))
888
890
 
889
891
  result['cursors'].collect do |cursor_info|
892
+ pinned_pool = @connection.pinned_pool
893
+ pinned_pool = pinned_pool[:pool] if pinned_pool.respond_to?(:keys)
894
+
890
895
  seed = {
891
896
  :cursor_id => cursor_info['cursor']['id'],
892
897
  :first_batch => cursor_info['cursor']['firstBatch'],
893
- :pool => @connection.pinned_pool
898
+ :pool => pinned_pool
894
899
  }
895
900
  Cursor.new(self, seed.merge!(opts))
896
901
  end
@@ -1024,7 +1029,7 @@ module Mongo
1024
1029
  #
1025
1030
  # @return [Hash] options that apply to this collection.
1026
1031
  def options
1027
- @db.collections_info(@name).next_document['options']
1032
+ @db.collections_info(@name).first['options']
1028
1033
  end
1029
1034
 
1030
1035
  # Return stats on the collection. Uses MongoDB's collstats command.
@@ -55,7 +55,7 @@ module Mongo
55
55
  @max_write_batch_size = @collection.db.connection.max_write_batch_size
56
56
  docs = documents.dup
57
57
  catch(:error) do
58
- until docs.empty? || (!errors.empty? && !collect_on_error) # process documents a batch at a time
58
+ until docs.empty? || (!errors.empty? && !collect_on_error && !continue_on_error) # process documents a batch at a time
59
59
  batch_docs = []
60
60
  batch_message_initialize(message, op_type, continue_on_error, write_concern)
61
61
  while !docs.empty? && batch_docs.size < @max_write_batch_size
@@ -33,7 +33,7 @@ module Mongo
33
33
 
34
34
  @socket_address = Socket.pack_sockaddr_un(@address)
35
35
  @socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
36
- connect
36
+ connect(@socket, @socket_address)
37
37
  end
38
38
  end
39
39
  end
@@ -564,9 +564,11 @@ module Mongo
564
564
  ensure
565
565
  socket.checkin unless @socket || socket.nil?
566
566
  end
567
- if !@socket && !@command
567
+
568
+ if pin_pool?(results.first)
568
569
  @connection.pin_pool(socket.pool, read_preference)
569
570
  end
571
+
570
572
  @returned += @n_received
571
573
  @cache += results
572
574
  @query_run = true
@@ -728,5 +730,10 @@ module Mongo
728
730
  end
729
731
  indexes.join("_")
730
732
  end
733
+
734
+ def pin_pool?(response)
735
+ ( response && (response['cursor'] || response['cursors']) ) ||
736
+ ( !@socket && !@command )
737
+ end
731
738
  end
732
739
  end
@@ -217,27 +217,25 @@ module Mongo
217
217
  #
218
218
  # @return [Hash] an object representing the user.
219
219
  def add_user(username, password=nil, read_only=false, opts={})
220
- begin
221
- user_info = command(:usersInfo => username)
222
- if user_info.key?('users') && !user_info['users'].empty?
223
- create_or_update_user(:updateUser, username, password, read_only, opts)
224
- else
225
- create_or_update_user(:createUser, username, password, read_only, opts)
226
- end
227
- # MongoDB >= 2.5.3 requires the use of commands to manage users.
228
- # "Command not found" error didn't return an error code (59) before
229
- # MongoDB 2.4.7 so we assume that a nil error code means the usersInfo
230
- # command doesn't exist and we should fall back to the legacy add user code.
231
- rescue OperationFailure => ex
232
- if Mongo::ErrorCode::COMMAND_NOT_FOUND_CODES.include?(ex.error_code)
233
- legacy_add_user(username, password, read_only, opts)
234
- elsif ex.error_code == Mongo::ErrorCode::UNAUTHORIZED
235
- # In MongoDB > 2.7 the localhost exception was narrowed, and the usersInfo
236
- # command is no longer allowed. In this case, add the first user.
237
- create_or_update_user(:createUser, username, password, read_only, opts)
238
- else
239
- raise ex
240
- end
220
+ user_info = command(:usersInfo => username)
221
+ if user_info.key?('users') && !user_info['users'].empty?
222
+ create_or_update_user(:updateUser, username, password, read_only, opts)
223
+ else
224
+ create_or_update_user(:createUser, username, password, read_only, opts)
225
+ end
226
+ # MongoDB >= 2.5.3 requires the use of commands to manage users.
227
+ # "Command not found" error didn't return an error code (59) before
228
+ # MongoDB 2.4.7 so we assume that a nil error code means the usersInfo
229
+ # command doesn't exist and we should fall back to the legacy add user code.
230
+ rescue OperationFailure => ex
231
+ if Mongo::ErrorCode::COMMAND_NOT_FOUND_CODES.include?(ex.error_code)
232
+ legacy_add_user(username, password, read_only, opts)
233
+ elsif ex.error_code == Mongo::ErrorCode::UNAUTHORIZED
234
+ # In MongoDB > 2.7 the localhost exception was narrowed, and the usersInfo
235
+ # command is no longer allowed. In this case, add the first user.
236
+ create_or_update_user(:createUser, username, password, read_only, opts)
237
+ else
238
+ raise ex
241
239
  end
242
240
  end
243
241
 
@@ -261,9 +259,14 @@ module Mongo
261
259
  #
262
260
  # @return [Array]
263
261
  def collection_names
264
- names = collections_info.collect { |doc| doc['name'] || '' }
265
- names = names.delete_if {|name| name.index(@name).nil? || name.index('$')}
266
- names.map {|name| name.sub(@name + '.', '')}
262
+ if @client.wire_version_feature?(Mongo::MongoClient::MONGODB_2_8)
263
+ names = collections_info.collect { |doc| doc['name'] || '' }
264
+ names.delete_if do |name|
265
+ name.index('$')
266
+ end
267
+ else
268
+ legacy_collection_names
269
+ end
267
270
  end
268
271
 
269
272
  # Get an array of Collection instances, one for each collection in this database.
@@ -281,11 +284,15 @@ module Mongo
281
284
  #
282
285
  # @param [String] coll_name return info for the specified collection only.
283
286
  #
284
- # @return [Mongo::Cursor]
287
+ # @return [Array] List of collection info.
285
288
  def collections_info(coll_name=nil)
286
- selector = {}
287
- selector[:name] = full_collection_name(coll_name) if coll_name
288
- Cursor.new(Collection.new(SYSTEM_NAMESPACE_COLLECTION, self), :selector => selector)
289
+ if @client.wire_version_feature?(Mongo::MongoClient::MONGODB_2_8)
290
+ cmd = BSON::OrderedHash[:listCollections, 1]
291
+ cmd.merge!(:filter => { :name => coll_name }) if coll_name
292
+ self.command(cmd)['collections']
293
+ else
294
+ legacy_collections_info(coll_name).to_a
295
+ end
289
296
  end
290
297
 
291
298
  # Create a collection.
@@ -482,12 +489,14 @@ module Mongo
482
489
  # @return [Hash] keys are index names and the values are lists of [key, type] pairs
483
490
  # defining the index.
484
491
  def index_information(collection_name)
485
- sel = {:ns => full_collection_name(collection_name)}
486
- info = {}
487
- Cursor.new(Collection.new(SYSTEM_INDEX_COLLECTION, self), :selector => sel).each do |index|
488
- info[index['name']] = index
492
+ if @client.wire_version_feature?(Mongo::MongoClient::MONGODB_2_8)
493
+ result = self.command(:listIndexes => collection_name)['indexes']
494
+ else
495
+ result = legacy_list_indexes(collection_name)
496
+ end
497
+ result.reduce({}) do |info, index|
498
+ info.merge!(index['name'] => index)
489
499
  end
490
- info
491
500
  end
492
501
 
493
502
  # Return stats on this database. Uses MongoDB's dbstats command.
@@ -736,5 +745,24 @@ module Mongo
736
745
  end
737
746
  user
738
747
  end
748
+
749
+ def legacy_list_indexes(collection_name)
750
+ sel = {:ns => full_collection_name(collection_name)}
751
+ Cursor.new(Collection.new(SYSTEM_INDEX_COLLECTION, self), :selector => sel)
752
+ end
753
+
754
+ def legacy_collections_info(coll_name=nil)
755
+ selector = {}
756
+ selector[:name] = full_collection_name(coll_name) if coll_name
757
+ Cursor.new(Collection.new(SYSTEM_NAMESPACE_COLLECTION, self), :selector => selector)
758
+ end
759
+
760
+ def legacy_collection_names
761
+ names = legacy_collections_info.collect { |doc| doc['name'] || '' }
762
+ names = names.delete_if do |name|
763
+ name.index(@name).nil? || name.index('$')
764
+ end
765
+ names.map {|name| name.sub(@name + '.', '')}
766
+ end
739
767
  end
740
768
  end
@@ -85,4 +85,61 @@ module Mongo
85
85
 
86
86
  # Raised for bulk write errors.
87
87
  class BulkWriteError < OperationFailure; end
88
+
89
+ # This exception is raised when the server nonce returned does not
90
+ # match the client nonce sent to it.
91
+ #
92
+ # @since 1.12.0
93
+ class InvalidNonce < OperationFailure
94
+
95
+ # @return [ String ] nonce The client nonce.
96
+ attr_reader :nonce
97
+
98
+ # @return [ String ] rnonce The server nonce.
99
+ attr_reader :rnonce
100
+
101
+ # Instantiate the new exception.
102
+ #
103
+ # @example Create the exception.
104
+ # InvalidNonce.new(nonce, rnonce)
105
+ #
106
+ # @param [ String ] nonce The client nonce.
107
+ # @param [ String ] rnonce The server nonce.
108
+ #
109
+ # @since 1.12.0
110
+ def initialize(nonce, rnonce)
111
+ @nonce = nonce
112
+ @rnonce = rnonce
113
+ super("Expected server rnonce '#{rnonce}' to start with client nonce '#{nonce}'.")
114
+ end
115
+ end
116
+
117
+ # This exception is raised when the server verifier does not match the
118
+ # expected signature on the client.
119
+ #
120
+ # @since 1.12.0
121
+ class InvalidSignature < OperationFailure
122
+
123
+ # @return [ String ] verifier The server verifier string.
124
+ attr_reader :verifier
125
+
126
+ # @return [ String ] server_signature The expected server signature.
127
+ attr_reader :server_signature
128
+
129
+ # Create the new exception.
130
+ #
131
+ # @example Create the new exception.
132
+ # InvalidSignature.new(verifier, server_signature)
133
+ #
134
+ # @param [ String ] verifier The verifier returned from the server.
135
+ # @param [ String ] server_signature The expected value from the
136
+ # server.
137
+ #
138
+ # @since 1.12.0
139
+ def initialize(verifier, server_signature)
140
+ @verifier = verifier
141
+ @server_signature = server_signature
142
+ super("Expected server verifier '#{verifier}' to match '#{server_signature}'.")
143
+ end
144
+ end
88
145
  end
@@ -17,3 +17,4 @@ require 'mongo/functional/logging'
17
17
  require 'mongo/functional/read_preference'
18
18
  require 'mongo/functional/write_concern'
19
19
  require 'mongo/functional/uri_parser'
20
+ require 'mongo/functional/scram'
@@ -18,8 +18,11 @@ module Mongo
18
18
  module Authentication
19
19
 
20
20
  DEFAULT_MECHANISM = 'MONGODB-CR'
21
- MECHANISMS = ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN']
22
- EXTRA = { 'GSSAPI' => [:gssapi_service_name, :canonicalize_host_name] }
21
+ MECHANISMS = ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN', 'SCRAM-SHA-1']
22
+ MECHANISM_ERROR = "Must use one of #{MECHANISMS.join(', ')} " +
23
+ "authentication mechanisms."
24
+ EXTRA = { 'GSSAPI' => [:service_name, :canonicalize_host_name,
25
+ :service_realm] }
23
26
 
24
27
  # authentication module methods
25
28
  class << self
@@ -49,15 +52,13 @@ module Mongo
49
52
  # @raise [MongoArgumentError] if the credential set is invalid.
50
53
  # @return [Hash] The validated credential set.
51
54
  def validate_credentials(auth)
52
- # set the default auth mechanism if not defined
53
- auth[:mechanism] ||= DEFAULT_MECHANISM
54
-
55
55
  # set the default auth source if not defined
56
56
  auth[:source] = auth[:source] || auth[:db_name] || 'admin'
57
57
 
58
- if (auth[:mechanism] == 'MONGODB-CR' || auth[:mechanism] == 'PLAIN') && !auth[:password]
58
+ if password_required?(auth[:mechanism]) && !auth[:password]
59
59
  raise MongoArgumentError,
60
- "When using the authentication mechanism #{auth[:mechanism]} " +
60
+ "When using the authentication mechanism " +
61
+ "#{auth[:mechanism].nil? ? 'MONGODB-CR or SCRAM-SHA-1' : auth[:mechanism]} " +
61
62
  "both username and password are required."
62
63
  end
63
64
  # if extra opts exist, validate them
@@ -92,6 +93,19 @@ module Mongo
92
93
  def hash_password(username, password)
93
94
  Digest::MD5.hexdigest("#{username}:mongo:#{password}")
94
95
  end
96
+
97
+ private
98
+
99
+ # Does the authentication require a password?
100
+ #
101
+ # @param [ String ] mech The authentication mechanism.
102
+ #
103
+ # @return [ true, false ] If a password is required.
104
+ #
105
+ # @since 1.12.0
106
+ def password_required?(mech)
107
+ mech == 'MONGODB-CR' || mech == 'PLAIN' || mech == 'SCRAM-SHA-1' || mech.nil?
108
+ end
95
109
  end
96
110
 
97
111
  # Saves a cache of authentication credentials to the current
@@ -104,7 +118,7 @@ module Mongo
104
118
  # @param source [String] (nil) The authentication source database
105
119
  # (if different than the current database).
106
120
  # @param mechanism [String] (nil) The authentication mechanism being used
107
- # (default: 'MONGODB-CR').
121
+ # (default: 'MONGODB-CR' or 'SCRAM-SHA-1' if server version >= 2.7.8).
108
122
  # @param extra [Hash] (nil) A optional hash of extra options to be stored with
109
123
  # the credential set.
110
124
  #
@@ -131,8 +145,8 @@ module Mongo
131
145
  end
132
146
 
133
147
  begin
134
- socket = self.checkout_reader(:mode => :primary_preferred)
135
- self.issue_authentication(auth, :socket => socket)
148
+ socket = checkout_reader(:mode => :primary_preferred)
149
+ issue_authentication(auth, :socket => socket)
136
150
  ensure
137
151
  socket.checkin if socket
138
152
  end
@@ -148,7 +162,10 @@ module Mongo
148
162
  # @return [Boolean] The result of the operation.
149
163
  def remove_auth(db_name)
150
164
  return false unless @auths
151
- @auths.reject! { |a| a[:source] == db_name } ? true : false
165
+ auths = @auths.to_a
166
+ removed = auths.reject! { |a| a[:source] == db_name }
167
+ @auths = Set.new(auths)
168
+ !!removed
152
169
  end
153
170
 
154
171
  # Remove all authentication information stored in this connection.
@@ -190,6 +207,11 @@ module Mongo
190
207
  # @raise [AuthenticationError] Raised if the authentication fails.
191
208
  # @return [Boolean] Result of the authentication operation.
192
209
  def issue_authentication(auth, opts={})
210
+ # set the default auth mechanism if not defined
211
+ auth[:mechanism] ||= default_mechanism
212
+
213
+ raise MongoArgumentError,
214
+ MECHANISM_ERROR unless MECHANISMS.include?(auth[:mechanism])
193
215
  result = case auth[:mechanism]
194
216
  when 'MONGODB-CR'
195
217
  issue_cr(auth, opts)
@@ -199,6 +221,8 @@ module Mongo
199
221
  issue_plain(auth, opts)
200
222
  when 'GSSAPI'
201
223
  issue_gssapi(auth, opts)
224
+ when 'SCRAM-SHA-1'
225
+ issue_scram(auth, opts)
202
226
  end
203
227
 
204
228
  unless Support.ok?(result)
@@ -212,6 +236,86 @@ module Mongo
212
236
 
213
237
  private
214
238
 
239
+ def default_mechanism
240
+ max_wire_version >= 3 ? 'SCRAM-SHA-1' : DEFAULT_MECHANISM
241
+ end
242
+
243
+ # Handles copying a database with SCRAM-SHA-1 authentication.
244
+ #
245
+ # @api private
246
+ #
247
+ # @param [ String ] username The user to authenticate on the
248
+ # 'from' database.
249
+ # @param [ String ] password The password for the user authenticated
250
+ # on the 'from' database.
251
+ # @param [ String ] from_host The host of the 'from' database.
252
+ # @param [ String ] from_db Name of the database to copy from.
253
+ # @param [ String ] to_db Name of the database to copy to.
254
+ #
255
+ # @return [ Hash ] The result of the copydb operation.
256
+ #
257
+ # @since 1.12.0
258
+ def copy_db_scram(username, password, from_host, from_db, to_db)
259
+ auth = { :db_name => from_db,
260
+ :username => username,
261
+ :password => password }
262
+
263
+ socket = checkout_reader(:mode => :primary_preferred)
264
+
265
+ copy_db = { :from_host => from_host, :from_db => from_db, :to_db => to_db }
266
+ scram = SCRAM.new(auth, Authentication.hash_password(username, password),
267
+ { :copy_db => copy_db })
268
+ result = auth_command(scram.copy_db_start, socket, 'admin').first
269
+ result = auth_command(scram.copy_db_continue(result), socket, 'admin').first
270
+ until result['done']
271
+ result = auth_command(scram.copy_db_continue(result), socket, 'admin').first
272
+ end
273
+ socket.checkin
274
+ result
275
+ end
276
+
277
+ # Handles copying a database with MONGODB-CR authentication.
278
+ #
279
+ # @api private
280
+ #
281
+ # @param [ String ] username The user to authenticate on the
282
+ # 'from' database.
283
+ # @param [ String ] password The password for the user authenticated
284
+ # on the 'from' database.
285
+ # @param [ String ] from_host The host of the 'from' database.
286
+ # @param [ String ] from_db Name of the database to copy from.
287
+ # @param [ String ] to_db Name of the database to copy to.
288
+ #
289
+ # @return [ Hash ] The result of the copydb operation.
290
+ #
291
+ # @since 1.12.0
292
+ def copy_db_mongodb_cr(username, password, from_host, from_db, to_db)
293
+ oh = BSON::OrderedHash.new
294
+ oh[:copydb] = 1
295
+ oh[:fromhost] = from_host
296
+ oh[:fromdb] = from_db
297
+ oh[:todb] = to_db
298
+
299
+ socket = checkout_reader(:mode => :primary_preferred)
300
+
301
+ if username || password
302
+ unless username && password
303
+ raise MongoArgumentError,
304
+ 'Both username and password must be supplied for authentication.'
305
+ end
306
+ nonce_cmd = BSON::OrderedHash.new
307
+ nonce_cmd[:copydbgetnonce] = 1
308
+ nonce_cmd[:fromhost] = from_host
309
+ result = auth_command(nonce_cmd, socket, 'admin').first
310
+ oh[:nonce] = result['nonce']
311
+ oh[:username] = username
312
+ oh[:key] = Authentication.auth_key(username, password, oh[:nonce])
313
+ end
314
+ result = auth_command(oh, socket, 'admin').first
315
+ socket.checkin
316
+ result
317
+ end
318
+
215
319
  # Handles issuing authentication commands for the MONGODB-CR auth mechanism.
216
320
  #
217
321
  # @param auth [Hash] The authentication credentials to be used.
@@ -287,6 +391,29 @@ module Mongo
287
391
  raise "In order to use Kerberos, please add the mongo-kerberos gem to your dependencies"
288
392
  end
289
393
 
394
+ # Handles issuing SCRAM-SHA-1 authentication.
395
+ #
396
+ # @api private
397
+ #
398
+ # @param [ Hash ] auth The authentication credentials.
399
+ # @param [ Hash ] opts The options.
400
+ #
401
+ # @options opts [ Socket ] socket The Socket instance to use.
402
+ #
403
+ # @return [ Hash ] The result of the authentication operation.
404
+ #
405
+ # @since 1.12.0
406
+ def issue_scram(auth, opts = {})
407
+ db_name = auth[:source]
408
+ scram = SCRAM.new(auth, Authentication.hash_password(auth[:username], auth[:password]))
409
+ result = auth_command(scram.start, opts[:socket], db_name).first
410
+ result = auth_command(scram.continue(result), opts[:socket], db_name).first
411
+ until result['done']
412
+ result = auth_command(scram.finalize(result), opts[:socket], db_name).first
413
+ end
414
+ result
415
+ end
416
+
290
417
  # Helper to fetch a nonce value from a given database instance.
291
418
  #
292
419
  # @param database [Mongo::DB] The DB instance to use for issue the nonce command.