diff --git a/data/client_side_encryption/explicit-encryption/range-encryptedFields-Date.json b/data/client_side_encryption/explicit-encryption/range-encryptedFields-Date.json new file mode 100644 index 0000000000..e19fc1e182 --- /dev/null +++ b/data/client_side_encryption/explicit-encryption/range-encryptedFields-Date.json @@ -0,0 +1,30 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedDate", + "bsonType": "date", + "queries": { + "queryType": "rangePreview", + "sparsity": { + "$numberLong": "1" + }, + "min": { + "$date": { + "$numberLong": "0" + } + }, + "max": { + "$date": { + "$numberLong": "200" + } + } + } + } + ] +} diff --git a/data/client_side_encryption/explicit-encryption/range-encryptedFields-DecimalNoPrecision.json b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DecimalNoPrecision.json new file mode 100644 index 0000000000..c6d129d4ca --- /dev/null +++ b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DecimalNoPrecision.json @@ -0,0 +1,21 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedDecimalNoPrecision", + "bsonType": "decimal", + "queries": { + "queryType": "rangePreview", + "sparsity": { + "$numberInt": "1" + } + } + } + ] + } + \ No newline at end of file diff --git a/data/client_side_encryption/explicit-encryption/range-encryptedFields-DecimalPrecision.json b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DecimalPrecision.json new file mode 100644 index 0000000000..c23c3fa923 --- /dev/null +++ b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DecimalPrecision.json @@ -0,0 +1,29 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedDecimalPrecision", + "bsonType": "decimal", + "queries": { + "queryType": "rangePreview", + "sparsity": { + "$numberInt": "1" + }, + "min": { + "$numberDecimal": "0.0" + }, + "max": { + "$numberDecimal": "200.0" + }, + "precision": { + "$numberInt": "2" + } + } + } + ] +} diff --git a/data/client_side_encryption/explicit-encryption/range-encryptedFields-DoubleNoPrecision.json b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DoubleNoPrecision.json new file mode 100644 index 0000000000..4af6422714 --- /dev/null +++ b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DoubleNoPrecision.json @@ -0,0 +1,21 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedDoubleNoPrecision", + "bsonType": "double", + "queries": { + "queryType": "rangePreview", + "sparsity": { + "$numberLong": "1" + } + } + } + ] + } + \ No newline at end of file diff --git a/data/client_side_encryption/explicit-encryption/range-encryptedFields-DoublePrecision.json b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DoublePrecision.json new file mode 100644 index 0000000000..c1f388219d --- /dev/null +++ b/data/client_side_encryption/explicit-encryption/range-encryptedFields-DoublePrecision.json @@ -0,0 +1,30 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedDoublePrecision", + "bsonType": "double", + "queries": { + "queryType": "rangePreview", + "sparsity": { + "$numberLong": "1" + }, + "min": { + "$numberDouble": "0.0" + }, + "max": { + "$numberDouble": "200.0" + }, + "precision": { + "$numberInt": "2" + } + } + } + ] + } + \ No newline at end of file diff --git a/data/client_side_encryption/explicit-encryption/range-encryptedFields-Int.json b/data/client_side_encryption/explicit-encryption/range-encryptedFields-Int.json new file mode 100644 index 0000000000..217bf6743c --- /dev/null +++ b/data/client_side_encryption/explicit-encryption/range-encryptedFields-Int.json @@ -0,0 +1,27 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedInt", + "bsonType": "int", + "queries": { + "queryType": "rangePreview", + "sparsity": { + "$numberLong": "1" + }, + "min": { + "$numberInt": "0" + }, + "max": { + "$numberInt": "200" + } + } + } + ] + } + \ No newline at end of file diff --git a/data/client_side_encryption/explicit-encryption/range-encryptedFields-Long.json b/data/client_side_encryption/explicit-encryption/range-encryptedFields-Long.json new file mode 100644 index 0000000000..0fb87edaef --- /dev/null +++ b/data/client_side_encryption/explicit-encryption/range-encryptedFields-Long.json @@ -0,0 +1,27 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedLong", + "bsonType": "long", + "queries": { + "queryType": "rangePreview", + "sparsity": { + "$numberLong": "1" + }, + "min": { + "$numberLong": "0" + }, + "max": { + "$numberLong": "200" + } + } + } + ] + } + \ No newline at end of file diff --git a/src/mongocxx/CMakeLists.txt b/src/mongocxx/CMakeLists.txt index e2b8bf16f4..3234a24de7 100644 --- a/src/mongocxx/CMakeLists.txt +++ b/src/mongocxx/CMakeLists.txt @@ -155,6 +155,7 @@ set(mongocxx_sources options/index_view.cpp options/insert.cpp options/pool.cpp + options/range.cpp options/replace.cpp options/rewrap_many_datakey.cpp options/server_api.cpp @@ -406,6 +407,8 @@ set_local_dist (src_mongocxx_DIST_local options/private/server_api.hh options/private/ssl.hh options/private/transaction.hh + options/range.cpp + options/range.hpp options/replace.cpp options/replace.hpp options/rewrap_many_datakey.cpp diff --git a/src/mongocxx/client_encryption.cpp b/src/mongocxx/client_encryption.cpp index 48542cadf2..7dfd3359ac 100644 --- a/src/mongocxx/client_encryption.cpp +++ b/src/mongocxx/client_encryption.cpp @@ -40,6 +40,11 @@ bsoncxx::types::bson_value::value client_encryption::encrypt(bsoncxx::types::bso return _impl->encrypt(value, opts); } +bsoncxx::document::value client_encryption::encrypt_expression( + bsoncxx::document::view_or_value expr, const options::encrypt& opts) { + return _impl->encrypt_expression(expr, opts); +} + bsoncxx::types::bson_value::value client_encryption::decrypt( bsoncxx::types::bson_value::view value) { return _impl->decrypt(value); diff --git a/src/mongocxx/client_encryption.hpp b/src/mongocxx/client_encryption.hpp index 15ab547035..5ec5ea966a 100644 --- a/src/mongocxx/client_encryption.hpp +++ b/src/mongocxx/client_encryption.hpp @@ -101,6 +101,22 @@ class MONGOCXX_API client_encryption { bsoncxx::types::bson_value::value encrypt(bsoncxx::types::bson_value::view value, const options::encrypt& opts); + /// + /// Encrypts a Match Expression or Aggregate Expression to query a range index. + /// + /// @note Only supported when queryType is "rangePreview" and algorithm is "RangePreview". + /// + /// @param expr A BSON document corresponding to either a Match Expression or an Aggregate + /// Expression. + /// @param opts Options must be given in order to specify queryType and algorithm. + /// + /// @returns The encrypted expression. + /// + /// @warning The Range algorithm is experimental only. It is not intended for public use. It is + /// subject to breaking changes. + bsoncxx::document::value encrypt_expression(bsoncxx::document::view_or_value expr, + const options::encrypt& opts); + /// /// Decrypts an encrypted value (BSON binary of subtype 6). /// diff --git a/src/mongocxx/options/auto_encryption.hpp b/src/mongocxx/options/auto_encryption.hpp index 43a4ec2f22..c1f31d2733 100644 --- a/src/mongocxx/options/auto_encryption.hpp +++ b/src/mongocxx/options/auto_encryption.hpp @@ -244,9 +244,6 @@ class MONGOCXX_API auto_encryption { /// an encryptedFields obtained from the server. It protects against a /// malicious server advertising a false encryptedFields. /// - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. - /// /// @param encrypted_fields_map /// The mapping of which fields to encrypt. /// @@ -257,17 +254,20 @@ class MONGOCXX_API auto_encryption { /// /// @see https://docs.mongodb.com/manual/core/security-client-side-encryption/ /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// auto_encryption& encrypted_fields_map(bsoncxx::document::view_or_value encrypted_fields_map); /// /// Get encrypted fields map /// - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. - /// /// @return /// An optional document containing the encrypted fields map /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// const stdx::optional& encrypted_fields_map() const; /// @@ -296,9 +296,6 @@ class MONGOCXX_API auto_encryption { /// Query analysis is disabled when the 'bypassQueryAnalysis' /// option is true. Default is 'false' (i.e. query analysis is enabled). /// - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. - /// /// @param should_bypass /// Whether or not to bypass query analysis. /// @@ -307,17 +304,20 @@ class MONGOCXX_API auto_encryption { /// /// @see https://docs.mongodb.com/manual/core/security-client-side-encryption/ /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// auto_encryption& bypass_query_analysis(bool should_bypass); /// /// Gets a boolean specifying whether or not query analysis is bypassed. /// - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. - /// /// @return /// A boolean specifying whether query analysis is bypassed. /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// bool bypass_query_analysis() const; /// diff --git a/src/mongocxx/options/encrypt.cpp b/src/mongocxx/options/encrypt.cpp index 6b2b7e249b..00cd54cd2b 100644 --- a/src/mongocxx/options/encrypt.cpp +++ b/src/mongocxx/options/encrypt.cpp @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + #include #include #include @@ -30,6 +32,10 @@ encrypt& encrypt::key_id(bsoncxx::types::bson_value::view_or_value key_id) { return *this; } +const stdx::optional& encrypt::key_id() const { + return _key_id; +} + encrypt& encrypt::key_alt_name(std::string name) { _key_alt_name = std::move(name); return *this; @@ -66,37 +72,93 @@ const stdx::optional& encrypt::query_type() cons return _query_type; } -const stdx::optional& encrypt::key_id() const { - return _key_id; +encrypt& encrypt::range_opts(options::range opts) { + _range_opts = std::move(opts); + return *this; +} + +const stdx::optional& encrypt::range_opts() const { + return _range_opts; } +namespace { + +struct scoped_bson_value { + bson_value_t value = {}; + + // Allow obtaining a pointer to this->value even in rvalue expressions. + bson_value_t* get() noexcept { + return &value; + } + + // Communicate this->value is to be initialized via the resulting pointer. + bson_value_t* value_for_init() noexcept { + return &this->value; + } + + template + auto convert(const T& value) + // Use trailing return type syntax to SFINAE without triggering GCC -Wignored-attributes + // warnings due to using decltype within template parameters. + -> decltype(bsoncxx::types::convert_to_libbson(std::declval(), + std::declval())) { + bsoncxx::types::convert_to_libbson(value, &this->value); + } + + template + explicit scoped_bson_value(const T& value) { + convert(value); + } + + explicit scoped_bson_value(const bsoncxx::types::bson_value::view& view) { + // Argument order is reversed for bsoncxx::types::bson_value::view. + bsoncxx::types::convert_to_libbson(&this->value, view); + } + + ~scoped_bson_value() { + bson_value_destroy(&value); + } + + // Expectation is that value_for_init() will be used to initialize this->value. + scoped_bson_value() = default; + + scoped_bson_value(const scoped_bson_value&) = delete; + scoped_bson_value(scoped_bson_value&&) = delete; + scoped_bson_value& operator=(const scoped_bson_value&) = delete; + scoped_bson_value& operator=(scoped_bson_value&&) = delete; +}; + +} // namespace + void* encrypt::convert() const { using libbson::scoped_bson_t; - mongoc_client_encryption_encrypt_opts_t* opts = libmongoc::client_encryption_encrypt_opts_new(); + struct encrypt_opts_deleter { + void operator()(mongoc_client_encryption_encrypt_opts_t* ptr) noexcept { + libmongoc::client_encryption_encrypt_opts_destroy(ptr); + } + }; + + auto opts_owner = + std::unique_ptr( + libmongoc::client_encryption_encrypt_opts_new()); + const auto opts = opts_owner.get(); // libmongoc will error if both key_id and key_alt_name are set, so no need to check here. if (_key_id) { if (_key_id->view().type() != bsoncxx::type::k_binary) { - libmongoc::client_encryption_encrypt_opts_destroy(opts); throw exception{error_code::k_invalid_parameter, "key id myst be a binary value"}; } auto key_id = _key_id->view().get_binary(); if (key_id.sub_type != bsoncxx::binary_sub_type::k_uuid) { - libmongoc::client_encryption_encrypt_opts_destroy(opts); throw exception{error_code::k_invalid_parameter, "key id must be a binary value with subtype 4 (UUID)"}; } - bson_value_t bson_uuid; - convert_to_libbson(key_id, &bson_uuid); - - libmongoc::client_encryption_encrypt_opts_set_keyid(opts, &bson_uuid); - - bson_value_destroy(&bson_uuid); + libmongoc::client_encryption_encrypt_opts_set_keyid(opts, scoped_bson_value(key_id).get()); } if (_key_alt_name) { @@ -121,8 +183,11 @@ void* encrypt::convert() const { libmongoc::client_encryption_encrypt_opts_set_algorithm( opts, MONGOC_ENCRYPT_ALGORITHM_UNINDEXED); break; + case encryption_algorithm::k_range_preview: + libmongoc::client_encryption_encrypt_opts_set_algorithm( + opts, MONGOC_ENCRYPT_ALGORITHM_RANGEPREVIEW); + break; default: - libmongoc::client_encryption_encrypt_opts_destroy(opts); throw exception{error_code::k_invalid_parameter, "unsupported encryption algorithm"}; } @@ -141,12 +206,56 @@ void* encrypt::convert() const { libmongoc::client_encryption_encrypt_opts_set_query_type( opts, MONGOC_ENCRYPT_QUERY_TYPE_EQUALITY); break; + case encryption_query_type::k_range_preview: + libmongoc::client_encryption_encrypt_opts_set_query_type( + opts, MONGOC_ENCRYPT_QUERY_TYPE_RANGEPREVIEW); + break; default: - libmongoc::client_encryption_encrypt_opts_destroy(opts); throw exception{error_code::k_invalid_parameter, "unsupported query type"}; } } - return opts; + + if (_range_opts) { + struct range_opts_deleter { + void operator()(mongoc_client_encryption_encrypt_range_opts_t* ptr) noexcept { + libmongoc::client_encryption_encrypt_range_opts_destroy(ptr); + } + }; + + auto range_opts_owner = + std::unique_ptr( + libmongoc::client_encryption_encrypt_range_opts_new()); + const auto range_opts = range_opts_owner.get(); + + const auto& min = _range_opts->min(); + const auto& max = _range_opts->max(); + const auto& precision = _range_opts->precision(); + const auto& sparsity = _range_opts->sparsity(); + + if (!!min != !!max) { + throw exception{error_code::k_invalid_parameter, + "one of min or max was set without the other"}; + } + + if (min && max) { + libmongoc::client_encryption_encrypt_range_opts_set_min_max( + range_opts, + scoped_bson_value(min->view()).get(), + scoped_bson_value(max->view()).get()); + } + + if (precision) { + libmongoc::client_encryption_encrypt_range_opts_set_precision(range_opts, *precision); + } + + if (sparsity) { + libmongoc::client_encryption_encrypt_range_opts_set_sparsity(range_opts, *sparsity); + } + + libmongoc::client_encryption_encrypt_opts_set_range_opts(opts, range_opts); + } + + return opts_owner.release(); } } // namespace options diff --git a/src/mongocxx/options/encrypt.hpp b/src/mongocxx/options/encrypt.hpp index 2f4bf6c885..f7b5e4f249 100644 --- a/src/mongocxx/options/encrypt.hpp +++ b/src/mongocxx/options/encrypt.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -51,6 +52,14 @@ class MONGOCXX_API encrypt { /// encrypt& key_id(bsoncxx::types::bson_value::view_or_value key_id); + /// + /// Gets the key_id. + /// + /// @return + /// An optional owning bson_value containing the key_id. + /// + const stdx::optional& key_id() const; + /// /// Sets a name by which to lookup a key from the key vault collection to use /// for this encryption operation. A key alt name can be used instead of a key id. @@ -77,10 +86,6 @@ class MONGOCXX_API encrypt { /// Determines which AEAD_AES_256_CBC algorithm to use with HMAC_SHA_512 when /// encrypting data. /// - /// Indexed and Unindexed are used for Queryable Encryption. - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. - /// enum class encryption_algorithm : std::uint8_t { /// /// Use deterministic encryption. @@ -95,29 +100,49 @@ class MONGOCXX_API encrypt { /// /// Use indexed encryption. /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption + /// should not be used in production and is subject to backwards breaking changes. + /// k_indexed, /// /// Use unindexed encryption. /// - k_unindexed + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption + /// should not be used in production and is subject to backwards breaking changes. + /// + k_unindexed, + + /// + /// Use range encryption. + /// + /// @warning The Range algorithm is experimental only. It is not intended for public use. It + /// is subject to breaking changes. + /// + k_range_preview, }; /// - /// queryType only applies when algorithm is "Indexed" or "RangePreview". - /// It is an error to set queryType when algorithm is not "Indexed" or "RangePreview". + /// queryType only applies when algorithm is "indexed" or "rangePreview". + /// It is an error to set queryType when algorithm is not "indexed" or "rangePreview". /// - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. /// - enum class encryption_query_type : std::uint8_t { k_equality }; + enum class encryption_query_type : std::uint8_t { + /// @brief Use query type "equality". + k_equality, + + /// @brief Use query type "rangePreview". + /// @warning The Range algorithm is experimental only. It is not intended for public use. It + /// is subject to breaking changes. + k_range_preview, + }; /// /// Sets the algorithm to use for encryption. /// /// Indexed and Unindexed are used for Queryable Encryption. - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. /// /// @param algorithm /// An algorithm, either deterministic, random, indexed, or unindexed to use for encryption. @@ -126,21 +151,26 @@ class MONGOCXX_API encrypt { /// configured with mongocxx::options::auto_encryption. /// mongocxx::options::auto_encryption::bypass_query_analysis may be true. /// mongocxx::options::auto_encryption::bypass_auto_encryption must be false. + /// /// @see /// https://docs.mongodb.com/manual/core/security-client-side-encryption/#encryption-algorithms /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// encrypt& algorithm(encryption_algorithm algorithm); /// /// Gets the current algorithm. /// /// Indexed and Unindexed are used for Queryable Encryption. - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. /// /// @return /// An optional algorithm. /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// const stdx::optional& algorithm() const; /// @@ -148,24 +178,23 @@ class MONGOCXX_API encrypt { /// contentionFactor only applies when algorithm is "Indexed" or "RangePreview". /// It is an error to set contentionFactor when algorithm is not "Indexed". /// - /// The contention factor is used for Queryable Encryption. - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. - /// /// @param contention_factor /// An integer specifiying the desired contention factor. /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// encrypt& contention_factor(int64_t contention_factor); /// /// Gets the current contention factor. /// - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. - /// /// @return /// An optional contention factor. /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// const stdx::optional& contention_factor() const; /// @@ -176,9 +205,8 @@ class MONGOCXX_API encrypt { /// query_type only applies when algorithm is "Indexed" or "RangePreview". /// It is an error to set query_type when algorithm is not "Indexed" or "RangePreview". /// - /// QueryType is used for Queryable Encryption. - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. /// encrypt& query_type(encryption_query_type query_type); @@ -188,19 +216,33 @@ class MONGOCXX_API encrypt { /// @return /// A query type. /// - /// QueryType is used for Queryable Encryption. - /// Queryable Encryption is in Public Technical Preview. Queryable Encryption should not be used - /// in production and is subject to backwards breaking changes. + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. /// const stdx::optional& query_type() const; /// - /// Gets the key_id. + /// Sets the range options to use for encryption. + /// + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// + /// @warning The Range algorithm is experimental only. It is not intended for public use. It + /// is subject to breaking changes. + encrypt& range_opts(options::range opts); + + /// + /// Gets the current range options. /// /// @return - /// An optional owning bson_value containing the key_id. + /// An optional range options. /// - const stdx::optional& key_id() const; + /// @warning Queryable Encryption is in Public Technical Preview. Queryable Encryption should + /// not be used in production and is subject to backwards breaking changes. + /// + /// @warning The Range algorithm is experimental only. It is not intended for public use. It + /// is subject to breaking changes. + const stdx::optional& range_opts() const; private: friend class mongocxx::client_encryption; @@ -211,6 +253,7 @@ class MONGOCXX_API encrypt { stdx::optional _algorithm; stdx::optional _contention_factor; stdx::optional _query_type; + stdx::optional _range_opts; }; } // namespace options diff --git a/src/mongocxx/options/range.cpp b/src/mongocxx/options/range.cpp new file mode 100644 index 0000000000..fc2effda3c --- /dev/null +++ b/src/mongocxx/options/range.cpp @@ -0,0 +1,61 @@ +// Copyright 2023 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. + +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN +namespace options { + +range& range::min(bsoncxx::types::bson_value::view_or_value value) { + _min = std::move(value); + return *this; +} + +const stdx::optional& range::min() const { + return _min; +} + +range& range::max(bsoncxx::types::bson_value::view_or_value value) { + _max = std::move(value); + return *this; +} + +const stdx::optional& range::max() const { + return _max; +} + +range& range::sparsity(std::int64_t value) { + _sparsity = value; + return *this; +} + +const stdx::optional& range::sparsity() const { + return _sparsity; +} + +range& range::precision(std::int32_t value) { + _precision = value; + return *this; +} + +const stdx::optional& range::precision() const { + return _precision; +} + +} // namespace options +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx diff --git a/src/mongocxx/options/range.hpp b/src/mongocxx/options/range.hpp new file mode 100644 index 0000000000..ebf7df3918 --- /dev/null +++ b/src/mongocxx/options/range.hpp @@ -0,0 +1,84 @@ +// Copyright 2023 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. + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace mongocxx { +MONGOCXX_INLINE_NAMESPACE_BEGIN + +namespace options { + +/// +/// @brief `RangeOpts` specifies index options for a Queryable Encryption field supporting +/// "rangePreview" queries. +/// +/// @note @ref min, @ref max, @ref sparsity, and @ref precision must match the values set in the +/// encryptedFields of the destination collection. +/// +/// @note For double and decimal128, @ref min, @ref max, and @ref precision must all be set, or all +/// be unset. +/// +/// @warning The Range algorithm is experimental only. It is not intended for public use. It is +/// subject to breaking changes. +class MONGOCXX_API range { + public: + /// @brief Sets `RangeOpts.min`. + /// @note Required if @ref precision is set. + range& min(bsoncxx::types::bson_value::view_or_value value); + + /// @brief Gets `RangeOpts.min`. + /// @note Required if @ref precision is set. + const stdx::optional& min() const; + + /// @brief Sets `RangeOpts.max`. + /// @note Required if @ref precision is set. + range& max(bsoncxx::types::bson_value::view_or_value value); + + /// @brief Gets `RangeOpts.max`. + /// @note Required if @ref precision is set. + const stdx::optional& max() const; + + /// @brief Sets `RangeOpts.sparsity`. + range& sparsity(std::int64_t value); + + /// @brief Gets `RangeOpts.sparsity`. + const stdx::optional& sparsity() const; + + /// @brief Sets `RangeOpts.precision`. + /// @note May only be set for `double` or `decimal128`. + range& precision(std::int32_t value); + + /// @brief Gets `RangeOpts.precision`. + /// @note May only be set for `double` or `decimal128`. + const stdx::optional& precision() const; + + private: + stdx::optional _min; + stdx::optional _max; + stdx::optional _sparsity; + stdx::optional _precision; +}; + +} // namespace options + +MONGOCXX_INLINE_NAMESPACE_END +} // namespace mongocxx diff --git a/src/mongocxx/private/client_encryption.hh b/src/mongocxx/private/client_encryption.hh index 3105503f0a..ed84da1903 100644 --- a/src/mongocxx/private/client_encryption.hh +++ b/src/mongocxx/private/client_encryption.hh @@ -14,6 +14,9 @@ #pragma once +#include +#include + #include #include #include @@ -34,155 +37,203 @@ namespace mongocxx { MONGOCXX_INLINE_NAMESPACE_BEGIN -using bsoncxx::types::convert_from_libbson; -using bsoncxx::types::convert_to_libbson; - class client_encryption::impl { + private: + using scoped_bson_t = mongocxx::libbson::scoped_bson_t; + + struct scoped_bson_value { + bson_value_t value = {}; + + // Allow obtaining a pointer to this->value even in rvalue expressions. + bson_value_t* get() noexcept { + return &value; + } + + // Communicate this->value is to be initialized via the resulting pointer. + bson_value_t* value_for_init() noexcept { + return &this->value; + } + + template + auto convert(const T& value) + // Use trailing return type syntax to SFINAE without triggering GCC -Wignored-attributes + // warnings due to using decltype within template parameters. + -> decltype(bsoncxx::types::convert_to_libbson(std::declval(), + std::declval())) { + bsoncxx::types::convert_to_libbson(value, &this->value); + } + + template + explicit scoped_bson_value(const T& value) { + convert(value); + } + + explicit scoped_bson_value(const bsoncxx::types::bson_value::view& view) { + // Argument order is reversed for bsoncxx::types::bson_value::view. + bsoncxx::types::convert_to_libbson(&this->value, view); + } + + ~scoped_bson_value() { + bson_value_destroy(&value); + } + + // Expectation is that value_for_init() will be used to initialize this->value. + scoped_bson_value() = default; + + scoped_bson_value(const scoped_bson_value&) = delete; + scoped_bson_value(scoped_bson_value&&) = delete; + scoped_bson_value& operator=(const scoped_bson_value&) = delete; + scoped_bson_value& operator=(scoped_bson_value&&) = delete; + }; + + struct encrypt_opts_deleter { + void operator()(mongoc_client_encryption_encrypt_opts_t* ptr) noexcept { + libmongoc::client_encryption_encrypt_opts_destroy(ptr); + } + }; + + using encrypt_opts_ptr = + std::unique_ptr; + public: impl(options::client_encryption opts) : _opts(std::move(opts)) { - bson_error_t error; + using opts_type = mongoc_client_encryption_opts_t; + + struct opts_deleter { + void operator()(opts_type* ptr) noexcept { + libmongoc::client_encryption_opts_destroy(ptr); + } + }; - auto encryption_opts = static_cast(_opts.convert()); - _client_encryption_t = libmongoc::client_encryption_new(encryption_opts, &error); + using encryption_opts_ptr = std::unique_ptr; - libmongoc::client_encryption_opts_destroy(encryption_opts); + bson_error_t error; + + _client_encryption.reset(libmongoc::client_encryption_new( + encryption_opts_ptr(static_cast(_opts.convert())).get(), &error)); - if (_client_encryption_t == nullptr) { + if (!_client_encryption) { throw_exception(error); } } - ~impl() { - libmongoc::client_encryption_destroy(_client_encryption_t); - } - bsoncxx::types::bson_value::value create_data_key(std::string kms_provider, const options::data_key& opts) { - bson_value_t keyid; - bson_error_t error; - - auto datakey_opts = static_cast(opts.convert()); + using opts_type = mongoc_client_encryption_datakey_opts_t; - auto cleanup = [&]() { - bson_value_destroy(&keyid); - libmongoc::client_encryption_datakey_opts_destroy(datakey_opts); + struct opts_deleter { + void operator()(opts_type* ptr) noexcept { + libmongoc::client_encryption_datakey_opts_destroy(ptr); + } }; - if (!libmongoc::client_encryption_create_datakey( - _client_encryption_t, kms_provider.c_str(), datakey_opts, &keyid, &error)) { - cleanup(); - throw_exception(error); - } + using datakey_opts_ptr = std::unique_ptr; - bsoncxx::types::bson_value::value out = - bsoncxx::types::bson_value::make_owning_bson(&keyid); + const auto datakey_opts = datakey_opts_ptr(static_cast(opts.convert())); - cleanup(); + scoped_bson_value keyid; + bson_error_t error; - return out; + if (!libmongoc::client_encryption_create_datakey(_client_encryption.get(), + kms_provider.c_str(), + datakey_opts.get(), + keyid.value_for_init(), + &error)) { + throw_exception(error); + } + + return bsoncxx::types::bson_value::make_owning_bson(keyid.get()); } bsoncxx::types::bson_value::value encrypt(bsoncxx::types::bson_value::view value, const options::encrypt& opts) { - bson_error_t error; - bson_value_t ciphertext; + const auto encrypt_opts = + encrypt_opts_ptr(static_cast(opts.convert())); - bson_value_t libbson_value; + scoped_bson_value ciphertext; + bson_error_t error; - convert_to_libbson(&libbson_value, value); + if (!libmongoc::client_encryption_encrypt(_client_encryption.get(), + scoped_bson_value(value).get(), + encrypt_opts.get(), + ciphertext.value_for_init(), + &error)) { + throw_exception(error); + } - mongoc_client_encryption_encrypt_opts_t* converted_opts; + return bsoncxx::types::bson_value::make_owning_bson(ciphertext.get()); + } - converted_opts = (mongoc_client_encryption_encrypt_opts_t*)opts.convert(); + bsoncxx::document::value encrypt_expression(bsoncxx::document::view_or_value expr, + const options::encrypt& opts) { + const auto encrypt_opts = + encrypt_opts_ptr(static_cast(opts.convert())); - auto r = libmongoc::client_encryption_encrypt( - _client_encryption_t, &libbson_value, converted_opts, &ciphertext, &error); + scoped_bson_t encrypted; + bson_error_t error = {}; - auto cleanup = [&]() { - bson_value_destroy(&libbson_value); - bson_value_destroy(&ciphertext); - libmongoc::client_encryption_encrypt_opts_destroy(converted_opts); - }; - - if (!r) { - cleanup(); + if (!libmongoc::client_encryption_encrypt_expression(_client_encryption.get(), + scoped_bson_t(expr).bson(), + encrypt_opts.get(), + encrypted.bson_for_init(), + &error)) { throw_exception(error); } - auto encrypted = bsoncxx::types::bson_value::make_owning_bson(&ciphertext); - - cleanup(); - - return encrypted; + return encrypted.steal(); } bsoncxx::types::bson_value::value decrypt(bsoncxx::types::bson_value::view value) { + scoped_bson_value decrypted_value; bson_error_t error; - bson_value_t decrypted_value; - bson_value_t encrypted; + if (!libmongoc::client_encryption_decrypt(_client_encryption.get(), + scoped_bson_value(value).get(), + decrypted_value.value_for_init(), + &error)) { + throw_exception(error); + } - convert_to_libbson(&encrypted, value); + return bsoncxx::types::bson_value::make_owning_bson(decrypted_value.get()); + } - auto r = libmongoc::client_encryption_decrypt( - _client_encryption_t, &encrypted, &decrypted_value, &error); + result::rewrap_many_datakey rewrap_many_datakey(bsoncxx::document::view_or_value filter, + const options::rewrap_many_datakey& opts) { + using result_type = mongoc_client_encryption_rewrap_many_datakey_result_t; - auto cleanup = [&]() { - bson_value_destroy(&decrypted_value); - bson_value_destroy(&encrypted); + struct result_deleter { + void operator()(result_type* ptr) noexcept { + libmongoc::client_encryption_rewrap_many_datakey_result_destroy(ptr); + } }; - if (!r) { - cleanup(); - throw_exception(error); - } + using result_ptr = std::unique_ptr; - auto decrypted = bsoncxx::types::bson_value::make_owning_bson(&decrypted_value); + auto result = result_ptr(libmongoc::client_encryption_rewrap_many_datakey_result_new()); - cleanup(); + const auto provider_terminated = opts.provider().terminated(); - return decrypted; - } + scoped_bson_t bson_master_key; - result::rewrap_many_datakey rewrap_many_datakey(bsoncxx::document::view_or_value filter, - const options::rewrap_many_datakey& opts) { - bson_error_t error; - - std::unique_ptr - result_ptr(libmongoc::client_encryption_rewrap_many_datakey_result_new(), - libmongoc::client_encryption_rewrap_many_datakey_result_destroy); - - const auto provider = opts.provider(); - const auto provider_terminated = provider.terminated(); - const char* provider_ptr = - provider_terminated.view().empty() ? nullptr : provider_terminated.data(); - - const auto optional_master_key = opts.master_key(); - stdx::optional bson_master_key; - if (optional_master_key) { - bson_master_key.emplace(); - bson_master_key->init_from_static(optional_master_key.value().view()); + if (const auto master_key_opt = opts.master_key()) { + bson_master_key.init_from_static(master_key_opt->view()); } - libbson::scoped_bson_t bson_filter; - bson_filter.init_from_static(filter); - - const auto r = libmongoc::client_encryption_rewrap_many_datakey( - _client_encryption_t, - bson_filter.bson(), - provider_ptr, - bson_master_key ? bson_master_key->bson() : nullptr, - result_ptr.get(), - &error); + bson_error_t error; - if (!r) { + if (!libmongoc::client_encryption_rewrap_many_datakey( + _client_encryption.get(), + scoped_bson_t(filter).bson(), + provider_terminated.view().empty() ? nullptr : provider_terminated.data(), + bson_master_key.bson(), + result.get(), + &error)) { throw_exception(error); } const bson_t* bulk_write_result = libmongoc::client_encryption_rewrap_many_datakey_result_get_bulk_write_result( - result_ptr.get()); + result.get()); if (bulk_write_result) { const auto doc = @@ -197,24 +248,16 @@ class client_encryption::impl { using bsoncxx::builder::basic::kvp; using bsoncxx::builder::basic::make_document; + scoped_bson_t reply; bson_error_t error; - libbson::scoped_bson_t reply_ptr; - - bson_value_t libbson_key; - - convert_to_libbson(&libbson_key, id); - - const auto r = libmongoc::client_encryption_delete_key( - _client_encryption_t, &libbson_key, reply_ptr.bson_for_init(), &error); - - bson_value_destroy(&libbson_key); - if (!r) { + if (!libmongoc::client_encryption_delete_key(_client_encryption.get(), + scoped_bson_value(id.view()).get(), + reply.bson_for_init(), + &error)) { throw_exception(error); } - const auto val = reply_ptr.view(); - // The C driver calls this field "deletedCount", but the C++ driver // refers to this as "nRemoved". Make a new document with the field name // changed to get around this. @@ -223,29 +266,21 @@ class client_encryption::impl { // Function: std::int32_t bulk_write::deleted_count() const { // return view()["nRemoved"].get_int32(); // } - return result::delete_result( - result::bulk_write(make_document(kvp("nRemoved", val["deletedCount"].get_int32())))); + return result::delete_result(result::bulk_write( + make_document(kvp("nRemoved", reply.view()["deletedCount"].get_int32())))); } stdx::optional get_key(bsoncxx::types::bson_value::view_or_value id) { - bson_error_t error; libbson::scoped_bson_t key_doc; + bson_error_t error; - bson_value_t libbson_value; - - convert_to_libbson(&libbson_value, id); - - const auto r = libmongoc::client_encryption_get_key( - _client_encryption_t, &libbson_value, key_doc.bson_for_init(), &error); - - const auto cleanup = [&]() { bson_value_destroy(&libbson_value); }; - - if (!r) { - cleanup(); + if (!libmongoc::client_encryption_get_key(_client_encryption.get(), + scoped_bson_value(id.view()).get(), + key_doc.bson_for_init(), + &error)) { throw_exception(error); } - cleanup(); return key_doc.view().empty() ? stdx::nullopt : stdx::optional{key_doc.steal()}; } @@ -253,89 +288,75 @@ class client_encryption::impl { mongocxx::cursor get_keys() { bson_error_t error; - mongoc_cursor_t* cursor = mongoc_client_encryption_get_keys(_client_encryption_t, &error); + mongoc_cursor_t* const cursor = + mongoc_client_encryption_get_keys(_client_encryption.get(), &error); if (!cursor) { throw_exception(error); } - mongocxx::cursor wrapped_cursor{cursor}; - return wrapped_cursor; + return mongocxx::cursor(cursor); } stdx::optional add_key_alt_name( bsoncxx::types::bson_value::view_or_value id, bsoncxx::string::view_or_value key_alt_name) { + scoped_bson_t key_doc; bson_error_t error; - libbson::scoped_bson_t key_doc; - bson_value_t key_id; - - convert_to_libbson(&key_id, id); - - const auto key_alt_name_terminated = key_alt_name.terminated(); - const auto r = libmongoc::client_encryption_add_key_alt_name(_client_encryption_t, - &key_id, - key_alt_name_terminated.data(), - key_doc.bson_for_init(), - &error); - const auto cleanup = [&]() { bson_value_destroy(&key_id); }; - - if (!r) { - cleanup(); + if (!libmongoc::client_encryption_add_key_alt_name(_client_encryption.get(), + scoped_bson_value(id.view()).get(), + key_alt_name.terminated().data(), + key_doc.bson_for_init(), + &error)) { throw_exception(error); } - cleanup(); return key_doc.view().empty() ? stdx::nullopt : stdx::optional{key_doc.steal()}; } stdx::optional get_key_by_alt_name( bsoncxx::string::view_or_value key_alt_name) { + scoped_bson_t key_doc; bson_error_t error; - libbson::scoped_bson_t key_doc; - - const auto key_alt_name_terminated = key_alt_name.terminated(); - const auto r = libmongoc::client_encryption_get_key_by_alt_name( - _client_encryption_t, key_alt_name_terminated.data(), key_doc.bson_for_init(), &error); - if (!r) { + if (!libmongoc::client_encryption_get_key_by_alt_name(_client_encryption.get(), + key_alt_name.terminated().data(), + key_doc.bson_for_init(), + &error)) { throw_exception(error); } + return key_doc.view().empty() ? stdx::nullopt : stdx::optional{key_doc.steal()}; } stdx::optional remove_key_alt_name( bsoncxx::types::bson_value::view_or_value id, bsoncxx::string::view_or_value key_alt_name) { + scoped_bson_t key_doc; bson_error_t error; - libbson::scoped_bson_t key_doc; - bson_value_t key_id; - convert_to_libbson(&key_id, id); - - const auto key_alt_name_terminated = key_alt_name.terminated(); - const auto r = - libmongoc::client_encryption_remove_key_alt_name(_client_encryption_t, - &key_id, - key_alt_name_terminated.data(), - key_doc.bson_for_init(), - &error); - - const auto cleanup = [&]() { bson_value_destroy(&key_id); }; - - if (!r) { - cleanup(); + if (!libmongoc::client_encryption_remove_key_alt_name(_client_encryption.get(), + scoped_bson_value(id.view()).get(), + key_alt_name.terminated().data(), + key_doc.bson_for_init(), + &error)) { throw_exception(error); } - cleanup(); return key_doc.view().empty() ? stdx::nullopt : stdx::optional{key_doc.steal()}; } + private: + struct encryption_deleter { + void operator()(mongoc_client_encryption_t* ptr) noexcept { + libmongoc::client_encryption_destroy(ptr); + } + }; + options::client_encryption _opts; - mongoc_client_encryption_t* _client_encryption_t; + std::unique_ptr _client_encryption; }; MONGOCXX_INLINE_NAMESPACE_END diff --git a/src/mongocxx/private/libmongoc_symbols.hh b/src/mongocxx/private/libmongoc_symbols.hh index 515b27b066..7612588e10 100644 --- a/src/mongocxx/private/libmongoc_symbols.hh +++ b/src/mongocxx/private/libmongoc_symbols.hh @@ -135,6 +135,7 @@ MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_decrypt) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_delete_key) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_destroy) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt) +MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_expression) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_destroy) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_new) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_set_algorithm) @@ -142,6 +143,12 @@ MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_set_contention_factor) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_set_keyaltname) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_set_keyid) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_set_query_type) +MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_opts_set_range_opts) +MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_range_opts_destroy) +MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_range_opts_new) +MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_range_opts_set_min_max) +MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_range_opts_set_precision) +MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_encrypt_range_opts_set_sparsity) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_get_key) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_get_key_by_alt_name) MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_get_keys) diff --git a/src/mongocxx/test/client_side_encryption.cpp b/src/mongocxx/test/client_side_encryption.cpp index 061cc41590..69db22c342 100644 --- a/src/mongocxx/test/client_side_encryption.cpp +++ b/src/mongocxx/test/client_side_encryption.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -92,7 +93,7 @@ bsoncxx::document::value _doc_from_file(stdx::string_view sub_path) { CAPTURE(path); std::ifstream file{path}; - REQUIRE(file); + REQUIRE(file.is_open()); std::string file_contents((std::istreambuf_iterator(file)), std::istreambuf_iterator()); @@ -2649,4 +2650,654 @@ TEST_CASE("Custom Key Material Test", "[client_side_encryption]") { REQUIRE(std::memcmp(expected.data(), encrypted_as_binary.bytes, expected.size()) == 0); } +enum struct RangeFieldType : int { + DecimalNoPrecision, + DecimalPrecision, + DoubleNoPrecision, + DoublePrecision, + Date, + Int, + Long, +}; + +std::string to_type_str(RangeFieldType field_type) { + switch (field_type) { + case RangeFieldType::DecimalNoPrecision: + return "DecimalNoPrecision"; + case RangeFieldType::DecimalPrecision: + return "DecimalPrecision"; + case RangeFieldType::DoubleNoPrecision: + return "DoubleNoPrecision"; + case RangeFieldType::DoublePrecision: + return "DoublePrecision"; + case RangeFieldType::Date: + return "Date"; + case RangeFieldType::Int: + return "Int"; + case RangeFieldType::Long: + return "Long"; + }; + + FAIL("unexpected field type " << static_cast(field_type)); + MONGOCXX_UNREACHABLE; +} + +bsoncxx::types::bson_value::value to_field_value(int test_value, RangeFieldType field_type) { + switch (field_type) { + case RangeFieldType::DecimalNoPrecision: + case RangeFieldType::DecimalPrecision: + return {bsoncxx::decimal128(std::to_string(test_value))}; + case RangeFieldType::DoubleNoPrecision: + case RangeFieldType::DoublePrecision: + return {static_cast(test_value)}; + case RangeFieldType::Date: + return {std::chrono::milliseconds(test_value)}; + case RangeFieldType::Int: + return {test_value}; + case RangeFieldType::Long: + return {std::int64_t{test_value}}; + } + + FAIL("unexpected field type " << static_cast(field_type)); + MONGOCXX_UNREACHABLE; +} + +options::range to_range_opts(RangeFieldType field_type) { + using namespace bsoncxx::types; + + switch (field_type) { + case RangeFieldType::DecimalNoPrecision: + return options::range().sparsity(1); + case RangeFieldType::DecimalPrecision: + return options::range() + .min(make_value(b_decimal128{bsoncxx::decimal128(std::to_string(0))})) + .max(make_value(b_decimal128{bsoncxx::decimal128(std::to_string(200))})) + .sparsity(1) + .precision(2); + case RangeFieldType::DoubleNoPrecision: + return options::range().sparsity(1); + case RangeFieldType::DoublePrecision: + return options::range() + .min(make_value(b_double{0.0})) + .max(make_value(b_double{200.0})) + .sparsity(1) + .precision(2); + case RangeFieldType::Date: + return options::range() + .min(make_value(b_date{std::chrono::milliseconds(0)})) + .max(make_value(b_date{std::chrono::milliseconds(200)})) + .sparsity(1); + case RangeFieldType::Int: + return options::range() + .min(make_value(b_int32{0})) + .max(make_value(b_int32{200})) + .sparsity(1); + case RangeFieldType::Long: + return options::range() + .min(make_value(b_int64{0})) + .max(make_value(b_int64{200})) + .sparsity(1); + } + + FAIL("unexpected field type " << static_cast(field_type)); + MONGOCXX_UNREACHABLE; +} + +struct field_type_values { + bsoncxx::types::bson_value::value v0; + bsoncxx::types::bson_value::value v6; + bsoncxx::types::bson_value::value v30; + bsoncxx::types::bson_value::value v200; + + explicit field_type_values(RangeFieldType field_type) + : v0(to_field_value(0, field_type)), + v6(to_field_value(6, field_type)), + v30(to_field_value(30, field_type)), + v200(to_field_value(200, field_type)) {} +}; + +struct range_explicit_encryption_objects { + options::range range_opts; + bsoncxx::document::value key1_document = make_document(); + bsoncxx::types::bson_value::view key1_id; + std::unique_ptr key_vault_client_ptr; + std::unique_ptr client_encryption_ptr; + std::unique_ptr encrypted_client_ptr; + std::string field_name; + std::unique_ptr field_values_ptr; +}; + +range_explicit_encryption_objects range_explicit_encryption_setup(const std::string& type_str, + RangeFieldType field_type) { + range_explicit_encryption_objects res; + + // Load the file for the specific data type being tested `range-encryptedFields-.json`. + const auto encrypted_fields = + _doc_from_file("/explicit-encryption/range-encryptedFields-" + type_str + ".json"); + const auto collection_options = make_document(kvp("encryptedFields", encrypted_fields)); + + // Load the file key1-document.json as `key1Document`. + auto& key1_document = + (res.key1_document = _doc_from_file("/explicit-encryption/key1-document.json")); + + // Read the "_id" field of key1Document as `key1ID`. + const auto& key1_id = (res.key1_id = key1_document["_id"].get_value()); + + const auto wc_majority = []() -> mongocxx::write_concern { + write_concern res; + res.acknowledge_level(write_concern::level::k_majority); + return res; + }(); + + const auto rc_majority = []() -> mongocxx::read_concern { + read_concern res; + res.acknowledge_level(read_concern::level::k_majority); + return res; + }(); + + const auto empty_doc = make_document(); + + auto client = mongocxx::client(uri(), test_util::add_test_server_api()); + + // Drop and create the collection `db.explicit_encryption` using `encryptedFields` as an option. + { + auto db = client["db"]; + db["explicit_encryption"].drop(); + db.create_collection("explicit_encryption", collection_options.view()); + } + + { + auto keyvault = client["keyvault"]; + + // Drop and create the collection `keyvault.datakeys`. + keyvault["datakeys"].drop(); + auto datakeys = keyvault.create_collection("datakeys"); + + // Insert `key1Document` in `keyvault.datakeys` with majority write concern. + datakeys.insert_one(key1_document.view(), options::insert().write_concern(wc_majority)); + } + + const auto kms_providers = _make_kms_doc(false); + + using bsoncxx::stdx::make_unique; + + // Create a MongoClient named `keyVaultClient`. + auto& key_vault_client = *(res.key_vault_client_ptr = make_unique( + uri(), test_util::add_test_server_api())); + + // Create a ClientEncryption object named `clientEncryption` with these options: + // ClientEncryptionOpts { + // keyVaultClient: ; + // keyVaultNamespace: "keyvault.datakeys"; + // kmsProviders: { "local": { "key": } } + // } + auto& client_encryption = + *(res.client_encryption_ptr = make_unique( + options::client_encryption() + .key_vault_client(&key_vault_client) + .key_vault_namespace({"keyvault", "datakeys"}) + .kms_providers(kms_providers.view()))); + + // Create a MongoClient named `encryptedClient` with these `AutoEncryptionOpts`: + // AutoEncryptionOpts { + // keyVaultNamespace: "keyvault.datakeys"; + // kmsProviders: { "local": { "key": } } + // bypassQueryAnalysis: true + // } + auto& encrypted_client = *(res.encrypted_client_ptr = make_unique( + uri(), + test_util::add_test_server_api().auto_encryption_opts( + options::auto_encryption() + .key_vault_namespace({"keyvault", "datakeys"}) + .kms_providers(kms_providers.view()) + .bypass_query_analysis(true)))); + + // Ensure the type matches with the type of the encrypted field. + const auto& field_values = *(res.field_values_ptr = make_unique(field_type)); + const auto& field_name = (res.field_name = "encrypted" + type_str); + const auto& range_opts = (res.range_opts = to_range_opts(field_type)); + + // Encrypt these values with the matching `RangeOpts` listed in Test Setup: RangeOpts and these + // `EncryptOpts`: + // class EncryptOpts { + // keyId : : + // algorithm: "RangePreview", + // contentionFactor: 0 + // } + const auto encrypt_opts = + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm(options::encrypt::encryption_algorithm::k_range_preview) + .contention_factor(0); + + // Use `clientEncryption` to encrypt these values: 0, 6, 30, and 200. + const auto encrypted_v0 = client_encryption.encrypt(field_values.v0, encrypt_opts); + const auto encrypted_v6 = client_encryption.encrypt(field_values.v6, encrypt_opts); + const auto encrypted_v30 = client_encryption.encrypt(field_values.v30, encrypt_opts); + const auto encrypted_v200 = client_encryption.encrypt(field_values.v200, encrypt_opts); + + auto explicit_encryption = encrypted_client["db"]["explicit_encryption"]; + + // Use `encryptedClient` to insert these documents into + // `db.explicit_encryption`: + // { "encrypted": , _id: 0 } + // { "encrypted": , _id: 1 } + // { "encrypted": , _id: 2 } + // { "encrypted": , _id: 3 } + explicit_encryption.insert_one(make_document(kvp(field_name, encrypted_v0), kvp("_id", 0))); + explicit_encryption.insert_one(make_document(kvp(field_name, encrypted_v6), kvp("_id", 1))); + explicit_encryption.insert_one(make_document(kvp(field_name, encrypted_v30), kvp("_id", 2))); + explicit_encryption.insert_one(make_document(kvp(field_name, encrypted_v200), kvp("_id", 3))); + + return res; +} + +// Prose Test 22 +TEST_CASE("Range Explicit Encryption", "[client_side_encryption]") { + instance::current(); + + if (!mongocxx::test_util::should_run_client_side_encryption_test()) { + return; + } + + // Tests for `DecimalNoPrecision` must only run against a replica set. + auto is_replica_set = false; + + { + auto client = mongocxx::client(mongocxx::uri(), test_util::add_test_server_api()); + + if (!test_util::newer_than(client, "7.0")) { + WARN("Skipping - MongoDB server 7.0 or newer required"); + return; + } + + if (test_util::get_topology(client) == "single") { + WARN("Skipping - must not run against a standalone server"); + return; + } + + is_replica_set = test_util::get_topology(client) == "replicaset"; + } + + const RangeFieldType field_types[] = { + RangeFieldType::DecimalNoPrecision, + RangeFieldType::DecimalPrecision, + RangeFieldType::DoubleNoPrecision, + RangeFieldType::DoublePrecision, + RangeFieldType::Date, + RangeFieldType::Int, + RangeFieldType::Long, + }; + + for (const auto& field_type : field_types) { + const auto type_str = to_type_str(field_type); + + DYNAMIC_SECTION("Field Type - " << type_str) { + if (field_type == RangeFieldType::DecimalNoPrecision && !is_replica_set) { + WARN("Skipping - must only run against a replica set"); + continue; + } + + auto test_objects = range_explicit_encryption_setup(type_str, field_type); + + REQUIRE(test_objects.client_encryption_ptr); + REQUIRE(test_objects.encrypted_client_ptr); + REQUIRE(test_objects.field_values_ptr); + + const auto& range_opts = test_objects.range_opts; + const auto& key1_id = test_objects.key1_id; + auto& client_encryption = *test_objects.client_encryption_ptr; + auto& encrypted_client = *test_objects.encrypted_client_ptr; + const auto& field_name = test_objects.field_name; + const auto& field_values = *test_objects.field_values_ptr; + + auto explicit_encryption = encrypted_client["db"]["explicit_encryption"]; + + SECTION("Case 1: can decrypt a payload") { + // Use `clientEncryption.encrypt()` to encrypt the value 6. + const auto& original = field_values.v6; + + // Encrypt with the matching `RangeOpts` listed in Test Setup: RangeOpts and these + // `EncryptOpts`: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // contentionFactor: 0 + // } + // Store the result in insertPayload. + const auto insert_payload = client_encryption.encrypt( + original.view(), + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm(options::encrypt::encryption_algorithm::k_range_preview) + .contention_factor(0)); + + // Use `clientEncryption` to decrypt `insertPayload`. + const auto result = client_encryption.decrypt(insert_payload); + + // Assert the returned value equals 6. + REQUIRE(result == original); + } + + SECTION("Case 2: can find encrypted range and return the maximum") { + // Use clientEncryption.encryptExpression() to encrypt this query: + // {"$and": [{"encrypted": {"$gte": 6}}, {"encrypted": {"$lte": + // 200}}]} + const auto query = make_document(kvp( + "$and", + make_array( + make_document(kvp(field_name, make_document(kvp("$gte", field_values.v6)))), + make_document( + kvp(field_name, make_document(kvp("$lte", field_values.v200))))))); + + // Use the matching `RangeOpts` listed in Test Setup: RangeOpts and these + // `EncryptOpts` to encrypt the query: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // queryType: "rangePreview", + // contentionFactor: 0 + // } + // Store the result in `findPayload`. + const auto find_payload = client_encryption.encrypt_expression( + query.view(), + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm(options::encrypt::encryption_algorithm::k_range_preview) + .query_type(options::encrypt::encryption_query_type::k_range_preview) + .contention_factor(0)); + + // Use encryptedClient to run a "find" operation on the `db.explicit_encryption` + // collection with the filter findPayload and sort the results by _id. + auto cursor = explicit_encryption.find( + find_payload.view(), + options::find() + .sort(make_document(kvp("_id", 1))) + .projection(make_document(kvp("_id", 0), kvp(field_name, 1)))); + + // Assert these three documents are returned: + // - { "encrypted": 6 } + // - { "encrypted": 30 } + // - { "encrypted": 200 } + const auto expected = std::vector({ + make_document(kvp(field_name, field_values.v6)), + make_document(kvp(field_name, field_values.v30)), + make_document(kvp(field_name, field_values.v200)), + }); + + const auto actual = + std::vector(cursor.begin(), cursor.end()); + + REQUIRE(actual == expected); + } + + SECTION("Case 3: can find encrypted range and return the minimum") { + // Use `clientEncryption.encryptExpression()` to encrypt this query: + // {"$and": [{"encrypted": {"$gte": 0}}, {"encrypted": {"$lte": 6}}]} + const auto query = make_document(kvp( + "$and", + make_array( + make_document(kvp(field_name, make_document(kvp("$gte", field_values.v0)))), + make_document( + kvp(field_name, make_document(kvp("$lte", field_values.v6))))))); + + // Use the matching `RangeOpts` listed in Test Setup: RangeOpts and these + // `EncryptOpts` to encrypt the query: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // queryType: "rangePreview", + // contentionFactor: 0 + // } + // Store the result in `findPayload`. + const auto find_payload = client_encryption.encrypt_expression( + query.view(), + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm(options::encrypt::encryption_algorithm::k_range_preview) + .query_type(options::encrypt::encryption_query_type::k_range_preview) + .contention_factor(0)); + + // Use `encryptedClient` to run a "find" operation on the `db.explicit_encryption` + // collection with the filter `findPayload` and sort the results by `_id`. + auto cursor = explicit_encryption.find( + find_payload.view(), + options::find() + .sort(make_document(kvp("_id", 1))) + .projection(make_document(kvp("_id", 0), kvp(field_name, 1)))); + + // Assert these two documents are returned: + // - { "encrypted": 0 } + // - { "encrypted": 6 } + const auto expected = std::vector({ + make_document(kvp(field_name, field_values.v0)), + make_document(kvp(field_name, field_values.v6)), + }); + + const auto actual = + std::vector(cursor.begin(), cursor.end()); + + REQUIRE(actual == expected); + } + + SECTION("Case 4: can find encrypted range with an open range query") { + // Use clientEncryption.encryptExpression() to encrypt this query: + // {"$and": [{"encrypted": {"$gt": 30}}]} + const auto query = make_document( + kvp("$and", + make_array(make_document( + kvp(field_name, make_document(kvp("$gt", field_values.v30))))))); + + // Use the matching `RangeOpts` listed in Test Setup: RangeOpts and these + // `EncryptOpts` to encrypt the query: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // queryType: "rangePreview", + // contentionFactor: 0 + // } + // Store the result in `findPayload`. + const auto find_payload = client_encryption.encrypt_expression( + query.view(), + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm(options::encrypt::encryption_algorithm::k_range_preview) + .query_type(options::encrypt::encryption_query_type::k_range_preview) + .contention_factor(0)); + + // Use encryptedClient to run a "find" operation on the `db.explicit_encryption` + // collection with the filter findPayload and sort the results by _id. + auto cursor = explicit_encryption.find( + find_payload.view(), + options::find() + .sort(make_document(kvp("_id", 1))) + .projection(make_document(kvp("_id", 0), kvp(field_name, 1)))); + + // Assert that only this document is returned: + // - { "encrypted": 200 } + const auto expected = std::vector({ + make_document(kvp(field_name, field_values.v200)), + }); + + const auto actual = + std::vector(cursor.begin(), cursor.end()); + + REQUIRE(actual == expected); + } + + SECTION("Case 5: can run an aggregation expression inside $expr") { + // Use clientEncryption.encryptExpression() to encrypt this query: + // {'$and': [ { '$lt': [ '$encrypted', 30 ] } ] } } + const auto query = make_document( + kvp("$and", + make_array(make_document( + kvp(field_name, make_document(kvp("$lt", field_values.v30))))))); + + // Use the matching `RangeOpts` listed in Test Setup: RangeOpts and these + // `EncryptOpts` to encrypt the query: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // queryType: "rangePreview", + // contentionFactor: 0 + // } + // Store the result in `findPayload`. + const auto find_payload = client_encryption.encrypt_expression( + query.view(), + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm(options::encrypt::encryption_algorithm::k_range_preview) + .query_type(options::encrypt::encryption_query_type::k_range_preview) + .contention_factor(0)); + + // Use encryptedClient to run a "find" operation on the `db.explicit_encryption` + // collection with the filter findPayload and sort the results by _id. + auto cursor = explicit_encryption.find( + find_payload.view(), + options::find() + .sort(make_document(kvp("_id", 1))) + .projection(make_document(kvp("_id", 0), kvp(field_name, 1)))); + + // Assert these two documents are returned: + // - { "encrypted": 0 } + // - { "encrypted": 6 } + const auto expected = std::vector({ + make_document(kvp(field_name, field_values.v0)), + make_document(kvp(field_name, field_values.v6)), + }); + + const auto actual = + std::vector(cursor.begin(), cursor.end()); + + REQUIRE(actual == expected); + } + + switch (field_type) { + case RangeFieldType::DoubleNoPrecision: + case RangeFieldType::DecimalNoPrecision: + // This test case should be skipped if the encrypted field is + // `encryptedDoubleNoPrecision` or `encryptedDecimalNoPrecision`. + break; + default: { + SECTION("Case 6: encrypting a document greater than the maximum errors") { + const auto original = to_field_value(201, field_type); + + // Use clientEncryption.encrypt() to try to encrypt the value 201 with the + // matching RangeOpts listed in Test Setup: RangeOpts and these EncryptOpts: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // contentionFactor: 0 + // } + // The error should be raised because 201 is greater than the maximum value + // in RangeOpts. Assert that an error was raised. + REQUIRE_THROWS_WITH( + client_encryption.encrypt( + original.view(), + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm( + options::encrypt::encryption_algorithm::k_range_preview) + .contention_factor(0)), + Catch::Contains( + "Value must be greater than or equal to the minimum value and " + "less than or equal to the maximum value")); + } + break; + } + } + + switch (field_type) { + case RangeFieldType::DoubleNoPrecision: + case RangeFieldType::DecimalNoPrecision: + // This test case should be skipped if the encrypted field is + // `encryptedDoubleNoPrecision`. + break; + default: { + SECTION("Case 7: encrypting a document of a different type errors") { + // For all the tests below use these EncryptOpts: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // contentionFactor: 0 + // } + const auto encrypt_opts = + options::encrypt() + .range_opts(range_opts) + .key_id(key1_id) + .algorithm(options::encrypt::encryption_algorithm::k_range_preview) + .contention_factor(0); + + // If the encrypted field is encryptedInt encrypt: + // { "encryptedInt": { "$numberDouble": "6" } } + // Otherwise, encrypt: + // { "encrypted": { "$numberInt": "6" } } + const auto value = field_type == RangeFieldType::Int + ? to_field_value(6, RangeFieldType::DoublePrecision) + : to_field_value(6, RangeFieldType::Int); + + // Assert an error was raised. + REQUIRE_THROWS_WITH( + client_encryption.encrypt(value.view(), encrypt_opts), + Catch::Contains("expected matching 'min' and value type")); + } + break; + } + } + + switch (field_type) { + case RangeFieldType::DoublePrecision: + case RangeFieldType::DoubleNoPrecision: + case RangeFieldType::DecimalPrecision: + case RangeFieldType::DecimalNoPrecision: + // This test case should be skipped if the encrypted field is + // `encryptedDoublePrecision` or `encryptedDoubleNoPrecision` or + // `encryptedDecimalPrecision` or `encryptedDecimalNoPrecision`. + break; + default: { + SECTION("Case 8: setting precision errors if the type is not a double") { + // Use `clientEncryption.encrypt()` to try to encrypt the value 6 with these + // `EncryptOpts` and these `RangeOpts`: + // class EncryptOpts { + // keyId : + // algorithm: "RangePreview", + // contentionFactor: 0 + // } + // + // class RangeOpts { + // min: 0, + // max: 200, + // sparsity: 1, + // precision: 2, + // } + // Assert an error was raised. + REQUIRE_THROWS_WITH( + client_encryption.encrypt( + field_values.v6, + options::encrypt() + .range_opts(options::range() + .min(to_field_value(0, field_type)) + .max(to_field_value(200, field_type)) + .sparsity(1) + .precision(2)) + .key_id(key1_id) + .algorithm( + options::encrypt::encryption_algorithm::k_range_preview) + .contention_factor(0)), + Catch::Contains( + "expected 'precision' to be set with double or decimal128 index")); + } + } break; + } + } + } +} + } // namespace diff --git a/src/mongocxx/test/spec/client_side_encryption.cpp b/src/mongocxx/test/spec/client_side_encryption.cpp index 5150df94ae..a599ba7ef5 100644 --- a/src/mongocxx/test/spec/client_side_encryption.cpp +++ b/src/mongocxx/test/spec/client_side_encryption.cpp @@ -265,15 +265,14 @@ void run_encryption_tests_in_file(const std::string& test_path) { for (auto&& op : test["operations"].get_array().value) { if (check_results_logging) { - fprintf(stdout, - "about to run operation %s\n", - to_json(op.get_document().value).c_str()); - fprintf(stdout, "collection contents before: \n"); + UNSCOPED_INFO("about to run operation " << to_json(op.get_document().value)); + std::string contents; auto cursor = test_coll.find({}); for (auto&& doc : cursor) { - fprintf(stdout, "%s\n", to_json(doc).c_str()); + contents += to_json(doc); + contents += '\n'; } - fprintf(stdout, "\n\n"); + UNSCOPED_INFO("collection contents before:\n" << contents); } run_operation_check_result(op.get_document().value, [&]() { @@ -281,12 +280,13 @@ void run_encryption_tests_in_file(const std::string& test_path) { }); if (check_results_logging) { - fprintf(stdout, "after running operation, collection contents:\n"); + std::string contents; auto cursor = test_coll.find({}); for (auto&& doc : cursor) { - fprintf(stdout, "%s\n", to_json(doc).c_str()); + contents += to_json(doc); + contents += '\n'; } - fprintf(stdout, "\n\n"); + UNSCOPED_INFO("after running operation, collection contents:\n" << contents); } }