Skip to content

[CXX-2546] Create Encrypted Collection #959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/mongocxx/client_encryption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <bsoncxx/stdx/make_unique.hpp>
#include <mongocxx/client_encryption.hpp>
#include <mongocxx/private/client_encryption.hh>
#include <mongocxx/private/database.hh>

#include <mongocxx/config/private/prelude.hh>

Expand Down Expand Up @@ -50,6 +51,18 @@ bsoncxx::types::bson_value::value client_encryption::decrypt(
return _impl->decrypt(value);
}

collection client_encryption::create_encrypted_collection(
const database& db,
const std::string& coll_name,
const bsoncxx::document::view& options,
bsoncxx::document::value& out_options,
const std::string& kms_provider,
const stdx::optional<bsoncxx::document::view>& masterkey) {
auto& db_impl = db._get_impl();
return _impl->create_encrypted_collection(
db, db_impl.database_t, coll_name, options, out_options, kms_provider, masterkey);
}

result::rewrap_many_datakey client_encryption::rewrap_many_datakey(
bsoncxx::document::view_or_value filter, const options::rewrap_many_datakey& opts) {
return _impl->rewrap_many_datakey(filter, opts);
Expand Down
26 changes: 26 additions & 0 deletions src/mongocxx/client_encryption.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@
#include <mongocxx/options/rewrap_many_datakey.hpp>
#include <mongocxx/result/delete.hpp>
#include <mongocxx/result/rewrap_many_datakey.hpp>
#include <mongocxx/stdx.hpp>

#include <mongocxx/config/prelude.hpp>

namespace mongocxx {
MONGOCXX_INLINE_NAMESPACE_BEGIN

class database;
class collection;

///
/// Class supporting operations for MongoDB Client-Side Field Level Encryption.
///
Expand Down Expand Up @@ -82,6 +86,28 @@ class MONGOCXX_API client_encryption {
bsoncxx::types::bson_value::value create_data_key(std::string kms_provider,
const options::data_key& opts = {});

/**
* @brief Create a collection with client-side-encryption enabled, automatically filling any
* datakeys for encrypted fields.
*
* @param db The database in which the collection will be created
* @param coll_name The name of the new collection
* @param options The options for creating the collection. @see database::create_collection
* @param out_options Output parameter to receive the generated collection options.
* @param kms_provider The KMS provider to use when creating data encryption keys for the
* collection's encrypted fields
* @param masterkey If non-null, specify the masterkey to be used when creating data keys in the
* collection.
* @return collection A handle to the newly created collection
*/
collection create_encrypted_collection(
const database& db,
const std::string& coll_name,
const bsoncxx::document::view& options,
bsoncxx::document::value& out_options,
const std::string& kms_provider,
const stdx::optional<bsoncxx::document::view>& masterkey = stdx::nullopt);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is no use case to call this overload with a nullopt for masterkey.

Consider combining the two overloads:

    /**
     * @brief Create a collection with client-side-encryption enabled, automatically filling any
     * datakeys for encrypted fields.
     *
     * @param db The database in which the collection will be created
     * @param coll_name The name of the new collection
     * @param options The options for creating the collection. @see database::create_collection
     * @param out_options Output parameter to receive the generated collection options.
     * @param kms_provider The KMS provider to use when creating data encryption keys for the
     * collection's encrypted fields
     * @param masterkey If non-null, specify the masterkey to be used when creating data keys in the
     * collection.
     * @return collection A handle to the newly created collection
     */
    collection create_encrypted_collection(
        const database& db,
        const std::string& coll_name,
        const bsoncxx::document::view& options,
        bsoncxx::document::value& out_options,
        const std::string& kms_provider,
        const stdx::optional<bsoncxx::document::view> masterkey = stdx::nullopt);

Or alternatively: make masterkey non-optional in this overload.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more due to my aversion to defaulted arguments. I don't know if there's precedent in the codebase. Combining them will result in a mostly-equivalent API. I'll combine them for now.

///
/// Encrypts a BSON value with a given key and algorithm.
///
Expand Down
6 changes: 4 additions & 2 deletions src/mongocxx/collection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ MONGOCXX_INLINE_NAMESPACE_BEGIN

class client;
class database;
class client_encryption;

///
/// Class representing server side document groupings within a MongoDB database.
Expand Down Expand Up @@ -1848,8 +1849,9 @@ class MONGOCXX_API collection {
///

private:
friend class bulk_write;
friend class database;
friend mongocxx::bulk_write;
friend mongocxx::database;
friend mongocxx::client_encryption;

MONGOCXX_PRIVATE collection(const database& database,
bsoncxx::string::view_or_value collection_name);
Expand Down
6 changes: 4 additions & 2 deletions src/mongocxx/database.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace mongocxx {
MONGOCXX_INLINE_NAMESPACE_BEGIN

class client;
class client_encryption;

///
/// Class representing a MongoDB database.
Expand Down Expand Up @@ -623,8 +624,9 @@ class MONGOCXX_API database {
///

private:
friend class client;
friend class collection;
friend mongocxx::client;
friend mongocxx::collection;
friend mongocxx::client_encryption;

MONGOCXX_PRIVATE database(const class client& client, bsoncxx::string::view_or_value name);

Expand Down
38 changes: 38 additions & 0 deletions src/mongocxx/private/client_encryption.hh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <memory>
#include <utility>

#include <bsoncxx/private/helpers.hh>
#include <bsoncxx/private/libbson.hh>
#include <bsoncxx/types/bson_value/private/value.hh>
#include <bsoncxx/types/bson_value/value.hpp>
Expand Down Expand Up @@ -348,6 +349,43 @@ class client_encryption::impl {
: stdx::optional<bsoncxx::document::value>{key_doc.steal()};
}

collection create_encrypted_collection(
const database& dbcxx,
mongoc_database_t* const db,
const std::string& coll_name,
const bsoncxx::document::view opts,
bsoncxx::document::value& out_options,
const std::string& kms_provider,
const stdx::optional<bsoncxx::document::view>& masterkey) {
bson_error_t error = {};
scoped_bson_t out_opts;
out_opts.init();

bson_t* opt_mkey_ptr = nullptr;
scoped_bson_t opt_mkey;
if (masterkey) {
opt_mkey.init_from_static(*masterkey);
opt_mkey_ptr = opt_mkey.bson();
}

scoped_bson_t coll_opts{opts};

auto coll_ptr =
libmongoc::client_encryption_create_encrypted_collection(_client_encryption.get(),
db,
coll_name.data(),
coll_opts.bson(),
out_opts.bson(),
kms_provider.data(),
opt_mkey_ptr,
&error);
out_options = bsoncxx::helpers::value_from_bson_t(out_opts.bson());
if (!coll_ptr) {
throw_exception<operation_exception>(error);
}
return collection(dbcxx, coll_ptr);
}

private:
struct encryption_deleter {
void operator()(mongoc_client_encryption_t* ptr) noexcept {
Expand Down
1 change: 1 addition & 0 deletions src/mongocxx/private/libmongoc_symbols.hh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ MONGOCXX_LIBMONGOC_SYMBOL(client_destroy)
MONGOCXX_LIBMONGOC_SYMBOL(client_enable_auto_encryption)
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_add_key_alt_name)
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_create_datakey)
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_create_encrypted_collection)
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_datakey_opts_destroy)
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_datakey_opts_new)
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_datakey_opts_set_keyaltnames)
Expand Down
141 changes: 141 additions & 0 deletions src/mongocxx/test/client_side_encryption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <string>
#include <tuple>

#include <bsoncxx/builder/stream/array.hpp>
#include <bsoncxx/builder/stream/document.hpp>
#include <bsoncxx/builder/stream/helpers.hpp>
#include <bsoncxx/document/element.hpp>
Expand Down Expand Up @@ -2489,6 +2490,146 @@ TEST_CASE("Explicit Encryption", "[client_side_encryption]") {
}
}

TEST_CASE("Create Encrypted Collection", "[client_side_encryption]") {
instance::current();
class client conn {
mongocxx::uri{}, test_util::add_test_server_api()
};

if (!mongocxx::test_util::should_run_client_side_encryption_test()) {
return;
}

if (!test_util::newer_than(conn, "7.0")) {
std::cerr << "Explicit Encryption tests require MongoDB server 7.0+." << std::endl;
return;
}

if (test_util::get_topology(conn) == "single") {
std::cerr << "Explicit Encryption tests must not run against a standalone." << std::endl;
return;
}

conn.database("keyvault").collection("datakeys").drop();

struct which {
std::string kms_provider;
stdx::optional<bsoncxx::document::value> master_key;
};

which w = GENERATE(Catch::Generators::values<which>({
{"aws",
make_document(
kvp("region", "us-east-1"),
kvp("key",
"arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0"))},
// When testing 'local', use master_key of 'null'
{"local", stdx::nullopt},
}));

options::client_encryption cse_opts;
_add_cse_opts(&cse_opts, &conn, true);
client_encryption cse{std::move(cse_opts)};

auto db = conn.database("cec-test-db");
db.drop();
auto fin_options = make_document();

DYNAMIC_SECTION("KMS Provider - " << w.kms_provider) {
SECTION("Case 1: Simple Creation and Validation") {
const auto create_opts = make_document(
kvp("encryptedFields",
make_document(
kvp("fields",
make_array(make_document(kvp("path", "ssn"),
kvp("bsonType", "string"),
kvp("keyId", bsoncxx::types::b_null{})))))));

auto coll = cse.create_encrypted_collection(
db,
"testing1",
create_opts,
fin_options,
w.kms_provider,
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
CAPTURE(fin_options, coll);
try {
coll.insert_one(make_document(kvp("ssn", "123-45-6789")));
FAIL_CHECK("Insert should have failed");
} catch (const mongocxx::operation_exception& e) {
CHECK(e.code().value() == 121); // VALIDATION_ERROR
}
}

SECTION("Case 2: Missing 'encryptedFields'") {
const auto create_opts = make_document();
try {
auto coll = cse.create_encrypted_collection(
db,
"testing1",
create_opts,
fin_options,
w.kms_provider,
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
CAPTURE(fin_options, coll);
FAIL_CHECK("Did not throw");
} catch (const mongocxx::operation_exception& e) {
CHECK(e.code().value() == 22); // INVALID_ARG
}
}

SECTION("Case 3: Invalid keyId") {
const auto create_opts = make_document(kvp(
"encryptedFields",
make_document(kvp(
"fields",
make_array(make_document(
kvp("path", "ssn"), kvp("bsonType", "string"), kvp("keyId", false)))))));

try {
auto coll = cse.create_encrypted_collection(
db,
"testing1",
create_opts,
fin_options,
w.kms_provider,
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
CAPTURE(fin_options, coll);
FAIL_CHECK("Did not throw");
} catch (const mongocxx::operation_exception& e) {
CHECK(e.code().value() == 14); // INVALID_REPLY
}
}

SECTION("Case 4: Insert encrypted value") {
const auto create_opts = make_document(
kvp("encryptedFields",
make_document(
kvp("fields",
make_array(make_document(kvp("path", "ssn"),
kvp("bsonType", "string"),
kvp("keyId", bsoncxx::types::b_null{})))))));

auto coll = cse.create_encrypted_collection(
db,
"testing1",
create_opts,
fin_options,
w.kms_provider,
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
CAPTURE(fin_options, coll);

bsoncxx::types::b_string ssn{"123-45-6789"};
auto key = fin_options["encryptedFields"]["fields"][0]["keyId"];
options::encrypt enc;
enc.key_id(key.get_value());
enc.algorithm(options::encrypt::encryption_algorithm::k_unindexed);
auto encrypted = cse.encrypt(bsoncxx::types::bson_value::view(ssn), enc);
CHECK_NOTHROW(coll.insert_one(make_document(kvp("ssn", encrypted))));
}
}
}

TEST_CASE("Unique Index on keyAltNames", "[client_side_encryption]") {
instance::current();

Expand Down