diff --git a/.gitignore b/.gitignore index 02e9bdf0e21..5581f51dc17 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ driver-benchmarks/.factorypath # bin build directories **/bin +docs/superpowers/ diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java index f509f8b3ea3..e39deea3eec 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java @@ -34,6 +34,7 @@ public class EncryptOptions { private String queryType; private RangeOptions rangeOptions; private TextOptions textOptions; + private StringOptions stringOptions; /** * Construct an instance with the given algorithm. @@ -54,12 +55,12 @@ public EncryptOptions(final String algorithm) { *
  • Indexed
  • *
  • Unindexed
  • *
  • Range
  • - *
  • TextPreview
  • + *
  • String
  • * * - *

    The "TextPreview" algorithm is in preview and should be used for experimental workloads only. - * These features are unstable and their security is not guaranteed until released as Generally Available (GA). - * The GA version of these features may not be backwards compatible with the preview version.

    + *

    The "String" algorithm supports Queryable Encryption prefix, suffix, and substring string queries. + * Use the "String" algorithm with query types "prefix"/"suffix"/"substring" (server 9.0+) or the deprecated + * aliases "prefixPreview"/"suffixPreview"/"substringPreview" (server 8.2 to pre-9.0).

    * * @return the encryption algorithm */ @@ -149,8 +150,12 @@ public Long getContentionFactor() { /** * The QueryType. * - *

    Currently, we support only "equality", "range", "prefixPreview", "suffixPreview" or "substringPreview" queryType.

    - *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "TextPreview".

    + *

    Currently, we support only "equality", "range", "prefix", "suffix", "substring", "prefixPreview", + * "suffixPreview" or "substringPreview" queryType.

    + *

    The "prefix", "suffix", "substring", "prefixPreview", "suffixPreview" and "substringPreview" query types are + * only valid with the "String" algorithm. "prefixPreview"/"suffixPreview"/"substringPreview" are deprecated + * aliases supported for servers 8.2 to pre-9.0; use "prefix"/"suffix"/"substring" on server 9.0+.

    + *

    It is an error to set queryType when the algorithm is not "Indexed", "Range" or "String".

    * @param queryType the query type * @return this * @since 4.7 @@ -164,7 +169,7 @@ public EncryptOptions queryType(@Nullable final String queryType) { /** * Gets the QueryType. * - *

    Currently, we support only "equality" or "range" queryType.

    + *

    See {@link #queryType(String)} for the supported query types.

    * @see #queryType(String) * @return the queryType or null * @since 4.7 @@ -205,13 +210,15 @@ public RangeOptions getRangeOptions() { /** * The TextOptions * - *

    It is an error to set TextOptions when the algorithm is not "TextPreview". + *

    It is an error to set TextOptions when the algorithm is not "String". * @param textOptions the text options * @return this * @since 5.6 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link #stringOptions(StringOptions)} instead. */ + @Deprecated @Alpha(Reason.SERVER) public EncryptOptions textOptions(@Nullable final TextOptions textOptions) { this.textOptions = textOptions; @@ -225,13 +232,45 @@ public EncryptOptions textOptions(@Nullable final TextOptions textOptions) { * @since 5.6 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link #getStringOptions()} instead. */ + @Deprecated @Alpha(Reason.SERVER) @Nullable public TextOptions getTextOptions() { return textOptions; } + /** + * The StringOptions + * + *

    It is an error to set StringOptions when the algorithm is not "String". + * @param stringOptions the string options + * @return this + * @since 5.9 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) { + this.stringOptions = stringOptions; + return this; + } + + /** + * Gets the StringOptions + * @see #stringOptions(StringOptions) + * @return the string options or null if not set + * @since 5.9 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ + @Alpha(Reason.SERVER) + @Nullable + public StringOptions getStringOptions() { + return stringOptions; + } + @Override public String toString() { return "EncryptOptions{" @@ -241,6 +280,8 @@ public String toString() { + ", contentionFactor=" + contentionFactor + ", queryType='" + queryType + '\'' + ", rangeOptions=" + rangeOptions + + ", textOptions=" + textOptions + + ", stringOptions=" + stringOptions + '}'; } } diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java new file mode 100644 index 00000000000..98795335082 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java @@ -0,0 +1,186 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.model.vault; + +import com.mongodb.annotations.Alpha; +import com.mongodb.annotations.Reason; +import com.mongodb.lang.Nullable; +import org.bson.BsonDocument; + +/** + * String options for a Queryable Encryption field that supports string queries (prefix, suffix, and substring). + * + *

    Note: StringOptions is in Alpha and subject to backwards breaking changes. + * + * @since 5.9 + * @mongodb.server.release 8.2 + * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + */ +@Alpha(Reason.SERVER) +public class StringOptions { + private boolean caseSensitive; + private boolean diacriticSensitive; + @Nullable + private BsonDocument prefixOptions; + @Nullable + private BsonDocument suffixOptions; + @Nullable + private BsonDocument substringOptions; + + /** + * Construct a new instance + */ + public StringOptions() { + } + + /** + * @return true if string indexes for this field are case sensitive. + */ + public boolean getCaseSensitive() { + return caseSensitive; + } + + /** + * Set case sensitivity + * + * @param caseSensitive true if string indexes are case sensitive + * @return this + */ + public StringOptions caseSensitive(final boolean caseSensitive) { + this.caseSensitive = caseSensitive; + return this; + } + + /** + * @return true if string indexes are diacritic sensitive + */ + public boolean getDiacriticSensitive() { + return diacriticSensitive; + } + + /** + * Set diacritic sensitivity + * + * @param diacriticSensitive true if string indexes are diacritic sensitive + * @return this + */ + public StringOptions diacriticSensitive(final boolean diacriticSensitive) { + this.diacriticSensitive = diacriticSensitive; + return this; + } + + /** + * Set the prefix options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param prefixOptions the prefix options or null + * @return this + */ + public StringOptions prefixOptions(@Nullable final BsonDocument prefixOptions) { + this.prefixOptions = prefixOptions; + return this; + } + + /** + * @see #prefixOptions(BsonDocument) + * @return the prefix options document or null + */ + @Nullable + public BsonDocument getPrefixOptions() { + return prefixOptions; + } + + /** + * Set the suffix options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param suffixOptions the suffix options or null + * @return this + */ + public StringOptions suffixOptions(@Nullable final BsonDocument suffixOptions) { + this.suffixOptions = suffixOptions; + return this; + } + + /** + * @see #suffixOptions(BsonDocument) + * @return the suffix options document or null + */ + @Nullable + public BsonDocument getSuffixOptions() { + return suffixOptions; + } + + /** + * Set the substring options. + * + *

    Expected to be a {@link BsonDocument} in the format of:

    + * + *
    +     * {@code
    +     *   {
    +     *    // strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
    +     *    strMaxLength: BsonInt32,
    +     *    // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
    +     *    strMinQueryLength: BsonInt32,
    +     *    // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
    +     *    strMaxQueryLength: BsonInt32
    +     *   }
    +     * }
    +     * 
    + * + * @param substringOptions the substring options or null + * @return this + */ + public StringOptions substringOptions(@Nullable final BsonDocument substringOptions) { + this.substringOptions = substringOptions; + return this; + } + + /** + * @see #substringOptions(BsonDocument) + * @return the substring options document or null + */ + @Nullable + public BsonDocument getSubstringOptions() { + return substringOptions; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java index 34dcd0d806d..a14ca54378e 100644 --- a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java @@ -29,11 +29,13 @@ * @since 5.6 * @mongodb.server.release 8.2 * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption + * @deprecated Use {@link StringOptions} instead. */ +@Deprecated @Alpha(Reason.SERVER) public class TextOptions { - private Boolean caseSensitive; - private Boolean diacriticSensitive; + private boolean caseSensitive; + private boolean diacriticSensitive; @Nullable private BsonDocument prefixOptions; @Nullable diff --git a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java index 2b472668d98..7a8dab950f4 100644 --- a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java +++ b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java @@ -17,8 +17,10 @@ import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.RangeOptions; +import com.mongodb.client.model.vault.StringOptions; import com.mongodb.client.model.vault.TextOptions; import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions; +import com.mongodb.lang.Nullable; import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonInt32; @@ -73,31 +75,51 @@ public static MongoExplicitEncryptOptions asMongoExplicitEncryptOptions(final En encryptOptionsBuilder.rangeOptions(rangeOptionsBsonDocument); } - TextOptions textOptions = options.getTextOptions(); - if (textOptions != null) { - BsonDocument textOptionsDocument = new BsonDocument(); - textOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(textOptions.getCaseSensitive())); - textOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(textOptions.getDiacriticSensitive())); + StringOptions stringOptions = resolveStringOptions(options); + if (stringOptions != null) { + BsonDocument stringOptionsDocument = new BsonDocument(); + stringOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(stringOptions.getCaseSensitive())); + stringOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(stringOptions.getDiacriticSensitive())); - BsonDocument substringOptions = textOptions.getSubstringOptions(); + BsonDocument substringOptions = stringOptions.getSubstringOptions(); if (substringOptions != null) { - textOptionsDocument.put("substring", substringOptions); + stringOptionsDocument.put("substring", substringOptions); } - BsonDocument prefixOptions = textOptions.getPrefixOptions(); + BsonDocument prefixOptions = stringOptions.getPrefixOptions(); if (prefixOptions != null) { - textOptionsDocument.put("prefix", prefixOptions); + stringOptionsDocument.put("prefix", prefixOptions); } - BsonDocument suffixOptions = textOptions.getSuffixOptions(); + BsonDocument suffixOptions = stringOptions.getSuffixOptions(); if (suffixOptions != null) { - textOptionsDocument.put("suffix", suffixOptions); + stringOptionsDocument.put("suffix", suffixOptions); } - encryptOptionsBuilder.textOptions(textOptionsDocument); + encryptOptionsBuilder.textOptions(stringOptionsDocument); } return encryptOptionsBuilder.build(); } + + @SuppressWarnings("deprecation") + @Nullable + private static StringOptions resolveStringOptions(final EncryptOptions options) { + StringOptions stringOptions = options.getStringOptions(); + if (stringOptions != null) { + return stringOptions; + } + TextOptions textOptions = options.getTextOptions(); + if (textOptions == null) { + return null; + } + return new StringOptions() + .caseSensitive(textOptions.getCaseSensitive()) + .diacriticSensitive(textOptions.getDiacriticSensitive()) + .prefixOptions(textOptions.getPrefixOptions()) + .suffixOptions(textOptions.getSuffixOptions()) + .substringOptions(textOptions.getSubstringOptions()); + } + private EncryptOptionsHelper() { } } diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java new file mode 100644 index 00000000000..2859d728e99 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.model.vault; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class EncryptOptionsTest { + + @Test + void shouldStoreStringOptions() { + StringOptions stringOptions = new StringOptions().caseSensitive(true); + EncryptOptions options = new EncryptOptions("String").stringOptions(stringOptions); + assertSame(stringOptions, options.getStringOptions()); + assertNull(options.getTextOptions()); + } + + @Test + @SuppressWarnings("deprecation") + void shouldStoreDeprecatedTextOptionsIndependently() { + TextOptions textOptions = new TextOptions().caseSensitive(true); + EncryptOptions options = new EncryptOptions("String").textOptions(textOptions); + assertSame(textOptions, options.getTextOptions()); + assertNull(options.getStringOptions()); + } + + @Test + void toStringShouldIncludeStringOptions() { + EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions()); + assertTrue(options.toString().contains("stringOptions=")); + } +} diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java new file mode 100644 index 00000000000..04f20239423 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.model.vault; + +import org.bson.BsonDocument; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StringOptionsTest { + + @Test + void shouldRoundTripAllProperties() { + BsonDocument prefix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); + BsonDocument suffix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"); + BsonDocument substring = BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}"); + + StringOptions options = new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(prefix) + .suffixOptions(suffix) + .substringOptions(substring); + + assertTrue(options.getCaseSensitive()); + assertTrue(options.getDiacriticSensitive()); + assertEquals(prefix, options.getPrefixOptions()); + assertEquals(suffix, options.getSuffixOptions()); + assertEquals(substring, options.getSubstringOptions()); + } + + @Test + void shouldDefaultOptionDocumentsToNull() { + StringOptions options = new StringOptions(); + assertNull(options.getPrefixOptions()); + assertNull(options.getSuffixOptions()); + assertNull(options.getSubstringOptions()); + } + + @Test + void shouldDefaultBooleanFlagsToFalse() { + StringOptions options = new StringOptions(); + assertFalse(options.getCaseSensitive()); + assertFalse(options.getDiacriticSensitive()); + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java new file mode 100644 index 00000000000..65815e668a7 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java @@ -0,0 +1,88 @@ +/* + * 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.internal.client.vault; + +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.StringOptions; +import com.mongodb.client.model.vault.TextOptions; +import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions; +import org.bson.BsonDocument; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class EncryptOptionsHelperTest { + + @Test + void shouldMapStringOptionsToTextOptionsDocument() { + EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(false) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); + + assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: false, " + + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), + result.getTextOptions()); + } + + @Test + @SuppressWarnings("deprecation") + void shouldFallBackToDeprecatedTextOptions() { + EncryptOptions options = new EncryptOptions("String").textOptions(new TextOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); + + assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, " + + "suffix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), + result.getTextOptions()); + } + + @Test + void shouldLeaveTextOptionsNullWhenNeitherSet() { + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions( + new EncryptOptions("Indexed")); + assertNull(result.getTextOptions()); + } + + @Test + @SuppressWarnings("deprecation") + void shouldPreferStringOptionsOverDeprecatedTextOptionsWhenBothSet() { + EncryptOptions options = new EncryptOptions("String") + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))) + .textOptions(new TextOptions() + .caseSensitive(false) + .diacriticSensitive(false) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 5, strMinQueryLength: 1}"))); + + MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options); + + // stringOptions wins: prefix present (from stringOptions), no suffix (from the ignored textOptions), + // and caseSensitive/diacriticSensitive reflect stringOptions (true), not textOptions (false). + assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, " + + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"), + result.getTextOptions()); + } +} diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java similarity index 87% rename from driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java rename to driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java index 2c7eda55aae..d53f4b2c331 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java @@ -18,14 +18,14 @@ import com.mongodb.ClientEncryptionSettings; import com.mongodb.MongoClientSettings; -import com.mongodb.client.AbstractClientEncryptionTextExplicitEncryptionTest; +import com.mongodb.client.AbstractClientEncryptionStringExplicitEncryptionTest; import com.mongodb.client.MongoClient; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.reactivestreams.client.syncadapter.SyncClientEncryption; import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; import com.mongodb.reactivestreams.client.vault.ClientEncryptions; -public class ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest { +public class ClientEncryptionStringExplicitEncryptionTest extends AbstractClientEncryptionStringExplicitEncryptionTest { @Override protected MongoClient createMongoClient(final MongoClientSettings settings) { return new SyncMongoClient(settings); diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java new file mode 100644 index 00000000000..011cf8325fe --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java @@ -0,0 +1,413 @@ +/* + * 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.MongoException; +import com.mongodb.MongoNamespace; +import com.mongodb.WriteConcern; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DropCollectionOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.StringOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.connection.ServerVersion; +import com.mongodb.fixture.EncryptionFixture; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static com.mongodb.ClusterFixture.getDefaultDatabaseName; +import static com.mongodb.ClusterFixture.getMongoCryptVersion; +import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled; +import static com.mongodb.ClusterFixture.isStandalone; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.Fixture.getDefaultDatabase; +import static com.mongodb.client.Fixture.getMongoClient; +import static com.mongodb.client.Fixture.getMongoClientSettings; +import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; +import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static util.JsonPoweredTestHelper.getTestDocument; + +public abstract class AbstractClientEncryptionStringExplicitEncryptionTest { + + private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1)); + // GA "substring" (server 9.0+) additionally needs libmongocrypt 1.20+; the class-level 1.19.1 minimum below + // already covers prefix/suffix GA and the pre-9.0 preview query types. + private static final ServerVersion SUBSTRING_GA_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 20, 0)); + private boolean gaSupported; + private boolean substringGaSupported; + private MongoClient explicitEncryptedClient; + private MongoClient autoEncryptedClient; + private MongoDatabase explicitEncryptedDatabase; + private MongoDatabase autoEncryptedDatabase; + private ClientEncryption clientEncryption; + private BsonBinary key1Id; + + protected abstract MongoClient createMongoClient(MongoClientSettings settings); + protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings); + + @BeforeEach + public void setUp() { + assumeTrue(hasEncryptionTestsEnabled(), "String explicit encryption tests disabled"); + assumeTrue(getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0, "Requires newer MongoCrypt version"); + assumeTrue(serverVersionAtLeast(8, 2)); + assumeFalse(isStandalone()); + + gaSupported = serverVersionAtLeast(9, 0); + substringGaSupported = gaSupported && getMongoCryptVersion().compareTo(SUBSTRING_GA_LIB_MONGOCRYPT_VERSION) >= 0; + + MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); + BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); + + Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); + + if (gaSupported) { + createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json"); + createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json"); + } else { + createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json"); + createEncryptedCollection("substring-preview", "encryptedFields-substring-preview.json"); + } + if (substringGaSupported) { + createEncryptedCollection("substring", "encryptedFields-substring.json"); + createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json"); + } + + MongoCollection dataKeysCollection = getMongoClient() + .getDatabase(dataKeysNamespace.getDatabaseName()) + .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class) + .withWriteConcern(WriteConcern.MAJORITY); + dataKeysCollection.drop(); + dataKeysCollection.insertOne(key1Document); + key1Id = key1Document.getBinary("_id"); + + clientEncryption = createClientEncryption(ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings(getMongoClientSettings()) + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()); + + explicitEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .bypassQueryAnalysis(true) + .build()) + .build()); + explicitEncryptedDatabase = explicitEncryptedClient.getDatabase(getDefaultDatabaseName()) + .withWriteConcern(WriteConcern.MAJORITY); + + autoEncryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()) + .build()); + autoEncryptedDatabase = autoEncryptedClient.getDatabase(getDefaultDatabaseName()) + .withWriteConcern(WriteConcern.MAJORITY); + + // Seed the prefix-suffix collection(s) with an encrypted "foobarbaz" document. + BsonBinary prefixSuffixSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + if (gaSupported) { + explicitEncryptedDatabase.getCollection("prefix-suffix") + .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); + } else { + explicitEncryptedDatabase.getCollection("prefix-suffix-preview") + .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed)); + } + + // Seed the substring collection: GA "substring" on 9.0+/libmongocrypt 1.20+, preview on pre-9.0. + // Skipped on 9.0+ with libmongocrypt < 1.20, where neither path is available. + if (substringGaSupported || !gaSupported) { + BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"), + new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); + explicitEncryptedDatabase.getCollection(gaSupported ? "substring" : "substring-preview") + .insertOne(new Document("_id", 0).append("encryptedText", substringSeed)); + } + } + + @Test + @DisplayName("Case 1: can find a document by prefix") + public void test1CanFindADocumentByPrefix() { + String queryType = gaSupported ? "prefix" : "prefixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForPrefix("foo", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrStartsWith(encrypted)).first(); + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 2: can find a document by suffix") + public void test2CanFindADocumentBySuffix() { + String queryType = gaSupported ? "suffix" : "suffixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForSuffix("baz", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrEndsWith(encrypted)).first(); + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 3: assert no document found by prefix") + public void test3AssertNoDocumentFoundByPrefix() { + String queryType = gaSupported ? "prefix" : "prefixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForPrefix("baz", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrStartsWith(encrypted)).first(); + assertNull(result); + } + + @Test + @DisplayName("Case 4: assert no document found by suffix") + public void test4AssertNoDocumentFoundBySuffix() { + String queryType = gaSupported ? "suffix" : "suffixPreview"; + String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview"; + BsonBinary encrypted = encryptForSuffix("foo", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrEndsWith(encrypted)).first(); + assertNull(result); + } + + @Test + @DisplayName("Case 5: can find a document by substring") + public void test5CanFindADocumentBySubstring() { + // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies. + assumeTrue(substringGaSupported || !gaSupported); + String queryType = gaSupported ? "substring" : "substringPreview"; + String collection = gaSupported ? "substring" : "substring-preview"; + BsonBinary encrypted = encryptForSubstring("bar", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrContains(encrypted)).first(); + assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); + } + + @Test + @DisplayName("Case 6: assert no document found by substring") + public void test6AssertNoDocumentFoundBySubstring() { + // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies. + assumeTrue(substringGaSupported || !gaSupported); + String queryType = gaSupported ? "substring" : "substringPreview"; + String collection = gaSupported ? "substring" : "substring-preview"; + BsonBinary encrypted = encryptForSubstring("qux", queryType, true, true); + Document result = explicitEncryptedDatabase.getCollection(collection) + .find(encStrContains(encrypted)).first(); + assertNull(result); + } + + @Test + @DisplayName("Case 7: assert `contentionFactor` is required") + public void test7AssertContentionFactorIsRequired() { + assumeTrue(gaSupported); + EncryptOptions encryptOptions = new EncryptOptions("String") + .keyId(key1Id) + .queryType("prefix") + .stringOptions(new StringOptions() + .caseSensitive(true) + .diacriticSensitive(true) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))); + MongoException exception = assertThrows(MongoException.class, + () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions)); + assertTrue(exception.getMessage().contains("contention factor is required for string algorithm")); + } + + @Test + @DisplayName("Case 8: can find an auto-encrypted case-insensitively indexed document by prefix and suffix") + public void test8AutoEncryptedCaseInsensitivePrefixAndSuffix() { + assumeTrue(gaSupported); + autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .insertOne(new Document("encryptedText", "BingQiLin")); + + BsonBinary prefix = encryptForPrefix("bing", "prefix", false, false); + Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrStartsWith(prefix)).first(); + assertEncryptedTextEquals("BingQiLin", byPrefix); + + BsonBinary suffix = encryptForSuffix("lin", "suffix", false, false); + Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrEndsWith(suffix)).first(); + assertEncryptedTextEquals("BingQiLin", bySuffix); + } + + @Test + @DisplayName("Case 9: can find an auto-encrypted diacritic-insensitively indexed document by prefix and suffix") + public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() { + assumeTrue(gaSupported); + autoEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .insertOne(new Document("encryptedText", "cafébarbäz")); + + BsonBinary prefix = encryptForPrefix("cafe", "prefix", false, false); + Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrStartsWith(prefix)).first(); + assertEncryptedTextEquals("cafébarbäz", byPrefix); + + BsonBinary suffix = encryptForSuffix("baz", "suffix", false, false); + Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di") + .find(encStrEndsWith(suffix)).first(); + assertEncryptedTextEquals("cafébarbäz", bySuffix); + } + + @Test + @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring") + public void test10AutoEncryptedCaseInsensitiveSubstring() { + // GA substring auto-encryption requires server 9.0+ and libmongocrypt 1.20+, like the prefix/suffix ci-di + // cases 8/9. The substring-ci-di collection is created only under those conditions, so this case runs there. + // TODO JAVA-6244 the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup + // requirement; running on 9.0+ pending upstream spec clarification (DRIVERS-3540). + assumeTrue(substringGaSupported); + autoEncryptedDatabase.getCollection("substring-ci-di") + .insertOne(new Document("encryptedText", "FooBarBaz")); + + BsonBinary substring = encryptForSubstring("bar", "substring", false, false); + Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") + .find(encStrContains(substring)).first(); + assertEncryptedTextEquals("FooBarBaz", result); + } + + @Test + @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring") + public void test11AutoEncryptedDiacriticInsensitiveSubstring() { + // Runs on server 9.0+ with GA substring (libmongocrypt 1.20+), like Case 10 (see its note). + assumeTrue(substringGaSupported); + autoEncryptedDatabase.getCollection("substring-ci-di") + .insertOne(new Document("encryptedText", "foocafébaz")); + + BsonBinary substring = encryptForSubstring("cafe", "substring", false, false); + Document result = explicitEncryptedDatabase.getCollection("substring-ci-di") + .find(encStrContains(substring)).first(); + assertEncryptedTextEquals("foocafébaz", result); + } + + @AfterEach + @SuppressWarnings("try") + public void cleanUp() { + getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop(); + try (ClientEncryption ignored = this.clientEncryption; + MongoClient ignored1 = this.explicitEncryptedClient; + MongoClient ignored2 = this.autoEncryptedClient + ) { + // just using try-with-resources to ensure they all get closed, even in the case of exceptions + } + } + + private void createEncryptedCollection(final String name, final String encryptedFieldsFile) { + BsonDocument encryptedFields = bsonDocumentFromPath(encryptedFieldsFile); + MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY); + database.getCollection(name).drop(new DropCollectionOptions().encryptedFields(encryptedFields)); + database.createCollection(name, new CreateCollectionOptions().encryptedFields(encryptedFields)); + } + + private BsonBinary encryptForPrefix(final String value, final String queryType, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType(queryType) + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private BsonBinary encryptForSuffix(final String value, final String queryType, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType(queryType) + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))); + } + + private BsonBinary encryptForSubstring(final String value, final String queryType, + final boolean caseSensitive, final boolean diacriticSensitive) { + return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String") + .keyId(key1Id) + .contentionFactor(0L) + .queryType(queryType) + .stringOptions(new StringOptions() + .caseSensitive(caseSensitive) + .diacriticSensitive(diacriticSensitive) + .substringOptions(BsonDocument.parse( + "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}")))); + } + + private static Document encStrStartsWith(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrStartsWith", + new Document("input", "$encryptedText").append("prefix", encrypted))); + } + + private static Document encStrEndsWith(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrEndsWith", + new Document("input", "$encryptedText").append("suffix", encrypted))); + } + + private static Document encStrContains(final BsonBinary encrypted) { + return new Document("$expr", new Document("$encStrContains", + new Document("input", "$encryptedText").append("substring", encrypted))); + } + + private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) { + actualDocument.remove("__safeContent__"); + assertEquals(expectedDocument, actualDocument); + } + + private static void assertEncryptedTextEquals(final String expectedText, final Document actualDocument) { + assertNotNull(actualDocument); + assertEquals(expectedText, actualDocument.getString("encryptedText")); + } + + private static BsonDocument bsonDocumentFromPath(final String path) { + return getTestDocument("client-side-encryption/etc/data/" + path); + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java deleted file mode 100644 index 1677e49a66f..00000000000 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * 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.MongoException; -import com.mongodb.MongoNamespace; -import com.mongodb.WriteConcern; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.DropCollectionOptions; -import com.mongodb.client.model.vault.EncryptOptions; -import com.mongodb.client.model.vault.TextOptions; -import com.mongodb.client.vault.ClientEncryption; -import com.mongodb.connection.ServerVersion; -import com.mongodb.fixture.EncryptionFixture; -import org.bson.BsonBinary; -import org.bson.BsonDocument; -import org.bson.BsonString; -import org.bson.Document; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static com.mongodb.ClusterFixture.getDefaultDatabaseName; -import static com.mongodb.ClusterFixture.getMongoCryptVersion; -import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled; -import static com.mongodb.ClusterFixture.isStandalone; -import static com.mongodb.ClusterFixture.serverVersionAtLeast; -import static com.mongodb.client.Fixture.getDefaultDatabase; -import static com.mongodb.client.Fixture.getMongoClient; -import static com.mongodb.client.Fixture.getMongoClientSettings; -import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; -import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; -import static java.util.Arrays.asList; -import static org.junit.Assume.assumeTrue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static util.JsonPoweredTestHelper.getTestDocument; - -public abstract class AbstractClientEncryptionTextExplicitEncryptionTest { - - private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 15, 1)); - private MongoClient encryptedClient; - private MongoDatabase encryptedDatabase; - private ClientEncryption clientEncryption; - private BsonBinary key1Id; - - protected abstract MongoClient createMongoClient(MongoClientSettings settings); - protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings); - - - @BeforeEach - public void setUp() { - assumeTrue("Text explicit encryption tests disabled", hasEncryptionTestsEnabled()); - assumeTrue("Requires newer MongoCrypt version", getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0); - assumeTrue(serverVersionAtLeast(8, 2)); - // TODO-JAVA-6168 update prose tests for post 9.0 - assumeTrue(!serverVersionAtLeast(9, 0)); - assumeFalse(isStandalone()); - - MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys"); - BsonDocument encryptedFieldsPrefixSuffix = bsonDocumentFromPath("encryptedFields-prefix-suffix.json"); - BsonDocument encryptedFieldsSubstring = bsonDocumentFromPath("encryptedFields-substring.json"); - BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json"); - - MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY); - database.getCollection("prefix-suffix") - .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix)); - database.createCollection("prefix-suffix", - new CreateCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix)); - - database.getCollection("substring") - .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsSubstring)); - database.createCollection("substring", - new CreateCollectionOptions().encryptedFields(encryptedFieldsSubstring)); - - MongoCollection dataKeysCollection = getMongoClient() - .getDatabase(dataKeysNamespace.getDatabaseName()) - .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class) - .withWriteConcern(WriteConcern.MAJORITY); - - dataKeysCollection.drop(); - dataKeysCollection.insertOne(key1Document); - key1Id = key1Document.getBinary("_id"); - - Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); - - clientEncryption = createClientEncryption(ClientEncryptionSettings.builder() - .keyVaultMongoClientSettings(getMongoClientSettings()) - .keyVaultNamespace(dataKeysNamespace.getFullName()) - .kmsProviders(kmsProviders) - .build()); - - encryptedClient = createMongoClient(getMongoClientSettingsBuilder() - .autoEncryptionSettings( - AutoEncryptionSettings.builder() - .keyVaultNamespace(dataKeysNamespace.getFullName()) - .kmsProviders(kmsProviders) - .bypassQueryAnalysis(true) - .build()) - .build()); - - encryptedDatabase = encryptedClient.getDatabase(getDefaultDatabaseName()).withWriteConcern(WriteConcern.MAJORITY); - - EncryptOptions prefixSuffixEncryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), prefixSuffixEncryptOptions); - - encryptedDatabase - .getCollection("prefix-suffix") - .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz)); - - EncryptOptions substringEncryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), substringEncryptOptions); - - encryptedDatabase - .getCollection("substring") - .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz)); - } - - @Test - @DisplayName("Case 1: can find a document by prefix") - public void test1CanFindADocumentByPrefix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("prefixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrStartsWith", - new Document("input", "$encryptedText").append("prefix", encrypted)))).first(); - - assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); - } - - @Test - @DisplayName("Case 2: can find a document by suffix") - public void test2CanFindADocumentBySuffix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("suffixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrEndsWith", - new Document("input", "$encryptedText").append("suffix", encrypted)))).first(); - - assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); - } - - @Test - @DisplayName("Case 3: assert no document found by prefix") - public void test3AssertNoDocumentFoundByPrefix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("prefixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrStartsWith", - new Document("input", "$encryptedText").append("prefix", encrypted)))).first(); - - assertNull(result); - } - - @Test - @DisplayName("Case 4: assert no document found by suffix") - public void test4AssertNoDocumentFoundByPrefix() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("suffixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions); - Document result = encryptedDatabase.getCollection("prefix-suffix") - .find(new Document("$expr", - new Document("$encStrEndsWith", - new Document("input", "$encryptedText").append("suffix", encrypted)))).first(); - - assertNull(result); - } - - @Test - @DisplayName("Case 5: can find a document by substring") - public void test5CanFindADocumentBySubstring() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("substringPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("bar"), encryptOptions); - Document result = encryptedDatabase.getCollection("substring") - .find(new Document("$expr", - new Document("$encStrContains", - new Document("input", "$encryptedText").append("substring", encrypted)))).first(); - - assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result); - } - - @Test - @DisplayName("Case 6: assert no document found by substring") - public void test6AssertNoDocumentFoundBySubstring() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .contentionFactor(0L) - .queryType("substringPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - - BsonBinary encrypted = clientEncryption.encrypt(new BsonString("qux"), encryptOptions); - Document result = encryptedDatabase.getCollection("substring") - .find(new Document("$expr", - new Document("$encStrContains", - new Document("input", "$encryptedText").append("substring", encrypted)))).first(); - - assertNull(result); - } - - @Test - @DisplayName("Case 7: assert `contentionFactor` is required") - public void test7AssertContentionFactorIsRequired() { - EncryptOptions encryptOptions = new EncryptOptions("TextPreview") - .keyId(key1Id) - .queryType("prefixPreview") - .textOptions(new TextOptions() - .caseSensitive(true) - .diacriticSensitive(true) - .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")) - ); - MongoException exception = assertThrows(MongoException.class, () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions)); - assertTrue(exception.getMessage().contains("contention factor is required for textPreview algorithm")); - } - - - @AfterEach - @SuppressWarnings("try") - public void cleanUp() { - //noinspection EmptyTryBlock - getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop(); - try (ClientEncryption ignored = this.clientEncryption; - MongoClient ignored1 = this.encryptedClient - ) { - // just using try-with-resources to ensure they all get closed, even in the case of exceptions - } - } - - private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) { - actualDocument.remove("__safeContent__"); - assertEquals(expectedDocument, actualDocument); - } - - private static BsonDocument bsonDocumentFromPath(final String path) { - return getTestDocument("client-side-encryption/etc/data/" + path); - } -} diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java similarity index 90% rename from driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java rename to driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java index 23bd9ec135d..e0614a0d450 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java @@ -21,7 +21,7 @@ import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryptions; -public class ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest { +public class ClientEncryptionStringExplicitEncryptionTest extends AbstractClientEncryptionStringExplicitEncryptionTest { @Override protected MongoClient createMongoClient(final MongoClientSettings settings) { return MongoClients.create(settings); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index 7b432acd050..15f62c58ee1 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java @@ -63,9 +63,6 @@ public static void applyCustomizations(final TestDef def) { // Client side encryption (QE) def.skipJira("https://jira.mongodb.org/browse/JAVA-5675 Support QE with Client.bulkWrite") .file("client-side-encryption/tests/unified", "client bulkWrite with queryable encryption"); - def.skipJira("https://jira.mongodb.org/browse/JAVA-6244 QE GA \"substring\" query type is not yet " - + "implemented (DRIVERS-3540)") - .file("client-side-encryption/tests/unified", "QE-Text-substring"); // client-side-operation-timeout (CSOT) // The expected change stream timeout-refresh behaviour is unspecified on server 9.0+ (DRIVERS-3006), so the diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 6a46f2dfc9a..208034beaad 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -17,6 +17,7 @@ import ProjectExtensions.configureJarManifest import ProjectExtensions.configureMavenPublication import de.undercouch.gradle.tasks.download.Download import java.io.ByteArrayOutputStream +import java.io.File import javax.inject.Inject import org.gradle.api.GradleException import org.gradle.process.ExecOperations @@ -66,7 +67,7 @@ val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath) // Download the libmongocrypt per-platform tarballs (and their signatures) to jnaDownloadsDir. // To upgrade: change downloadRevision, run `./gradlew clean downloadJnaLibs`, and verify the build. -val downloadRevision = "1.18.1" +val downloadRevision = "1.20.0" val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/download/$downloadRevision" /** @@ -75,7 +76,7 @@ val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/downloa * layout differ per platform, so both must be tracked explicitly. * * libmongocrypt's signature assets replace the `.tar.gz` suffix with `.asc` (e.g. - * `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-1.18.1.asc`). + * `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-.asc`). */ data class CryptBinary(val jnaPlatform: String, val tarball: String, val libPathInTarball: String) { val signature: String = tarball.removeSuffix(".tar.gz") + ".asc" @@ -137,8 +138,9 @@ tasks.register("downloadCryptLibs") { * Per DRIVERS-3441, drivers that bundle libmongocrypt must verify GPG signatures of * release tarballs against the official MongoDB libmongocrypt signing key. * - * The keyring is kept under `build/` so this task does not touch the developer's - * system GPG keyring and so `./gradlew clean` resets the trust state. + * The task uses a scratch keyring (not the developer's system GPG keyring), which {@code verify()} + * recreates on every run so the trust state is always reset. It lives under the system temp dir + * rather than {@code build/} — see the {@code gnupgHome} assignment in {@code verifyCryptLibs} for why. */ val skipCryptVerify = providers.gradleProperty("skipCryptVerify").map { it.toBoolean() }.orElse(false) @@ -275,7 +277,16 @@ tasks.register("verifyCryptLibs") { publicKey.set(file("$jnaDownloadsDir/$libmongocryptPublicKeyFile")) skipVerify.set(skipCryptVerify) expectedFingerprint.set("F2F5BF4ABF517E039AFCADAA81F1404DEBACA586") - gnupgHome.set(layout.buildDirectory.dir("jnaLibs/gnupg")) + // Keep the scratch GPG keyring under the system temp dir, not the module build dir. gpg derives + // its agent socket path from the homedir, and macOS caps AF_UNIX socket paths (sun_path) at 104 + // bytes. A deeply-nested checkout pushes "/build/jnaLibs/gnupg/S.gpg-agent" past that + // limit, so on a fresh keyring gpg fails ("can't connect to the gpg-agent: File name too long") + // and exits non-zero even though the import/verify would otherwise succeed. A short, + // checkout-independent homedir avoids this; the hash suffix isolates builds of different + // checkouts. verify() recreates this directory on every run, so a stable name is safe. + val cryptGpgHomeName = + "crypt-gpg-" + layout.buildDirectory.get().asFile.absolutePath.hashCode().toUInt().toString(16) + gnupgHome.set(layout.dir(providers.provider { File(System.getProperty("java.io.tmpdir"), cryptGpgHomeName) })) verificationStamp.set(layout.buildDirectory.file("jnaLibs/verified.stamp")) /* Bypass entirely when the caller has supplied a local libmongocrypt directory. */ diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java index c08608ca595..8b37549ca37 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java @@ -87,7 +87,7 @@ public Builder algorithm(final String algorithm) { /** * The contention factor. * - *

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    + *

    Only applies when algorithm is "Indexed", "Range", or "String".

    * @param contentionFactor the contention factor * @return this * @since 1.5 @@ -100,7 +100,7 @@ public Builder contentionFactor(final Long contentionFactor) { /** * The QueryType. * - *

    Only applies when algorithm is "Indexed", "Range", or "TextPreview".

    + *

    Only applies when algorithm is "Indexed", "Range", or "String".

    * * @param queryType the query type * @return this @@ -126,11 +126,12 @@ public Builder rangeOptions(final BsonDocument rangeOptions) { } /** - * The Text Options. + * The String Options. * - *

    Only applies when algorithm is "TextPreview".

    + *

    Only applies when algorithm is "String". The method name mirrors the libmongocrypt {@code textOptions} + * BSON field, which is unchanged.

    * - * @param textOptions the text options + * @param textOptions the string options, as a BSON document * @return this * @since 5.6 */