diff --git a/bindings/python/pymongocrypt/asynchronous/explicit_encrypter.py b/bindings/python/pymongocrypt/asynchronous/explicit_encrypter.py index dcab42d82..33abd2f09 100644 --- a/bindings/python/pymongocrypt/asynchronous/explicit_encrypter.py +++ b/bindings/python/pymongocrypt/asynchronous/explicit_encrypter.py @@ -93,6 +93,7 @@ async def encrypt( contention_factor=None, range_opts=None, is_expression=False, + text_opts=None, ): """Encrypts a BSON value. @@ -114,6 +115,8 @@ async def encrypt( with the "range" algorithm encoded as a BSON document. - `is_expression` (boolean): True if this is an encryptExpression() context. Defaults to False. + - `text_opts` (bytes): Options for explicit encryption + with the "textPreview" algorithm encoded as a BSON document. :Returns: The encrypted BSON value. @@ -122,6 +125,8 @@ async def encrypt( Added the `query_type` and `contention_factor` parameters. .. versionchanged:: 1.5 Added the `range_opts` and `is_expression` parameters. + .. versionchanged:: 1.16 + Added the `text_opts` parameter. """ # CDRIVER-3275 key_alt_name needs to be wrapped in a bson document. if key_alt_name is not None: @@ -134,6 +139,7 @@ async def encrypt( contention_factor, range_opts, is_expression, + text_opts, ) with self.mongocrypt.explicit_encryption_context(value, opts) as ctx: return await run_state_machine(ctx, self.callback) diff --git a/bindings/python/pymongocrypt/mongocrypt.py b/bindings/python/pymongocrypt/mongocrypt.py index 608d39e32..8202e1959 100644 --- a/bindings/python/pymongocrypt/mongocrypt.py +++ b/bindings/python/pymongocrypt/mongocrypt.py @@ -501,6 +501,11 @@ def __init__(self, ctx, kms_providers, value, opts): ): self._raise_from_status() + if opts.text_opts is not None: + with MongoCryptBinaryIn(opts.text_opts) as text_opts: + if not lib.mongocrypt_ctx_setopt_algorithm_text(ctx, text_opts.bin): + self._raise_from_status() + with MongoCryptBinaryIn(value) as binary: if opts.is_expression: if not lib.mongocrypt_ctx_explicit_encrypt_expression_init( diff --git a/bindings/python/pymongocrypt/options.py b/bindings/python/pymongocrypt/options.py index c1e27b1d5..f61372f23 100644 --- a/bindings/python/pymongocrypt/options.py +++ b/bindings/python/pymongocrypt/options.py @@ -170,6 +170,7 @@ def __init__( contention_factor=None, range_opts=None, is_expression=False, + text_opts=None, ): """Options for explicit encryption. @@ -186,11 +187,15 @@ def __init__( with the "range" algorithm encoded as a BSON document. - `is_expression` (boolean): True if this is an encryptExpression() context. Defaults to False. + - `text_opts` (bytes): Options for explicit encryption + with the "textPreview" algorithm encoded as a BSON document. .. versionchanged:: 1.3 Added the `query_type` and `contention_factor` parameters. .. versionchanged:: 1.5 Added the `range_opts` and `is_expression` parameters. + .. versionchanged:: 1.16 + Added the `text_opts` parameter. """ self.algorithm = algorithm self.key_id = key_id @@ -212,6 +217,11 @@ def __init__( ) self.range_opts = range_opts self.is_expression = is_expression + if text_opts is not None and not isinstance(text_opts, bytes): + raise TypeError( + f"text_opts must be an bytes or None, not: {type(text_opts)}" + ) + self.text_opts = text_opts class DataKeyOpts: diff --git a/bindings/python/pymongocrypt/synchronous/explicit_encrypter.py b/bindings/python/pymongocrypt/synchronous/explicit_encrypter.py index f9095c5f0..846ee647f 100644 --- a/bindings/python/pymongocrypt/synchronous/explicit_encrypter.py +++ b/bindings/python/pymongocrypt/synchronous/explicit_encrypter.py @@ -93,6 +93,7 @@ def encrypt( contention_factor=None, range_opts=None, is_expression=False, + text_opts=None, ): """Encrypts a BSON value. @@ -114,6 +115,8 @@ def encrypt( with the "range" algorithm encoded as a BSON document. - `is_expression` (boolean): True if this is an encryptExpression() context. Defaults to False. + - `text_opts` (bytes): Options for explicit encryption + with the "textPreview" algorithm encoded as a BSON document. :Returns: The encrypted BSON value. @@ -122,6 +125,8 @@ def encrypt( Added the `query_type` and `contention_factor` parameters. .. versionchanged:: 1.5 Added the `range_opts` and `is_expression` parameters. + .. versionchanged:: 1.16 + Added the `text_opts` parameter. """ # CDRIVER-3275 key_alt_name needs to be wrapped in a bson document. if key_alt_name is not None: @@ -134,6 +139,7 @@ def encrypt( contention_factor, range_opts, is_expression, + text_opts, ) with self.mongocrypt.explicit_encryption_context(value, opts) as ctx: return run_state_machine(ctx, self.callback) diff --git a/bindings/python/test/data/fle2-text-search/encrypted-payload.json b/bindings/python/test/data/fle2-text-search/encrypted-payload.json new file mode 100644 index 000000000..6d3929229 --- /dev/null +++ b/bindings/python/test/data/fle2-text-search/encrypted-payload.json @@ -0,0 +1,6 @@ +{ + "v": { + "$binary": "EqQAAAADdHMAhQAAAANlAH0AAAAFZAAgAAAAACmNEIGYF35VPFzmyWuvaCXPAVtXyzAMQ3fdWSFJ3Ji9BXMAIAAAAAA3qH8Y7MnTiDaHYF8L84k4YsWj2IP25sY5lBUo5s2aOgVsACAAAAAAVdUcnSWSe5b9XPo4L/3LELFwsT0PwiNBPYNs3UIhEWQAABJjbQAAAAAAAAAAAAhjZgABCGRmAAEA", + "$type": "06" + } +} diff --git a/bindings/python/test/data/fle2-text-search/textopts.json b/bindings/python/test/data/fle2-text-search/textopts.json new file mode 100644 index 000000000..e16581ea5 --- /dev/null +++ b/bindings/python/test/data/fle2-text-search/textopts.json @@ -0,0 +1,8 @@ +{ + "caseSensitive": true, + "diacriticSensitive": true, + "prefix": { + "strMaxQueryLength": 10, + "strMinQueryLength": 2 + } +} diff --git a/bindings/python/test/test_mongocrypt.py b/bindings/python/test/test_mongocrypt.py index f55412306..6a3be5cb9 100644 --- a/bindings/python/test/test_mongocrypt.py +++ b/bindings/python/test/test_mongocrypt.py @@ -38,6 +38,7 @@ import unittest.mock import respx +from pymongo import MongoClient from pymongo_auth_aws.auth import AwsCredential from pymongocrypt.asynchronous.auto_encrypter import AsyncAutoEncrypter @@ -1013,6 +1014,30 @@ async def test_range_query_int32(self): encrypted_val, adjust_range_counter(encrypted_val, expected) ) + async def test_text_query(self): + key_path = "keys/ABCDEFAB123498761234123456789012-local-document.json" + key_id = json_data(key_path)["_id"] + encrypter = AsyncExplicitEncrypter( + MockAsyncCallback( + key_docs=[bson_data(key_path)], kms_reply=http_data("kms-reply.txt") + ), + self.mongo_crypt_opts(), + ) + self.addCleanup(encrypter.close) + + text_opts = bson_data("fle2-text-search/textopts.json") + expected = bson_data("fle2-text-search/encrypted-payload.json") + value = bson.encode({"v": "foo"}) + encrypted = await encrypter.encrypt( + value, + "textPreview", + key_id=key_id, + query_type="suffixPreview", + contention_factor=0, + text_opts=text_opts, + ) + self.assertEqual(encrypted, expected) + class TestNeedKMSAzureCredentials(unittest.TestCase): maxDiff = None @@ -1459,6 +1484,30 @@ def test_rangePreview_query_int32(self): is_expression=True, ) + def test_text_query(self): + key_path = "keys/ABCDEFAB123498761234123456789012-local-document.json" + key_id = json_data(key_path)["_id"] + encrypter = ExplicitEncrypter( + MockCallback( + key_docs=[bson_data(key_path)], kms_reply=http_data("kms-reply.txt") + ), + self.mongo_crypt_opts(), + ) + self.addCleanup(encrypter.close) + + text_opts = bson_data("fle2-text-search/textopts.json") + expected = bson_data("fle2-text-search/encrypted-payload.json") + value = bson.encode({"v": "foo"}) + encrypted = encrypter.encrypt( + value, + "textPreview", + key_id=key_id, + query_type="suffixPreview", + contention_factor=0, + text_opts=text_opts, + ) + self.assertEqual(encrypted, expected) + def read(filename, **kwargs): with open(os.path.join(DATA_DIR, filename), **kwargs) as fp: