mongo 2.24.1 → 2.25.0

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mongo_console +2 -2
  3. data/lib/mongo/address/validator.rb +1 -1
  4. data/lib/mongo/address.rb +1 -3
  5. data/lib/mongo/auth/aws/conversation.rb +0 -4
  6. data/lib/mongo/auth/aws/credentials_retriever.rb +3 -8
  7. data/lib/mongo/auth/base.rb +1 -1
  8. data/lib/mongo/auth/sasl_conversation_base.rb +1 -1
  9. data/lib/mongo/auth/scram_conversation_base.rb +1 -8
  10. data/lib/mongo/auth/stringprep.rb +1 -6
  11. data/lib/mongo/auth/user/view.rb +1 -1
  12. data/lib/mongo/bulk_write/result_combiner.rb +3 -3
  13. data/lib/mongo/bulk_write.rb +1 -1
  14. data/lib/mongo/client.rb +82 -12
  15. data/lib/mongo/cluster/reapers/cursor_reaper.rb +12 -5
  16. data/lib/mongo/cluster/sdam_flow.rb +4 -4
  17. data/lib/mongo/cluster/topology/base.rb +1 -1
  18. data/lib/mongo/cluster.rb +12 -28
  19. data/lib/mongo/collection/view/aggregation/behavior.rb +1 -1
  20. data/lib/mongo/collection/view/aggregation.rb +2 -4
  21. data/lib/mongo/collection/view/builder/aggregation.rb +3 -5
  22. data/lib/mongo/collection/view/builder/map_reduce.rb +1 -1
  23. data/lib/mongo/collection/view/change_stream.rb +14 -9
  24. data/lib/mongo/collection/view/map_reduce.rb +2 -11
  25. data/lib/mongo/collection/view/readable.rb +11 -7
  26. data/lib/mongo/collection/view/writable.rb +4 -4
  27. data/lib/mongo/collection/view.rb +8 -4
  28. data/lib/mongo/collection.rb +1 -1
  29. data/lib/mongo/crypt/auto_encrypter.rb +1 -1
  30. data/lib/mongo/crypt/binary.rb +7 -4
  31. data/lib/mongo/crypt/binding.rb +88 -10
  32. data/lib/mongo/crypt/context.rb +50 -19
  33. data/lib/mongo/crypt/encryption_io.rb +11 -5
  34. data/lib/mongo/crypt/handle.rb +17 -5
  35. data/lib/mongo/crypt/kms/credentials.rb +70 -31
  36. data/lib/mongo/crypt/kms/kmip/master_document.rb +30 -1
  37. data/lib/mongo/crypt/kms/master_key_document.rb +11 -6
  38. data/lib/mongo/crypt/kms.rb +12 -1
  39. data/lib/mongo/cursor.rb +8 -2
  40. data/lib/mongo/database/cursor_command_view.rb +95 -0
  41. data/lib/mongo/database.rb +144 -18
  42. data/lib/mongo/error/invalid_uri.rb +5 -1
  43. data/lib/mongo/error/operation_failure.rb +2 -2
  44. data/lib/mongo/error/parser.rb +3 -3
  45. data/lib/mongo/error/sdam_error_detection.rb +2 -3
  46. data/lib/mongo/grid/fs_bucket.rb +27 -21
  47. data/lib/mongo/grid/stream/write.rb +2 -2
  48. data/lib/mongo/index/view.rb +13 -7
  49. data/lib/mongo/monitoring/event/cmap/connection_check_out_failed.rb +1 -0
  50. data/lib/mongo/monitoring/event/cmap/connection_check_out_started.rb +1 -0
  51. data/lib/mongo/monitoring/event/cmap/connection_checked_in.rb +1 -0
  52. data/lib/mongo/monitoring/event/cmap/connection_checked_out.rb +1 -0
  53. data/lib/mongo/monitoring/event/cmap/connection_closed.rb +1 -0
  54. data/lib/mongo/monitoring/event/cmap/connection_created.rb +1 -0
  55. data/lib/mongo/monitoring/event/cmap/connection_ready.rb +1 -0
  56. data/lib/mongo/monitoring/event/cmap/pool_cleared.rb +1 -0
  57. data/lib/mongo/monitoring/event/cmap/pool_closed.rb +1 -0
  58. data/lib/mongo/monitoring/event/cmap/pool_created.rb +1 -0
  59. data/lib/mongo/monitoring/event/cmap/pool_ready.rb +1 -0
  60. data/lib/mongo/monitoring/event/command_failed.rb +1 -0
  61. data/lib/mongo/monitoring/event/command_started.rb +1 -0
  62. data/lib/mongo/monitoring/event/command_succeeded.rb +1 -0
  63. data/lib/mongo/monitoring/event/secure.rb +12 -17
  64. data/lib/mongo/monitoring/event/server_closed.rb +1 -0
  65. data/lib/mongo/monitoring/event/server_description_changed.rb +1 -0
  66. data/lib/mongo/monitoring/event/server_heartbeat_failed.rb +1 -0
  67. data/lib/mongo/monitoring/event/server_heartbeat_started.rb +1 -0
  68. data/lib/mongo/monitoring/event/server_heartbeat_succeeded.rb +1 -0
  69. data/lib/mongo/monitoring/event/server_opening.rb +1 -0
  70. data/lib/mongo/monitoring/event/topology_changed.rb +1 -0
  71. data/lib/mongo/monitoring/event/topology_closed.rb +1 -0
  72. data/lib/mongo/monitoring/event/topology_opening.rb +1 -0
  73. data/lib/mongo/operation/create/op_msg.rb +1 -0
  74. data/lib/mongo/operation/create_index/op_msg.rb +2 -1
  75. data/lib/mongo/operation/cursor_command/op_msg.rb +37 -0
  76. data/lib/mongo/operation/cursor_command/result.rb +60 -0
  77. data/lib/mongo/operation/cursor_command.rb +33 -0
  78. data/lib/mongo/operation/delete/op_msg.rb +1 -0
  79. data/lib/mongo/operation/drop/op_msg.rb +1 -0
  80. data/lib/mongo/operation/drop_database/op_msg.rb +1 -0
  81. data/lib/mongo/operation/drop_index/op_msg.rb +1 -0
  82. data/lib/mongo/operation/find/builder/command.rb +1 -1
  83. data/lib/mongo/operation/insert/bulk_result.rb +2 -4
  84. data/lib/mongo/operation/insert/op_msg.rb +1 -0
  85. data/lib/mongo/operation/result.rb +12 -25
  86. data/lib/mongo/operation/shared/causal_consistency_supported.rb +4 -2
  87. data/lib/mongo/operation/shared/executable.rb +1 -9
  88. data/lib/mongo/operation/shared/result/aggregatable.rb +2 -2
  89. data/lib/mongo/operation/shared/sessions_supported.rb +3 -5
  90. data/lib/mongo/operation/update/bulk_result.rb +1 -1
  91. data/lib/mongo/operation/update/op_msg.rb +1 -0
  92. data/lib/mongo/operation/write_command/op_msg.rb +2 -0
  93. data/lib/mongo/operation.rb +1 -0
  94. data/lib/mongo/options/redacted.rb +2 -2
  95. data/lib/mongo/protocol/compressed.rb +15 -11
  96. data/lib/mongo/protocol/message.rb +16 -16
  97. data/lib/mongo/protocol/registry.rb +1 -1
  98. data/lib/mongo/protocol/serializers.rb +1 -3
  99. data/lib/mongo/query_cache.rb +3 -5
  100. data/lib/mongo/server/connection.rb +3 -1
  101. data/lib/mongo/server/connection_common.rb +9 -1
  102. data/lib/mongo/server/connection_pool/generation_manager.rb +1 -1
  103. data/lib/mongo/server/connection_pool.rb +24 -10
  104. data/lib/mongo/server/description/features.rb +1 -1
  105. data/lib/mongo/server/description.rb +1 -1
  106. data/lib/mongo/server/monitor/connection.rb +3 -2
  107. data/lib/mongo/server/monitor.rb +133 -30
  108. data/lib/mongo/server/pending_connection.rb +1 -0
  109. data/lib/mongo/server/push_monitor.rb +16 -2
  110. data/lib/mongo/server.rb +27 -7
  111. data/lib/mongo/server_selector/base.rb +3 -6
  112. data/lib/mongo/server_selector/secondary_preferred.rb +4 -1
  113. data/lib/mongo/session.rb +22 -23
  114. data/lib/mongo/socket/ssl.rb +3 -3
  115. data/lib/mongo/socket.rb +1 -7
  116. data/lib/mongo/srv/monitor.rb +11 -2
  117. data/lib/mongo/srv/resolver.rb +1 -1
  118. data/lib/mongo/srv/result.rb +5 -3
  119. data/lib/mongo/timeout.rb +10 -15
  120. data/lib/mongo/tracing/open_telemetry/command_tracer.rb +16 -2
  121. data/lib/mongo/uri/options_mapper.rb +21 -9
  122. data/lib/mongo/uri/srv_protocol.rb +4 -4
  123. data/lib/mongo/uri.rb +56 -8
  124. data/lib/mongo/utils.rb +1 -1
  125. data/lib/mongo/version.rb +1 -1
  126. metadata +6 -4
  127. data/lib/mongo/auth/stringprep/unicode_normalize/normalize.rb +0 -173
  128. data/lib/mongo/auth/stringprep/unicode_normalize/tables.rb +0 -1164
@@ -678,7 +678,7 @@ module Mongo
678
678
  # @api private
679
679
  def read_concern
680
680
  if options[:session] && options[:session].in_transaction?
681
- options[:session].send(:txn_read_concern) || collection.client.read_concern
681
+ options[:session].txn_read_concern || collection.client.read_concern
682
682
  else
683
683
  collection.read_concern
684
684
  end
@@ -749,12 +749,10 @@ module Mongo
749
749
  end
750
750
  end
751
751
 
752
- private
753
-
754
- def collation(doc = nil)
755
- configure(:collation, doc)
756
- end
757
-
752
+ # The server selector for this view, derived from its read
753
+ # preference (or the collection/client default).
754
+ #
755
+ # @api private
758
756
  def server_selector
759
757
  @server_selector ||= if options[:session] && options[:session].in_transaction?
760
758
  ServerSelector.get(read_preference || client.server_selector)
@@ -763,6 +761,12 @@ module Mongo
763
761
  end
764
762
  end
765
763
 
764
+ private
765
+
766
+ def collation(doc = nil)
767
+ configure(:collation, doc)
768
+ end
769
+
766
770
  def validate_doc!(doc)
767
771
  raise Error::InvalidDocument.new unless doc.respond_to?(:keys)
768
772
  end
@@ -624,8 +624,8 @@ module Mongo
624
624
  # @raise [ Error::InvalidUpdateDocument ] if the first key in the
625
625
  # document does not start with a $.
626
626
  def validate_update_documents!(spec)
627
- return unless update = spec.is_a?(Array) ? spec&.first : spec
628
- return unless key = update.keys&.first
627
+ return unless (update = spec.is_a?(Array) ? spec&.first : spec)
628
+ return unless (key = update.keys&.first)
629
629
  return if key.to_s.start_with?('$')
630
630
  raise Error::InvalidUpdateDocument.new(key: key) if Mongo.validate_update_replace
631
631
 
@@ -641,8 +641,8 @@ module Mongo
641
641
  # @raise [ Error::InvalidUpdateDocument ] if the first key in the
642
642
  # document does not start with a $.
643
643
  def validate_replacement_documents!(spec)
644
- return unless replace = spec.is_a?(Array) ? spec&.first : spec
645
- return unless key = replace.keys&.first
644
+ return unless (replace = spec.is_a?(Array) ? spec&.first : spec)
645
+ return unless (key = replace.keys&.first)
646
646
  return unless key.to_s.start_with?('$')
647
647
  raise Error::InvalidReplacementDocument.new(key: key) if Mongo.validate_update_replace
648
648
 
@@ -236,6 +236,14 @@ module Mongo
236
236
  end
237
237
  end
238
238
 
239
+ # Executes the provided block within the context of a session, using
240
+ # this view's options merged with the given ones.
241
+ #
242
+ # @api private
243
+ def with_session(opts = {}, &block)
244
+ client.with_session(@options.merge(opts), &block)
245
+ end
246
+
239
247
  private
240
248
 
241
249
  def initialize_copy(other)
@@ -252,10 +260,6 @@ module Mongo
252
260
  def view
253
261
  self
254
262
  end
255
-
256
- def with_session(opts = {}, &block)
257
- client.with_session(@options.merge(opts), &block)
258
- end
259
263
  end
260
264
  end
261
265
  end
@@ -393,7 +393,7 @@ module Mongo
393
393
  operation = { create: name }.merge(options)
394
394
  operation.delete(:write)
395
395
  operation.delete(:write_concern)
396
- client.send(:with_session, opts) do |session|
396
+ client.with_session(opts) do |session|
397
397
  write_concern = if opts[:write_concern]
398
398
  WriteConcern.get(opts[:write_concern])
399
399
  else
@@ -287,7 +287,7 @@ module Mongo
287
287
  @internal_client ||= client.with(
288
288
  auto_encryption_options: nil,
289
289
  min_pool_size: 0,
290
- monitoring: client.send(:monitoring)
290
+ monitoring: client.monitoring
291
291
  )
292
292
  end
293
293
  end
@@ -139,14 +139,17 @@ module Mongo
139
139
  # Wraps a String with a mongocrypt_binary_t, yielding an FFI::Pointer
140
140
  # to the wrapped struct.
141
141
  def self.wrap_string(str)
142
- binary_p = Binding.mongocrypt_binary_new_from_data(
143
- FFI::MemoryPointer.from_string(str),
144
- str.bytesize
145
- )
142
+ # mongocrypt_binary_new_from_data does not copy the buffer, so the
143
+ # MemoryPointer must outlive the mongocrypt_binary_t. Hold it in a
144
+ # local so it stays referenced for the whole method frame; otherwise
145
+ # GC can free the buffer while libmongocrypt still points into it.
146
+ data_p = FFI::MemoryPointer.from_string(str)
147
+ binary_p = Binding.mongocrypt_binary_new_from_data(data_p, str.bytesize)
146
148
  begin
147
149
  yield binary_p
148
150
  ensure
149
151
  Binding.mongocrypt_binary_destroy(binary_p)
152
+ data_p # rubocop:disable Lint/Void -- keep the buffer alive past destroy
150
153
  end
151
154
  end
152
155
  end
@@ -106,7 +106,7 @@ module Mongo
106
106
  def self.parse_version(version)
107
107
  Gem::Version.new(version)
108
108
  rescue ArgumentError
