mongo 1.11.1 → 1.12.0.rc0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -45,7 +45,8 @@ module Mongo
45
45
  'mapreduce',
46
46
  'replsetgetstatus',
47
47
  'ismaster',
48
- 'parallelcollectionscan'
48
+ 'parallelcollectionscan',
49
+ 'text'
49
50
  ]
50
51
 
51
52
  def self.mongos(mode, tag_sets)
@@ -74,7 +75,8 @@ module Mongo
74
75
  # the server only looks at the first key in the out object
75
76
  return out.respond_to?(:keys) && out.keys.first.to_s.downcase == 'inline'
76
77
  elsif command == 'aggregate'
77
- return selector['pipeline'].none? { |op| op.key?('$out') || op.key?(:$out) }
78
+ pipeline = selector['pipeline'] || selector[:pipeline]
79
+ return pipeline.none? { |op| op.key?('$out') || op.key?(:$out) }
78
80
  end
79
81
  SECONDARY_OK_COMMANDS.member?(command)
80
82
  end
@@ -144,31 +146,38 @@ module Mongo
144
146
  end
145
147
 
146
148
  def select_secondary_pool(candidates, read_pref)
147
- tag_sets = read_pref[:tags]
148
-
149
- if !tag_sets.empty?
150
- matches = []
151
- tag_sets.detect do |tag_set|
152
- matches = candidates.select do |candidate|
153
- tag_set.none? { |k,v| candidate.tags[k.to_s] != v } &&
154
- candidate.ping_time
155
- end
156
- !matches.empty?
157
- end
158
- else
159
- matches = candidates
160
- end
161
-
162
- matches.empty? ? nil : select_near_pool(matches, read_pref)
149
+ matching_pools = match_tag_sets(secondary_pools, read_pref[:tags])
150
+ select_near_pools(matching_pools, read_pref).first
163
151
  end
164
152
 
165
153
  def select_near_pool(candidates, read_pref)
154
+ matching_pools = match_tag_sets(candidates, read_pref[:tags])
155
+ select_near_pools(matching_pools, read_pref).first
156
+ end
157
+
158
+ private
159
+
160
+ def select_near_pools(candidates, read_pref)
161
+ return candidates if candidates.empty?
166
162
  latency = read_pref[:latency]
167
- nearest_pool = candidates.min_by { |candidate| candidate.ping_time }
168
- near_pools = candidates.select do |candidate|
169
- (candidate.ping_time - nearest_pool.ping_time) <= latency
163
+ nearest_pool = candidates.min_by(&:ping_time)
164
+ max_latency = nearest_pool.ping_time + latency
165
+ near_pools = candidates.select { |candidate| candidate.ping_time <= max_latency }
166
+ near_pools.shuffle!
167
+ end
168
+
169
+ def match_tag_sets(candidates, tag_sets = [])
170
+ return candidates if tag_sets.empty?
171
+ matches = []
172
+ tag_sets.find do |tag_set|
173
+ matches = candidates.select do |candidate|
174
+ tag_set.none? do |k,v|
175
+ candidate.tags[k.to_s] != v
176
+ end
177
+ end
178
+ !matches.empty?
170
179
  end
171
- near_pools[ rand(near_pools.length) ]
180
+ matches
172
181
  end
173
182
  end
174
183
  end
