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
|
# 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
module Crypt
# An AutoEcnrypter is an object that encapsulates the behavior of
# automatic encryption. It controls all resources associated with
# auto-encryption, including the libmongocrypt handle, key vault client
# object, mongocryptd client object, and encryption I/O.
#
# The AutoEncrypter is kept as an instance on a Mongo::Client. Client
# objects with the same auto_encryption_options Hash may share
# AutoEncrypters.
#
# @api private
class AutoEncrypter
attr_reader :mongocryptd_client
attr_reader :key_vault_client
attr_reader :metadata_client
attr_reader :options
# A Hash of default values for the :extra_options option
DEFAULT_EXTRA_OPTIONS = Options::Redacted.new({
mongocryptd_uri: 'mongodb://localhost:27020',
mongocryptd_bypass_spawn: false,
mongocryptd_spawn_path: 'mongocryptd',
mongocryptd_spawn_args: ['--idleShutdownTimeoutSecs=60'],
})
# Set up encryption-related options and instance variables
# on the class that includes this module. Calls the same method
# on the Mongo::Crypt::Encrypter module.
#
# @param [ Hash ] options
#
# @option options [ Mongo::Client ] :client A client connected to the
# encrypted collection.
# @option options [ Mongo::Client | nil ] :key_vault_client A client connected
# to the MongoDB instance containing the encryption key vault; optional.
# If not provided, will default to :client option.
# @option options [ String ] :key_vault_namespace The namespace of the key
# vault in the format database.collection.
# @option options [ Hash | nil ] :schema_map The JSONSchema of the collection(s)
# with encrypted fields. This option is mutually exclusive with :schema_map_path.
# @option options [ String | nil ] :schema_map_path A path to a file contains the JSON schema
# of the collection that stores auto encrypted documents. This option is
# mutually exclusive with :schema_map.
# @option options [ Boolean | nil ] :bypass_auto_encryption When true, disables
# auto-encryption. Default is false.
# @option options [ Hash | nil ] :extra_options Options related to spawning
# mongocryptd. These are set to default values if no option is passed in.
# @option options [ Hash ] :kms_providers A hash of key management service
# configuration information.
# @see Mongo::Crypt::KMS::Credentials for list of options for every
# supported provider.
# @note There may be more than one KMS provider specified.
# @option options [ Hash ] :kms_tls_options TLS options to connect to KMS
# providers. Keys of the hash should be KSM provider names; values
# should be hashes of TLS connection options. The options are equivalent
# to TLS connection options of Mongo::Client.
# @see Mongo::Client#initialize for list of TLS options.
# @option options [ Hash | nil ] :encrypted_fields_map maps a collection
# namespace to an encryptedFields.
# - Note: If a collection is present on both the encryptedFieldsMap
# and schemaMap, an error will be raised.
# @option options [ Boolean | nil ] :bypass_query_analysis When true
# disables automatic analysis of outgoing commands.
# @option options [ String | nil ] :crypt_shared_lib_path Path that should
# be the used to load the crypt shared library. Providing this option
# overrides default crypt shared library load paths for libmongocrypt.
# @option options [ Boolean | nil ] :crypt_shared_lib_required Whether
# crypt shared library is required. If 'true', an error will be raised
# if a crypt_shared library cannot be loaded by libmongocrypt.
#
# @raise [ ArgumentError ] If required options are missing or incorrectly
# formatted.
def initialize(options)
Crypt.validate_ffi!
# Note that this call may eventually, via other method invocations,
# create additional clients which have to be cleaned up.
@options = set_default_options(options).freeze
@crypt_handle = Crypt::Handle.new(
Crypt::KMS::Credentials.new(@options[:kms_providers]),
Crypt::KMS::Validations.validate_tls_options(@options[:kms_tls_options]),
schema_map: @options[:schema_map],
schema_map_path: @options[:schema_map_path],
encrypted_fields_map: @options[:encrypted_fields_map],
bypass_query_analysis: @options[:bypass_query_analysis],
crypt_shared_lib_path: @options[:extra_options][:crypt_shared_lib_path],
crypt_shared_lib_required: @options[:extra_options][:crypt_shared_lib_required],
)
@mongocryptd_options = @options[:extra_options].slice(
:mongocryptd_uri,
:mongocryptd_bypass_spawn,
:mongocryptd_spawn_path,
:mongocryptd_spawn_args
)
@mongocryptd_options[:mongocryptd_bypass_spawn] = @options[:bypass_auto_encryption] ||
@options[:extra_options][:mongocryptd_bypass_spawn] ||
@crypt_handle.crypt_shared_lib_available? ||
@options[:extra_options][:crypt_shared_lib_required]
unless @options[:extra_options][:crypt_shared_lib_required] || @crypt_handle.crypt_shared_lib_available? || @options[:bypass_query_analysis]
@mongocryptd_client = Client.new(
@options[:extra_options][:mongocryptd_uri],
monitoring_io: @options[:client].options[:monitoring_io],
populator_io: @options[:client].options[:populator_io],
server_selection_timeout: 10,
database: @options[:client].options[:database]
)
end
begin
@encryption_io = EncryptionIO.new(
client: @options[:client],
mongocryptd_client: @mongocryptd_client,
key_vault_namespace: @options[:key_vault_namespace],
key_vault_client: @key_vault_client,
metadata_client: @metadata_client,
mongocryptd_options: @mongocryptd_options
)
rescue
begin
@mongocryptd_client&.close
rescue => e
log_warn("Error closing mongocryptd client in auto encrypter's constructor: #{e.class}: #{e}")
# Drop this exception so that the original exception is raised
end
raise
end
rescue
if @key_vault_client && @key_vault_client != options[:client] &&
@key_vault_client.cluster != options[:client].cluster
then
begin
@key_vault_client.close
rescue => e
log_warn("Error closing key vault client in auto encrypter's constructor: #{e.class}: #{e}")
# Drop this exception so that the original exception is raised
end
end
if @metadata_client && @metadata_client != options[:client] &&
@metadata_client.cluster != options[:client].cluster
then
begin
@metadata_client.close
rescue => e
log_warn("Error closing metadata client in auto encrypter's constructor: #{e.class}: #{e}")
# Drop this exception so that the original exception is raised
end
end
raise
end
# Whether this encrypter should perform encryption (returns false if
# the :bypass_auto_encryption option is set to true).
#
# @return [ Boolean ] Whether to perform encryption.
def encrypt?
!@options[:bypass_auto_encryption]
end
# Encrypt a database command.
#
# @param [ String ] database_name The name of the database on which the
# command is being run.
# @param [ Hash ] command The command to be encrypted.
#
# @return [ BSON::Document ] The encrypted command.
def encrypt(database_name, command, timeout_holder)
AutoEncryptionContext.new(
@crypt_handle,
@encryption_io,
database_name,
command
).run_state_machine(timeout_holder)
end
# Decrypt a database command.
#
# @param [ Hash ] command The command with encrypted fields.
#
# @return [ BSON::Document ] The decrypted command.
def decrypt(command, timeout_holder)
AutoDecryptionContext.new(
@crypt_handle,
@encryption_io,
command
).run_state_machine(timeout_holder)
end
# Close the resources created by the AutoEncrypter.
#
# @return [ true ] Always true.
def close
@mongocryptd_client.close if @mongocryptd_client
if @key_vault_client && @key_vault_client != options[:client] &&
@key_vault_client.cluster != options[:client].cluster
then
@key_vault_client.close
end
if @metadata_client && @metadata_client != options[:client] &&
@metadata_client.cluster != options[:client].cluster
then
@metadata_client.close
end
true
end
private
# Returns a new set of options with the following changes:
# - sets default values for all extra_options
# - adds --idleShtudownTimeoutSecs=60 to extra_options[:mongocryptd_spawn_args]
# if not already present
# - sets bypass_auto_encryption to false
# - sets default key vault client
def set_default_options(options)
opts = options.dup
extra_options = opts.delete(:extra_options) || Options::Redacted.new
extra_options = DEFAULT_EXTRA_OPTIONS.merge(extra_options)
has_timeout_string_arg = extra_options[:mongocryptd_spawn_args].any? do |elem|
elem.is_a?(String) && elem.match(/\A--idleShutdownTimeoutSecs=\d+\z/)
end
timeout_int_arg_idx = extra_options[:mongocryptd_spawn_args].index('--idleShutdownTimeoutSecs')
has_timeout_int_arg = timeout_int_arg_idx && extra_options[:mongocryptd_spawn_args][timeout_int_arg_idx + 1].is_a?(Integer)
unless has_timeout_string_arg || has_timeout_int_arg
extra_options[:mongocryptd_spawn_args] << '--idleShutdownTimeoutSecs=60'
end
opts[:bypass_auto_encryption] ||= false
set_or_create_clients(opts)
opts[:key_vault_client] = @key_vault_client
Options::Redacted.new(opts).merge(extra_options: extra_options)
end
# Create additional clients for auto encryption, if necessary
#
# @param [ Hash ] options Auto encryption options.
def set_or_create_clients(options)
client = options[:client]
@key_vault_client = if options[:key_vault_client]
options[:key_vault_client]
elsif client.options[:max_pool_size] == 0
client
else
internal_client(client)
end
@metadata_client = if options[:bypass_auto_encryption]
nil
elsif client.options[:max_pool_size] == 0
client
else
internal_client(client)
end
end
# Creates or return already created internal client to be used for
# auto encryption.
#
# @param [ Mongo::Client ] client A client connected to the
# encrypted collection.
#
# @return [ Mongo::Client ] Client to be used as internal client for
# auto encryption.
def internal_client(client)
@internal_client ||= client.with(
auto_encryption_options: nil,
min_pool_size: 0,
monitoring: client.send(:monitoring),
)
end
end
end
end
|