109
- match = version.match(/\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?(-[A-Za-z+\d]+)?\z/)
109
+ match = version.match(/\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?(?:-[A-Za-z+\d]+)?\z/)
110
110
  raise ArgumentError.new("Malformed version number string #{version}") if match.nil?
111
111
 
112
112
  Gem::Version.new(
@@ -823,14 +823,15 @@ module Mongo
823
823
 
824
824
  # An enum labeling different libmognocrypt state machine states
825
825
  enum :mongocrypt_ctx_state, [
826
- :error, 0,
827
- :need_mongo_collinfo, 1,
828
- :need_mongo_markings, 2,
829
- :need_mongo_keys, 3,
830
- :need_kms, 4,
831
- :ready, 5,
832
- :done, 6,
833
- :need_kms_credentials, 7,
826
+ :error, 0,
827
+ :need_mongo_collinfo, 1,
828
+ :need_mongo_markings, 2,
829
+ :need_mongo_keys, 3,
830
+ :need_kms, 4,
831
+ :ready, 5,
832
+ :done, 6,
833
+ :need_kms_credentials, 7,
834
+ :need_mongo_collinfo_with_db, 8,
834
835
  ]
835
836
 
836
837
  # @!method self.mongocrypt_ctx_state(ctx)
@@ -1105,10 +1106,20 @@ module Mongo
1105
1106
  def self.check_kms_ctx_status(kms_context)
1106
1107
  return if yield
1107
1108
 
1109
+ kms_ctx_status(kms_context).raise_crypt_error(kms: true)
1110
+ end
1111
+
1112
+ # Read the status information for the given KmsContext into a Status
1113
+ # object without raising.
1114
+ #
1115
+ # @param [ Mongo::Crypt::KmsContext ] kms_context
1116
+ #
1117
+ # @return [ Mongo::Crypt::Status ] The KMS context status.
1118
+ def self.kms_ctx_status(kms_context)
1108
1119
  status = Status.new
1109
1120
 
1110
1121
  mongocrypt_kms_ctx_status(kms_context.kms_ctx_p, status.ref)
1111
- status.raise_crypt_error(kms: true)
1122
+ status
1112
1123
  end
1113
1124
 
1114
1125
  # @!method self.mongocrypt_kms_ctx_usleep(ctx)
@@ -1646,6 +1657,73 @@ module Mongo
1646
1657
  mongocrypt_setopt_use_need_kms_credentials_state(handle.ref)
1647
1658
  end
1648
1659
 
1660
+ # @!method self.mongocrypt_setopt_enable_multiple_collinfo(crypt)
1661
+ # @api private
1662
+ #
1663
+ # Enable support for multiple collection schemas. Required to support $lookup.
1664
+ #
1665
+ # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
1666
+ # @pre mongocrypt_init has not been called on crypt.
1667
+ # @return [ Boolean ] Whether the option was set successfully.
1668
+ attach_function(
1669
+ :mongocrypt_setopt_enable_multiple_collinfo,
1670
+ [ :pointer ],
1671
+ :bool
1672
+ )
1673
+
1674
+ # Enable support for multiple collection schemas. Required to support $lookup.
1675
+ #
1676
+ # @param [ Mongo::Crypt::Handle ] handle
1677
+ # @return [ Boolean ] Whether the option was set successfully.
1678
+ def self.setopt_enable_multiple_collinfo(handle)
1679
+ check_status(handle) do
1680
+ mongocrypt_setopt_enable_multiple_collinfo(handle.ref)
1681
+ end
1682
+ end
1683
+
1684
+ # @!method self.mongocrypt_setopt_use_need_mongo_collinfo_with_db_state(crypt)
1685
+ # @api private
1686
+ #
1687
+ # Opt into handling the MONGOCRYPT_CTX_NEED_MONGO_COLLINFO_WITH_DB state.
1688
+ # A context enters this state when processing a bulkWrite command whose
1689
+ # target database differs from the command database ("admin").
1690
+ #
1691
+ # @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
1692
+ attach_function(
1693
+ :mongocrypt_setopt_use_need_mongo_collinfo_with_db_state,
1694
+ [ :pointer ],
1695
+ :void
1696
+ )
1697
+
1698
+ # Opt into handling the MONGOCRYPT_CTX_NEED_MONGO_COLLINFO_WITH_DB state.
1699
+ #
1700
+ # @param [ Mongo::Crypt::Handle ] handle
1701
+ def self.setopt_use_need_mongo_collinfo_with_db_state(handle)
1702
+ mongocrypt_setopt_use_need_mongo_collinfo_with_db_state(handle.ref)
1703
+ end
1704
+
1705
+ # @!method self.mongocrypt_ctx_mongo_db(ctx)
1706
+ # @api private
1707
+ #
1708
+ # Get the database name for the current collinfo operation. Used in the
1709
+ # MONGOCRYPT_CTX_NEED_MONGO_COLLINFO_WITH_DB state to determine which
1710
+ # database to run listCollections against.
1711
+ #
1712
+ # @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
1713
+ # @return [ String, nil ] The database name, or nil on error.
1714
+ attach_function :mongocrypt_ctx_mongo_db, [ :pointer ], :string
1715
+
1716
+ # Get the database name for the current collinfo operation.
1717
+ #
1718
+ # @param [ Mongo::Crypt::Context ] context
1719
+ # @return [ String ] The database name.
1720
+ def self.ctx_mongo_db(context)
1721
+ db_name = mongocrypt_ctx_mongo_db(context.ctx_p)
1722
+ return db_name if db_name
1723
+
1724
+ check_ctx_status(context)
1725
+ end
1726
+
1649
1727
  # @!method self.mongocrypt_ctx_provide_kms_providers(ctx, kms_providers)
1650
1728
  # @api private
1651
1729
  #
@@ -76,7 +76,7 @@ module Mongo
76
76
  # This method is not currently unit tested. It is integration tested
77
77
  # in spec/integration/explicit_encryption_spec.rb
78
78
  def run_state_machine(timeout_holder)
79
- while true
79
+ loop do
80
80
  timeout_ms = timeout_holder.remaining_timeout_ms!
81
81
  case state
82
82
  when :error
@@ -90,6 +90,8 @@ module Mongo
90
90
  provide_keys(timeout_ms)
91
91
  when :need_mongo_collinfo
92
92
  provide_collection_info(timeout_ms)
93
+ when :need_mongo_collinfo_with_db
94
+ provide_collection_info_with_db(timeout_ms)
93
95
  when :need_mongo_markings
94
96
  provide_markings(timeout_ms)
95
97
  when :need_kms
@@ -122,17 +124,26 @@ module Mongo
122
124
  def provide_markings(timeout_ms)
123
125
  cmd = Binding.ctx_mongo_op(self)
124
126
 
125
- result = @encryption_io.mark_command(cmd, timeout_ms: timeout_ms)
127
+ result = @encryption_io.mark_command(cmd, db_name: @db_name, timeout_ms: timeout_ms)
126
128
  mongocrypt_feed(result)
127
129
 
128
130
  mongocrypt_done
129
131
  end
130
132
 
131
133
  def provide_collection_info(timeout_ms)
134
+ feed_collection_info(@db_name, timeout_ms)
135
+ end
136
+
137
+ def provide_collection_info_with_db(timeout_ms)
138
+ feed_collection_info(Binding.ctx_mongo_db(self), timeout_ms)
139
+ end
140
+
141
+ def feed_collection_info(db_name, timeout_ms)
132
142
  filter = Binding.ctx_mongo_op(self)
133
143
 
134
- result = @encryption_io.collection_info(@db_name, filter, timeout_ms: timeout_ms)
135
- mongocrypt_feed(result) if result
144
+ @encryption_io.collection_info(db_name, filter, timeout_ms: timeout_ms).each do |result|
145
+ mongocrypt_feed(result)
146
+ end
136
147
 
137
148
  mongocrypt_done
138
149
  end
@@ -159,12 +170,29 @@ module Mongo
159
170
  raise unless e.network_error?
160
171
  next if Binding.kms_ctx_fail(kms_context)
161
172
 
162
- raise
173
+ raise_kms_retry_error(kms_context, e)
163
174
  end
164
175
  end
165
176
  Binding.ctx_kms_done(self)
166
177
  end
167
178
 
179
+ # Raise a KmsError that wraps the KMS status message (which describes the
180
+ # retry exhaustion) with the error from the last attempt.
181
+ #
182
+ # @param [ Mongo::Crypt::KmsContext ] kms_context
183
+ # @param [ Mongo::Error::KmsError ] last_error The error from the last
184
+ # KMS request attempt.
185
+ #
186
+ # @raise [ Mongo::Error::KmsError ]
187
+ def raise_kms_retry_error(kms_context, last_error)
188
+ status = Binding.kms_ctx_status(kms_context)
189
+ raise Error::KmsError.new(
190
+ "#{status.message}, last attempt failed with: #{last_error.message}",
191
+ code: status.code,
192
+ network_error: true
193
+ )
194
+ end
195
+
168
196
  # Indicate that state machine is done feeding I/O responses back to libmongocrypt
169
197
  def mongocrypt_done
170
198
  Binding.mongocrypt_ctx_mongo_done(ctx_p)
@@ -188,21 +216,24 @@ module Mongo
188
216
  # KMS providers.
189
217
  def retrieve_kms_credentials(timeout_holder)
190
218
  providers = {}
191
- if kms_providers.aws && kms_providers.aws.empty?
192
- begin
193
- aws_credentials = Mongo::Auth::Aws::CredentialsRetriever.new.credentials(timeout_holder)
194
- rescue Auth::Aws::CredentialsNotFound
195
- raise Error::CryptError.new(
196
- 'Could not locate AWS credentials (checked environment variables, ECS and EC2 metadata)'
197
- )
219
+ kms_providers.credentials_map.each do |identifier, creds|
220
+ next unless creds.empty?
221
+
222
+ case KMS.provider_base_type(identifier)
223
+ when 'aws'
224
+ begin
225
+ aws_credentials = Mongo::Auth::Aws::CredentialsRetriever.new.credentials(timeout_holder)
226
+ rescue Auth::Aws::CredentialsNotFound
227
+ raise Error::CryptError.new(
228
+ 'Could not locate AWS credentials (checked environment variables, ECS and EC2 metadata)'
229
+ )
230
+ end
231
+ providers[identifier] = aws_credentials.to_h
232
+ when 'gcp'
233
+ providers[identifier] = { access_token: gcp_access_token(timeout_holder) }
234
+ when 'azure'
235
+ providers[identifier] = { access_token: azure_access_token(timeout_holder) }
198
236
  end
199
- providers[:aws] = aws_credentials.to_h
200
- end
201
- if kms_providers.gcp && kms_providers.gcp.empty?
202
- providers[:gcp] = { access_token: gcp_access_token(timeout_holder) }
203
- end
204
- if kms_providers.azure && kms_providers.azure.empty?
205
- providers[:azure] = { access_token: azure_access_token(timeout_holder) }
206
237
  end
207
238
  KMS::Credentials.new(providers)
208
239
  end
@@ -97,7 +97,7 @@ module Mongo
97
97
  # Must be a non-negative integer. An explicit value of 0 means infinite.
98
98
  # The default value is unset which means the feature is not enabled.
99
99
  #
100
- # @return [ Hash ] The collection information
100
+ # @return [ Array<BSON::Document> ] The collection information documents
101
101
  def collection_info(db_name, filter, timeout_ms: nil)
102
102
  unless @metadata_client
103
103
  raise ArgumentError,
@@ -108,18 +108,22 @@ module Mongo
108
108
  .use(db_name)
109
109
  .database
110
110
  .list_collections(filter: filter, deserialize_as_bson: true, timeout_ms: timeout_ms)
111
- .first
111
+ .to_a
112
112
  end
113
113
 
114
114
  # Send the command to mongocryptd to be marked with intent-to-encrypt markings
115
115
  #
116
116
  # @param [ Hash ] cmd
117
+ # @param [ String | nil ] :db_name The database against which the command
118
+ # is being run. When provided, the command is sent to mongocryptd using
119
+ # this database so that the namespace in the command matches the namespace
120
+ # in encryptionInformation.
117
121
  # @param [ Integer ] :timeout_ms The operation timeout in milliseconds.
118
122
  # Must be a non-negative integer. An explicit value of 0 means infinite.
119
123
  # The default value is unset which means the feature is not enabled.
120
124
  #
121
125
  # @return [ Hash ] The marked command
122
- def mark_command(cmd, timeout_ms: nil)
126
+ def mark_command(cmd, db_name: nil, timeout_ms: nil)
123
127
  unless @mongocryptd_client
124
128
  raise ArgumentError,
125
129
  'mark_command requires mongocryptd_client to have been passed to the constructor, but it was not'
@@ -132,13 +136,15 @@ module Mongo
132
136
  timeout_ms: timeout_ms
133
137
  }
