From 59585def7fc31c100fd97fb7c3d4ec15a796b6ce Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Thu, 9 Feb 2023 09:22:38 -0700 Subject: [PATCH] Add the `ClientEncryption.createEncryptedCollection` helper method (#1079) Add the `ClientEncryption.createEncryptedCollection` helper method JAVA-4679 --- config/spotbugs/exclude.xml | 14 + .../MongoUpdatedEncryptedFieldsException.java | 67 +++++ .../client/model/CreateCollectionOptions.java | 26 ++ .../CreateEncryptedCollectionParams.java | 83 ++++++ .../internal/vault/ClientEncryptionImpl.java | 86 ++++++ .../client/vault/ClientEncryption.java | 32 +++ .../ClientSideEncryptionAutoDataKeysTest.java | 38 +++ .../syncadapter/SyncClientEncryption.java | 19 ++ .../org/mongodb/scala/model/package.scala | 24 ++ .../scala/org/mongodb/scala/package.scala | 10 + .../scala/vault/ClientEncryption.scala | 41 ++- .../model/vault/ClientEncryptionSpec.scala | 23 +- .../client/internal/ClientEncryptionImpl.java | 69 +++++ .../client/vault/ClientEncryption.java | 31 +++ ...tClientSideEncryptionAutoDataKeysTest.java | 248 ++++++++++++++++++ .../ClientSideEncryptionAutoDataKeysTest.java | 34 +++ 16 files changed, 842 insertions(+), 3 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/MongoUpdatedEncryptedFieldsException.java create mode 100644 driver-core/src/main/com/mongodb/client/model/CreateEncryptedCollectionParams.java create mode 100644 driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionAutoDataKeysTest.java create mode 100644 driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionAutoDataKeysTest.java create mode 100644 driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionAutoDataKeysTest.java diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 2a8b69a2e7b..8cfa95fb4ec 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -240,4 +240,18 @@ + + + + + + + + + + + + diff --git a/driver-core/src/main/com/mongodb/MongoUpdatedEncryptedFieldsException.java b/driver-core/src/main/com/mongodb/MongoUpdatedEncryptedFieldsException.java new file mode 100644 index 00000000000..fbc55f76b05 --- /dev/null +++ b/driver-core/src/main/com/mongodb/MongoUpdatedEncryptedFieldsException.java @@ -0,0 +1,67 @@ +/* + * Copyright 2008-present 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. + */ +package com.mongodb; + +import com.mongodb.annotations.Beta; +import org.bson.BsonDocument; + +import static com.mongodb.assertions.Assertions.assertNotNull; + +/** + * An exception thrown by methods that may automatically create data encryption keys + * where needed based on the {@code encryptedFields} configuration. + * + * @since 4.9 + */ +@Beta(Beta.Reason.SERVER) +public final class MongoUpdatedEncryptedFieldsException extends MongoClientException { + private static final long serialVersionUID = 1; + + private final BsonDocument encryptedFields; + + /** + * Not part of the public API. + * + * @param encryptedFields The (partially) updated {@code encryptedFields} document, + * which allows users to infer which data keys are known to be created before the exception happened + * (see {@link #getEncryptedFields()} for more details). + * Reporting this back to a user may be helpful because creation of a data key includes persisting it in the key vault. + * @param msg The message. + * @param cause The cause. + */ + public MongoUpdatedEncryptedFieldsException(final BsonDocument encryptedFields, final String msg, final Throwable cause) { + super(msg, assertNotNull(cause)); + this.encryptedFields = assertNotNull(encryptedFields); + } + + /** + * The {@code encryptedFields} document that allows inferring which data keys are known to be created + * before {@code this} exception happened by comparing this document with the original {@code encryptedFields} configuration. + * Creation of a data key includes persisting it in the key vault. + *

+ * Note that the returned {@code encryptedFields} document is not guaranteed to contain information about all the data keys that + * may be created, only about those that the driver is certain about. For example, if persisting a data key times out, + * the driver does not know whether it can be considered created or not, and does not include the information about the key in + * the {@code encryptedFields} document. You can analyze whether the {@linkplain #getCause() cause} is a definite or indefinite + * error, and rely on the returned {@code encryptedFields} to be containing information on all created keys + * only if the error is definite.

+ * + * @return The updated {@code encryptedFields} document. + */ + public BsonDocument getEncryptedFields() { + return encryptedFields; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java b/driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java index 8c19c57df5b..9f432320001 100644 --- a/driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java @@ -45,6 +45,32 @@ public class CreateCollectionOptions { private ClusteredIndexOptions clusteredIndexOptions; private Bson encryptedFields; + public CreateCollectionOptions() { + } + + /** + * A shallow copy constructor. + * + * @param options The options to copy. + * + * @since 4.9 + */ + public CreateCollectionOptions(final CreateCollectionOptions options) { + notNull("options", options); + maxDocuments = options.maxDocuments; + capped = options.capped; + sizeInBytes = options.sizeInBytes; + storageEngineOptions = options.storageEngineOptions; + indexOptionDefaults = options.indexOptionDefaults; + validationOptions = options.validationOptions; + collation = options.collation; + expireAfterSeconds = options.expireAfterSeconds; + timeSeriesOptions = options.timeSeriesOptions; + changeStreamPreAndPostImagesOptions = options.changeStreamPreAndPostImagesOptions; + clusteredIndexOptions = options.clusteredIndexOptions; + encryptedFields = options.encryptedFields; + } + /** * Gets the maximum number of documents allowed in a capped collection. * diff --git a/driver-core/src/main/com/mongodb/client/model/CreateEncryptedCollectionParams.java b/driver-core/src/main/com/mongodb/client/model/CreateEncryptedCollectionParams.java new file mode 100644 index 00000000000..eba101ac000 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/CreateEncryptedCollectionParams.java @@ -0,0 +1,83 @@ +/* + * Copyright 2008-present 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. + */ + +package com.mongodb.client.model; + +import com.mongodb.annotations.Beta; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.lang.Nullable; +import org.bson.BsonDocument; + +import static com.mongodb.assertions.Assertions.notNull; + +/** + * Auxiliary parameters for creating an encrypted collection. + * + * @since 4.9 + */ +@Beta(Beta.Reason.SERVER) +public final class CreateEncryptedCollectionParams { + private final String kmsProvider; + @Nullable + private BsonDocument masterKey; + + /** + * @param kmsProvider The name of the KMS provider. + */ + public CreateEncryptedCollectionParams(final String kmsProvider) { + this.kmsProvider = notNull("kmsProvider", kmsProvider); + masterKey = null; + } + + /** + * The name of the KMS provider. + * + * @return The name of the KMS provider. + */ + public String getKmsProvider() { + return kmsProvider; + } + + /** + * Sets the {@linkplain DataKeyOptions#getMasterKey() master key} for creating a data key. + * + * @param masterKey The master key for creating a data key. + * @return {@code this}. + */ + public CreateEncryptedCollectionParams masterKey(@Nullable final BsonDocument masterKey) { + this.masterKey = masterKey; + return this; + } + + /** + * The {@linkplain DataKeyOptions#getMasterKey() master key} for creating a data key. + * The default is {@code null}. + * + * @return The master key for creating a data key. + */ + @Nullable + public BsonDocument getMasterKey() { + return masterKey; + } + + @Override + public String toString() { + return "CreateEncryptedCollectionParams{" + + ", kmsProvider=" + kmsProvider + + ", masterKey=" + masterKey + + '}'; + } +} diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java index 29c94ef4598..4887e0109a9 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java @@ -17,9 +17,14 @@ package com.mongodb.reactivestreams.client.internal.vault; import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoConfigurationException; import com.mongodb.MongoNamespace; +import com.mongodb.MongoUpdatedEncryptedFieldsException; import com.mongodb.ReadConcern; import com.mongodb.WriteConcern; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; import com.mongodb.client.model.Filters; import com.mongodb.client.model.UpdateOneModel; import com.mongodb.client.model.Updates; @@ -32,22 +37,30 @@ import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoClients; import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; import com.mongodb.reactivestreams.client.internal.crypt.Crypt; import com.mongodb.reactivestreams.client.internal.crypt.Crypts; import com.mongodb.reactivestreams.client.vault.ClientEncryption; import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonDocument; +import org.bson.BsonNull; import org.bson.BsonString; import org.bson.BsonValue; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.capi.MongoCryptHelper.validateRewrapManyDataKeyOptions; +import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -183,6 +196,79 @@ public Publisher rewrapManyDataKey(final Bson filter, f })); } + @Override + public Publisher createEncryptedCollection(final MongoDatabase database, final String collectionName, + final CreateCollectionOptions createCollectionOptions, final CreateEncryptedCollectionParams createEncryptedCollectionParams) { + notNull("collectionName", collectionName); + notNull("createCollectionOptions", createCollectionOptions); + notNull("createEncryptedCollectionParams", createEncryptedCollectionParams); + MongoNamespace namespace = new MongoNamespace(database.getName(), collectionName); + Bson rawEncryptedFields = createCollectionOptions.getEncryptedFields(); + if (rawEncryptedFields == null) { + throw new MongoConfigurationException(format("`encryptedFields` is not configured for the collection %s.", namespace)); + } + CodecRegistry codecRegistry = options.getKeyVaultMongoClientSettings() == null + ? MongoClientSettings.getDefaultCodecRegistry() + : options.getKeyVaultMongoClientSettings().getCodecRegistry(); + BsonDocument encryptedFields = rawEncryptedFields.toBsonDocument(BsonDocument.class, codecRegistry); + BsonValue fields = encryptedFields.get("fields"); + if (fields != null && fields.isArray()) { + String kmsProvider = createEncryptedCollectionParams.getKmsProvider(); + DataKeyOptions dataKeyOptions = new DataKeyOptions(); + BsonDocument masterKey = createEncryptedCollectionParams.getMasterKey(); + if (masterKey != null) { + dataKeyOptions.masterKey(masterKey); + } + String keyIdBsonKey = "keyId"; + return Mono.defer(() -> { + // `Mono.defer` results in `maybeUpdatedEncryptedFields` and `dataKeyMightBeCreated` (mutable state) + // being created once per `Subscriber`, which allows the produced `Mono` to support multiple `Subscribers`. + BsonDocument maybeUpdatedEncryptedFields = encryptedFields.clone(); + AtomicBoolean dataKeyMightBeCreated = new AtomicBoolean(); + Iterable> publishersOfUpdatedFields = () -> maybeUpdatedEncryptedFields.get("fields").asArray() + .stream() + .filter(BsonValue::isDocument) + .map(BsonValue::asDocument) + .filter(field -> field.containsKey(keyIdBsonKey)) + .filter(field -> Objects.equals(field.get(keyIdBsonKey), BsonNull.VALUE)) + // here we rely on the `createDataKey` publisher being cold, i.e., doing nothing until it is subscribed to + .map(field -> Mono.fromDirect(createDataKey(kmsProvider, dataKeyOptions)) + // This is the closest we can do with reactive streams to setting the `dataKeyMightBeCreated` flag + // immediately before calling `createDataKey`. + .doOnSubscribe(subscription -> dataKeyMightBeCreated.set(true)) + .doOnNext(dataKeyId -> field.put(keyIdBsonKey, dataKeyId)) + .map(dataKeyId -> field) + ) + .iterator(); + // `Flux.concat` ensures that data keys are created / fields are updated sequentially one by one + Flux publisherOfUpdatedFields = Flux.concat(publishersOfUpdatedFields); + return publisherOfUpdatedFields + // All write actions in `doOnNext` above happen-before the completion (`onComplete`/`onError`) signals + // for this publisher, because all signals are serial. `thenEmpty` further guarantees that the completion signal + // for this publisher happens-before the `onSubscribe` signal for the publisher passed to it + // (the next publisher, which creates a collection). + // `defer` defers calling `createCollection` until the next publisher is subscribed to. + // Therefore, all write actions in `doOnNext` above happen-before the invocation of `createCollection`, + // which means `createCollection` is guaranteed to observe all those write actions, i.e., + // it is guaranteed to observe the updated document via the `maybeUpdatedEncryptedFields` reference. + // + // Similarly, the `Subscriber` of the returned `Publisher` is guaranteed to observe all those write actions + // via the `maybeUpdatedEncryptedFields` reference, which is emitted as a result of `thenReturn`. + .thenEmpty(Mono.defer(() -> Mono.fromDirect(database.createCollection(collectionName, + new CreateCollectionOptions(createCollectionOptions).encryptedFields(maybeUpdatedEncryptedFields)))) + ) + .onErrorMap(e -> dataKeyMightBeCreated.get(), e -> + new MongoUpdatedEncryptedFieldsException(maybeUpdatedEncryptedFields, + format("Failed to create %s.", namespace), e) + ) + .thenReturn(maybeUpdatedEncryptedFields); + }); + } else { + return Mono.fromDirect(database.createCollection(collectionName, createCollectionOptions)) + .thenReturn(encryptedFields); + } + } + @Override public void close() { keyVaultClient.close(); diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/vault/ClientEncryption.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/vault/ClientEncryption.java index b65b9a9c4a9..0ba04eef8d3 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/vault/ClientEncryption.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/vault/ClientEncryption.java @@ -16,7 +16,11 @@ package com.mongodb.reactivestreams.client.vault; +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.MongoUpdatedEncryptedFieldsException; import com.mongodb.annotations.Beta; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; import com.mongodb.client.model.vault.DataKeyOptions; import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.RewrapManyDataKeyOptions; @@ -24,6 +28,7 @@ import com.mongodb.client.result.DeleteResult; import com.mongodb.lang.Nullable; import com.mongodb.reactivestreams.client.FindPublisher; +import com.mongodb.reactivestreams.client.MongoDatabase; import org.bson.BsonBinary; import org.bson.BsonDocument; import org.bson.BsonValue; @@ -187,6 +192,33 @@ public interface ClientEncryption extends Closeable { */ Publisher rewrapManyDataKey(Bson filter, RewrapManyDataKeyOptions options); + /** + * {@linkplain MongoDatabase#createCollection(String, CreateCollectionOptions) Create} a new collection with encrypted fields, + * automatically {@linkplain #createDataKey(String, DataKeyOptions) creating} + * new data encryption keys when needed based on the configured + * {@link CreateCollectionOptions#getEncryptedFields() encryptedFields}, which must be specified. + * This method does not modify the configured {@code encryptedFields} when creating new data keys, + * instead it creates a new configuration if needed. + * + * @param database The database to use for creating the collection. + * @param collectionName The name for the collection to create. + * @param createCollectionOptions Options for creating the collection. + * @param createEncryptedCollectionParams Auxiliary parameters for creating an encrypted collection. + * @return A publisher of the (potentially updated) {@code encryptedFields} configuration that was used to create the + * collection. A user may use this document to configure {@link AutoEncryptionSettings#getEncryptedFieldsMap()}. + *

+ * {@linkplain org.reactivestreams.Subscriber#onError(Throwable) Signals} {@link MongoUpdatedEncryptedFieldsException} + * if an exception happens after creating at least one data key. This exception makes the updated {@code encryptedFields} + * {@linkplain MongoUpdatedEncryptedFieldsException#getEncryptedFields() available} to the caller.

+ * + * @since 4.9 + * @mongodb.server.release 6.0 + * @mongodb.driver.manual reference/command/create Create Command + */ + @Beta(Beta.Reason.SERVER) + Publisher createEncryptedCollection(MongoDatabase database, String collectionName, + CreateCollectionOptions createCollectionOptions, CreateEncryptedCollectionParams createEncryptedCollectionParams); + @Override void close(); } diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionAutoDataKeysTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionAutoDataKeysTest.java new file mode 100644 index 00000000000..e6c3fda311b --- /dev/null +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryptionAutoDataKeysTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2008-present 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. + */ + +package com.mongodb.reactivestreams.client; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.AbstractClientSideEncryptionAutoDataKeysTest; +import com.mongodb.client.MongoClient; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.reactivestreams.client.syncadapter.SyncClientEncryption; +import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; +import com.mongodb.reactivestreams.client.vault.ClientEncryptions; + +final class ClientSideEncryptionAutoDataKeysTest extends AbstractClientSideEncryptionAutoDataKeysTest { + @Override + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return new SyncMongoClient(MongoClients.create(settings)); + } + + @Override + protected ClientEncryption createClientEncryption(final ClientEncryptionSettings settings) { + return new SyncClientEncryption(ClientEncryptions.create(settings)); + } +} diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientEncryption.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientEncryption.java index 2e43625599a..44f1d1b687e 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientEncryption.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncClientEncryption.java @@ -16,7 +16,11 @@ package com.mongodb.reactivestreams.client.syncadapter; +import com.mongodb.MongoUpdatedEncryptedFieldsException; import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; import com.mongodb.client.model.vault.DataKeyOptions; import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.RewrapManyDataKeyOptions; @@ -32,6 +36,7 @@ import static com.mongodb.ClusterFixture.TIMEOUT_DURATION; import static com.mongodb.reactivestreams.client.syncadapter.ContextHelper.CONTEXT; import static java.util.Objects.requireNonNull; +import static org.bson.assertions.Assertions.fail; public class SyncClientEncryption implements ClientEncryption { @@ -106,6 +111,20 @@ public RewrapManyDataKeyResult rewrapManyDataKey(final Bson filter, final Rewrap return requireNonNull(Mono.from(wrapped.rewrapManyDataKey(filter, options)).contextWrite(CONTEXT).block(TIMEOUT_DURATION)); } + @Override + public BsonDocument createEncryptedCollection(final MongoDatabase database, final String collectionName, + final CreateCollectionOptions createCollectionOptions, final CreateEncryptedCollectionParams createEncryptedCollectionParams) + throws MongoUpdatedEncryptedFieldsException { + if (database instanceof SyncMongoDatabase) { + com.mongodb.reactivestreams.client.MongoDatabase reactiveDatabase = ((SyncMongoDatabase) database).getWrapped(); + return requireNonNull(Mono.fromDirect(wrapped.createEncryptedCollection( + reactiveDatabase, collectionName, createCollectionOptions, createEncryptedCollectionParams)) + .contextWrite(CONTEXT).block(TIMEOUT_DURATION)); + } else { + throw fail(database.getClass().toString()); + } + } + @Override public void close() { wrapped.close(); diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/package.scala index 65f5c92a873..4e28793a4a7 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/package.scala @@ -16,6 +16,8 @@ package org.mongodb.scala +import com.mongodb.annotations.Beta + import scala.collection.JavaConverters._ import com.mongodb.client.model.{ GeoNearOptions, MongoTimeUnit => JMongoTimeUnit, WindowOutputField } import org.mongodb.scala.bson.conversions.Bson @@ -161,6 +163,28 @@ package object model { */ object CreateCollectionOptions { def apply(): CreateCollectionOptions = new com.mongodb.client.model.CreateCollectionOptions() + + def apply(options: CreateCollectionOptions): CreateCollectionOptions = + new com.mongodb.client.model.CreateCollectionOptions(options) + } + + /** + * Auxiliary parameters for creating an encrypted collection. + * + * @since 4.9 + */ + @Beta(Array(Beta.Reason.SERVER)) + type CreateEncryptedCollectionParams = com.mongodb.client.model.CreateEncryptedCollectionParams + + /** + * Auxiliary parameters for creating an encrypted collection. + * + * @since 4.9 + */ + @Beta(Array(Beta.Reason.SERVER)) + object CreateEncryptedCollectionParams { + def apply(kmsProvider: String) = + new com.mongodb.client.model.CreateEncryptedCollectionParams(kmsProvider) } /** diff --git a/driver-scala/src/main/scala/org/mongodb/scala/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/package.scala index bdc0b42b686..b52ff13fd61 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/package.scala @@ -16,6 +16,7 @@ package org.mongodb +import com.mongodb.annotations.Beta import org.bson.BsonDocumentReader import org.bson.codecs.{ DecoderContext, DocumentCodec } import org.mongodb.scala.bson.BsonDocument @@ -360,6 +361,15 @@ package object scala extends ClientSessionImplicits with ObservableImplicits wit */ type MongoConnectionPoolClearedException = com.mongodb.MongoConnectionPoolClearedException + /** + * An exception thrown by methods that may automatically create data encryption keys + * where needed based on the `encryptedFields` configuration. + * + * @since 4.9 + */ + @Beta(Array(Beta.Reason.SERVER)) + type MongoUpdatedEncryptedFieldsException = com.mongodb.MongoUpdatedEncryptedFieldsException + /** * The client-side automatic encryption settings. In-use encryption enables an application to specify what fields in a collection * must be encrypted, and the driver automatically encrypts commands sent to MongoDB and decrypts responses. diff --git a/driver-scala/src/main/scala/org/mongodb/scala/vault/ClientEncryption.scala b/driver-scala/src/main/scala/org/mongodb/scala/vault/ClientEncryption.scala index 637ac2103fe..f5cbd6c0565 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/vault/ClientEncryption.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/vault/ClientEncryption.scala @@ -17,11 +17,12 @@ package org.mongodb.scala.vault import com.mongodb.annotations.Beta +import com.mongodb.client.model.{ CreateCollectionOptions, CreateEncryptedCollectionParams } import java.io.Closeable import com.mongodb.reactivestreams.client.vault.{ ClientEncryption => JClientEncryption } -import org.bson.{ BsonBinary, BsonValue } -import org.mongodb.scala.{ Document, SingleObservable, ToSingleObservablePublisher } +import org.bson.{ BsonBinary, BsonDocument, BsonValue } +import org.mongodb.scala.{ Document, MongoDatabase, SingleObservable, ToSingleObservablePublisher } import org.mongodb.scala.model.vault.{ DataKeyOptions, EncryptOptions } /** @@ -103,6 +104,42 @@ case class ClientEncryption(private val wrapped: JClientEncryption) extends Clos */ def decrypt(value: BsonBinary): SingleObservable[BsonValue] = wrapped.decrypt(value) + /** + * Create a new collection with encrypted fields, + * automatically creating + * new data encryption keys when needed based on the configured + * `encryptedFields`, which must be specified. + * This method does not modify the configured `encryptedFields` when creating new data keys, + * instead it creates a new configuration if needed. + * + * @param database The database to use for creating the collection. + * @param collectionName The name for the collection to create. + * @param createCollectionOptions Options for creating the collection. + * @param createEncryptedCollectionParams Auxiliary parameters for creating an encrypted collection. + * @return A publisher of the (potentially updated) `encryptedFields` configuration that was used to create the collection. + * A user may use this document to configure `com.mongodb.AutoEncryptionSettings.getEncryptedFieldsMap`. + * + * Produces [[com.mongodb.MongoUpdatedEncryptedFieldsException]] + * if an exception happens after creating at least one data key. This exception makes the updated `encryptedFields` + * available to the caller. + * @since 4.9 + * @note Requires MongoDB 6.0 or greater. + * @see [[https://www.mongodb.com/docs/manual/reference/command/create/ Create Command]] + */ + @Beta(Array(Beta.Reason.SERVER)) + def createEncryptedCollection( + database: MongoDatabase, + collectionName: String, + createCollectionOptions: CreateCollectionOptions, + createEncryptedCollectionParams: CreateEncryptedCollectionParams + ): SingleObservable[BsonDocument] = + wrapped.createEncryptedCollection( + database.wrapped, + collectionName, + createCollectionOptions, + createEncryptedCollectionParams + ) + override def close(): Unit = wrapped.close() } diff --git a/driver-scala/src/test/scala/org/mongodb/scala/model/vault/ClientEncryptionSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/model/vault/ClientEncryptionSpec.scala index dd3ecb8c7c2..682d60207b4 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/model/vault/ClientEncryptionSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/model/vault/ClientEncryptionSpec.scala @@ -16,12 +16,15 @@ package org.mongodb.scala.model.vault +import com.mongodb.client.model.CreateEncryptedCollectionParams + import com.mongodb.reactivestreams.client.vault.{ ClientEncryption => JClientEncryption } import org.mockito.ArgumentMatchers.{ any, same } import org.mockito.Mockito.verify -import org.mongodb.scala.BaseSpec +import org.mongodb.scala.{ BaseSpec, MongoDatabase } import org.mongodb.scala.bson.collection.immutable.Document import org.mongodb.scala.bson.{ BsonBinary, BsonString } +import org.mongodb.scala.model.CreateCollectionOptions import org.mongodb.scala.vault.ClientEncryption import org.scalatestplus.mockito.MockitoSugar @@ -80,4 +83,22 @@ class ClientEncryptionSpec extends BaseSpec with MockitoSugar { verify(wrapped).decrypt(bsonBinary) } + it should "call createEncryptedCollection" in { + val database = mock[MongoDatabase] + val collectionName = "collectionName" + val createCollectionOptions = new CreateCollectionOptions() + val createEncryptedCollectionParams = new CreateEncryptedCollectionParams("kmsProvider") + clientEncryption.createEncryptedCollection( + database, + collectionName, + createCollectionOptions, + createEncryptedCollectionParams + ) + verify(wrapped).createEncryptedCollection( + same(database.wrapped), + same(collectionName), + same(createCollectionOptions), + same(createEncryptedCollectionParams) + ) + } } diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java index 48058389e20..d1080245922 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java @@ -17,13 +17,19 @@ package com.mongodb.client.internal; import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoConfigurationException; import com.mongodb.MongoNamespace; +import com.mongodb.MongoUpdatedEncryptedFieldsException; import com.mongodb.ReadConcern; import com.mongodb.WriteConcern; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; import com.mongodb.client.model.Filters; import com.mongodb.client.model.UpdateOneModel; import com.mongodb.client.model.Updates; @@ -36,14 +42,20 @@ import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonDocument; +import org.bson.BsonNull; import org.bson.BsonString; import org.bson.BsonValue; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.capi.MongoCryptHelper.validateRewrapManyDataKeyOptions; +import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -171,6 +183,63 @@ public RewrapManyDataKeyResult rewrapManyDataKey(final Bson filter, final Rewrap return new RewrapManyDataKeyResult(collection.bulkWrite(updateModels)); } + @Override + public BsonDocument createEncryptedCollection(final MongoDatabase database, final String collectionName, + final CreateCollectionOptions createCollectionOptions, final CreateEncryptedCollectionParams createEncryptedCollectionParams) { + notNull("collectionName", collectionName); + notNull("createCollectionOptions", createCollectionOptions); + notNull("createEncryptedCollectionParams", createEncryptedCollectionParams); + MongoNamespace namespace = new MongoNamespace(database.getName(), collectionName); + Bson rawEncryptedFields = createCollectionOptions.getEncryptedFields(); + if (rawEncryptedFields == null) { + throw new MongoConfigurationException(format("`encryptedFields` is not configured for the collection %s.", namespace)); + } + CodecRegistry codecRegistry = options.getKeyVaultMongoClientSettings() == null + ? MongoClientSettings.getDefaultCodecRegistry() + : options.getKeyVaultMongoClientSettings().getCodecRegistry(); + BsonDocument encryptedFields = rawEncryptedFields.toBsonDocument(BsonDocument.class, codecRegistry); + BsonValue fields = encryptedFields.get("fields"); + if (fields != null && fields.isArray()) { + String kmsProvider = createEncryptedCollectionParams.getKmsProvider(); + DataKeyOptions dataKeyOptions = new DataKeyOptions(); + BsonDocument masterKey = createEncryptedCollectionParams.getMasterKey(); + if (masterKey != null) { + dataKeyOptions.masterKey(masterKey); + } + String keyIdBsonKey = "keyId"; + BsonDocument maybeUpdatedEncryptedFields = encryptedFields.clone(); + // only the mutability of `dataKeyMightBeCreated` is important, it does not need to be thread-safe + AtomicBoolean dataKeyMightBeCreated = new AtomicBoolean(); + try { + maybeUpdatedEncryptedFields.get("fields").asArray() + .stream() + .filter(BsonValue::isDocument) + .map(BsonValue::asDocument) + .filter(field -> field.containsKey(keyIdBsonKey)) + .filter(field -> Objects.equals(field.get(keyIdBsonKey), BsonNull.VALUE)) + .forEachOrdered(field -> { + // It is crucial to set the `dataKeyMightBeCreated` flag either immediately before calling `createDataKey`, + // or after that in a `finally` block. + dataKeyMightBeCreated.set(true); + BsonBinary dataKeyId = createDataKey(kmsProvider, dataKeyOptions); + field.put(keyIdBsonKey, dataKeyId); + }); + database.createCollection(collectionName, + new CreateCollectionOptions(createCollectionOptions).encryptedFields(maybeUpdatedEncryptedFields)); + return maybeUpdatedEncryptedFields; + } catch (Exception e) { + if (dataKeyMightBeCreated.get()) { + throw new MongoUpdatedEncryptedFieldsException(maybeUpdatedEncryptedFields, format("Failed to create %s.", namespace), e); + } else { + throw e; + } + } + } else { + database.createCollection(collectionName, createCollectionOptions); + return encryptedFields; + } + } + @Override public void close() { crypt.close(); diff --git a/driver-sync/src/main/com/mongodb/client/vault/ClientEncryption.java b/driver-sync/src/main/com/mongodb/client/vault/ClientEncryption.java index 822ff83fbac..3c712c0caea 100644 --- a/driver-sync/src/main/com/mongodb/client/vault/ClientEncryption.java +++ b/driver-sync/src/main/com/mongodb/client/vault/ClientEncryption.java @@ -16,8 +16,13 @@ package com.mongodb.client.vault; +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.MongoUpdatedEncryptedFieldsException; import com.mongodb.annotations.Beta; import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; import com.mongodb.client.model.vault.DataKeyOptions; import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.RewrapManyDataKeyOptions; @@ -190,6 +195,32 @@ public interface ClientEncryption extends Closeable { */ RewrapManyDataKeyResult rewrapManyDataKey(Bson filter, RewrapManyDataKeyOptions options); + /** + * {@linkplain MongoDatabase#createCollection(String, CreateCollectionOptions) Create} a new collection with encrypted fields, + * automatically {@linkplain #createDataKey(String, DataKeyOptions) creating} + * new data encryption keys when needed based on the configured + * {@link CreateCollectionOptions#getEncryptedFields() encryptedFields}, which must be specified. + * This method does not modify the configured {@code encryptedFields} when creating new data keys, + * instead it creates a new configuration if needed. + * + * @param database The database to use for creating the collection. + * @param collectionName The name for the collection to create. + * @param createCollectionOptions Options for creating the collection. + * @param createEncryptedCollectionParams Auxiliary parameters for creating an encrypted collection. + * @return The (potentially updated) {@code encryptedFields} configuration that was used to create the collection. + * A user may use this document to configure {@link AutoEncryptionSettings#getEncryptedFieldsMap()}. + * @throws MongoUpdatedEncryptedFieldsException If an exception happens after creating at least one data key. + * This exception makes the updated {@code encryptedFields} + * {@linkplain MongoUpdatedEncryptedFieldsException#getEncryptedFields() available} to the caller. + * + * @since 4.9 + * @mongodb.server.release 6.0 + * @mongodb.driver.manual reference/command/create Create Command + */ + @Beta(Beta.Reason.SERVER) + BsonDocument createEncryptedCollection(MongoDatabase database, String collectionName, CreateCollectionOptions createCollectionOptions, + CreateEncryptedCollectionParams createEncryptedCollectionParams); + @Override void close(); } diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionAutoDataKeysTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionAutoDataKeysTest.java new file mode 100644 index 00000000000..a5781d2ef16 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionAutoDataKeysTest.java @@ -0,0 +1,248 @@ +/* + * Copyright 2008-present 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. + * + * + */ + +package com.mongodb.client; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoCommandException; +import com.mongodb.MongoConfigurationException; +import com.mongodb.MongoNamespace; +import com.mongodb.MongoWriteException; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Base64; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static com.mongodb.ClusterFixture.isStandalone; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.Fixture.getMongoClientSettings; +import static java.util.Collections.emptyMap; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.StreamSupport.stream; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.params.ParameterizedTest.DISPLAY_NAME_PLACEHOLDER; + +/** + * See + * 21. Automatic Data Encryption Keys. + */ +public abstract class AbstractClientSideEncryptionAutoDataKeysTest { + private static final String COLL_NAME = "testing1"; + private static final MongoNamespace KEY_VAULT_NAMESPACE = new MongoNamespace("keyvault", "datakeys"); + + private MongoClient client; + private MongoDatabase db; + private ClientEncryption clientEncryption; + + @BeforeEach + public void setUp() { + assumeTrue(serverVersionAtLeast(6, 0)); + assumeFalse(isStandalone()); + + client = createMongoClient(getMongoClientSettings()); + Set kmsProviders = KmsProvider.detect(); + clientEncryption = createClientEncryption(ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(getMongoClientSettings()) + .keyVaultNamespace(KEY_VAULT_NAMESPACE.getFullName()) + .kmsProviders(kmsProviders.stream().collect(toMap( + provider -> provider.name, provider -> emptyMap()))) + .kmsProviderPropertySuppliers(kmsProviders.stream().collect(toMap( + provider -> provider.name, provider -> provider.propertiesSupplier))) + .build()); + client.getDatabase(KEY_VAULT_NAMESPACE.getDatabaseName()).drop(); + db = client.getDatabase(AbstractClientSideEncryptionAutoDataKeysTest.class.getSimpleName()); + db.drop(); + } + + @AfterEach + @SuppressWarnings("try") + public void cleanUp() { + try (ClientEncryption ignored = clientEncryption; + MongoClient ignored1 = client) { + // empty + } + } + + /** + * See + * + * Case 1: Simple Creation and Validation. + */ + @ParameterizedTest(name = DISPLAY_NAME_PLACEHOLDER + " {0}") + @MethodSource("arguments") + void simpleCreationAndValidation(final KmsProvider kmsProvider) { + CreateCollectionOptions createCollectionOptions = new CreateCollectionOptions().encryptedFields(Document.parse( + "{" + + " fields: [{" + + " path: 'ssn'," + + " bsonType: 'string'," + + " keyId: null" + + " }]" + + "}")); + clientEncryption.createEncryptedCollection(db, COLL_NAME, createCollectionOptions, + kmsProvider.createEncryptedCollectionParamsSupplier.get()); + MongoCollection coll = db.getCollection(COLL_NAME); + assertEquals( + 121, // DocumentValidationFailure + assertThrows(MongoWriteException.class, () -> coll.insertOne(Document.parse("{ ssn: '123-45-6789' }"))) + .getCode()); + } + + /** + * See + * + * Case 2: Missing encryptedFields. + */ + @ParameterizedTest(name = DISPLAY_NAME_PLACEHOLDER + " {0}") + @MethodSource("arguments") + void missingEncryptedFields(final KmsProvider kmsProvider) { + assertThrows(MongoConfigurationException.class, () -> clientEncryption.createEncryptedCollection( + db, COLL_NAME, new CreateCollectionOptions(), kmsProvider.createEncryptedCollectionParamsSupplier.get())); + assertTrue(stream(db.listCollectionNames().spliterator(), false).noneMatch(name -> name.equals(COLL_NAME))); + } + + /** + * See + * + * Case 3: Invalid keyId. + */ + @ParameterizedTest(name = DISPLAY_NAME_PLACEHOLDER + " {0}") + @MethodSource("arguments") + void invalidKeyId(final KmsProvider kmsProvider) { + CreateCollectionOptions createCollectionOptions = new CreateCollectionOptions().encryptedFields(Document.parse( + "{" + + " fields: [{" + + " path: 'ssn'," + + " bsonType: 'string'," + + " keyId: false" + + " }]" + + "}")); + assertEquals( + 14, // TypeMismatch + assertThrows(MongoCommandException.class, () -> clientEncryption.createEncryptedCollection( + db, COLL_NAME, createCollectionOptions, kmsProvider.createEncryptedCollectionParamsSupplier.get())) + .getCode()); + } + + /** + * See + * + * Case 4: Insert encrypted value. + */ + @ParameterizedTest(name = DISPLAY_NAME_PLACEHOLDER + " {0}") + @MethodSource("arguments") + void insertEncryptedValue(final KmsProvider kmsProvider) { + CreateCollectionOptions createCollectionOptions = new CreateCollectionOptions().encryptedFields(Document.parse( + "{" + + " fields: [{" + + " path: 'ssn'," + + " bsonType: 'string'," + + " keyId: null" + + " }]" + + "}")); + BsonDocument encryptedFields = clientEncryption.createEncryptedCollection(db, COLL_NAME, createCollectionOptions, + kmsProvider.createEncryptedCollectionParamsSupplier.get()); + MongoCollection coll = db.getCollection(COLL_NAME); + BsonBinary dataKeyId = encryptedFields.getArray("fields").get(0).asDocument().getBinary("keyId"); + BsonBinary encryptedValue = clientEncryption.encrypt(new BsonString("123-45-6789"), + new EncryptOptions("Unindexed").keyId(dataKeyId)); + coll.insertOne(new Document("ssn", encryptedValue)); + } + + protected abstract MongoClient createMongoClient(MongoClientSettings settings); + + protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings); + + private static Stream arguments() { + return KmsProvider.detect().stream().map(Arguments::of); + } + + private enum KmsProvider { + LOCAL("local", + kmsProviderProperties -> kmsProviderProperties.put("key", Base64.getDecoder().decode( + "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZ" + + "GJkTXVyZG9uSjFk")), + createEncryptedCollectionParams -> {} + ), + AWS("aws", + kmsProviderProperties -> { + kmsProviderProperties.put("accessKeyId", System.getProperty("org.mongodb.test.awsAccessKeyId")); + kmsProviderProperties.put("secretAccessKey", System.getProperty("org.mongodb.test.awsSecretAccessKey")); + }, + createEncryptedCollectionParams -> createEncryptedCollectionParams.masterKey(BsonDocument.parse( + "{" + + " region: 'us-east-1'," + + " key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0'" + + "}")) + ); + + private final String name; + private final Supplier> propertiesSupplier; + private final Supplier createEncryptedCollectionParamsSupplier; + + private static Set detect() { + String awsAccessKeyId = System.getProperty("org.mongodb.test.awsAccessKeyId"); + return awsAccessKeyId != null && !awsAccessKeyId.isEmpty() + ? EnumSet.allOf(KmsProvider.class) + : EnumSet.of(KmsProvider.LOCAL); + } + + KmsProvider(final String name, final Consumer> propertiesUpdater, + final Consumer encryptedCollectionParamsUpdater) { + this.name = name; + this.propertiesSupplier = () -> { + Map result = new HashMap<>(); + propertiesUpdater.accept(result); + return result; + }; + this.createEncryptedCollectionParamsSupplier = () -> { + CreateEncryptedCollectionParams result = new CreateEncryptedCollectionParams(name); + encryptedCollectionParamsUpdater.accept(result); + return result; + }; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionAutoDataKeysTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionAutoDataKeysTest.java new file mode 100644 index 00000000000..3b0c378f73e --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionAutoDataKeysTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2008-present 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. + */ + +package com.mongodb.client; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; + +final class ClientSideEncryptionAutoDataKeysTest extends AbstractClientSideEncryptionAutoDataKeysTest { + @Override + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return MongoClients.create(settings); + } + + @Override + protected ClientEncryption createClientEncryption(final ClientEncryptionSettings settings) { + return ClientEncryptions.create(settings); + } +}