Skip to content

Commit 803386f

Browse files
[CXX-2546] Create Encrypted Collection (#959)
* Implement the CreateEncryptedCollection interface This changeset introduces the create encrypted collection convenience API, as outlined in DRIVERS-2312. This is used to create data encryption keys on-the-fly when creating a new encrypted collection. The C++ API is mostly just a wrapper around the underlying C API.
1 parent 3b73a2a commit 803386f

File tree

7 files changed

+227
-4
lines changed

7 files changed

+227
-4
lines changed

src/mongocxx/client_encryption.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <bsoncxx/stdx/make_unique.hpp>
1616
#include <mongocxx/client_encryption.hpp>
1717
#include <mongocxx/private/client_encryption.hh>
18+
#include <mongocxx/private/database.hh>
1819

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

@@ -50,6 +51,18 @@ bsoncxx::types::bson_value::value client_encryption::decrypt(
5051
return _impl->decrypt(value);
5152
}
5253

54+
collection client_encryption::create_encrypted_collection(
55+
const database& db,
56+
const std::string& coll_name,
57+
const bsoncxx::document::view& options,
58+
bsoncxx::document::value& out_options,
59+
const std::string& kms_provider,
60+
const stdx::optional<bsoncxx::document::view>& masterkey) {
61+
auto& db_impl = db._get_impl();
62+
return _impl->create_encrypted_collection(
63+
db, db_impl.database_t, coll_name, options, out_options, kms_provider, masterkey);
64+
}
65+
5366
result::rewrap_many_datakey client_encryption::rewrap_many_datakey(
5467
bsoncxx::document::view_or_value filter, const options::rewrap_many_datakey& opts) {
5568
return _impl->rewrap_many_datakey(filter, opts);

src/mongocxx/client_encryption.hpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@
2323
#include <mongocxx/options/rewrap_many_datakey.hpp>
2424
#include <mongocxx/result/delete.hpp>
2525
#include <mongocxx/result/rewrap_many_datakey.hpp>
26+
#include <mongocxx/stdx.hpp>
2627

2728
#include <mongocxx/config/prelude.hpp>
2829

2930
namespace mongocxx {
3031
MONGOCXX_INLINE_NAMESPACE_BEGIN
3132

33+
class database;
34+
class collection;
35+
3236
///
3337
/// Class supporting operations for MongoDB Client-Side Field Level Encryption.
3438
///
@@ -82,6 +86,28 @@ class MONGOCXX_API client_encryption {
8286
bsoncxx::types::bson_value::value create_data_key(std::string kms_provider,
8387
const options::data_key& opts = {});
8488

89+
/**
90+
* @brief Create a collection with client-side-encryption enabled, automatically filling any
91+
* datakeys for encrypted fields.
92+
*
93+
* @param db The database in which the collection will be created
94+
* @param coll_name The name of the new collection
95+
* @param options The options for creating the collection. @see database::create_collection
96+
* @param out_options Output parameter to receive the generated collection options.
97+
* @param kms_provider The KMS provider to use when creating data encryption keys for the
98+
* collection's encrypted fields
99+
* @param masterkey If non-null, specify the masterkey to be used when creating data keys in the
100+
* collection.
101+
* @return collection A handle to the newly created collection
102+
*/
103+
collection create_encrypted_collection(
104+
const database& db,
105+
const std::string& coll_name,
106+
const bsoncxx::document::view& options,
107+
bsoncxx::document::value& out_options,
108+
const std::string& kms_provider,
109+
const stdx::optional<bsoncxx::document::view>& masterkey = stdx::nullopt);
110+
85111
///
86112
/// Encrypts a BSON value with a given key and algorithm.
87113
///

src/mongocxx/collection.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ MONGOCXX_INLINE_NAMESPACE_BEGIN
6565

6666
class client;
6767
class database;
68+
class client_encryption;
6869

6970
///
7071
/// Class representing server side document groupings within a MongoDB database.
@@ -1848,8 +1849,9 @@ class MONGOCXX_API collection {
18481849
///
18491850

18501851
private:
1851-
friend class bulk_write;
1852-
friend class database;
1852+
friend mongocxx::bulk_write;
1853+
friend mongocxx::database;
1854+
friend mongocxx::client_encryption;
18531855

18541856
MONGOCXX_PRIVATE collection(const database& database,
18551857
bsoncxx::string::view_or_value collection_name);

src/mongocxx/database.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace mongocxx {
3333
MONGOCXX_INLINE_NAMESPACE_BEGIN
3434

3535
class client;
36+
class client_encryption;
3637

3738
///
3839
/// Class representing a MongoDB database.
@@ -623,8 +624,9 @@ class MONGOCXX_API database {
623624
///
624625

625626
private:
626-
friend class client;
627-
friend class collection;
627+
friend mongocxx::client;
628+
friend mongocxx::collection;
629+
friend mongocxx::client_encryption;
628630

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

src/mongocxx/private/client_encryption.hh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <memory>
1818
#include <utility>
1919

20+
#include <bsoncxx/private/helpers.hh>
2021
#include <bsoncxx/private/libbson.hh>
2122
#include <bsoncxx/types/bson_value/private/value.hh>
2223
#include <bsoncxx/types/bson_value/value.hpp>
@@ -348,6 +349,43 @@ class client_encryption::impl {
348349
: stdx::optional<bsoncxx::document::value>{key_doc.steal()};
349350
}
350351

352+
collection create_encrypted_collection(
353+
const database& dbcxx,
354+
mongoc_database_t* const db,
355+
const std::string& coll_name,
356+
const bsoncxx::document::view opts,
357+
bsoncxx::document::value& out_options,
358+
const std::string& kms_provider,
359+
const stdx::optional<bsoncxx::document::view>& masterkey) {
360+
bson_error_t error = {};
361+
scoped_bson_t out_opts;
362+
out_opts.init();
363+
364+
bson_t* opt_mkey_ptr = nullptr;
365+
scoped_bson_t opt_mkey;
366+
if (masterkey) {
367+
opt_mkey.init_from_static(*masterkey);
368+
opt_mkey_ptr = opt_mkey.bson();
369+
}
370+
371+
scoped_bson_t coll_opts{opts};
372+
373+
auto coll_ptr =
374+
libmongoc::client_encryption_create_encrypted_collection(_client_encryption.get(),
375+
db,
376+
coll_name.data(),
377+
coll_opts.bson(),
378+
out_opts.bson(),
379+
kms_provider.data(),
380+
opt_mkey_ptr,
381+
&error);
382+
out_options = bsoncxx::helpers::value_from_bson_t(out_opts.bson());
383+
if (!coll_ptr) {
384+
throw_exception<operation_exception>(error);
385+
}
386+
return collection(dbcxx, coll_ptr);
387+
}
388+
351389
private:
352390
struct encryption_deleter {
353391
void operator()(mongoc_client_encryption_t* ptr) noexcept {

src/mongocxx/private/libmongoc_symbols.hh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ MONGOCXX_LIBMONGOC_SYMBOL(client_destroy)
126126
MONGOCXX_LIBMONGOC_SYMBOL(client_enable_auto_encryption)
127127
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_add_key_alt_name)
128128
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_create_datakey)
129+
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_create_encrypted_collection)
129130
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_datakey_opts_destroy)
130131
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_datakey_opts_new)
131132
MONGOCXX_LIBMONGOC_SYMBOL(client_encryption_datakey_opts_set_keyaltnames)

src/mongocxx/test/client_side_encryption.cpp

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <string>
1919
#include <tuple>
2020

21+
#include <bsoncxx/builder/stream/array.hpp>
2122
#include <bsoncxx/builder/stream/document.hpp>
2223
#include <bsoncxx/builder/stream/helpers.hpp>
2324
#include <bsoncxx/document/element.hpp>
@@ -2489,6 +2490,146 @@ TEST_CASE("Explicit Encryption", "[client_side_encryption]") {
24892490
}
24902491
}
24912492

2493+
TEST_CASE("Create Encrypted Collection", "[client_side_encryption]") {
2494+
instance::current();
2495+
class client conn {
2496+
mongocxx::uri{}, test_util::add_test_server_api()
2497+
};
2498+
2499+
if (!mongocxx::test_util::should_run_client_side_encryption_test()) {
2500+
return;
2501+
}
2502+
2503+
if (!test_util::newer_than(conn, "7.0")) {
2504+
std::cerr << "Explicit Encryption tests require MongoDB server 7.0+." << std::endl;
2505+
return;
2506+
}
2507+
2508+
if (test_util::get_topology(conn) == "single") {
2509+
std::cerr << "Explicit Encryption tests must not run against a standalone." << std::endl;
2510+
return;
2511+
}
2512+
2513+
conn.database("keyvault").collection("datakeys").drop();
2514+
2515+
struct which {
2516+
std::string kms_provider;
2517+
stdx::optional<bsoncxx::document::value> master_key;
2518+
};
2519+
2520+
which w = GENERATE(Catch::Generators::values<which>({
2521+
{"aws",
2522+
make_document(
2523+
kvp("region", "us-east-1"),
2524+
kvp("key",
2525+
"arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0"))},
2526+
// When testing 'local', use master_key of 'null'
2527+
{"local", stdx::nullopt},
2528+
}));
2529+
2530+
options::client_encryption cse_opts;
2531+
_add_cse_opts(&cse_opts, &conn, true);
2532+
client_encryption cse{std::move(cse_opts)};
2533+
2534+
auto db = conn.database("cec-test-db");
2535+
db.drop();
2536+
auto fin_options = make_document();
2537+
2538+
DYNAMIC_SECTION("KMS Provider - " << w.kms_provider) {
2539+
SECTION("Case 1: Simple Creation and Validation") {
2540+
const auto create_opts = make_document(
2541+
kvp("encryptedFields",
2542+
make_document(
2543+
kvp("fields",
2544+
make_array(make_document(kvp("path", "ssn"),
2545+
kvp("bsonType", "string"),
2546+
kvp("keyId", bsoncxx::types::b_null{})))))));
2547+
2548+
auto coll = cse.create_encrypted_collection(
2549+
db,
2550+
"testing1",
2551+
create_opts,
2552+
fin_options,
2553+
w.kms_provider,
2554+
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
2555+
CAPTURE(fin_options, coll);
2556+
try {
2557+
coll.insert_one(make_document(kvp("ssn", "123-45-6789")));
2558+
FAIL_CHECK("Insert should have failed");
2559+
} catch (const mongocxx::operation_exception& e) {
2560+
CHECK(e.code().value() == 121); // VALIDATION_ERROR
2561+
}
2562+
}
2563+
2564+
SECTION("Case 2: Missing 'encryptedFields'") {
2565+
const auto create_opts = make_document();
2566+
try {
2567+
auto coll = cse.create_encrypted_collection(
2568+
db,
2569+
"testing1",
2570+
create_opts,
2571+
fin_options,
2572+
w.kms_provider,
2573+
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
2574+
CAPTURE(fin_options, coll);
2575+
FAIL_CHECK("Did not throw");
2576+
} catch (const mongocxx::operation_exception& e) {
2577+
CHECK(e.code().value() == 22); // INVALID_ARG
2578+
}
2579+
}
2580+
2581+
SECTION("Case 3: Invalid keyId") {
2582+
const auto create_opts = make_document(kvp(
2583+
"encryptedFields",
2584+
make_document(kvp(
2585+
"fields",
2586+
make_array(make_document(
2587+
kvp("path", "ssn"), kvp("bsonType", "string"), kvp("keyId", false)))))));
2588+
2589+
try {
2590+
auto coll = cse.create_encrypted_collection(
2591+
db,
2592+
"testing1",
2593+
create_opts,
2594+
fin_options,
2595+
w.kms_provider,
2596+
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
2597+
CAPTURE(fin_options, coll);
2598+
FAIL_CHECK("Did not throw");
2599+
} catch (const mongocxx::operation_exception& e) {
2600+
CHECK(e.code().value() == 14); // INVALID_REPLY
2601+
}
2602+
}
2603+
2604+
SECTION("Case 4: Insert encrypted value") {
2605+
const auto create_opts = make_document(
2606+
kvp("encryptedFields",
2607+
make_document(
2608+
kvp("fields",
2609+
make_array(make_document(kvp("path", "ssn"),
2610+
kvp("bsonType", "string"),
2611+
kvp("keyId", bsoncxx::types::b_null{})))))));
2612+
2613+
auto coll = cse.create_encrypted_collection(
2614+
db,
2615+
"testing1",
2616+
create_opts,
2617+
fin_options,
2618+
w.kms_provider,
2619+
w.master_key ? stdx::make_optional(w.master_key->view()) : stdx::nullopt);
2620+
CAPTURE(fin_options, coll);
2621+
2622+
bsoncxx::types::b_string ssn{"123-45-6789"};
2623+
auto key = fin_options["encryptedFields"]["fields"][0]["keyId"];
2624+
options::encrypt enc;
2625+
enc.key_id(key.get_value());
2626+
enc.algorithm(options::encrypt::encryption_algorithm::k_unindexed);
2627+
auto encrypted = cse.encrypt(bsoncxx::types::bson_value::view(ssn), enc);
2628+
CHECK_NOTHROW(coll.insert_one(make_document(kvp("ssn", encrypted))));
2629+
}
2630+
}
2631+
}
2632+
24922633
TEST_CASE("Unique Index on keyAltNames", "[client_side_encryption]") {
24932634
instance::current();
24942635

0 commit comments

Comments
 (0)