diff --git a/build.gradle b/build.gradle index 176b4422ea4..8c75eea018a 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ ext { nettyVersion = '4.1.17.Final' snappyVersion = '1.1.4' zstdVersion = '1.3.8-3' - mongoCryptVersion = '1.0.0' + mongoCryptVersion = '1.0.1' gitVersion = getGitVersion() } @@ -54,7 +54,7 @@ def javaCodeCheckedProjects = subprojects.findAll { !['util', 'mongo-java-driver configure(coreProjects) { evaluationDependsOn(':util') group = 'org.mongodb' - version = '3.12.0' + version = '3.12.1' repositories { mavenLocal() diff --git a/docs/reference/content/driver-async/tutorials/client-side-encryption.md b/docs/reference/content/driver-async/tutorials/client-side-encryption.md index 859bd676a92..01d6eeec075 100644 --- a/docs/reference/content/driver-async/tutorials/client-side-encryption.md +++ b/docs/reference/content/driver-async/tutorials/client-side-encryption.md @@ -33,7 +33,7 @@ See the [installation]({{< relref "driver-async/getting-started/installation.md" There is a separate jar file containing`libmongocrypt` bindings. -{{< install artifactId="mongodb-crypt" version="1.0.0">}} +{{< install artifactId="mongodb-crypt" version="1.0.1">}} ### mongocryptd configuration diff --git a/docs/reference/content/driver/tutorials/client-side-encryption.md b/docs/reference/content/driver/tutorials/client-side-encryption.md index b821e93ea04..f1683c4d91c 100644 --- a/docs/reference/content/driver/tutorials/client-side-encryption.md +++ b/docs/reference/content/driver/tutorials/client-side-encryption.md @@ -29,14 +29,14 @@ See the [installation]({{< relref "driver/getting-started/installation.md" >}}) There is a separate jar file containing`libmongocrypt` bindings. -{{< install artifactId="mongodb-crypt" version="1.0.0">}} +{{< install artifactId="mongodb-crypt" version="1.0.1">}} ### mongocryptd configuration `libmongocrypt` requires the `mongocryptd` daemon / process to be running. A specific daemon / process uri can be configured in the `AutoEncryptionSettings` class by setting `mongocryptdURI` in the `extraOptions`. -More information about mongocryptd will soon be available from the official documentation. +For more information about mongocryptd see the [official documentation](https://docs.mongodb.com/manual/core/security-client-side-encryption/). ### Examples @@ -157,4 +157,95 @@ AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() }}).build(); ``` -**Coming soon:** An example using the community version and demonstrating explicit encryption/decryption. +#### Explicit Encryption and Decryption +Explicit encryption and decryption is a **MongoDB community** feature and does not use the `mongocryptd` process. Explicit encryption is +provided by the `ClientEncryption` class. +The full code snippet can be found in [`ClientSideEncryptionExplicitEncryptionAndDecryptionTour.java`]({{< srcref "driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionAndDecryptionTour.java">}}): + +``` +// This would have to be the same master key as was used to create the encryption key +final byte[] localMasterKey = new byte[96]; +new SecureRandom().nextBytes(localMasterKey); + +Map> kmsProviders = new HashMap>() {{ + put("local", new HashMap() {{ + put("key", localMasterKey); + }}); +}}; + +MongoClientSettings clientSettings = MongoClientSettings.builder().build(); +MongoClient mongoClient = MongoClients.create(clientSettings); + +// Set up the key vault for this example +MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); +MongoCollection keyVaultCollection = mongoClient + .getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); +keyVaultCollection.drop(); + +// Ensure that two data keys cannot share the same keyAltName. +keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true) + .partialFilterExpression(Filters.exists("keyAltNames"))); + +MongoCollection collection = mongoClient.getDatabase("test").getCollection("coll"); +collection.drop(); // Clear old data + +// Create the ClientEncryption instance +ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(MongoClientSettings.builder() + .applyConnectionString(new ConnectionString("mongodb://localhost")) + .build()) + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build(); + +ClientEncryption clientEncryption = ClientEncryptions.create(clientEncryptionSettings); + +BsonBinary dataKeyId = clientEncryption.createDataKey("local", new DataKeyOptions()); + +// Explicitly encrypt a field +BsonBinary encryptedFieldValue = clientEncryption.encrypt(new BsonString("123456789"), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(dataKeyId)); + +collection.insertOne(new Document("encryptedField", encryptedFieldValue)); + +Document doc = collection.find().first(); +System.out.println(doc.toJson()); + +// Explicitly decrypt the field +BsonString decryptedFieldValue = clientEncryption.decrypt( + new BsonBinary(doc.get("encryptedField", Binary.class).getData())).asString(); +System.out.println(decryptedFieldValue.getValue()); +``` + +#### Explicit Encryption and Auto Decryption + +Although automatic encryption requires MongoDB 4.2 enterprise or a MongoDB 4.2 Atlas cluster, automatic decryption is supported for all +users. To configure automatic decryption without automatic encryption set `bypassAutoEncryption(true)`. The full code snippet can be found in [`ClientSideEncryptionExplicitEncryptionOnlyTour.java`]({{< srcref "driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java">}}): + +``` +... +MongoClientSettings clientSettings = MongoClientSettings.builder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassAutoEncryption(true) + .build()) + .build(); +MongoClient mongoClient = MongoClients.create(clientSettings); + +... + +// Explicitly encrypt a field +BsonBinary encryptedFieldValue = clientEncryption.encrypt(new BsonString("123456789"), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(dataKeyId)); + +collection.insertOne(new Document("encryptedField", encryptedFieldValue)); + +// Automatically decrypts the encrypted field. +Document doc = collection.find().first(); +System.out.println(doc.toJson()); +System.out.println(doc.get("encryptedField")); + +``` \ No newline at end of file diff --git a/driver-async/src/main/com/mongodb/async/client/internal/CommandMarker.java b/driver-async/src/main/com/mongodb/async/client/internal/CommandMarker.java index 01a6cbd50fc..34bf58f59ea 100644 --- a/driver-async/src/main/com/mongodb/async/client/internal/CommandMarker.java +++ b/driver-async/src/main/com/mongodb/async/client/internal/CommandMarker.java @@ -33,20 +33,19 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.capi.MongoCryptOptionsHelper.createMongocryptdSpawnArgs; @SuppressWarnings("UseOfProcessBuilder") class CommandMarker implements Closeable { - private MongoClient client; + private final MongoClient client; private final ProcessBuilder processBuilder; - CommandMarker(final Map options) { - String connectionString; - - if (options.containsKey("mongocryptdURI")) { - connectionString = (String) options.get("mongocryptdURI"); - } else { - connectionString = "mongodb://localhost:27020"; + CommandMarker(final boolean isBypassAutoEncryption, final Map options) { + if (isBypassAutoEncryption) { + processBuilder = null; + client = null; + return; } if (!options.containsKey("mongocryptdBypassSpawn") || !((Boolean) options.get("mongocryptdBypassSpawn"))) { @@ -56,6 +55,14 @@ class CommandMarker implements Closeable { processBuilder = null; } + String connectionString; + + if (options.containsKey("mongocryptdURI")) { + connectionString = (String) options.get("mongocryptdURI"); + } else { + connectionString = "mongodb://localhost:27020"; + } + client = MongoClients.create(MongoClientSettings.builder() .applyConnectionString(new ConnectionString(connectionString)) .applyToClusterSettings(new Block() { @@ -68,6 +75,8 @@ public void apply(final ClusterSettings.Builder builder) { } void mark(final String databaseName, final RawBsonDocument command, final SingleResultCallback callback) { + notNull("client", client, callback); + final SingleResultCallback wrappedCallback = new SingleResultCallback() { @Override public void onResult(final RawBsonDocument result, final Throwable t) { @@ -103,7 +112,9 @@ public void onResult(final Void result, final Throwable t) { @Override public void close() { - client.close(); + if (client != null) { + client.close(); + } } private void runCommand(final String databaseName, final RawBsonDocument command, diff --git a/driver-async/src/main/com/mongodb/async/client/internal/Crypts.java b/driver-async/src/main/com/mongodb/async/client/internal/Crypts.java index 65ee594f36a..06f5bade1e1 100644 --- a/driver-async/src/main/com/mongodb/async/client/internal/Crypts.java +++ b/driver-async/src/main/com/mongodb/async/client/internal/Crypts.java @@ -35,7 +35,7 @@ public final class Crypts { public static Crypt createCrypt(final MongoClient client, final AutoEncryptionSettings options) { return new Crypt(MongoCrypts.create(createMongoCryptOptions(options.getKmsProviders(), options.getSchemaMap())), new CollectionInfoRetriever(client), - new CommandMarker(options.getExtraOptions()), + new CommandMarker(options.isBypassAutoEncryption(), options.getExtraOptions()), createKeyRetriever(client, options.getKeyVaultMongoClientSettings(), options.getKeyVaultNamespace()), createKeyManagementService(), options.isBypassAutoEncryption()); diff --git a/driver-async/src/test/functional/com/mongodb/async/client/ClientSideEncryptionBypassAutoEncryptionTest.java b/driver-async/src/test/functional/com/mongodb/async/client/ClientSideEncryptionBypassAutoEncryptionTest.java new file mode 100644 index 00000000000..be712a1d35a --- /dev/null +++ b/driver-async/src/test/functional/com/mongodb/async/client/ClientSideEncryptionBypassAutoEncryptionTest.java @@ -0,0 +1,124 @@ +/* + * 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.async.client; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.async.FutureResultCallback; +import com.mongodb.async.client.vault.ClientEncryption; +import com.mongodb.async.client.vault.ClientEncryptions; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import org.bson.BsonBinary; +import org.bson.BsonString; +import org.bson.Document; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import static com.mongodb.ClusterFixture.isNotAtLeastJava8; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.async.client.Fixture.getDefaultDatabaseName; +import static com.mongodb.async.client.Fixture.getMongoClient; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +public class ClientSideEncryptionBypassAutoEncryptionTest { + private MongoClient clientEncrypted; + private ClientEncryption clientEncryption; + + @Before + public void setUp() { + assumeFalse(isNotAtLeastJava8()); + assumeTrue(serverVersionAtLeast(4, 1)); + + MongoClient mongoClient = getMongoClient(); + + final byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + + Map> kmsProviders = new HashMap>() {{ + put("local", new HashMap() {{ + put("key", localMasterKey); + }}); + }}; + + + MongoNamespace keyVaultNamespace = new MongoNamespace(Fixture.getDefaultDatabaseName(), "testKeyVault"); + + Fixture.dropDatabase(Fixture.getDefaultDatabaseName()); + + ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(Fixture.getMongoClientSettings()) + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build(); + + clientEncryption = ClientEncryptions.create(clientEncryptionSettings); + + AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassAutoEncryption(true) + .build(); + + MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder() + .autoEncryptionSettings(autoEncryptionSettings) + .build(); + clientEncrypted = MongoClients.create(clientSettings); + } + + @Test + public void shouldAutoDecryptManuallyEncryptedData() { + String fieldValue = "123456789"; + + FutureResultCallback binaryCallback = new FutureResultCallback(); + clientEncryption.createDataKey("local", new DataKeyOptions(), binaryCallback); + BsonBinary dataKeyId = binaryCallback.get(); + + binaryCallback = new FutureResultCallback(); + clientEncryption.encrypt(new BsonString(fieldValue), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(dataKeyId), binaryCallback); + BsonBinary encryptedFieldValue = binaryCallback.get(); + + MongoCollection collection = clientEncrypted.getDatabase(Fixture.getDefaultDatabaseName()).getCollection("test"); + + FutureResultCallback insertCallback = new FutureResultCallback(); + collection.insertOne(new Document("encryptedField", encryptedFieldValue), insertCallback); + insertCallback.get(); + + FutureResultCallback resultCallback = new FutureResultCallback(); + collection.find().first(resultCallback); + + assertEquals(fieldValue, resultCallback.get().getString("encryptedField")); + } + + @After + public void after() { + if (clientEncrypted != null) { + Fixture.dropDatabase(getDefaultDatabaseName()); + clientEncrypted.close(); + } + } +} diff --git a/driver-async/src/test/functional/com/mongodb/async/client/ClientSideEncryptionMongocryptdSpawnBypassTest.java b/driver-async/src/test/functional/com/mongodb/async/client/ClientSideEncryptionMongocryptdSpawnBypassTest.java new file mode 100644 index 00000000000..9218c3efd98 --- /dev/null +++ b/driver-async/src/test/functional/com/mongodb/async/client/ClientSideEncryptionMongocryptdSpawnBypassTest.java @@ -0,0 +1,117 @@ +/* + * 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.async.client; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.async.FutureResultCallback; +import org.bson.Document; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import static com.mongodb.ClusterFixture.isNotAtLeastJava8; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertFalse; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + + +public class ClientSideEncryptionMongocryptdSpawnBypassTest extends DatabaseTestCase { + private final File pidFile; + private final Map> kmsProviders; + private final MongoNamespace keyVaultNamespace = new MongoNamespace("admin.datakeys"); + + public ClientSideEncryptionMongocryptdSpawnBypassTest() throws IOException { + assumeFalse(isNotAtLeastJava8()); + assumeTrue(serverVersionAtLeast(4, 2)); + + pidFile = new File("bypass-spawning-mongocryptd.pid"); + + byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + + Map keyMap = new HashMap(); + keyMap.put("key", localMasterKey); + kmsProviders = new HashMap>(); + kmsProviders.put("local", keyMap); + } + + + @Test + public void shouldNotSpawnWhenMongocryptdBypassSpawnIsTrue() { + assumeTrue(serverVersionAtLeast(4, 1)); + Map extraOptions = new HashMap(); + extraOptions.put("mongocryptdBypassSpawn", true); + extraOptions.put("mongocryptdSpawnArgs", asList("--pidfilepath=" + pidFile.getAbsolutePath(), "--port=27099")); + + AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .extraOptions(extraOptions) + .build(); + + MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder() + .autoEncryptionSettings(autoEncryptionSettings) + .build(); + MongoClient clientEncrypted = MongoClients.create(clientSettings); + try { + FutureResultCallback pingCallback = new FutureResultCallback(); + clientEncrypted.getDatabase("admin").runCommand(new Document("ping", 1), pingCallback); + pingCallback.get(); + + assertFalse(pidFile.exists()); + } finally { + clientEncrypted.close(); + } + } + + @Test + public void shouldNotSpawnWhenBypassAutoEncryptionIsTrue() { + assumeTrue(serverVersionAtLeast(4, 1)); + Map extraOptions = new HashMap(); + extraOptions.put("mongocryptdSpawnArgs", asList("--pidfilepath=" + pidFile.getAbsolutePath(), "--port=27099")); + + AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .extraOptions(extraOptions) + .bypassAutoEncryption(true) + .build(); + + MongoClientSettings clientSettings = Fixture.getMongoClientSettingsBuilder() + .autoEncryptionSettings(autoEncryptionSettings) + .build(); + MongoClient clientEncrypted = MongoClients.create(clientSettings); + + try { + FutureResultCallback pingCallback = new FutureResultCallback(); + clientEncrypted.getDatabase("admin").runCommand(new Document("ping", 1), pingCallback); + pingCallback.get(); + + assertFalse(pidFile.exists()); + } finally { + clientEncrypted.close(); + } + } +} diff --git a/driver-async/src/test/functional/com/mongodb/async/client/Fixture.java b/driver-async/src/test/functional/com/mongodb/async/client/Fixture.java index 82530478a19..09bdc6a59bb 100644 --- a/driver-async/src/test/functional/com/mongodb/async/client/Fixture.java +++ b/driver-async/src/test/functional/com/mongodb/async/client/Fixture.java @@ -59,6 +59,10 @@ public static synchronized MongoClient getMongoClient() { return mongoClient; } + public static MongoClientSettings.Builder getMongoClientSettingsBuilder() { + return getMongoClientBuilderFromConnectionString(); + } + public static MongoClientSettings getMongoClientSettings() { return getMongoClientBuilderFromConnectionString().build(); } diff --git a/driver-core/build.gradle b/driver-core/build.gradle index b96839cc943..06f3d50dad3 100644 --- a/driver-core/build.gradle +++ b/driver-core/build.gradle @@ -33,6 +33,9 @@ clirr { failOnErrors = false } +// Add native-image.properties +sourceSets.main.resources.srcDirs = ['src/resources'] + dependencies { compile project(':bson') @@ -62,19 +65,17 @@ jar { instruction 'Import-Package', 'org.bson.*', // unfortunate that this is necessary, but if it's left out then it's not included 'javax.crypto.*', - 'javax.crypto.spec.*', 'javax.management.*', 'javax.naming.*', - 'javax.naming.directory.*', 'javax.net.*', - 'javax.net.ssl.*', 'javax.security.sasl.*', 'javax.security.auth.callback.*', 'org.ietf.jgss.*', 'io.netty.*;resolution:=optional', 'org.xerial.snappy.*;resolution:=optional', 'com.github.luben.zstd.*;resolution:=optional', - 'org.slf4j;resolution:=optional', - 'jnr.unixsocket;resolution:=optional' + 'org.slf4j.*;resolution:=optional', + 'jnr.unixsocket.*;resolution:=optional', + """com.mongodb.crypt.capi.*;resolution:=optional;version='$mongoCryptVersion'""" } } diff --git a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java index 31511841fa3..611f12a0f90 100644 --- a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java +++ b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java @@ -17,7 +17,6 @@ package com.mongodb.connection; import com.mongodb.ConnectionString; -import com.mongodb.MongoClientException; import com.mongodb.MongoClientSettings; import com.mongodb.ServerAddress; import com.mongodb.annotations.Immutable; @@ -143,8 +142,17 @@ public Builder description(final String description) { /** * Sets the host name to use in order to look up an SRV DNS record to find the MongoDB hosts. * + *

+ * Note that when setting srvHost via {@code ClusterSettings.Builder}, the driver will NOT process any associated TXT records + * associated with the host. In order to enable the processing of TXT records while still using {@code MongoClientSettings}, + * specify the SRV host via connection string and apply the connection string to the settings, e.g. + * {@code MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb+srv://host1.acme.com")) }. + *

+ * * @param srvHost the SRV host name * @return this + * @see com.mongodb.MongoClientSettings.Builder#applyConnectionString(ConnectionString) + * @see ClusterSettings.Builder#applyConnectionString(ConnectionString) */ public Builder srvHost(final String srvHost) { if (this.hosts != DEFAULT_HOSTS) { @@ -596,14 +604,13 @@ public String getShortDescription() { } private ClusterSettings(final Builder builder) { - // TODO: Unit test this if (builder.srvHost != null) { if (builder.srvHost.contains(":")) { throw new IllegalArgumentException("The srvHost can not contain a host name that specifies a port"); } - if (builder.hosts.get(0).getHost().split("\\.").length < 3) { - throw new MongoClientException(format("An SRV host name '%s' was provided that does not contain at least three parts. " + if (builder.srvHost.split("\\.").length < 3) { + throw new IllegalArgumentException(format("An SRV host name '%s' was provided that does not contain at least three parts. " + "It must contain a hostname, domain name and a top level domain.", builder.hosts.get(0).getHost())); } } @@ -612,10 +619,6 @@ private ClusterSettings(final Builder builder) { throw new IllegalArgumentException("Multiple hosts cannot be specified when using ClusterType.STANDALONE."); } - if (builder.mode != null && builder.mode == ClusterConnectionMode.SINGLE && builder.hosts.size() > 1) { - throw new IllegalArgumentException("Can not directly connect to more than one server"); - } - if (builder.requiredReplicaSetName != null) { if (builder.requiredClusterType == ClusterType.UNKNOWN) { builder.requiredClusterType = ClusterType.REPLICA_SET; @@ -628,7 +631,18 @@ private ClusterSettings(final Builder builder) { description = builder.description; srvHost = builder.srvHost; hosts = builder.hosts; - mode = builder.mode != null ? builder.mode : hosts.size() == 1 ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE; + if (srvHost != null) { + if (builder.mode == ClusterConnectionMode.SINGLE) { + throw new IllegalArgumentException("An SRV host name was provided but the connection mode is not MULTIPLE"); + } + mode = ClusterConnectionMode.MULTIPLE; + } else { + if (builder.mode == ClusterConnectionMode.SINGLE && builder.hosts.size() > 1) { + throw new IllegalArgumentException("Can not directly connect to more than one server"); + } + + mode = builder.mode != null ? builder.mode : hosts.size() == 1 ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE; + } requiredReplicaSetName = builder.requiredReplicaSetName; requiredClusterType = builder.requiredClusterType; localThresholdMS = builder.localThresholdMS; diff --git a/driver-core/src/main/com/mongodb/internal/connection/SaslAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/SaslAuthenticator.java index a0c04640aa9..778b77254c7 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SaslAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SaslAuthenticator.java @@ -66,6 +66,14 @@ public Void run() { res = sendSaslContinue(conversationId, response, connection); } + if (!saslClient.isComplete()) { + saslClient.evaluateChallenge((res.getBinary("payload")).getData()); + if (!saslClient.isComplete()) { + throw new MongoSecurityException(getMongoCredential(), + "SASL protocol error: server completed challenges before client completed responses " + + getMongoCredential()); + } + } } catch (Exception e) { throw wrapException(e); } finally { @@ -93,7 +101,7 @@ public void onResult(final BsonDocument result, final Throwable t) { if (t != null) { callback.onResult(null, wrapException(t)); } else if (result.getBoolean("done").getValue()) { - callback.onResult(null, null); + verifySaslClientComplete(saslClient, result, callback); } else { new Continuator(saslClient, result, connection, callback).start(); } @@ -121,6 +129,26 @@ private void throwIfSaslClientIsNull(final SaslClient saslClient) { } } + private void verifySaslClientComplete(final SaslClient saslClient, final BsonDocument result, + final SingleResultCallback callback) { + if (saslClient.isComplete()) { + callback.onResult(null, null); + } else { + try { + saslClient.evaluateChallenge(result.getBinary("payload").getData()); + if (saslClient.isComplete()) { + callback.onResult(null, null); + } else { + callback.onResult(null, new MongoSecurityException(getMongoCredential(), + "SASL protocol error: server completed challenges before client completed responses " + + getMongoCredential())); + } + } catch (SaslException e) { + callback.onResult(null, wrapException(e)); + } + } + } + @Nullable private Subject getSubject() { return getMongoCredential().getMechanismProperty(JAVA_SUBJECT_KEY, null); @@ -202,7 +230,7 @@ public void onResult(final BsonDocument result, final Throwable t) { callback.onResult(null, wrapException(t)); disposeOfSaslClient(saslClient); } else if (result.getBoolean("done").getValue()) { - callback.onResult(null, null); + verifySaslClientComplete(saslClient, result, callback); disposeOfSaslClient(saslClient); } else { continueConversation(result); diff --git a/driver-core/src/main/com/mongodb/internal/connection/tlschannel/async/AsynchronousTlsChannelGroup.java b/driver-core/src/main/com/mongodb/internal/connection/tlschannel/async/AsynchronousTlsChannelGroup.java index 356d47aaf6b..70c47668e11 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/tlschannel/async/AsynchronousTlsChannelGroup.java +++ b/driver-core/src/main/com/mongodb/internal/connection/tlschannel/async/AsynchronousTlsChannelGroup.java @@ -442,7 +442,12 @@ private void processPendingInterests() { RegisteredSocket socket = (RegisteredSocket) key.attachment(); int pending = socket.pendingOps.getAndSet(0); if (pending != 0) { - key.interestOps(key.interestOps() | pending); + try { + key.interestOps(key.interestOps() | pending); + } catch (CancelledKeyException e) { + // can happen when channels are closed with pending operations + break; + } } } } diff --git a/driver-core/src/main/com/mongodb/operation/AsyncChangeStreamBatchCursor.java b/driver-core/src/main/com/mongodb/operation/AsyncChangeStreamBatchCursor.java index b1023babebf..383e3557e3c 100644 --- a/driver-core/src/main/com/mongodb/operation/AsyncChangeStreamBatchCursor.java +++ b/driver-core/src/main/com/mongodb/operation/AsyncChangeStreamBatchCursor.java @@ -17,6 +17,7 @@ package com.mongodb.operation; import com.mongodb.MongoChangeStreamException; +import com.mongodb.MongoException; import com.mongodb.async.AsyncAggregateResponseBatchCursor; import com.mongodb.async.AsyncBatchCursor; import com.mongodb.async.SingleResultCallback; @@ -34,6 +35,7 @@ import static com.mongodb.operation.ChangeStreamBatchCursorHelper.isRetryableError; import static com.mongodb.operation.OperationHelper.LOGGER; import static com.mongodb.operation.OperationHelper.withAsyncReadConnection; +import static java.lang.String.format; final class AsyncChangeStreamBatchCursor implements AsyncAggregateResponseBatchCursor { private final AsyncReadBinding binding; @@ -42,6 +44,12 @@ final class AsyncChangeStreamBatchCursor implements AsyncAggregateResponseBat private volatile BsonDocument resumeToken; private volatile AsyncAggregateResponseBatchCursor wrapped; + /* protected by `this` */ + private boolean isClosed = false; + private boolean isOperationInProgress = false; + private boolean isClosePending = false; + /* protected by `this` */ + AsyncChangeStreamBatchCursor(final ChangeStreamOperation changeStreamOperation, final AsyncAggregateResponseBatchCursor wrapped, final AsyncReadBinding binding, @@ -66,7 +74,7 @@ public void apply(final AsyncAggregateResponseBatchCursor curso cursor.next(callback); cachePostBatchResumeToken(cursor); } - }, convertResultsCallback(callback)); + }, convertResultsCallback(callback), false); } @Override @@ -78,12 +86,24 @@ public void apply(final AsyncAggregateResponseBatchCursor curso cursor.tryNext(callback); cachePostBatchResumeToken(cursor); } - }, convertResultsCallback(callback)); + }, convertResultsCallback(callback), true); } @Override public void close() { - if (!isClosed()) { + boolean closeCursor = false; + + synchronized (this) { + if (isOperationInProgress) { + isClosePending = true; + } else { + closeCursor = !isClosed; + isClosed = true; + isClosePending = false; + } + } + + if (closeCursor) { wrapped.close(); binding.release(); } @@ -101,7 +121,9 @@ public int getBatchSize() { @Override public boolean isClosed() { - return wrapped.isClosed(); + synchronized (this) { + return isClosed; + } } @Override @@ -125,6 +147,17 @@ private void cachePostBatchResumeToken(final AsyncAggregateResponseBatchCursor> convertResultsCallback(final SingleResultCallback> callback) { return errorHandlingCallback(new SingleResultCallback>() { @Override @@ -155,23 +188,35 @@ private interface AsyncBlock { void apply(AsyncAggregateResponseBatchCursor cursor, SingleResultCallback> callback); } - private void resumeableOperation(final AsyncBlock asyncBlock, final SingleResultCallback> callback) { + private void resumeableOperation(final AsyncBlock asyncBlock, final SingleResultCallback> callback, + final boolean tryNext) { + synchronized (this) { + if (isClosed) { + callback.onResult(null, new MongoException(format("%s called after the cursor was closed.", + tryNext ? "tryNext()" : "next()"))); + return; + } + isOperationInProgress = true; + } asyncBlock.apply(wrapped, new SingleResultCallback>() { @Override public void onResult(final List result, final Throwable t) { if (t == null) { + endOperationInProgress(); callback.onResult(result, null); } else if (isRetryableError(t)) { wrapped.close(); - retryOperation(asyncBlock, callback); + retryOperation(asyncBlock, callback, tryNext); } else { + endOperationInProgress(); callback.onResult(null, t); } } }); } - private void retryOperation(final AsyncBlock asyncBlock, final SingleResultCallback> callback) { + private void retryOperation(final AsyncBlock asyncBlock, final SingleResultCallback> callback, + final boolean tryNext) { withAsyncReadConnection(binding, new AsyncCallableWithSource() { @Override public void call(final AsyncConnectionSource source, final Throwable t) { @@ -188,7 +233,7 @@ public void onResult(final AsyncBatchCursor result, final Throwable t) { } else { wrapped = ((AsyncChangeStreamBatchCursor) result).getWrapped(); binding.release(); // release the new change stream batch cursor's reference to the binding - resumeableOperation(asyncBlock, callback); + resumeableOperation(asyncBlock, callback, tryNext); } } }); diff --git a/driver-core/src/main/com/mongodb/operation/AsyncQueryBatchCursor.java b/driver-core/src/main/com/mongodb/operation/AsyncQueryBatchCursor.java index ab52b5cdf76..3ff985f0470 100644 --- a/driver-core/src/main/com/mongodb/operation/AsyncQueryBatchCursor.java +++ b/driver-core/src/main/com/mongodb/operation/AsyncQueryBatchCursor.java @@ -38,7 +38,6 @@ import org.bson.codecs.Decoder; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -65,7 +64,6 @@ class AsyncQueryBatchCursor implements AsyncAggregateResponseBatchCursor { private final Decoder decoder; private final long maxTimeMS; private final AsyncConnectionSource connectionSource; - private final AtomicBoolean isClosed = new AtomicBoolean(); private final AtomicReference cursor; private volatile QueryResult firstBatch; private volatile int batchSize; @@ -74,6 +72,12 @@ class AsyncQueryBatchCursor implements AsyncAggregateResponseBatchCursor { private volatile BsonTimestamp operationTime; private volatile boolean firstBatchEmpty; + /* protected by `this` */ + private boolean isOperationInProgress = false; + private boolean isClosed = false; + private boolean isClosePending = false; + /* protected by `this` */ + AsyncQueryBatchCursor(final QueryResult firstBatch, final int limit, final int batchSize, final long maxTimeMS, final Decoder decoder, final AsyncConnectionSource connectionSource, final AsyncConnection connection) { this(firstBatch, limit, batchSize, maxTimeMS, decoder, connectionSource, connection, null); @@ -108,7 +112,19 @@ class AsyncQueryBatchCursor implements AsyncAggregateResponseBatchCursor { @Override public void close() { - if (!isClosed.getAndSet(true)) { + boolean killCursor = false; + + synchronized (this) { + if (isOperationInProgress) { + isClosePending = true; + } else { + killCursor = !isClosed; + isClosed = true; + isClosePending = false; + } + } + + if (killCursor) { killCursorOnClose(); } } @@ -125,19 +141,25 @@ public void tryNext(final SingleResultCallback> callback) { @Override public void setBatchSize(final int batchSize) { - isTrue("open", !isClosed.get()); + synchronized (this) { + isTrue("open", !isClosed); + } this.batchSize = batchSize; } @Override public int getBatchSize() { - isTrue("open", !isClosed.get()); + synchronized (this) { + isTrue("open", !isClosed); + } return batchSize; } @Override public boolean isClosed() { - return isClosed.get(); + synchronized (this) { + return isClosed; + } } @Override @@ -170,9 +192,19 @@ private void next(final SingleResultCallback> callback, final boolean tr } else { ServerCursor localCursor = getServerCursor(); if (localCursor == null) { - isClosed.set(true); + synchronized (this) { + isClosed = true; + } callback.onResult(null, null); } else { + synchronized (this) { + if (isClosed) { + callback.onResult(null, new MongoException(format("%s called after the cursor was closed.", + tryNext ? "tryNext()" : "next()"))); + return; + } + isOperationInProgress = true; + } getMore(localCursor, callback, tryNext); } } @@ -187,6 +219,7 @@ private void getMore(final ServerCursor cursor, final SingleResultCallback> callback, final QueryResult result, final boolean tryNext) { - if (isClosed()) { - connection.release(); - callback.onResult(null, new MongoException(format("The cursor was closed before %s completed.", - tryNext ? "tryNext()" : "next()"))); - return; - } - - cursor.getAndSet(result.getCursor()); + cursor.set(result.getCursor()); if (!tryNext && result.getResults().isEmpty() && result.getCursor() != null) { getMore(connection, result.getCursor(), callback, false); } else { @@ -298,6 +334,7 @@ private void handleGetMoreQueryResult(final AsyncConnection connection, final Si connectionSource.release(); } } + endOperationInProgress(); if (result.getResults().isEmpty()) { callback.onResult(null, null); @@ -328,6 +365,7 @@ public void onResult(final BsonDocument result, final Throwable t) { ? translateCommandException((MongoCommandException) t, cursor) : t; connection.release(); + endOperationInProgress(); callback.onResult(null, translatedException); } else { QueryResult queryResult = getMoreCursorDocumentToQueryResult(result.getDocument(CURSOR), @@ -354,6 +392,7 @@ private class QueryResultSingleResultCallback implements SingleResultCallback result, final Throwable t) { if (t != null) { connection.release(); + endOperationInProgress(); callback.onResult(null, t); } else { handleGetMoreQueryResult(connection, callback, result, tryNext); diff --git a/driver-core/src/main/com/mongodb/operation/MapReduceHelper.java b/driver-core/src/main/com/mongodb/operation/MapReduceHelper.java index bfaf8c217e0..0dfeab35cbb 100644 --- a/driver-core/src/main/com/mongodb/operation/MapReduceHelper.java +++ b/driver-core/src/main/com/mongodb/operation/MapReduceHelper.java @@ -17,28 +17,29 @@ package com.mongodb.operation; import org.bson.BsonDocument; +import org.bson.BsonInt32; final class MapReduceHelper { static MapReduceStatistics createStatistics(final BsonDocument result) { return new MapReduceStatistics(getInputCount(result), getOutputCount(result), getEmitCount(result), - getDuration(result)); + getDuration(result)); } private static int getInputCount(final BsonDocument result) { - return result.getDocument("counts").getNumber("input").intValue(); + return result.getDocument("counts", new BsonDocument()).getNumber("input", new BsonInt32(0)).intValue(); } private static int getOutputCount(final BsonDocument result) { - return result.getDocument("counts").getNumber("output").intValue(); + return result.getDocument("counts", new BsonDocument()).getNumber("output", new BsonInt32(0)).intValue(); } private static int getEmitCount(final BsonDocument result) { - return result.getDocument("counts").getNumber("emit").intValue(); + return result.getDocument("counts", new BsonDocument()).getNumber("emit", new BsonInt32(0)).intValue(); } private static int getDuration(final BsonDocument result) { - return result.getNumber("timeMillis").intValue(); + return result.getNumber("timeMillis", new BsonInt32(0)).intValue(); } private MapReduceHelper() { diff --git a/driver-core/src/main/com/mongodb/operation/MapReduceToCollectionOperation.java b/driver-core/src/main/com/mongodb/operation/MapReduceToCollectionOperation.java index 5979fdc2894..2ce23d002b6 100644 --- a/driver-core/src/main/com/mongodb/operation/MapReduceToCollectionOperation.java +++ b/driver-core/src/main/com/mongodb/operation/MapReduceToCollectionOperation.java @@ -42,20 +42,21 @@ import static com.mongodb.assertions.Assertions.isTrue; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback; +import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotTwo; +import static com.mongodb.internal.operation.WriteConcernHelper.appendWriteConcernToCommand; +import static com.mongodb.internal.operation.WriteConcernHelper.throwOnWriteConcernError; import static com.mongodb.operation.CommandOperationHelper.executeCommand; import static com.mongodb.operation.CommandOperationHelper.executeCommandAsync; +import static com.mongodb.operation.DocumentHelper.putIfNotNull; import static com.mongodb.operation.DocumentHelper.putIfNotZero; import static com.mongodb.operation.DocumentHelper.putIfTrue; import static com.mongodb.operation.OperationHelper.AsyncCallableWithConnection; +import static com.mongodb.operation.OperationHelper.CallableWithConnection; import static com.mongodb.operation.OperationHelper.LOGGER; -import static com.mongodb.operation.OperationHelper.validateCollation; import static com.mongodb.operation.OperationHelper.releasingCallback; -import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotTwo; -import static com.mongodb.operation.OperationHelper.CallableWithConnection; -import static com.mongodb.operation.OperationHelper.withConnection; +import static com.mongodb.operation.OperationHelper.validateCollation; import static com.mongodb.operation.OperationHelper.withAsyncConnection; -import static com.mongodb.internal.operation.WriteConcernHelper.appendWriteConcernToCommand; -import static com.mongodb.internal.operation.WriteConcernHelper.throwOnWriteConcernError; +import static com.mongodb.operation.OperationHelper.withConnection; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -595,20 +596,21 @@ public MapReduceStatistics apply(final BsonDocument result, final AsyncConnectio private BsonDocument getCommand(final ConnectionDescription description) { BsonDocument outputDocument = new BsonDocument(getAction(), new BsonString(getCollectionName())); - outputDocument.append("sharded", BsonBoolean.valueOf(isSharded())); - outputDocument.append("nonAtomic", BsonBoolean.valueOf(isNonAtomic())); + putIfTrue(outputDocument, "sharded", isSharded()); + putIfTrue(outputDocument, "nonAtomic", isNonAtomic()); if (getDatabaseName() != null) { outputDocument.put("db", new BsonString(getDatabaseName())); } BsonDocument commandDocument = new BsonDocument("mapreduce", new BsonString(namespace.getCollectionName())) .append("map", getMapFunction()) .append("reduce", getReduceFunction()) - .append("out", outputDocument) - .append("query", asValueOrNull(getFilter())) - .append("sort", asValueOrNull(getSort())) - .append("finalize", asValueOrNull(getFinalizeFunction())) - .append("scope", asValueOrNull(getScope())) - .append("verbose", BsonBoolean.valueOf(isVerbose())); + .append("out", outputDocument); + + putIfNotNull(commandDocument, "query", getFilter()); + putIfNotNull(commandDocument, "sort", getSort()); + putIfNotNull(commandDocument, "finalize", getFinalizeFunction()); + putIfNotNull(commandDocument, "scope", getScope()); + putIfTrue(commandDocument, "verbose", isVerbose()); putIfNotZero(commandDocument, "limit", getLimit()); putIfNotZero(commandDocument, "maxTimeMS", getMaxTime(MILLISECONDS)); putIfTrue(commandDocument, "jsMode", isJsMode()); diff --git a/driver-core/src/main/com/mongodb/operation/MapReduceWithInlineResultsOperation.java b/driver-core/src/main/com/mongodb/operation/MapReduceWithInlineResultsOperation.java index b953f47b2ed..953dde87c7b 100644 --- a/driver-core/src/main/com/mongodb/operation/MapReduceWithInlineResultsOperation.java +++ b/driver-core/src/main/com/mongodb/operation/MapReduceWithInlineResultsOperation.java @@ -33,7 +33,6 @@ import com.mongodb.operation.CommandOperationHelper.CommandReadTransformer; import com.mongodb.operation.CommandOperationHelper.CommandReadTransformerAsync; import com.mongodb.session.SessionContext; -import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.BsonJavaScript; @@ -50,6 +49,7 @@ import static com.mongodb.operation.CommandOperationHelper.CommandCreator; import static com.mongodb.operation.CommandOperationHelper.executeCommand; import static com.mongodb.operation.CommandOperationHelper.executeCommandAsync; +import static com.mongodb.operation.DocumentHelper.putIfNotNull; import static com.mongodb.operation.DocumentHelper.putIfNotZero; import static com.mongodb.operation.DocumentHelper.putIfTrue; import static com.mongodb.operation.ExplainHelper.asExplainCommand; @@ -430,13 +430,13 @@ private BsonDocument getCommand(final SessionContext sessionContext) { BsonDocument commandDocument = new BsonDocument("mapreduce", new BsonString(namespace.getCollectionName())) .append("map", getMapFunction()) .append("reduce", getReduceFunction()) - .append("out", new BsonDocument("inline", new BsonInt32(1))) - .append("query", asValueOrNull(getFilter())) - .append("sort", asValueOrNull(getSort())) - .append("finalize", asValueOrNull(getFinalizeFunction())) - .append("scope", asValueOrNull(getScope())) - .append("verbose", BsonBoolean.valueOf(isVerbose())); + .append("out", new BsonDocument("inline", new BsonInt32(1))); + putIfNotNull(commandDocument, "query", getFilter()); + putIfNotNull(commandDocument, "sort", getSort()); + putIfNotNull(commandDocument, "finalize", getFinalizeFunction()); + putIfNotNull(commandDocument, "scope", getScope()); + putIfTrue(commandDocument, "verbose", isVerbose()); appendReadConcernToCommand(sessionContext, commandDocument); putIfNotZero(commandDocument, "limit", getLimit()); putIfNotZero(commandDocument, "maxTimeMS", getMaxTime(MILLISECONDS)); diff --git a/driver-core/src/resources/META-INF/native-image/org.mongodb/mongodb-driver-core/native-image.properties b/driver-core/src/resources/META-INF/native-image/org.mongodb/mongodb-driver-core/native-image.properties new file mode 100644 index 00000000000..adb5c5e1804 --- /dev/null +++ b/driver-core/src/resources/META-INF/native-image/org.mongodb/mongodb-driver-core/native-image.properties @@ -0,0 +1,16 @@ +# +# 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. +# +Args = --initialize-at-run-time=com.mongodb.UnixServerAddress,com.mongodb.internal.connection.SnappyCompressor diff --git a/driver-core/src/test/functional/com/mongodb/client/model/SortsFunctionalSpecification.groovy b/driver-core/src/test/functional/com/mongodb/client/model/SortsFunctionalSpecification.groovy index fd78a97fef5..af5788f5052 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/SortsFunctionalSpecification.groovy +++ b/driver-core/src/test/functional/com/mongodb/client/model/SortsFunctionalSpecification.groovy @@ -27,13 +27,13 @@ import static com.mongodb.client.model.Sorts.orderBy class SortsFunctionalSpecification extends OperationFunctionalSpecification { def a = new Document('_id', 1).append('x', 1) - .append('y', 'b') + .append('y', 'bear') def b = new Document('_id', 2).append('x', 1) - .append('y', 'a') + .append('y', 'albatross') def c = new Document('_id', 3).append('x', 2) - .append('y', 'c') + .append('y', 'cat') def setup() { getCollectionHelper().insertDocuments(a, b, c) @@ -44,7 +44,11 @@ class SortsFunctionalSpecification extends OperationFunctionalSpecification { } def 'find'(Bson sort, Bson projection) { - getCollectionHelper().find(new Document(), sort, projection) + find(new Document(), sort, projection) + } + + def 'find'(Bson filter, Bson sort, Bson projection) { + getCollectionHelper().find(filter, sort, projection) } def 'ascending'() { @@ -66,7 +70,8 @@ class SortsFunctionalSpecification extends OperationFunctionalSpecification { getCollectionHelper().createIndex(new Document('y', 'text')) expect: - find(metaTextScore('score'), new Document('score', new Document('$meta', 'textScore')))*.containsKey('score') + find(new Document('$text', new Document('$search', 'bear')), metaTextScore('score'), + new Document('score', new Document('$meta', 'textScore')))*.containsKey('score') } def 'orderBy'() { diff --git a/driver-core/src/test/functional/com/mongodb/operation/ChangeStreamOperationSpecification.groovy b/driver-core/src/test/functional/com/mongodb/operation/ChangeStreamOperationSpecification.groovy index 80bf8ec1eb5..bc6c9613792 100644 --- a/driver-core/src/test/functional/com/mongodb/operation/ChangeStreamOperationSpecification.groovy +++ b/driver-core/src/test/functional/com/mongodb/operation/ChangeStreamOperationSpecification.groovy @@ -478,9 +478,13 @@ class ChangeStreamOperationSpecification extends OperationFunctionalSpecificatio when: helper.killCursor(helper.getNamespace(), cursor.getWrapped().getServerCursor()) expected = insertDocuments(helper, [3, 4]) + def results = nextAndClean(cursor, async) + if (results.size() < expected.size()) { + results.addAll(nextAndClean(cursor, async)) + } then: - nextAndClean(cursor, async) == expected + results == expected then: tryNextAndClean(cursor, async) == null @@ -489,8 +493,13 @@ class ChangeStreamOperationSpecification extends OperationFunctionalSpecificatio expected = insertDocuments(helper, [5, 6]) helper.killCursor(helper.getNamespace(), cursor.getWrapped().getServerCursor()) + results = nextAndClean(cursor, async) + if (results.size() < expected.size()) { + results.addAll(nextAndClean(cursor, async)) + } + then: - nextAndClean(cursor, async) == expected + results == expected cleanup: cursor?.close() diff --git a/driver-core/src/test/functional/com/mongodb/operation/MapReduceToCollectionOperationSpecification.groovy b/driver-core/src/test/functional/com/mongodb/operation/MapReduceToCollectionOperationSpecification.groovy index 9fbede92c42..dee9b3c4a86 100644 --- a/driver-core/src/test/functional/com/mongodb/operation/MapReduceToCollectionOperationSpecification.groovy +++ b/driver-core/src/test/functional/com/mongodb/operation/MapReduceToCollectionOperationSpecification.groovy @@ -27,18 +27,21 @@ import com.mongodb.client.model.ValidationOptions import com.mongodb.client.test.CollectionHelper import org.bson.BsonBoolean import org.bson.BsonDocument +import org.bson.BsonDouble import org.bson.BsonInt32 import org.bson.BsonInt64 import org.bson.BsonJavaScript -import org.bson.BsonNull import org.bson.BsonString import org.bson.Document +import org.bson.codecs.BsonDocumentCodec import org.bson.codecs.DocumentCodec import spock.lang.IgnoreIf import static com.mongodb.ClusterFixture.getBinding import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet import static com.mongodb.ClusterFixture.serverVersionAtLeast +import static com.mongodb.ClusterFixture.serverVersionGreaterThan +import static com.mongodb.ClusterFixture.serverVersionLessThan import static com.mongodb.client.model.Filters.gte import static java.util.concurrent.TimeUnit.MILLISECONDS @@ -49,9 +52,9 @@ class MapReduceToCollectionOperationSpecification extends OperationFunctionalSpe new BsonJavaScript('function(){ emit( this.name , 1 ); }'), new BsonJavaScript('function(key, values){ return values.length; }'), mapReduceOutputNamespace.getCollectionName()) - def expectedResults = [['_id': 'Pete', 'value': 2.0] as Document, - ['_id': 'Sam', 'value': 1.0] as Document] - def helper = new CollectionHelper(new DocumentCodec(), mapReduceOutputNamespace) + def expectedResults = [new BsonDocument('_id', new BsonString('Pete')).append('value', new BsonDouble(2.0)), + new BsonDocument('_id', new BsonString('Sam')).append('value', new BsonDouble(1.0))] as Set + def helper = new CollectionHelper(new BsonDocumentCodec(), mapReduceOutputNamespace) def setup() { CollectionHelper helper = new CollectionHelper(new DocumentCodec(), mapReduceInputNamespace) @@ -138,6 +141,7 @@ class MapReduceToCollectionOperationSpecification extends OperationFunctionalSpe operation.getCollation() == defaultCollation } + @IgnoreIf({ serverVersionGreaterThan('4.2') }) def 'should return the correct statistics and save the results'() { when: MapReduceStatistics results = execute(mapReduceOperation, async) @@ -147,7 +151,23 @@ class MapReduceToCollectionOperationSpecification extends OperationFunctionalSpe results.inputCount == 3 results.outputCount == 2 helper.count() == 2 - helper.find() == expectedResults + helper.find() as Set == expectedResults + + where: + async << [true, false] + } + + @IgnoreIf({ serverVersionLessThan('4.3') }) + def 'should return zero-valued statistics and save the results'() { + when: + MapReduceStatistics results = execute(mapReduceOperation, async) + + then: + results.emitCount == 0 + results.inputCount == 0 + results.outputCount == 0 + helper.count() == 2 + helper.find() as Set == expectedResults where: async << [true, false] @@ -233,12 +253,7 @@ class MapReduceToCollectionOperationSpecification extends OperationFunctionalSpe def expectedCommand = new BsonDocument('mapreduce', new BsonString(getCollectionName())) .append('map', mapF) .append('reduce', reduceF) - .append('out', BsonDocument.parse('{replace: "outCollection", sharded: false, nonAtomic: false}')) - .append('query', BsonNull.VALUE) - .append('sort', BsonNull.VALUE) - .append('finalize', BsonNull.VALUE) - .append('scope', BsonNull.VALUE) - .append('verbose', BsonBoolean.FALSE) + .append('out', BsonDocument.parse('{replace: "outCollection"}')) if (includeWriteConcern) { expectedCommand.append('writeConcern', WriteConcern.MAJORITY.asDocument()) @@ -260,7 +275,7 @@ class MapReduceToCollectionOperationSpecification extends OperationFunctionalSpe .bypassDocumentValidation(true) .verbose(true) - expectedCommand.append('out', BsonDocument.parse('{merge: "outCollection", sharded: false, nonAtomic: false, db: "dbName"}')) + expectedCommand.append('out', BsonDocument.parse('{merge: "outCollection", db: "dbName"}')) .append('query', filter) .append('sort', sort) .append('finalize', finalizeF) diff --git a/driver-core/src/test/functional/com/mongodb/operation/MapReduceWithInlineResultsOperationSpecification.groovy b/driver-core/src/test/functional/com/mongodb/operation/MapReduceWithInlineResultsOperationSpecification.groovy index d16936d7d79..46710b2ecba 100644 --- a/driver-core/src/test/functional/com/mongodb/operation/MapReduceWithInlineResultsOperationSpecification.groovy +++ b/driver-core/src/test/functional/com/mongodb/operation/MapReduceWithInlineResultsOperationSpecification.groovy @@ -36,13 +36,14 @@ import com.mongodb.connection.ServerVersion import com.mongodb.session.SessionContext import org.bson.BsonBoolean import org.bson.BsonDocument +import org.bson.BsonDouble import org.bson.BsonInt32 import org.bson.BsonInt64 import org.bson.BsonJavaScript -import org.bson.BsonNull import org.bson.BsonString import org.bson.BsonTimestamp import org.bson.Document +import org.bson.codecs.BsonDocumentCodec import org.bson.codecs.DocumentCodec import spock.lang.IgnoreIf @@ -53,18 +54,18 @@ import static com.mongodb.operation.OperationReadConcernHelper.appendReadConcern import static java.util.concurrent.TimeUnit.MILLISECONDS class MapReduceWithInlineResultsOperationSpecification extends OperationFunctionalSpecification { - private final documentCodec = new DocumentCodec() - def mapReduceOperation = new MapReduceWithInlineResultsOperation( + private final bsonDocumentCodec = new BsonDocumentCodec() + def mapReduceOperation = new MapReduceWithInlineResultsOperation( getNamespace(), new BsonJavaScript('function(){ emit( this.name , 1 ); }'), new BsonJavaScript('function(key, values){ return values.length; }'), - documentCodec) + bsonDocumentCodec) - def expectedResults = [['_id': 'Pete', 'value': 2.0] as Document, - ['_id': 'Sam', 'value': 1.0] as Document] + def expectedResults = [new BsonDocument('_id', new BsonString('Pete')).append('value', new BsonDouble(2.0)), + new BsonDocument('_id', new BsonString('Sam')).append('value', new BsonDouble(1.0))] as Set def setup() { - CollectionHelper helper = new CollectionHelper(documentCodec, getNamespace()) + CollectionHelper helper = new CollectionHelper(bsonDocumentCodec, getNamespace()) Document pete = new Document('name', 'Pete').append('job', 'handyman') Document sam = new Document('name', 'Sam').append('job', 'plumber') Document pete2 = new Document('name', 'Pete').append('job', 'electrician') @@ -75,7 +76,7 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction when: def mapF = new BsonJavaScript('function(){ }') def reduceF = new BsonJavaScript('function(key, values){ }') - def operation = new MapReduceWithInlineResultsOperation(helper.namespace, mapF, reduceF, documentCodec) + def operation = new MapReduceWithInlineResultsOperation(helper.namespace, mapF, reduceF, bsonDocumentCodec) then: operation.getMapFunction() == mapF @@ -99,7 +100,7 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction def finalizeF = new BsonJavaScript('function(key, value){}') def mapF = new BsonJavaScript('function(){ }') def reduceF = new BsonJavaScript('function(key, values){ }') - def operation = new MapReduceWithInlineResultsOperation(helper.namespace, mapF, reduceF, documentCodec) + def operation = new MapReduceWithInlineResultsOperation(helper.namespace, mapF, reduceF, bsonDocumentCodec) .filter(filter) .finalizeFunction(finalizeF) .scope(scope) @@ -129,7 +130,7 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction def operation = mapReduceOperation when: - def results = executeAndCollectBatchCursorResults(operation, async) + def results = executeAndCollectBatchCursorResults(operation, async) as Set then: results == expectedResults @@ -141,7 +142,7 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction def 'should use the ReadBindings readPreference to set slaveOK'() { when: def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), - new BsonJavaScript('function(key, values){ }'), documentCodec) + new BsonJavaScript('function(key, values){ }'), bsonDocumentCodec) then: testOperationSlaveOk(operation, [3, 4, 0], readPreference, async, helper.commandResult) @@ -153,16 +154,11 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction def 'should create the expected command'() { when: def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), - new BsonJavaScript('function(key, values){ }'), documentCodec) + new BsonJavaScript('function(key, values){ }'), bsonDocumentCodec) def expectedCommand = new BsonDocument('mapreduce', new BsonString(helper.namespace.getCollectionName())) .append('map', operation.getMapFunction()) .append('reduce', operation.getReduceFunction()) .append('out', new BsonDocument('inline', new BsonInt32(1))) - .append('query', new BsonNull()) - .append('sort', new BsonNull()) - .append('finalize', new BsonNull()) - .append('scope', new BsonNull()) - .append('verbose', BsonBoolean.FALSE) then: testOperation(operation, serverVersion, expectedCommand, async, helper.commandResult) @@ -205,8 +201,8 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction def 'should throw an exception when using an unsupported ReadConcern'() { given: - def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), - new BsonJavaScript('function(key, values){ }'), documentCodec) + def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), + new BsonJavaScript('function(key, values){ }'), bsonDocumentCodec) when: testOperationThrows(operation, [3, 0, 0], readConcern, async) @@ -221,8 +217,8 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction def 'should throw an exception when using an unsupported Collation'() { given: - def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), - new BsonJavaScript('function(key, values){ }'), documentCodec).collation(defaultCollation) + def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), + new BsonJavaScript('function(key, values){ }'), bsonDocumentCodec).collation(defaultCollation) when: testOperationThrows(operation, [3, 2, 0], async) @@ -240,11 +236,11 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction given: def document = Document.parse('{_id: 1, str: "foo"}') getCollectionHelper().insertDocuments(document) - def operation = new MapReduceWithInlineResultsOperation( + def operation = new MapReduceWithInlineResultsOperation( namespace, - new BsonJavaScript('function(){ emit( this._id, this.str ); }'), - new BsonJavaScript('function(key, values){ return key, values; }'), - documentCodec) + new BsonJavaScript('function(){ emit( this.str, 1 ); }'), + new BsonJavaScript('function(key, values){ return Array.sum(values); }'), + bsonDocumentCodec) .filter(BsonDocument.parse('{str: "FOO"}')) .collation(caseInsensitiveCollation) @@ -252,7 +248,7 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction def results = executeAndCollectBatchCursorResults(operation, async) then: - results == [Document.parse('{_id: 1.0, value: "foo"}')] + results == [new BsonDocument('_id', new BsonString('foo')).append('value', new BsonDouble(1))] where: async << [true, false] @@ -273,16 +269,11 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction "map" : { "$code" : "function(){ }" }, "reduce" : { "$code" : "function(key, values){ }" }, "out" : { "inline" : 1 }, - "query" : null, - "sort" : null, - "finalize" : null, - "scope" : null, - "verbose" : false, }''') appendReadConcernToCommand(sessionContext, commandDocument) - def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), - new BsonJavaScript('function(key, values){ }'), documentCodec) + def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), + new BsonJavaScript('function(key, values){ }'), bsonDocumentCodec) when: operation.execute(binding) @@ -325,16 +316,11 @@ class MapReduceWithInlineResultsOperationSpecification extends OperationFunction "map" : { "$code" : "function(){ }" }, "reduce" : { "$code" : "function(key, values){ }" }, "out" : { "inline" : 1 }, - "query" : null, - "sort" : null, - "finalize" : null, - "scope" : null, - "verbose" : false, }''') appendReadConcernToCommand(sessionContext, commandDocument) - def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), - new BsonJavaScript('function(key, values){ }'), documentCodec) + def operation = new MapReduceWithInlineResultsOperation(helper.namespace, new BsonJavaScript('function(){ }'), + new BsonJavaScript('function(key, values){ }'), bsonDocumentCodec) when: executeAsync(operation, binding) diff --git a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy index 2313c72211f..92504480268 100644 --- a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy @@ -28,7 +28,6 @@ import spock.lang.Specification import java.util.concurrent.TimeUnit -// TODO: add SRV tests class ClusterSettingsSpecification extends Specification { def hosts = [new ServerAddress('localhost'), new ServerAddress('localhost', 30000)] def serverSelector = new WritableServerSelector() @@ -105,6 +104,48 @@ class ClusterSettingsSpecification extends Specification { ClusterSettings.builder(customSettings).applySettings(defaultSettings).build() == defaultSettings } + def 'when hosts contains more than one element and mode is SINGLE, should throw IllegalArgumentException'() { + when: + def builder = ClusterSettings.builder() + builder.hosts([new ServerAddress('host1'), new ServerAddress('host2')]) + builder.mode(ClusterConnectionMode.SINGLE) + builder.build() + + then: + thrown(IllegalArgumentException) + } + + def 'when srvHost is specified, should set mode to MULTIPLE'() { + when: + def builder = ClusterSettings.builder() + builder.srvHost('foo.bar.com') + def settings = builder.build() + + then: + settings.getSrvHost() == 'foo.bar.com' + settings.getMode() == ClusterConnectionMode.MULTIPLE + } + + def 'when srvHost contains a colon, should throw IllegalArgumentException'() { + when: + def builder = ClusterSettings.builder() + builder.srvHost('foo.bar.com:27017') + builder.build() + + then: + thrown(IllegalArgumentException) + } + + def 'when srvHost contains less than three parts (host, domain, top-level domain, should throw IllegalArgumentException'() { + when: + def builder = ClusterSettings.builder() + builder.srvHost('foo.bar') + builder.build() + + then: + thrown(IllegalArgumentException) + } + def 'should allow configure serverSelectors correctly'() { given: def latMinServerSelector = new LatencyMinimizingServerSelector(10, TimeUnit.MILLISECONDS) diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/ScramShaAuthenticatorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/ScramShaAuthenticatorSpecification.groovy index 7cb475245a9..e294c6f9c66 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/ScramShaAuthenticatorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/ScramShaAuthenticatorSpecification.groovy @@ -360,13 +360,65 @@ class ScramShaAuthenticatorSpecification extends Specification { async << [true, false] } - def createConnection(List serverResponses) { + def 'should complete authentication when done is set to true prematurely SHA-256'() { + given: + def serverResponses = createMessages(''' + S: r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096 + S: v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4= + ''').last() + def authenticator = new ScramShaAuthenticator(SHA256_CREDENTIAL, { 'rOprNGfwEbeRWgbNEkqO' }, { 'pencil' }) + + when: + // server sends done=true on first response, client is not complete after processing response + authenticate(createConnection(serverResponses, 0), authenticator, async) + + then: + def e = thrown(MongoSecurityException) + e.getMessage().contains('server completed challenges before client completed responses') + + when: + // server sends done=true on second response, client is complete after processing response + authenticate(createConnection(serverResponses, 1), authenticator, async) + + then: + noExceptionThrown() + + where: + async << [true, false] + } + + def 'should throw exception when done is set to true prematurely and server response is invalid SHA-256'() { + given: + def serverResponses = createMessages(''' + S: r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096 + S: v=invalidResponse + ''').last() + def authenticator = new ScramShaAuthenticator(SHA256_CREDENTIAL, { 'rOprNGfwEbeRWgbNEkqO' }, { 'pencil' }) + + when: + // server sends done=true on second response, client throws exception on invalid server response + authenticate(createConnection(serverResponses, 1), authenticator, async) + + then: + def e = thrown(MongoSecurityException) + e.getCause() instanceof SaslException + e.getCause().getMessage() == 'Server signature was invalid.' + + where: + async << [true, false] + } + + def createConnection(List serverResponses, int responseWhereDoneIsTrue = -1) { TestInternalConnection connection = new TestInternalConnection(serverId) - serverResponses.each { + serverResponses.eachWithIndex { response, index -> + def isDone = (index == responseWhereDoneIsTrue).booleanValue() connection.enqueueReply( - buildSuccessfulReply("{conversationId: 1, payload: BinData(0, '${encode64(it)}'), done: false, ok: 1}") - ) } - connection.enqueueReply(buildSuccessfulReply('{conversationId: 1, done: true, ok: 1}')) + buildSuccessfulReply("{conversationId: 1, payload: BinData(0, '${encode64(response)}'), done: ${isDone}, ok: 1}") + ) + } + if (responseWhereDoneIsTrue < 0) { + connection.enqueueReply(buildSuccessfulReply('{conversationId: 1, done: true, ok: 1}')) + } connection } diff --git a/driver-core/src/test/unit/com/mongodb/operation/AsyncChangeStreamBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/operation/AsyncChangeStreamBatchCursorSpecification.groovy index 977e33ebe58..043d2aa8274 100644 --- a/driver-core/src/test/unit/com/mongodb/operation/AsyncChangeStreamBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/operation/AsyncChangeStreamBatchCursorSpecification.groovy @@ -16,10 +16,15 @@ package com.mongodb.operation +import com.mongodb.MongoException +import com.mongodb.async.FutureResultCallback import com.mongodb.async.SingleResultCallback import com.mongodb.binding.AsyncReadBinding +import org.bson.Document import spock.lang.Specification +import static java.util.concurrent.TimeUnit.SECONDS + class AsyncChangeStreamBatchCursorSpecification extends Specification { def 'should call the underlying AsyncQueryBatchCursor'() { @@ -52,9 +57,6 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { cursor.close() then: - 1 * wrapped.isClosed() >> { - false - } 1 * wrapped.close() 1 * binding.release() @@ -62,10 +64,91 @@ class AsyncChangeStreamBatchCursorSpecification extends Specification { cursor.close() then: - 1 * wrapped.isClosed() >> { - true - } 0 * wrapped.close() 0 * binding.release() } + + def 'should not close the cursor in next if the cursor was closed before next completed'() { + def changeStreamOpertation = Stub(ChangeStreamOperation) + def binding = Mock(AsyncReadBinding) + def wrapped = Mock(AsyncQueryBatchCursor) + def callback = Stub(SingleResultCallback) + def cursor = new AsyncChangeStreamBatchCursor(changeStreamOpertation, wrapped, binding, null) + + when: + cursor.next(callback) + + then: + 1 * wrapped.next(_) >> { + // Simulate the user calling close while wrapped.next() is in flight + cursor.close() + it[0].onResult(null, null) + } + + then: + noExceptionThrown() + + then: + cursor.isClosed() + } + + def 'should not close the cursor in tryNext if the cursor was closed before tryNext completed'() { + def changeStreamOpertation = Stub(ChangeStreamOperation) + def binding = Mock(AsyncReadBinding) + def wrapped = Mock(AsyncQueryBatchCursor) + def callback = Stub(SingleResultCallback) + def cursor = new AsyncChangeStreamBatchCursor(changeStreamOpertation, wrapped, binding, null) + + when: + cursor.tryNext(callback) + + then: + 1 * wrapped.tryNext(_) >> { + // Simulate the user calling close while wrapped.next() is in flight + cursor.close() + it[0].onResult(null, null) + } + + then: + noExceptionThrown() + + then: + cursor.isClosed() + } + + def 'should throw a MongoException when next/tryNext is called after the cursor is closed'() { + def changeStreamOpertation = Stub(ChangeStreamOperation) + def binding = Mock(AsyncReadBinding) + def wrapped = Mock(AsyncQueryBatchCursor) + def cursor = new AsyncChangeStreamBatchCursor(changeStreamOpertation, wrapped, binding, null) + + given: + cursor.close() + + when: + nextBatch(cursor) + + then: + def exception = thrown(MongoException) + exception.getMessage() == 'next() called after the cursor was closed.' + + when: + tryNextBatch(cursor) + + then: + exception = thrown(MongoException) + exception.getMessage() == 'tryNext() called after the cursor was closed.' + } + + List nextBatch(AsyncChangeStreamBatchCursor cursor) { + def futureResultCallback = new FutureResultCallback() + cursor.next(futureResultCallback) + futureResultCallback.get(1, SECONDS) + } + + List tryNextBatch(AsyncChangeStreamBatchCursor cursor) { + def futureResultCallback = new FutureResultCallback() + cursor.tryNext(futureResultCallback) + futureResultCallback.get(1, SECONDS) + } } diff --git a/driver-core/src/test/unit/com/mongodb/operation/AsyncQueryBatchCursorSpecification.groovy b/driver-core/src/test/unit/com/mongodb/operation/AsyncQueryBatchCursorSpecification.groovy index 3ee04dd887e..c176c2d6779 100644 --- a/driver-core/src/test/unit/com/mongodb/operation/AsyncQueryBatchCursorSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/operation/AsyncQueryBatchCursorSpecification.groovy @@ -147,7 +147,15 @@ class AsyncQueryBatchCursorSpecification extends Specification { nextBatch(cursor) then: - thrown(MongoException) + def exception = thrown(MongoException) + exception.getMessage() == 'next() called after the cursor was closed.' + + when: + tryNextBatch(cursor) + + then: + exception = thrown(MongoException) + exception.getMessage() == 'tryNext() called after the cursor was closed.' } def 'should return the expected results from tryNext'() { @@ -429,7 +437,7 @@ class AsyncQueryBatchCursorSpecification extends Specification { new ServerVersion([3, 0, 0]) | false | queryResult(SECOND_BATCH) } - def 'should kill the cursor in the getMore callback if it was closed before getMore returned'() { + def 'should close cursor after getMore finishes if cursor was closed while getMore was in progress and getMore returns a response'() { given: def connectionA = referenceCountedAsyncConnection(serverVersion) def connectionB = referenceCountedAsyncConnection(serverVersion) @@ -444,27 +452,73 @@ class AsyncQueryBatchCursorSpecification extends Specification { batch == FIRST_BATCH when: - batch = nextBatch(cursor) + nextBatch(cursor) then: if (commandAsync) { - 1 * connectionA.commandAsync(_, _, _, _, _, _, _) >> { + _ * connectionA.commandAsync(_, _, _, _, _, _, _) >> { // Simulate the user calling close while the getMore is in flight cursor.close() it[6].onResult(response, null) - } - 1 * connectionB.commandAsync(NAMESPACE.databaseName, createKillCursorsDocument(initialResult.cursor), _, primary(), - _, _, _) >> { - it[6].onResult(null, null) + } >> { + it[6].onResult(response2, null) } } else { - 1 * connectionA.getMoreAsync(_, _, _, _, _) >> { + _ * connectionA.getMoreAsync(_, _, _, _, _) >> { // Simulate the user calling close while the getMore is in flight cursor.close() it[4].onResult(response, null) + } >> { + it[4].onResult(response2, null) } - 1 * connectionB.killCursorAsync(NAMESPACE, [initialResult.cursor.id], _) >> { - it[2].onResult(null, null) + } + + then: + noExceptionThrown() + + then: + connectionA.getCount() == 0 + connectionSource.getCount() == 0 + cursor.isClosed() + + where: + serverVersion | commandAsync | response | response2 + new ServerVersion([3, 2, 0]) | true | documentResponse([]) | documentResponse([], 0) + new ServerVersion([3, 2, 0]) | true | documentResponse([], 0) | null + new ServerVersion([3, 0, 0]) | false | new QueryResult(NAMESPACE, [], 42, SERVER_ADDRESS) | + new QueryResult(NAMESPACE, [], 0, SERVER_ADDRESS) + new ServerVersion([3, 0, 0]) | false | new QueryResult(NAMESPACE, [], 0, SERVER_ADDRESS) | null + } + + def 'should close cursor after getMore finishes if cursor was closed while getMore was in progress and getMore throws exception'() { + given: + def connectionA = referenceCountedAsyncConnection(serverVersion) + def connectionB = referenceCountedAsyncConnection(serverVersion) + def connectionSource = getAsyncConnectionSource(connectionA, connectionB) + def initialResult = queryResult() + + when: + def cursor = new AsyncQueryBatchCursor(initialResult, 0, 0, 0, CODEC, connectionSource, null) + def batch = nextBatch(cursor) + + then: + batch == FIRST_BATCH + + when: + nextBatch(cursor) + + then: + if (commandAsync) { + 1 * connectionA.commandAsync(_, _, _, _, _, _, _) >> { + // Simulate the user calling close while the getMore is throwing a MongoException + cursor.close() + it[6].onResult(null, MONGO_EXCEPTION) + } + } else { + 1 * connectionA.getMoreAsync(_, _, _, _, _) >> { + // Simulate the user calling close while the getMore is throwing a MongoException + cursor.close() + it[4].onResult(null, MONGO_EXCEPTION) } } @@ -473,13 +527,12 @@ class AsyncQueryBatchCursorSpecification extends Specification { then: connectionA.getCount() == 0 - connectionB.getCount() == 0 - connectionSource.getCount() == 0 + cursor.isClosed() where: - serverVersion | commandAsync | response - new ServerVersion([3, 2, 0]) | true | documentResponse([]) - new ServerVersion([3, 0, 0]) | false | new QueryResult(NAMESPACE, [], 42, SERVER_ADDRESS) + serverVersion | commandAsync + new ServerVersion([3, 2, 0]) | true + new ServerVersion([3, 0, 0]) | false } def 'should handle errors when calling close'() { @@ -492,11 +545,19 @@ class AsyncQueryBatchCursorSpecification extends Specification { nextBatch(cursor) then: - thrown(MongoException) + def exception = thrown(MongoException) + exception.getMessage() == 'next() called after the cursor was closed.' then: cursor.isClosed() connectionSource.getCount() == 0 + + when: + tryNextBatch(cursor) + + then: + exception = thrown(MongoException) + exception.getMessage() == 'tryNext() called after the cursor was closed.' } diff --git a/driver-legacy/src/test/functional/com/mongodb/MapReduceTest.java b/driver-legacy/src/test/functional/com/mongodb/MapReduceTest.java index bb5b18bd8d2..a6d52f0d096 100644 --- a/driver-legacy/src/test/functional/com/mongodb/MapReduceTest.java +++ b/driver-legacy/src/test/functional/com/mongodb/MapReduceTest.java @@ -30,6 +30,7 @@ import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet; import static com.mongodb.ClusterFixture.isSharded; import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.ClusterFixture.serverVersionLessThan; import static com.mongodb.DBObjectMatchers.hasFields; import static com.mongodb.DBObjectMatchers.hasSubdocument; import static java.util.concurrent.TimeUnit.SECONDS; @@ -150,6 +151,7 @@ public void testMapReduceWithOutputToAnotherDatabase() { MapReduceCommand.OutputType.REPLACE, new BasicDBObject()); command.setOutputDB(MR_DATABASE); + getClient().getDatabase(MR_DATABASE).createCollection(DEFAULT_COLLECTION); MapReduceOutput output = collection.mapReduce(command); @@ -309,11 +311,15 @@ public void shouldReturnStatisticsForInlineMapReduce() { MapReduceOutput output = collection.mapReduce(command); //then - //duration is not working on the unstable server version - // assertThat(output.getDuration(), is(greaterThan(0))); - assertThat(output.getEmitCount(), is(6)); - assertThat(output.getInputCount(), is(3)); - assertThat(output.getOutputCount(), is(4)); + if (serverVersionLessThan(4, 3)) { + assertThat(output.getEmitCount(), is(6)); + assertThat(output.getInputCount(), is(3)); + assertThat(output.getOutputCount(), is(4)); + } else { + assertThat(output.getEmitCount(), is(0)); + assertThat(output.getInputCount(), is(0)); + assertThat(output.getOutputCount(), is(0)); + } } @Test @@ -329,10 +335,17 @@ public void shouldReturnStatisticsForMapReduceIntoACollection() { MapReduceOutput output = collection.mapReduce(command); //then - assertThat(output.getDuration(), is(greaterThanOrEqualTo(0))); - assertThat(output.getEmitCount(), is(6)); - assertThat(output.getInputCount(), is(3)); - assertThat(output.getOutputCount(), is(4)); + if (serverVersionLessThan(4, 3)) { + assertThat(output.getDuration(), is(greaterThanOrEqualTo(0))); + assertThat(output.getEmitCount(), is(6)); + assertThat(output.getInputCount(), is(3)); + assertThat(output.getOutputCount(), is(4)); + } else { + assertThat(output.getDuration(), is(0)); + assertThat(output.getEmitCount(), is(0)); + assertThat(output.getInputCount(), is(0)); + assertThat(output.getOutputCount(), is(0)); + } } diff --git a/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionAndDecryptionTour.java b/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionAndDecryptionTour.java new file mode 100644 index 00000000000..34f715d2c09 --- /dev/null +++ b/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionAndDecryptionTour.java @@ -0,0 +1,113 @@ +/* + * 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 tour; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; +import org.bson.BsonBinary; +import org.bson.BsonString; +import org.bson.Document; +import org.bson.types.Binary; + +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +/** + * ClientSideEncryption explicit encryption and decryption tour + */ +public class ClientSideEncryptionExplicitEncryptionAndDecryptionTour { + + /** + * Run this main method to see the output of this quick example. + * + * @param args ignored args + */ + public static void main(final String[] args) { + + // This would have to be the same master key as was used to create the encryption key + final byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + + Map> kmsProviders = new HashMap>() {{ + put("local", new HashMap() {{ + put("key", localMasterKey); + }}); + }}; + + + MongoClientSettings clientSettings = MongoClientSettings.builder().build(); + MongoClient mongoClient = MongoClients.create(clientSettings); + + // Set up the key vault for this example + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true) + .partialFilterExpression(Filters.exists("keyAltNames"))); + + MongoCollection collection = mongoClient.getDatabase("test").getCollection("coll"); + collection.drop(); // Clear old data + + // Create the ClientEncryption instance + ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(MongoClientSettings.builder() + .applyConnectionString(new ConnectionString("mongodb://localhost")) + .build()) + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build(); + + ClientEncryption clientEncryption = ClientEncryptions.create(clientEncryptionSettings); + + BsonBinary dataKeyId = clientEncryption.createDataKey("local", new DataKeyOptions()); + + // Explicitly encrypt a field + BsonBinary encryptedFieldValue = clientEncryption.encrypt(new BsonString("123456789"), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(dataKeyId)); + + collection.insertOne(new Document("encryptedField", encryptedFieldValue)); + + Document doc = collection.find().first(); + System.out.println(doc.toJson()); + + // Explicitly decrypt the field + BsonString decryptedFieldValue = clientEncryption.decrypt(new BsonBinary(doc.get("encryptedField", Binary.class).getData())) + .asString(); + System.out.println(decryptedFieldValue.getValue()); + + // release resources + clientEncryption.close(); + mongoClient.close(); + } +} diff --git a/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java b/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java new file mode 100644 index 00000000000..32761045b4e --- /dev/null +++ b/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java @@ -0,0 +1,116 @@ +/* + * 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 tour; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; +import org.bson.BsonBinary; +import org.bson.BsonString; +import org.bson.Document; + +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +/** + * ClientSideEncryption explicit encryption and decryption tour + */ +public class ClientSideEncryptionExplicitEncryptionOnlyTour { + + /** + * Run this main method to see the output of this quick example. + * + * @param args ignored args + */ + public static void main(final String[] args) { + + // This would have to be the same master key as was used to create the encryption key + final byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + + Map> kmsProviders = new HashMap>() {{ + put("local", new HashMap() {{ + put("key", localMasterKey); + }}); + }}; + + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + + MongoClientSettings clientSettings = MongoClientSettings.builder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassAutoEncryption(true) + .build()) + .build(); + MongoClient mongoClient = MongoClients.create(clientSettings); + + // Set up the key vault for this example + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true) + .partialFilterExpression(Filters.exists("keyAltNames"))); + + MongoCollection collection = mongoClient.getDatabase("test").getCollection("coll"); + collection.drop(); // Clear old data + + // Create the ClientEncryption instance + ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(MongoClientSettings.builder() + .applyConnectionString(new ConnectionString("mongodb://localhost")) + .build()) + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build(); + + ClientEncryption clientEncryption = ClientEncryptions.create(clientEncryptionSettings); + + BsonBinary dataKeyId = clientEncryption.createDataKey("local", new DataKeyOptions()); + + // Explicitly encrypt a field + BsonBinary encryptedFieldValue = clientEncryption.encrypt(new BsonString("123456789"), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(dataKeyId)); + + collection.insertOne(new Document("encryptedField", encryptedFieldValue)); + + // Automatically decrypts the encrypted field. + Document doc = collection.find().first(); + System.out.println(doc.toJson()); + System.out.println(doc.get("encryptedField")); + + // release resources + clientEncryption.close(); + mongoClient.close(); + } +} diff --git a/driver-sync/src/main/com/mongodb/client/internal/CommandMarker.java b/driver-sync/src/main/com/mongodb/client/internal/CommandMarker.java index 4af45a1b11d..6b70a43aedf 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/CommandMarker.java +++ b/driver-sync/src/main/com/mongodb/client/internal/CommandMarker.java @@ -34,20 +34,19 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.capi.MongoCryptOptionsHelper.createMongocryptdSpawnArgs; @SuppressWarnings("UseOfProcessBuilder") class CommandMarker implements Closeable { - private MongoClient client; + private final MongoClient client; private final ProcessBuilder processBuilder; - CommandMarker(final Map options) { - String connectionString; - - if (options.containsKey("mongocryptdURI")) { - connectionString = (String) options.get("mongocryptdURI"); - } else { - connectionString = "mongodb://localhost:27020"; + CommandMarker(final boolean isBypassAutoEncryption, final Map options) { + if (isBypassAutoEncryption) { + processBuilder = null; + client = null; + return; } if (!options.containsKey("mongocryptdBypassSpawn") || !((Boolean) options.get("mongocryptdBypassSpawn"))) { @@ -57,6 +56,14 @@ class CommandMarker implements Closeable { processBuilder = null; } + String connectionString; + + if (options.containsKey("mongocryptdURI")) { + connectionString = (String) options.get("mongocryptdURI"); + } else { + connectionString = "mongodb://localhost:27020"; + } + client = MongoClients.create(MongoClientSettings.builder() .applyConnectionString(new ConnectionString(connectionString)) .applyToClusterSettings(new Block() { @@ -66,9 +73,11 @@ public void apply(final ClusterSettings.Builder builder) { } }) .build()); + } RawBsonDocument mark(final String databaseName, final RawBsonDocument command) { + notNull("client", client); try { try { return executeCommand(databaseName, command); @@ -86,7 +95,9 @@ RawBsonDocument mark(final String databaseName, final RawBsonDocument command) { @Override public void close() { - client.close(); + if (client != null) { + client.close(); + } } private RawBsonDocument executeCommand(final String databaseName, final RawBsonDocument markableCommand) { diff --git a/driver-sync/src/main/com/mongodb/client/internal/Crypts.java b/driver-sync/src/main/com/mongodb/client/internal/Crypts.java index 76a205915b8..7bac04f0dac 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/Crypts.java +++ b/driver-sync/src/main/com/mongodb/client/internal/Crypts.java @@ -35,7 +35,7 @@ public static Crypt createCrypt(final SimpleMongoClient client, final AutoEncryp return new Crypt(MongoCrypts.create(createMongoCryptOptions(options.getKmsProviders(), options.getSchemaMap())), new CollectionInfoRetriever(client), - new CommandMarker(options.getExtraOptions()), + new CommandMarker(options.isBypassAutoEncryption(), options.getExtraOptions()), createKeyRetriever(client, options.getKeyVaultMongoClientSettings(), options.getKeyVaultNamespace()), createKeyManagementService(), options.isBypassAutoEncryption()); diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionBypassAutoEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionBypassAutoEncryptionTest.java new file mode 100644 index 00000000000..1907c2d9d24 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionBypassAutoEncryptionTest.java @@ -0,0 +1,125 @@ +/* + * 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.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; +import org.bson.BsonBinary; +import org.bson.BsonString; +import org.bson.Document; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import static com.mongodb.ClusterFixture.isNotAtLeastJava8; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.Fixture.getMongoClient; +import static com.mongodb.client.Fixture.getMongoClientSettings; +import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +public class ClientSideEncryptionBypassAutoEncryptionTest { + private MongoClient clientEncrypted; + private ClientEncryption clientEncryption; + + @Before + public void setUp() { + assumeFalse(isNotAtLeastJava8()); + assumeTrue(serverVersionAtLeast(4, 1)); + + MongoClient mongoClient = getMongoClient(); + + final byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + + Map> kmsProviders = new HashMap>() {{ + put("local", new HashMap() {{ + put("key", localMasterKey); + }}); + }}; + + // Set up the key vault for this example + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true) + .partialFilterExpression(Filters.exists("keyAltNames"))); + + MongoDatabase db = mongoClient.getDatabase(Fixture.getDefaultDatabaseName()); + db.getCollection("test").drop(); + + // Create the ClientEncryption instance + ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(getMongoClientSettings()) + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build(); + + clientEncryption = ClientEncryptions.create(clientEncryptionSettings); + + AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassAutoEncryption(true) + .build(); + + MongoClientSettings clientSettings = getMongoClientSettingsBuilder() + .autoEncryptionSettings(autoEncryptionSettings) + .build(); + clientEncrypted = MongoClients.create(clientSettings); + } + + @Test + public void shouldAutoDecryptManuallyEncryptedData() { + String fieldValue = "123456789"; + BsonBinary dataKeyId = clientEncryption.createDataKey("local", new DataKeyOptions()); + BsonBinary encryptedFieldValue = clientEncryption.encrypt(new BsonString(fieldValue), + new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").keyId(dataKeyId)); + + MongoCollection collection = clientEncrypted.getDatabase(Fixture.getDefaultDatabaseName()).getCollection("test"); + collection.insertOne(new Document("encryptedField", encryptedFieldValue)); + + assertEquals(fieldValue, collection.find().first().getString("encryptedField")); + } + + @After + public void after() { + if (clientEncrypted != null) { + clientEncrypted.getDatabase(Fixture.getDefaultDatabaseName()).drop(); + clientEncrypted.close(); + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionMongocryptdSpawnBypassTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionMongocryptdSpawnBypassTest.java new file mode 100644 index 00000000000..aae2fe24793 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionMongocryptdSpawnBypassTest.java @@ -0,0 +1,113 @@ +/* + * 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.AutoEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import org.bson.Document; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import static com.mongodb.ClusterFixture.isNotAtLeastJava8; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertFalse; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + + +public class ClientSideEncryptionMongocryptdSpawnBypassTest extends DatabaseTestCase { + private final File pidFile; + private final Map> kmsProviders; + private final MongoNamespace keyVaultNamespace = new MongoNamespace("admin.datakeys"); + + public ClientSideEncryptionMongocryptdSpawnBypassTest() throws IOException { + assumeFalse(isNotAtLeastJava8()); + assumeTrue(serverVersionAtLeast(4, 2)); + + pidFile = new File("bypass-spawning-mongocryptd.pid"); + + byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + + Map keyMap = new HashMap(); + keyMap.put("key", localMasterKey); + kmsProviders = new HashMap>(); + kmsProviders.put("local", keyMap); + } + + + @Test + public void shouldNotSpawnWhenMongocryptdBypassSpawnIsTrue() { + assumeTrue(serverVersionAtLeast(4, 1)); + Map extraOptions = new HashMap(); + extraOptions.put("mongocryptdBypassSpawn", true); + extraOptions.put("mongocryptdSpawnArgs", asList("--pidfilepath=" + pidFile.getAbsolutePath(), "--port=27099")); + + AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .extraOptions(extraOptions) + .build(); + + MongoClientSettings clientSettings = getMongoClientSettingsBuilder() + .autoEncryptionSettings(autoEncryptionSettings) + .build(); + MongoClient clientEncrypted = MongoClients.create(clientSettings); + try { + clientEncrypted.getDatabase("admin").runCommand(new Document("ping", 1)); + + assertFalse(pidFile.exists()); + } finally { + clientEncrypted.close(); + } + } + + @Test + public void shouldNotSpawnWhenBypassAutoEncryptionIsTrue() { + assumeTrue(serverVersionAtLeast(4, 1)); + Map extraOptions = new HashMap(); + extraOptions.put("mongocryptdSpawnArgs", asList("--pidfilepath=" + pidFile.getAbsolutePath(), "--port=27099")); + + AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultNamespace.getFullName()) + .kmsProviders(kmsProviders) + .extraOptions(extraOptions) + .bypassAutoEncryption(true) + .build(); + + MongoClientSettings clientSettings = getMongoClientSettingsBuilder() + .autoEncryptionSettings(autoEncryptionSettings) + .build(); + MongoClient clientEncrypted = MongoClients.create(clientSettings); + + try { + clientEncrypted.getDatabase("admin").runCommand(new Document("ping", 1)); + + assertFalse(pidFile.exists()); + } finally { + clientEncrypted.close(); + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/MongoCollectionTest.java b/driver-sync/src/test/functional/com/mongodb/client/MongoCollectionTest.java index ae8f0357c43..2421a02633c 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/MongoCollectionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/MongoCollectionTest.java @@ -39,6 +39,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; public class MongoCollectionTest extends DatabaseTestCase { @@ -135,8 +136,8 @@ public void testMapReduceWithGenerics() { List result = collection.mapReduce(mapFunction, reduceFunction, Name.class).into(new ArrayList()); // then - assertEquals(new Name("Pete", 2), result.get(0)); - assertEquals(new Name("Sam", 1), result.get(1)); + assertTrue(result.contains(new Name("Pete", 2))); + assertTrue(result.contains(new Name("Sam", 1))); } @Test