134
138
 
139
+ mongocryptd = db_name ? @mongocryptd_client.use(db_name) : @mongocryptd_client
140
+
135
141
  begin
136
- response = @mongocryptd_client.database.command(cmd, options)
142
+ response = mongocryptd.database.command(cmd, options)
137
143
  rescue Error::NoServerAvailable => e
138
144
  raise e if @options[:mongocryptd_bypass_spawn]
139
145
 
140
146
  spawn_mongocryptd
141
- response = @mongocryptd_client.database.command(cmd, options)
147
+ response = mongocryptd.database.command(cmd, options)
142
148
  end
143
149
 
144
150
  response.first
@@ -74,6 +74,8 @@ module Mongo
74
74
  Binding.method(:mongocrypt_destroy)
75
75
  )
76
76
  Binding.kms_ctx_setopt_retry_kms(self, true)
77
+ Binding.setopt_enable_multiple_collinfo(self)
78
+ Binding.setopt_use_need_mongo_collinfo_with_db_state(self)
77
79
  @kms_providers = kms_providers
78
80
  @kms_tls_options = kms_tls_options
79
81
 
@@ -101,7 +103,7 @@ module Mongo
101
103
 
102
104
  Binding.setopt_kms_providers(self, @kms_providers.to_document)
103
105
 
