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
@@ -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