File: pending_connection.rb

package info (click to toggle)
ruby-mongo 2.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,020 kB
  • sloc: ruby: 110,810; makefile: 5
file content (305 lines) | stat: -rw-r--r-- 12,700 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# frozen_string_literal: true
# rubocop:todo all

# Copyright (C) 2019-2020 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
  class Server

    # This class encapsulates connections during handshake and authentication.
    #
    # @api private
    class PendingConnection < ConnectionBase
      extend Forwardable

      def initialize(socket, server, monitoring, options = {})
        @socket = socket
        @options = options
        @server = server
        @monitoring = monitoring
        @id = options[:id]
      end

      # @return [ Integer ] The ID for the connection. This is the same ID
      #   as that of the regular Connection object for which this
      #   PendingConnection instance was created.
      attr_reader :id

      def handshake_and_authenticate!
        speculative_auth_doc = nil
        if options[:user] || options[:auth_mech]
          # To create an Auth instance, we need to specify the mechanism,
          # but at this point we don't know the mechanism that ultimately
          # will be used (since this depends on the data returned by
          # the handshake, specifically server version).
          # However, we know that only 4.4+ servers support speculative
          # authentication, and those servers also generally support
          # SCRAM-SHA-256. We expect that user accounts created for 4.4+
          # servers would generally allow SCRAM-SHA-256 authentication;
          # user accounts migrated from pre-4.4 servers may only allow
          # SCRAM-SHA-1. The use of SCRAM-SHA-256 by default is thus
          # sensible, and it is also mandated by the speculative auth spec.
          # If no mechanism was specified and we are talking to a 3.0+
          # server, we'll send speculative auth document, the server will
          # ignore it and we'll perform authentication using explicit
          # command after having defaulted the mechanism later to CR.
          # If no mechanism was specified and we are talking to a 4.4+
          # server and the user account doesn't allow SCRAM-SHA-256, we will
          # authenticate in a separate command with SCRAM-SHA-1 after
          # going through SCRAM mechanism negotiation.
          default_options = Options::Redacted.new(:auth_mech => :scram256)
          speculative_auth_user = Auth::User.new(default_options.merge(options))
          speculative_auth = Auth.get(speculative_auth_user, self)
          speculative_auth_doc = speculative_auth.conversation.speculative_auth_document
        end

        result = handshake!(speculative_auth_doc: speculative_auth_doc)

        if description.unknown?
          raise Error::InternalDriverError, "Connection description cannot be unknown after successful handshake: #{description.inspect}"
        end

        begin
          if speculative_auth_doc && (speculative_auth_result = result['speculativeAuthenticate'])
            unless description.features.scram_sha_1_enabled?
              raise Error::InvalidServerAuthResponse, "Speculative auth succeeded on a pre-3.0 server"
            end
            case speculative_auth_user.mechanism
            when :mongodb_x509
              # Done
            # We default auth mechanism to scram256, but if user specified
            # scram explicitly we may be able to authenticate speculatively
            # with scram.
            when :scram, :scram256
              authenticate!(
                speculative_auth_client_nonce: speculative_auth.conversation.client_nonce,
                speculative_auth_mech: speculative_auth_user.mechanism,
                speculative_auth_result: speculative_auth_result,
              )
            else
              raise Error::InternalDriverError, "Speculative auth unexpectedly succeeded for mechanism #{speculative_auth_user.mechanism.inspect}"
            end
          elsif !description.arbiter?
            authenticate!
          end
        rescue Mongo::Error, Mongo::Error::AuthError => exc
          exc.service_id = service_id
          raise
        end

        if description.unknown?
          raise Error::InternalDriverError, "Connection description cannot be unknown after successful authentication: #{description.inspect}"
        end

        if server.load_balancer? && !description.mongos?
          raise Error::BadLoadBalancerTarget, "Load-balanced operation requires being connected a mongos, but the server at #{address.seed} reported itself as #{description.server_type.to_s.gsub('_', ' ')}"
        end
      end

      private

      # Sends the hello command to the server, then receive and deserialize
      # the response.
      #
      # This method is extracted to be mocked in the tests.
      #
      # @param [ Protocol::Message ] Command that should be sent to a server
      #   for handshake purposes.
      #
      # @return [ Mongo::Protocol::Reply ] Deserialized server response.
      def get_handshake_response(hello_command)
        @server.round_trip_time_calculator.measure do
          add_server_diagnostics do
            socket.write(hello_command.serialize.to_s)
            Protocol::Message.deserialize(socket, Protocol::Message::MAX_MESSAGE_SIZE)
          end
        end
      end

      # @param [ BSON::Document | nil ] speculative_auth_doc The document to
      #   provide in speculativeAuthenticate field of handshake command.
      #
      # @return [ BSON::Document ] The document of the handshake response for
      #   this particular connection.
      def handshake!(speculative_auth_doc: nil)
        unless socket
          raise Error::InternalDriverError, "Cannot handshake because there is no usable socket (for #{address})"
        end

        hello_command = handshake_command(
          handshake_document(
            app_metadata,
            speculative_auth_doc: speculative_auth_doc,
            load_balancer: server.load_balancer?,
            server_api: options[:server_api]
          )
        )
        doc = nil
        @server.handle_handshake_failure! do
          begin
            response = get_handshake_response(hello_command)
            result = Operation::Result.new([response])
            result.validate!
            doc = result.documents.first
          rescue => exc
            msg = "Failed to handshake with #{address}"
            Utils.warn_bg_exception(msg, exc,
              logger: options[:logger],
              log_prefix: options[:log_prefix],
              bg_error_backtrace: options[:bg_error_backtrace],
            )
            raise
          end
        end

        if @server.force_load_balancer?
          doc['serviceId'] ||= "fake:#{rand(2**32-1)+1}"
        end

        post_handshake(
          doc,
          @server.round_trip_time_calculator.average_round_trip_time,
          @server.round_trip_time_calculator.minimum_round_trip_time
        )

        doc
      end

      # @param [ String | nil ] speculative_auth_client_nonce The client
      #   nonce used in speculative auth on this connection that
      #   produced the specified speculative auth result.
      # @param [ Symbol | nil ] speculative_auth_mech Auth mechanism used
      #   for speculative auth, if speculative auth succeeded. If speculative
      #   auth was not performed or it failed, this must be nil.
      # @param [ BSON::Document | nil ] speculative_auth_result The
      #   value of speculativeAuthenticate field of hello response of
      #   the handshake on this connection.
      def authenticate!(
        speculative_auth_client_nonce: nil,
        speculative_auth_mech: nil,
        speculative_auth_result: nil
      )
        if options[:user] || options[:auth_mech]
          @server.handle_auth_failure! do
            begin
              auth = Auth.get(
                resolved_user(speculative_auth_mech: speculative_auth_mech),
                self,
                speculative_auth_client_nonce: speculative_auth_client_nonce,
                speculative_auth_result: speculative_auth_result,
              )
              auth.login
            rescue => exc
              msg = "Failed to authenticate to #{address}"
              Utils.warn_bg_exception(msg, exc,
                logger: options[:logger],
                log_prefix: options[:log_prefix],
                bg_error_backtrace: options[:bg_error_backtrace],
              )
              raise
            end
          end
        end
      end

      def ensure_connected
        yield @socket
      end

      # This is a separate method to keep the nesting level down.
      #
      # @return [ Server::Description ] The server description calculated from
      #   the handshake response for this particular connection.
      def post_handshake(response, average_rtt, minimum_rtt)
        if response["ok"] == 1
          # Auth mechanism is entirely dependent on the contents of
          # hello response *for this connection*.
          # Hello received by the monitoring connection should advertise
          # the same wire protocol, but if it doesn't, we use whatever
          # the monitoring connection advertised for filling out the
          # server description and whatever the non-monitoring connection
          # (that's this one) advertised for performing auth on that
          # connection.
          @sasl_supported_mechanisms = response['saslSupportedMechs']
          set_compressor!(response)
        else
          @sasl_supported_mechanisms = nil
        end

        @description = Description.new(
          address, response,
          average_round_trip_time: average_rtt,
          load_balancer: server.load_balancer?,
          force_load_balancer: options[:connect] == :load_balanced,
        ).tap do |new_description|
          @server.cluster.run_sdam_flow(@server.description, new_description)
        end
      end

      # The user as going to be used for authentication. This user has the
      # auth mechanism set and, if necessary, auth source.
      #
      # @param [ Symbol | nil ] speculative_auth_mech Auth mechanism used
      #   for speculative auth, if speculative auth succeeded. If speculative
      #   auth was not performed or it failed, this must be nil.
      #
      # @return [ Auth::User ] The resolved user.
      def resolved_user(speculative_auth_mech: nil)
        @resolved_user ||= begin
          unless options[:user] || options[:auth_mech]
            raise Mongo::Error, 'No authentication information specified in the client'
          end

          user_options = Options::Redacted.new(
            # When speculative auth is performed, we always use SCRAM-SHA-256.
            # At the same time we perform SCRAM mechanism negotiation in the
            # hello request.
            # If the credentials we are trying to authenticate with do not
            # map to an existing user, SCRAM mechanism negotiation will not
            # return anything which would cause the driver to use
            # SCRAM-SHA-1. However, on 4.4+ servers speculative auth would
            # succeed (technically just the first round-trip, not the entire
            # authentication flow) and we would be continuing it here;
            # in this case, we must use SCRAM-SHA-256 as the mechanism since
            # that is what the conversation was started with, even though
            # SCRAM mechanism negotiation did not return SCRAM-SHA-256 as a
            # valid mechanism to use for these credentials.
            :auth_mech => speculative_auth_mech || default_mechanism,
          ).merge(options)
          if user_options[:auth_mech] == :mongodb_x509
            user_options[:auth_source] = '$external'
          end
          Auth::User.new(user_options)
        end
      end

      def default_mechanism
        if description.nil?
          raise Mongo::Error, 'Trying to query default mechanism when handshake has not completed'
        end

        if description.features.scram_sha_1_enabled?
          if @sasl_supported_mechanisms&.include?('SCRAM-SHA-256')
            :scram256
          else
            :scram
          end
        else
          :mongodb_cr
        end
      end
    end
  end
end