@@ -0,0 +1,555 @@
1
+ # Copyright (C) 2014 MongoDB Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'base64'
16
+ require 'openssl'
17
+ require 'digest/md5'
18
+
19
+ module Mongo
20
+ module Authentication
21
+
22
+ # Defines behaviour around a single SCRAM-SHA-1 conversation between the
23
+ # client and server.
24
+ #
25
+ # @since 1.12.0
26
+ class SCRAM
27
+
28
+ # The client key string.
29
+ #
30
+ # @since 1.12.0
31
+ CLIENT_KEY = 'Client Key'.freeze
32
+
33
+ # The digest to use for encryption.
34
+ #
35
+ # @since 1.12.0
36
+ DIGEST = OpenSSL::Digest::SHA1.new.freeze
37
+
38
+ # The key for the done field in the responses.
39
+ #
40
+ # @since 1.12.0
41
+ DONE = 'done'.freeze
42
+
43
+ # The conversation id field.
44
+ #
45
+ # @since 1.12.0
46
+ ID = 'conversationId'.freeze
47
+
48
+ # The iterations key in the responses.
49
+ #
50
+ # @since 1.12.0
51
+ ITERATIONS = /i=(\d+)/.freeze
52
+
53
+ # The payload field.
54
+ #
55
+ # @since 1.12.0
56
+ PAYLOAD = 'payload'.freeze
57
+
58
+ # The rnonce key in the responses.
59
+ #
60
+ # @since 1.12.0
61
+ RNONCE = /r=([^,]*)/.freeze
62
+
63
+ # The salt key in the responses.
64
+ #
65
+ # @since 1.12.0
66
+ SALT = /s=([^,]*)/.freeze
67
+
68
+ # The server key string.
69
+ #
70
+ # @since 1.12.0
71
+ SERVER_KEY = 'Server Key'.freeze
72
+
73
+ # The server signature verifier in the response.
74
+ #
75
+ # @since 1.12.0
76
+ VERIFIER = /v=([^,]*)/.freeze
77
+
78
+ # @return [ String ] nonce The initial user nonce.
79
+ attr_reader :nonce
80
+
81
+ # @return [ BSON::OrderedHash ] reply The current reply in the conversation.
82
+ attr_reader :reply
83
+
84
+ # @return [ Hash ] auth The authentication details.
85
+ attr_reader :auth
86
+
87
+ # @return [ String ] hashed_password The user's hashed password
88
+ attr_reader :hashed_password
89
+
90
+ # Continue the SCRAM conversation. This sends the client final message
91
+ # to the server after setting the reply from the previous server
92
+ # communication.
93
+ #
94
+ # @example Continue the conversation.
95
+ # conversation.continue(reply)
96
+ #
97
+ # @param [ BSON::OrderedHash ] reply The reply of the previous
98
+ # message.
99
+ #
100
+ # @return [ BSON::OrderedHash ] The next message to send.
101
+ #
102
+ # @since 1.12.0
103
+ def continue(reply)
104
+ validate_first_message!(reply)
105
+ command = BSON::OrderedHash.new
106
+ command['saslContinue'] = 1
107
+ command[PAYLOAD] = client_final_message
108
+ command[ID] = id
109
+ command
110
+ end
111
+
112
+ # Continue the SCRAM conversation for copydb. This sends the client final message
113
+ # to the server after setting the reply from the previous server
114
+ # communication.
115
+ #
116
+ # @example Continue the conversation when copying a database.
117
+ # conversation.copy_db_continue(reply)
118
+ #
119
+ # @param [ BSON::OrderedHash ] reply The reply of the previous
120
+ # message.
121
+ #
122
+ # @return [ BSON::OrderedHash ] The next message to send.
123
+ #
124
+ # @since 1.12.0
125
+ def copy_db_continue(reply)
126
+ validate_first_message!(reply)
127
+ command = BSON::OrderedHash.new
128
+ command['copydb'] = 1
129
+ command['fromhost'] = @copy_db[:from_host]
130
+ command['fromdb'] = @copy_db[:from_db]
131
+ command['todb'] = @copy_db[:to_db]
132
+ command[PAYLOAD] = client_final_message
133
+ command[ID] = id
134
+ command
135
+ end
136
+
137
+ # Finalize the SCRAM conversation. This is meant to be iterated until
138
+ # the provided reply indicates the conversation is finished.
139
+ #
140
+ # @example Finalize the conversation.
141
+ # conversation.finalize(reply)
142
+ #
143
+ # @param [ BSON::OrderedHash ] reply The reply of the previous
144
+ # message.
145
+ #
146
+ # @return [ BSON::OrderedHash ] The next message to send.
147
+ #
148
+ # @since 1.12.0
149
+ def finalize(reply)
150
+ validate_final_message!(reply)
151
+ command = BSON::OrderedHash.new
152
+ command['saslContinue'] = 1
153
+ command[PAYLOAD] = client_empty_message
154
+ command[ID] = id
155
+ command
156
+ end
157
+
158
+ # Start the SCRAM conversation. This returns the first message that
159
+ # needs to be send to the server.
160
+ #
161
+ # @example Start the conversation.
162
+ # conversation.start
163
+ #
164
+ # @return [ BSON::OrderedHash ] The first SCRAM conversation message.
165
+ #
166
+ # @since 1.12.0
167
+ def start
168
+ command = BSON::OrderedHash.new
169
+ command['saslStart'] = 1
170
+ command['autoAuthorize'] = 1
171
+ command[PAYLOAD] = client_first_message
172
+ command['mechanism'] = 'SCRAM-SHA-1'
173
+ command
174
+ end
175
+
176
+ # Start the SCRAM conversation for copying a database.
177
+ # This returns the first message that needs to be sent to the server.
178
+ #
179
+ # @example Start the copydb conversation.
180
+ # conversation.copy_db_start
181
+ #
182
+ # @return [ BSON::OrderedHash ] The first SCRAM copy_db conversation message.
183
+ #
184
+ # @since 1.12.0
185
+ def copy_db_start
186
+ command = BSON::OrderedHash.new
187
+ command['copydbsaslstart'] = 1
188
+ command['autoAuthorize'] = 1
189
+ command['fromhost'] = @copy_db[:from_host]
190
+ command['fromdb'] = @copy_db[:from_db]
191
+ command[PAYLOAD] = client_first_message
192
+ command['mechanism'] = 'SCRAM-SHA-1'
193
+ command
194
+ end
195
+
196
+ # Get the id of the conversation.
197
+ #
198
+ # @example Get the id of the conversation.
199
+ # conversation.id
200
+ #
201
+ # @return [ Integer ] The conversation id.
202
+ #
203
+ # @since 1.12.0
204
+ def id
205
+ reply[ID]
206
+ end
207
+
208
+ # Create the new conversation.
209
+ #
210
+ # @example Create the new conversation.
211
+ # Conversation.new(auth, password)
212
+ #
213
+ # @since 1.12.0
214
+ def initialize(auth, hashed_password, opts={})
215
+ @auth = auth
216
+ @hashed_password = hashed_password
217
+ @nonce = SecureRandom.base64
218
+ @copy_db = opts[:copy_db] if opts[:copy_db]
219
+ end
220
+
221
+ private
222
+
223
+ # Auth message algorithm implementation.
224
+ #
225
+ # @api private
226
+ #
227
+ # @see http://tools.ietf.org/html/rfc5802#section-3
228
+ #
229
+ # @since 1.12.0
230
+ def auth_message
231
+ @auth_message ||= "#{first_bare},#{payload_data},#{without_proof}"
232
+ end
233
+
234
+ # Get the empty client message.
235
+ #
236
+ # @api private
237
+ #
238
+ # @since 1.12.0
239
+ def client_empty_message
240
+ BSON::Binary.new('')
241
+ end
242
+
243
+ # Get the final client message.
244
+ #
245
+ # @api private
246
+ #
247
+ # @see http://tools.ietf.org/html/rfc5802#section-3
248
+ #
249
+ # @since 1.12.0
250
+ def client_final_message
251
+ BSON::Binary.new("#{without_proof},p=#{client_final}")
252
+ end
253
+
254
+ # Get the client first message
255
+ #
256
+ # @api private
257
+ #
258
+ # @see http://tools.ietf.org/html/rfc5802#section-3
259
+ #
260
+ # @since 1.12.0
261
+ def client_first_message
262
+ BSON::Binary.new("n,,#{first_bare}")
263
+ end
264
+
265
+ # Client final implementation.
266
+ #
267
+ # @api private
268
+ #
269
+ # @see http://tools.ietf.org/html/rfc5802#section-7
270
+ #
271
+ # @since 1.12.0
272
+ def client_final
273
+ @client_final ||= client_proof(client_key, client_signature(stored_key(client_key), auth_message))
274
+ end
275
+
276
+ # Client key algorithm implementation.
277
+ #
278
+ # @api private
279
+ #
280
+ # @see http://tools.ietf.org/html/rfc5802#section-3
281
+ #
282
+ # @since 1.12.0
283
+ def client_key
284
+ @client_key ||= hmac(salted_password, CLIENT_KEY)
285
+ end
286
+
287
+ if Base64.respond_to?(:strict_encode64)
288
+
289
+ # Client proof algorithm implementation.
290
+ #
291
+ # @api private
292
+ #
293
+ # @see http://tools.ietf.org/html/rfc5802#section-3
294
+ #
295
+ # @since 1.12.0
296
+ def client_proof(key, signature)
297
+ @client_proof ||= Base64.strict_encode64(xor(key, signature))
298
+ end
299
+ else
300
+
301
+ # Client proof algorithm implementation.
302
+ #
303
+ # @api private
304
+ #
305
+ # @see http://tools.ietf.org/html/rfc5802#section-3
306
+ #
307
+ # @since 1.12.0
308
+ def client_proof(key, signature)
309
+ @client_proof ||= Base64.encode64(xor(key, signature)).gsub("\n",'')
310
+ end
311
+ end
312
+
313
+ # Client signature algorithm implementation.
314
+ #
315
+ # @api private
316
+ #
317
+ # @see http://tools.ietf.org/html/rfc5802#section-3
318
+ #
319
+ # @since 1.12.0
320
+ def client_signature(key, message)
321
+ @client_signature ||= hmac(key, message)
322
+ end
323
+
324
+ if Base64.respond_to?(:strict_decode64)
325
+
326
+ # Get the base 64 decoded salt.
327
+ #
328
+ # @api private
329
+ #
330
+ # @since 1.12.0
331
+ def decoded_salt
332
+ @decoded_salt ||= Base64.strict_decode64(salt)
333
+ end
334
+ else
335
+
336
+ # Get the base 64 decoded salt.
337
+ #
338
+ # @api private
339
+ #
340
+ # @since 1.12.0
341
+ def decoded_salt
342
+ @decoded_salt ||= Base64.decode64(salt)
343
+ end
344
+ end
345
+
346
+ # First bare implementation.
347
+ #
348
+ # @api private
349
+ #
350
+ # @see http://tools.ietf.org/html/rfc5802#section-7
351
+ #
352
+ # @since 1.12.0
353
+ def first_bare
354
+ @first_bare ||= "n=#{auth[:username].gsub('=','=3D').gsub(',','=2C')},r=#{nonce}"
355
+ end
356
+
357
+ # H algorithm implementation.
358
+ #
359
+ # @api private
360
+ #
361
+ # @see http://tools.ietf.org/html/rfc5802#section-2.2
362
+ #
363
+ # @since 1.12.0
364
+ def h(string)
365
+ DIGEST.digest(string)
366
+ end
367
+
368
+ if defined?(OpenSSL::PKCS5)
369
+
370
+ # HI algorithm implementation.
371
+ #
372
+ # @api private
373
+ #
374
+ # @see http://tools.ietf.org/html/rfc5802#section-2.2
375
+ #
376
+ # @since 1.12.0
377
+ def hi(data)
378
+ OpenSSL::PKCS5.pbkdf2_hmac_sha1(data, decoded_salt, iterations, DIGEST.size)
379
+ end
380
+ else
381
+
382
+ # HI algorithm implementation.
383
+ #
384
+ # @api private
385
+ #
386
+ # @see http://tools.ietf.org/html/rfc5802#section-2.2
387
+ #
388
+ # @since 1.12.0
389
+ def hi(data)
390
+ u = hmac(data, decoded_salt + [1].pack("N"))
391
+ v = u
392
+ 2.upto(iterations) do |i|
393
+ u = hmac(data, u)
394
+ v = xor(v, u)
395
+ end
396
+ v
397
+ end
398
+ end
399
+
400
+ # HMAC algorithm implementation.
401
+ #
402
+ # @api private
403
+ #
404
+ # @see http://tools.ietf.org/html/rfc5802#section-2.2
405
+ #
406
+ # @since 1.12.0
407
+ def hmac(data, key)
408
+ OpenSSL::HMAC.digest(DIGEST, data, key)
409
+ end
410
+
411
+ # Get the iterations from the server response.
412
+ #
413
+ # @api private
414
+ #
415
+ # @since 1.12.0
416
+ def iterations
417
+ @iterations ||= payload_data.match(ITERATIONS)[1].to_i
418
+ end
419
+
420
+ # Get the data from the returned payload.
421
+ #
422
+ # @api private
423
+ #
424
+ # @since 1.12.0
425
+ def payload_data
426
+ reply[PAYLOAD].to_s
427
+ end
428
+
429
+ # Get the server nonce from the payload.
430
+ #
431
+ # @api private
432
+ #
433
+ # @since 1.12.0
434
+ def rnonce
435
+ @rnonce ||= payload_data.match(RNONCE)[1]
436
+ end
437
+
438
+ # Gets the salt from the server response.
439
+ #
440
+ # @api private
441
+ #
442
+ # @since 1.12.0
443
+ def salt
444
+ @salt ||= payload_data.match(SALT)[1]
445
+ end
446
+
447
+ # Salted password algorithm implementation.
448
+ #
449
+ # @api private
450
+ #
451
+ # @see http://tools.ietf.org/html/rfc5802#section-3
452
+ #
453
+ # @since 1.12.0
454
+ def salted_password
455
+ @salted_password ||= hi(hashed_password)
456
+ end
457
+
458
+ # Server key algorithm implementation.
459
+ #
460
+ # @api private
461
+ #
462
+ # @see http://tools.ietf.org/html/rfc5802#section-3
463
+ #
464
+ # @since 1.12.0
465
+ def server_key
466
+ @server_key ||= hmac(salted_password, SERVER_KEY)
467
+ end
468
+
469
+ if Base64.respond_to?(:strict_encode64)
470
+
471
+ # Server signature algorithm implementation.
472
+ #
473
+ # @api private
474
+ #
475
+ # @see http://tools.ietf.org/html/rfc5802#section-3
476
+ #
477
+ # @since 1.12.0
478
+ def server_signature
479
+ @server_signature ||= Base64.strict_encode64(hmac(server_key, auth_message))
480
+ end
481
+ else
482
+
483
+ # Server signature algorithm implementation.
484
+ #
485
+ # @api private
486
+ #
487
+ # @see http://tools.ietf.org/html/rfc5802#section-3
488
+ #
489
+ # @since 1.12.0
490
+ def server_signature
491
+ @server_signature ||= Base64.encode64(hmac(server_key, auth_message)).gsub("\n",'')
492
+ end
493
+ end
494
+
495
+ # Stored key algorithm implementation.
496
+ #
497
+ # @api private
498
+ #
499
+ # @see http://tools.ietf.org/html/rfc5802#section-3
500
+ #
501
+ # @since 1.12.0
502
+ def stored_key(key)
503
+ h(key)
504
+ end
505
+
506
+ # Get the verifier token from the server response.
507
+ #
508
+ # @api private
509
+ #
510
+ # @since 1.12.0
511
+ def verifier
512
+ @verifier ||= payload_data.match(VERIFIER)[1]
513
+ end
514
+
515
+ # Get the without proof message.
516
+ #
517
+ # @api private
518
+ #
519
+ # @see http://tools.ietf.org/html/rfc5802#section-7
520
+ #
521
+ # @since 1.12.0
522
+ def without_proof
523
+ @without_proof ||= "c=biws,r=#{rnonce}"
524
+ end
525
+
526
+ # XOR operation for two strings.
527
+ #
528
+ # @api private
529
+ #
530
+ # @since 1.12.0
531
+ def xor(first, second)
532
+ first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('')
533
+ end
534
+
535
+ def validate_final_message!(reply)
536
+ validate!(reply)
537
+ unless verifier == server_signature
538
+ raise InvalidSignature.new(verifier, server_signature)
539
+ end
540
+ end
541
+
542
+ def validate_first_message!(reply)
543
+ validate!(reply)
544
+ raise InvalidNonce.new(nonce, rnonce) unless rnonce.start_with?(nonce)
545
+ end
546
+
547
+ def validate!(reply)
548
+ unless Support.ok?(reply)
549
+ raise AuthenticationError, "Could not authorize user #{auth[:username]} on database #{auth[:db_name]}."
550
+ end
551
+ @reply = reply
552
+ end
553
+ end
554
+ end
555
+ end