104
- if @kms_providers.aws&.empty? || @kms_providers.gcp&.empty? || @kms_providers.azure&.empty?
106
+ if @kms_providers.any_on_demand?
105
107
  Binding.setopt_use_need_kms_credentials_state(self)
106
108
  end
107
109
 
@@ -123,13 +125,23 @@ module Mongo
123
125
  end
124
126
 
125
127
  # Return TLS options for KMS provider. If there are no TLS options set,
126
- # empty hash is returned.
128
+ # empty hash is returned. Named providers (e.g. "kmip:name1") fall back
129
+ # to the base-type options (e.g. :kmip) when no exact match is found.
127
130
  #
128
- # @param [ String ] provider KSM provider name.
131
+ # @param [ String ] provider KMS provider name or named identifier.
129
132
  #
130
133
  # @return [ Hash ] TLS options to connect to KMS provider.
131
134
  def kms_tls_options(provider)
132
- @kms_tls_options.fetch(provider, {})
135
+ provider_str = provider.to_s
136
+ base_type = KMS.provider_base_type(provider_str)
137
+
138
+ @kms_tls_options.fetch(provider_str) do
139
+ @kms_tls_options.fetch(provider_str.to_sym) do
140
+ @kms_tls_options.fetch(base_type) do
141
+ @kms_tls_options.fetch(base_type.to_sym, {})
142
+ end
143
+ end
144
+ end
133
145
  end
134
146
 
135
147
  def crypt_shared_lib_version
@@ -191,7 +203,7 @@ module Mongo
191
203
  # Send the logs from libmongocrypt to the Mongo::Logger
192
204
  def set_logger_callback
193
205
  @log_callback = proc do |level, msg|
194
- @logger.send(level, msg)
206
+ @logger.public_send(level, msg)
195
207
  end
196
208
 
197
209
  Binding.setopt_log_handler(@mongocrypt, @log_callback)
@@ -21,28 +21,17 @@ module Mongo
21
21
  #
22
22
  # @api private
23
23
  class Credentials
24
- # @return [ Credentials::AWS | nil ] AWS KMS credentials.
25
- attr_reader :aws
24
+ # KMS provider types that support on-demand credential retrieval.
25
+ ON_DEMAND_PROVIDERS = %w[aws gcp azure].freeze
26
26
 
27
- # @return [ Credentials::Azure | nil ] Azure KMS credentials.
28
- attr_reader :azure
27
+ attr_reader :credentials_map
29
28
 
30
- # @return [ Credentials::GCP | nil ] GCP KMS credentials.
31
- attr_reader :gcp
32
-
33
- # @return [ Credentials::KMIP | nil ] KMIP KMS credentials.
34
- attr_reader :kmip
35
-
36
- # @return [ Credentials::Local | nil ] Local KMS credentials.
37
- attr_reader :local
38
-
39
- # Creates a KMS credentials object form a parameters hash.
29
+ # Creates a KMS credentials object from a parameters hash.
40
30
  #
41
- # @param [ Hash ] kms_providers A hash that contains credential for
42
- # KMS providers. The hash should have KMS provider names as keys,
43
- # and required parameters for every provider as values.
44
- # Required parameters for KMS providers are described in corresponding
45
- # classes inside Mongo::Crypt::KMS module.
31
+ # @param [ Hash ] kms_providers A hash that contains credentials for
32
+ # KMS providers. Keys may be provider types (:aws, :local, etc.) or
33
+ # named provider identifiers ("aws:name1", "local:name2", etc.).
34
+ # Values are hashes of credentials for the corresponding provider type.
46
35
  #
47
36
  # @note There may be more than one KMS provider specified.
48
37
  #
@@ -51,29 +40,79 @@ module Mongo
51
40
  def initialize(kms_providers)
52
41
  raise ArgumentError.new('KMS providers options must not be nil') if kms_providers.nil?
53
42
 
54
- @aws = AWS::Credentials.new(kms_providers[:aws]) if kms_providers.key?(:aws)
55
- @azure = Azure::Credentials.new(kms_providers[:azure]) if kms_providers.key?(:azure)
56
- @gcp = GCP::Credentials.new(kms_providers[:gcp]) if kms_providers.key?(:gcp)
57
- @kmip = KMIP::Credentials.new(kms_providers[:kmip]) if kms_providers.key?(:kmip)
58
- @local = Local::Credentials.new(kms_providers[:local]) if kms_providers.key?(:local)
59
- return unless @aws.nil? && @azure.nil? && @gcp.nil? && @kmip.nil? && @local.nil?
43
+ @credentials_map = {}
44
+
45
+ kms_providers.each do |identifier, opts|
46
+ identifier_str = identifier.to_s
47
+ provider_type = KMS.provider_base_type(identifier_str)
48
+
49
+ creds = case provider_type
50
+ when 'aws' then AWS::Credentials.new(opts)
51
+ when 'azure' then Azure::Credentials.new(opts)
52
+ when 'gcp' then GCP::Credentials.new(opts)
53
+ when 'kmip' then KMIP::Credentials.new(opts)
54
+ when 'local' then Local::Credentials.new(opts)
55
+ else
56
+ raise ArgumentError.new(
57
+ 'KMS providers options must have one of the following keys: ' \
58
+ ':aws, :azure, :gcp, :kmip, :local'
59
+ )
60
+ end
61
+
62
+ @credentials_map[identifier_str] = creds
63
+ end
64
+
65
+ return unless @credentials_map.empty?
60
66
 
61
67
  raise ArgumentError.new(
62
- 'KMS providers options must have one of the following keys: ' +
68
+ 'KMS providers options must have one of the following keys: ' \
63
69
  ':aws, :azure, :gcp, :kmip, :local'
64
70
  )
65
71
  end
66
72
 
73
+ # @return [ Credentials::AWS | nil ] AWS KMS credentials (unnamed provider only).
74
+ def aws
75
+ @credentials_map['aws']
76
+ end
77
+
78
+ # @return [ Credentials::Azure | nil ] Azure KMS credentials (unnamed provider only).
79
+ def azure
80
+ @credentials_map['azure']
81
+ end
82
+
83
+ # @return [ Credentials::GCP | nil ] GCP KMS credentials (unnamed provider only).
84
+ def gcp
85
+ @credentials_map['gcp']
86
+ end
87
+
88
+ # @return [ Credentials::KMIP | nil ] KMIP KMS credentials (unnamed provider only).
89
+ def kmip
90
+ @credentials_map['kmip']
91
+ end
92
+
93
+ # @return [ Credentials::Local | nil ] Local KMS credentials (unnamed provider only).
94
+ def local
95
+ @credentials_map['local']
96
+ end
97
+
98
+ # Returns true if any configured provider supports on-demand credential
99
+ # retrieval and has been configured with empty credentials.
100
+ #
101
+ # @return [ Boolean ]
102
+ def any_on_demand?
103
+ @credentials_map.any? do |identifier, creds|
104
+ ON_DEMAND_PROVIDERS.include?(KMS.provider_base_type(identifier)) && creds.empty?
105
+ end
106
+ end
107
+
67
108
  # Convert credentials object to a BSON document in libmongocrypt format.
68
109
  #
69
110
  # @return [ BSON::Document ] Credentials as BSON document.
70
111
  def to_document
71
112
  BSON::Document.new.tap do |bson|
72
- bson[:aws] = @aws.to_document if @aws
73
- bson[:azure] = @azure.to_document if @azure
74
- bson[:gcp] = @gcp.to_document if @gcp
75
- bson[:kmip] = @kmip.to_document if @kmip
76
- bson[:local] = @local.to_document if @local
113
+ @credentials_map.each do |identifier, creds|
114
+ bson[identifier] = creds.to_document
115
+ end
77
116
  end
78
117
  end
79
118
  end
@@ -31,8 +31,12 @@ module Mongo
31
31
  # @return [ String | nil ] KMIP KMS endpoint with optional port.
32
32
  attr_reader :endpoint
33
33
 
34
+ # @return [ true | false | nil ] Whether the KMIP server performs
35
+ # encryption and decryption of the data key.
36
+ attr_reader :delegated
37
+
34
38
  FORMAT_HINT = 'KMIP KMS key document must be in the format: ' +
35
- "{ key_id: 'KEY-ID', endpoint: 'ENDPOINT' }"
39
+ "{ key_id: 'KEY-ID', endpoint: 'ENDPOINT', delegated: true|false }"
36
40
 
37
41
  # Creates a master key document object form a parameters hash.
38
42
  #
@@ -42,6 +46,9 @@ module Mongo
42
46
  # a 96 byte KMIP Secret Data managed object, optional. If key_id
43
47
  # is omitted, the driver creates a random 96 byte identifier.
44
48
  # @option opts [ String | nil ] :endpoint KMIP endpoint, optional.
49
+ # @option opts [ true | false | nil ] :delegated If true, the KMIP
50
+ # server performs encryption and decryption of the data key,
51
+ # optional. Defaults to false.
45
52
  #
46
53
  # @raise [ ArgumentError ] If required options are missing or incorrectly
47
54
  # formatted.
@@ -52,6 +59,7 @@ module Mongo
52
59
  @endpoint = validate_param(
53
60
  :endpoint, opts, FORMAT_HINT, required: false
54
61
  )
62
+ @delegated = validate_delegated(opts)
55
63
  end
56
64
 
57
65
  # Convert master key document object to a BSON document in libmongocrypt format.
@@ -63,8 +71,29 @@ module Mongo
63
71
  }).tap do |bson|
64
72
  bson.update({ endpoint: endpoint }) unless endpoint.nil?
65
73
  bson.update({ keyId: key_id }) unless key_id.nil?
74
+ bson.update({ delegated: delegated }) unless delegated.nil?
66
75
  end
67
76
  end
77
+
78
+ private
79
+
80
+ # Validate the optional :delegated KMIP master key option.
81
+ #
82
+ # @param [ Hash ] opts Master key options.
83
+ #
84
+ # @return [ true | false | nil ] The delegated value, or nil if absent.
85
+ #
86
+ # @raise [ ArgumentError ] If delegated is present but not a boolean.
87
+ def validate_delegated(opts)
88
+ return nil unless opts.key?(:delegated)
89
+
90
+ value = opts[:delegated]
91
+ return value if value == true || value == false || value.nil?
92
+
93
+ raise ArgumentError.new(
94
+ "The delegated option must be a boolean; currently have #{value}"
95
+ )
96
+ end
68
97
  end
69
98
  end
70
99
  end