Merge branch 'master' of github.com:mongodb/mongo-python-driver
This commit is contained in:
commit
7ae185a425
@ -4,6 +4,13 @@ Changes in Version 4.15.0 (XXXX/XX/XX)
|
||||
--------------------------------------
|
||||
PyMongo 4.15 brings a number of changes including:
|
||||
|
||||
- Added :class:`~pymongo.encryption_options.TextOpts`,
|
||||
:attr:`~pymongo.encryption.Algorithm.TEXTPREVIEW`,
|
||||
:attr:`~pymongo.encryption.QueryType.PREFIXPREVIEW`,
|
||||
:attr:`~pymongo.encryption.QueryType.SUFFIXPREVIEW`,
|
||||
:attr:`~pymongo.encryption.QueryType.SUBSTRINGPREVIEW`,
|
||||
as part of the experimental Queryable Encryption text queries beta.
|
||||
``pymongocrypt>=1.16`` is required for text query support.
|
||||
- Added :class:`bson.decimal128.DecimalEncoder` and :class:`bson.decimal128.DecimalDecoder`
|
||||
to support encoding and decoding of BSON Decimal128 values to decimal.Decimal values using the TypeRegistry API.
|
||||
|
||||
|
||||
13
justfile
13
justfile
@ -1,10 +1,11 @@
|
||||
# See https://just.systems/man/en/ for instructions
|
||||
set shell := ["bash", "-c"]
|
||||
# Do not modify the lock file when running justfile commands.
|
||||
export UV_FROZEN := "1"
|
||||
|
||||
# Commonly used command segments.
|
||||
uv_run := "uv run --frozen "
|
||||
typing_run := uv_run + "--group typing --extra aws --extra encryption --extra ocsp --extra snappy --extra test --extra zstd"
|
||||
docs_run := uv_run + "--extra docs"
|
||||
typing_run := "uv run --group typing --extra aws --extra encryption --extra ocsp --extra snappy --extra test --extra zstd"
|
||||
docs_run := "uv run --extra docs"
|
||||
doc_build := "./doc/_build"
|
||||
mypy_args := "--install-types --non-interactive"
|
||||
|
||||
@ -50,15 +51,15 @@ typing-pyright: && resync
|
||||
|
||||
[group('lint')]
|
||||
lint: && resync
|
||||
{{uv_run}} pre-commit run --all-files
|
||||
uv run pre-commit run --all-files
|
||||
|
||||
[group('lint')]
|
||||
lint-manual: && resync
|
||||
{{uv_run}} pre-commit run --all-files --hook-stage manual
|
||||
uv run pre-commit run --all-files --hook-stage manual
|
||||
|
||||
[group('test')]
|
||||
test *args="-v --durations=5 --maxfail=10": && resync
|
||||
{{uv_run}} --extra test pytest {{args}}
|
||||
uv run --extra test pytest {{args}}
|
||||
|
||||
[group('test')]
|
||||
run-tests *args: && resync
|
||||
|
||||
@ -67,7 +67,7 @@ from pymongo.asynchronous.mongo_client import AsyncMongoClient
|
||||
from pymongo.asynchronous.pool import AsyncBaseConnection
|
||||
from pymongo.common import CONNECT_TIMEOUT
|
||||
from pymongo.daemon import _spawn_daemon
|
||||
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
|
||||
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
|
||||
from pymongo.errors import (
|
||||
ConfigurationError,
|
||||
EncryptedCollectionError,
|
||||
@ -516,6 +516,11 @@ class Algorithm(str, enum.Enum):
|
||||
|
||||
.. versionadded:: 4.4
|
||||
"""
|
||||
TEXTPREVIEW = "TextPreview"
|
||||
"""**BETA** - TextPreview.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
|
||||
class QueryType(str, enum.Enum):
|
||||
@ -541,6 +546,24 @@ class QueryType(str, enum.Enum):
|
||||
.. versionadded:: 4.4
|
||||
"""
|
||||
|
||||
PREFIXPREVIEW = "prefixPreview"
|
||||
"""**BETA** - Used to encrypt a value for a prefixPreview query.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
SUFFIXPREVIEW = "suffixPreview"
|
||||
"""**BETA** - Used to encrypt a value for a suffixPreview query.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
SUBSTRINGPREVIEW = "substringPreview"
|
||||
"""**BETA** - Used to encrypt a value for a substringPreview query.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
|
||||
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
|
||||
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
|
||||
@ -876,6 +899,7 @@ class AsyncClientEncryption(Generic[_DocumentType]):
|
||||
contention_factor: Optional[int] = None,
|
||||
range_opts: Optional[RangeOpts] = None,
|
||||
is_expression: bool = False,
|
||||
text_opts: Optional[TextOpts] = None,
|
||||
) -> Any:
|
||||
self._check_closed()
|
||||
if isinstance(key_id, uuid.UUID):
|
||||
@ -895,6 +919,12 @@ class AsyncClientEncryption(Generic[_DocumentType]):
|
||||
range_opts.document,
|
||||
codec_options=self._codec_options,
|
||||
)
|
||||
text_opts_bytes = None
|
||||
if text_opts:
|
||||
text_opts_bytes = encode(
|
||||
text_opts.document,
|
||||
codec_options=self._codec_options,
|
||||
)
|
||||
with _wrap_encryption_errors():
|
||||
encrypted_doc = await self._encryption.encrypt(
|
||||
value=doc,
|
||||
@ -905,6 +935,7 @@ class AsyncClientEncryption(Generic[_DocumentType]):
|
||||
contention_factor=contention_factor,
|
||||
range_opts=range_opts_bytes,
|
||||
is_expression=is_expression,
|
||||
text_opts=text_opts_bytes,
|
||||
)
|
||||
return decode(encrypted_doc)["v"]
|
||||
|
||||
@ -917,6 +948,7 @@ class AsyncClientEncryption(Generic[_DocumentType]):
|
||||
query_type: Optional[str] = None,
|
||||
contention_factor: Optional[int] = None,
|
||||
range_opts: Optional[RangeOpts] = None,
|
||||
text_opts: Optional[TextOpts] = None,
|
||||
) -> Binary:
|
||||
"""Encrypt a BSON value with a given key and algorithm.
|
||||
|
||||
@ -937,9 +969,14 @@ class AsyncClientEncryption(Generic[_DocumentType]):
|
||||
used.
|
||||
:param range_opts: Index options for `range` queries. See
|
||||
:class:`RangeOpts` for some valid options.
|
||||
:param text_opts: Index options for `textPreview` queries. See
|
||||
:class:`TextOpts` for some valid options.
|
||||
|
||||
:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
|
||||
|
||||
.. versionchanged:: 4.9
|
||||
Added the `text_opts` parameter.
|
||||
|
||||
.. versionchanged:: 4.9
|
||||
Added the `range_opts` parameter.
|
||||
|
||||
@ -960,6 +997,7 @@ class AsyncClientEncryption(Generic[_DocumentType]):
|
||||
contention_factor=contention_factor,
|
||||
range_opts=range_opts,
|
||||
is_expression=False,
|
||||
text_opts=text_opts,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Mapping, Optional
|
||||
from typing import TYPE_CHECKING, Any, Mapping, Optional, TypedDict
|
||||
|
||||
from pymongo.uri_parser_shared import _parse_kms_tls_options
|
||||
|
||||
@ -295,3 +295,85 @@ class RangeOpts:
|
||||
if v is not None:
|
||||
doc[k] = v
|
||||
return doc
|
||||
|
||||
|
||||
class TextOpts:
|
||||
"""**BETA** Options to configure encrypted queries using the text algorithm.
|
||||
|
||||
TextOpts is currently unstable API and subject to backwards breaking changes."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
substring: Optional[SubstringOpts] = None,
|
||||
prefix: Optional[PrefixOpts] = None,
|
||||
suffix: Optional[SuffixOpts] = None,
|
||||
case_sensitive: Optional[bool] = None,
|
||||
diacritic_sensitive: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""Options to configure encrypted queries using the text algorithm.
|
||||
|
||||
:param substring: Further options to support substring queries.
|
||||
:param prefix: Further options to support prefix queries.
|
||||
:param suffix: Further options to support suffix queries.
|
||||
:param case_sensitive: Whether text indexes for this field are case sensitive.
|
||||
:param diacritic_sensitive: Whether text indexes for this field are diacritic sensitive.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
self.substring = substring
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
self.case_sensitive = case_sensitive
|
||||
self.diacritic_sensitive = diacritic_sensitive
|
||||
|
||||
@property
|
||||
def document(self) -> dict[str, Any]:
|
||||
doc = {}
|
||||
for k, v in [
|
||||
("substring", self.substring),
|
||||
("prefix", self.prefix),
|
||||
("suffix", self.suffix),
|
||||
("caseSensitive", self.case_sensitive),
|
||||
("diacriticSensitive", self.diacritic_sensitive),
|
||||
]:
|
||||
if v is not None:
|
||||
doc[k] = v
|
||||
return doc
|
||||
|
||||
|
||||
class SubstringOpts(TypedDict):
|
||||
"""**BETA** Options for substring text queries.
|
||||
|
||||
SubstringOpts is currently unstable API and subject to backwards breaking changes.
|
||||
"""
|
||||
|
||||
# strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
|
||||
strMaxLength: int
|
||||
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
|
||||
strMinQueryLength: int
|
||||
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
|
||||
strMaxQueryLength: int
|
||||
|
||||
|
||||
class PrefixOpts(TypedDict):
|
||||
"""**BETA** Options for prefix text queries.
|
||||
|
||||
PrefixOpts is currently unstable API and subject to backwards breaking changes.
|
||||
"""
|
||||
|
||||
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
|
||||
strMinQueryLength: int
|
||||
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
|
||||
strMaxQueryLength: int
|
||||
|
||||
|
||||
class SuffixOpts(TypedDict):
|
||||
"""**BETA** Options for suffix text queries.
|
||||
|
||||
SuffixOpts is currently unstable API and subject to backwards breaking changes.
|
||||
"""
|
||||
|
||||
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
|
||||
strMinQueryLength: int
|
||||
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
|
||||
strMaxQueryLength: int
|
||||
|
||||
@ -61,7 +61,7 @@ from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument, _inflate_bs
|
||||
from pymongo import _csot
|
||||
from pymongo.common import CONNECT_TIMEOUT
|
||||
from pymongo.daemon import _spawn_daemon
|
||||
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
|
||||
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
|
||||
from pymongo.errors import (
|
||||
ConfigurationError,
|
||||
EncryptedCollectionError,
|
||||
@ -513,6 +513,11 @@ class Algorithm(str, enum.Enum):
|
||||
|
||||
.. versionadded:: 4.4
|
||||
"""
|
||||
TEXTPREVIEW = "TextPreview"
|
||||
"""**BETA** - TextPreview.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
|
||||
class QueryType(str, enum.Enum):
|
||||
@ -538,6 +543,24 @@ class QueryType(str, enum.Enum):
|
||||
.. versionadded:: 4.4
|
||||
"""
|
||||
|
||||
PREFIXPREVIEW = "prefixPreview"
|
||||
"""**BETA** - Used to encrypt a value for a prefixPreview query.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
SUFFIXPREVIEW = "suffixPreview"
|
||||
"""**BETA** - Used to encrypt a value for a suffixPreview query.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
SUBSTRINGPREVIEW = "substringPreview"
|
||||
"""**BETA** - Used to encrypt a value for a substringPreview query.
|
||||
|
||||
.. versionadded:: 4.15
|
||||
"""
|
||||
|
||||
|
||||
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
|
||||
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
|
||||
@ -869,6 +892,7 @@ class ClientEncryption(Generic[_DocumentType]):
|
||||
contention_factor: Optional[int] = None,
|
||||
range_opts: Optional[RangeOpts] = None,
|
||||
is_expression: bool = False,
|
||||
text_opts: Optional[TextOpts] = None,
|
||||
) -> Any:
|
||||
self._check_closed()
|
||||
if isinstance(key_id, uuid.UUID):
|
||||
@ -888,6 +912,12 @@ class ClientEncryption(Generic[_DocumentType]):
|
||||
range_opts.document,
|
||||
codec_options=self._codec_options,
|
||||
)
|
||||
text_opts_bytes = None
|
||||
if text_opts:
|
||||
text_opts_bytes = encode(
|
||||
text_opts.document,
|
||||
codec_options=self._codec_options,
|
||||
)
|
||||
with _wrap_encryption_errors():
|
||||
encrypted_doc = self._encryption.encrypt(
|
||||
value=doc,
|
||||
@ -898,6 +928,7 @@ class ClientEncryption(Generic[_DocumentType]):
|
||||
contention_factor=contention_factor,
|
||||
range_opts=range_opts_bytes,
|
||||
is_expression=is_expression,
|
||||
text_opts=text_opts_bytes,
|
||||
)
|
||||
return decode(encrypted_doc)["v"]
|
||||
|
||||
@ -910,6 +941,7 @@ class ClientEncryption(Generic[_DocumentType]):
|
||||
query_type: Optional[str] = None,
|
||||
contention_factor: Optional[int] = None,
|
||||
range_opts: Optional[RangeOpts] = None,
|
||||
text_opts: Optional[TextOpts] = None,
|
||||
) -> Binary:
|
||||
"""Encrypt a BSON value with a given key and algorithm.
|
||||
|
||||
@ -930,9 +962,14 @@ class ClientEncryption(Generic[_DocumentType]):
|
||||
used.
|
||||
:param range_opts: Index options for `range` queries. See
|
||||
:class:`RangeOpts` for some valid options.
|
||||
:param text_opts: Index options for `textPreview` queries. See
|
||||
:class:`TextOpts` for some valid options.
|
||||
|
||||
:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
|
||||
|
||||
.. versionchanged:: 4.9
|
||||
Added the `text_opts` parameter.
|
||||
|
||||
.. versionchanged:: 4.9
|
||||
Added the `range_opts` parameter.
|
||||
|
||||
@ -953,6 +990,7 @@ class ClientEncryption(Generic[_DocumentType]):
|
||||
contention_factor=contention_factor,
|
||||
range_opts=range_opts,
|
||||
is_expression=False,
|
||||
text_opts=text_opts,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ import unittest
|
||||
import warnings
|
||||
from inspect import iscoroutinefunction
|
||||
|
||||
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
|
||||
from pymongo.errors import AutoReconnect
|
||||
from pymongo.synchronous.uri_parser import parse_uri
|
||||
|
||||
@ -524,6 +525,19 @@ class ClientContext:
|
||||
"Server version must be at most %s" % str(other_version),
|
||||
)
|
||||
|
||||
def require_libmongocrypt_min(self, *ver):
|
||||
other_version = Version(*ver)
|
||||
if not _HAVE_PYMONGOCRYPT:
|
||||
version = Version.from_string("0.0.0")
|
||||
else:
|
||||
from pymongocrypt import libmongocrypt_version
|
||||
|
||||
version = Version.from_string(libmongocrypt_version())
|
||||
return self._require(
|
||||
lambda: version >= other_version,
|
||||
"Libmongocrypt version must be at least %s" % str(other_version),
|
||||
)
|
||||
|
||||
def require_auth(self, func):
|
||||
"""Run a test only if the server is running with auth enabled."""
|
||||
return self._require(
|
||||
|
||||
@ -33,6 +33,7 @@ import warnings
|
||||
from inspect import iscoroutinefunction
|
||||
|
||||
from pymongo.asynchronous.uri_parser import parse_uri
|
||||
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
|
||||
from pymongo.errors import AutoReconnect
|
||||
|
||||
try:
|
||||
@ -524,6 +525,19 @@ class AsyncClientContext:
|
||||
"Server version must be at most %s" % str(other_version),
|
||||
)
|
||||
|
||||
def require_libmongocrypt_min(self, *ver):
|
||||
other_version = Version(*ver)
|
||||
if not _HAVE_PYMONGOCRYPT:
|
||||
version = Version.from_string("0.0.0")
|
||||
else:
|
||||
from pymongocrypt import libmongocrypt_version
|
||||
|
||||
version = Version.from_string(libmongocrypt_version())
|
||||
return self._require(
|
||||
lambda: version >= other_version,
|
||||
"Libmongocrypt version must be at least %s" % str(other_version),
|
||||
)
|
||||
|
||||
def require_auth(self, func):
|
||||
"""Run a test only if the server is running with auth enabled."""
|
||||
return self._require(
|
||||
|
||||
@ -89,7 +89,7 @@ from pymongo.asynchronous import encryption
|
||||
from pymongo.asynchronous.encryption import Algorithm, AsyncClientEncryption, QueryType
|
||||
from pymongo.asynchronous.mongo_client import AsyncMongoClient
|
||||
from pymongo.cursor_shared import CursorType
|
||||
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts, RangeOpts
|
||||
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts, RangeOpts, TextOpts
|
||||
from pymongo.errors import (
|
||||
AutoReconnect,
|
||||
BulkWriteError,
|
||||
@ -3443,6 +3443,261 @@ class TestAutomaticDecryptionKeys(AsyncEncryptionIntegrationTest):
|
||||
self.assertIsInstance(exc.exception.encrypted_fields["fields"][0]["keyId"], Binary)
|
||||
|
||||
|
||||
# https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#27-text-explicit-encryption
|
||||
class TestExplicitTextEncryptionProse(AsyncEncryptionIntegrationTest):
|
||||
@async_client_context.require_no_standalone
|
||||
@async_client_context.require_version_min(8, 2, -1)
|
||||
@async_client_context.require_libmongocrypt_min(1, 15, 1)
|
||||
async def asyncSetUp(self):
|
||||
await super().asyncSetUp()
|
||||
# Load the file key1-document.json as key1Document.
|
||||
self.key1_document = json_data("etc", "data", "keys", "key1-document.json")
|
||||
# Read the "_id" field of key1Document as key1ID.
|
||||
self.key1_id = self.key1_document["_id"]
|
||||
# Drop and create the collection keyvault.datakeys.
|
||||
# Insert key1Document in keyvault.datakeys with majority write concern.
|
||||
self.key_vault = await create_key_vault(self.client.keyvault.datakeys, self.key1_document)
|
||||
self.addAsyncCleanup(self.key_vault.drop)
|
||||
# Create a ClientEncryption object named clientEncryption with these options.
|
||||
self.kms_providers = {"local": {"key": LOCAL_MASTER_KEY}}
|
||||
self.client_encryption = self.create_client_encryption(
|
||||
self.kms_providers,
|
||||
self.key_vault.full_name,
|
||||
self.client,
|
||||
OPTS,
|
||||
)
|
||||
# Create a MongoClient named encryptedClient with these AutoEncryptionOpts.
|
||||
opts = AutoEncryptionOpts(
|
||||
self.kms_providers,
|
||||
"keyvault.datakeys",
|
||||
bypass_query_analysis=True,
|
||||
)
|
||||
self.client_encrypted = await self.async_rs_or_single_client(auto_encryption_opts=opts)
|
||||
|
||||
# Using QE CreateCollection() and Collection.Drop(), drop and create the following collections with majority write concern:
|
||||
# db.prefix-suffix using the encryptedFields option set to the contents of encryptedFields-prefix-suffix.json.
|
||||
db = self.client_encrypted.db
|
||||
await db.drop_collection("prefix-suffix")
|
||||
encrypted_fields = json_data("etc", "data", "encryptedFields-prefix-suffix.json")
|
||||
await self.client_encryption.create_encrypted_collection(
|
||||
db, "prefix-suffix", kms_provider="local", encrypted_fields=encrypted_fields
|
||||
)
|
||||
# db.substring using the encryptedFields option set to the contents of encryptedFields-substring.json.
|
||||
await db.drop_collection("substring")
|
||||
encrypted_fields = json_data("etc", "data", "encryptedFields-substring.json")
|
||||
await self.client_encryption.create_encrypted_collection(
|
||||
db, "substring", kms_provider="local", encrypted_fields=encrypted_fields
|
||||
)
|
||||
|
||||
# Use clientEncryption to encrypt the string "foobarbaz" with the following EncryptOpts.
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
suffix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"foobarbaz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to insert the following document into db.prefix-suffix with majority write concern.
|
||||
coll = self.client_encrypted.db["prefix-suffix"].with_options(
|
||||
write_concern=WriteConcern(w="majority")
|
||||
)
|
||||
await coll.insert_one({"_id": 0, "encryptedText": encrypted_value})
|
||||
|
||||
# Use clientEncryption to encrypt the string "foobarbaz" with the following EncryptOpts.
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
substring=dict(strMaxLength=10, strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"foobarbaz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to insert the following document into db.substring with majority write concern.
|
||||
coll = self.client_encrypted.db["substring"].with_options(
|
||||
write_concern=WriteConcern(w="majority")
|
||||
)
|
||||
await coll.insert_one({"_id": 0, "encryptedText": encrypted_value})
|
||||
|
||||
async def test_01_can_find_a_document_by_prefix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts.
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"foo",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.PREFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter.
|
||||
value = await self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrStartsWith": {"input": "$encryptedText", "prefix": encrypted_value}}}
|
||||
)
|
||||
# Assert the following document is returned.
|
||||
expected = {"_id": 0, "encryptedText": "foobarbaz"}
|
||||
value.pop("__safeContent__", None)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
async def test_02_can_find_a_document_by_suffix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "baz" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
suffix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"baz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUFFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter:
|
||||
value = await self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrEndsWith": {"input": "$encryptedText", "suffix": encrypted_value}}}
|
||||
)
|
||||
# Assert the following document is returned.
|
||||
expected = {"_id": 0, "encryptedText": "foobarbaz"}
|
||||
value.pop("__safeContent__", None)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
async def test_03_no_document_found_by_prefix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "baz" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"baz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.PREFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter:
|
||||
value = await self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrStartsWith": {"input": "$encryptedText", "prefix": encrypted_value}}}
|
||||
)
|
||||
# Assert that no documents are returned.
|
||||
self.assertIsNone(value)
|
||||
|
||||
async def test_04_no_document_found_by_suffix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
suffix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"foo",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUFFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter:
|
||||
value = await self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrEndsWith": {"input": "$encryptedText", "suffix": encrypted_value}}}
|
||||
)
|
||||
# Assert that no documents are returned.
|
||||
self.assertIsNone(value)
|
||||
|
||||
async def test_05_can_find_a_document_by_substring(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "bar" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
substring=dict(strMaxLength=10, strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"bar",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUBSTRINGPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.substring collection with the following filter:
|
||||
value = await self.client_encrypted.db["substring"].find_one(
|
||||
{
|
||||
"$expr": {
|
||||
"$encStrContains": {"input": "$encryptedText", "substring": encrypted_value}
|
||||
}
|
||||
}
|
||||
)
|
||||
# Assert the following document is returned:
|
||||
expected = {"_id": 0, "encryptedText": "foobarbaz"}
|
||||
value.pop("__safeContent__", None)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
async def test_06_no_document_found_by_substring(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "qux" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
substring=dict(strMaxLength=10, strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = await self.client_encryption.encrypt(
|
||||
"qux",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUBSTRINGPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.substring collection with the following filter:
|
||||
value = await self.client_encrypted.db["substring"].find_one(
|
||||
{
|
||||
"$expr": {
|
||||
"$encStrContains": {"input": "$encryptedText", "substring": encrypted_value}
|
||||
}
|
||||
}
|
||||
)
|
||||
# Assert that no documents are returned.
|
||||
self.assertIsNone(value)
|
||||
|
||||
async def test_07_contentionFactor_is_required(self):
|
||||
from pymongocrypt.errors import MongoCryptError
|
||||
|
||||
# Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
with self.assertRaises(EncryptionError) as ctx:
|
||||
await self.client_encryption.encrypt(
|
||||
"foo",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.PREFIXPREVIEW,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Expect an error from libmongocrypt with a message containing the string: "contention factor is required for textPreview algorithm".
|
||||
self.assertIsInstance(ctx.exception.cause, MongoCryptError)
|
||||
self.assertEqual(
|
||||
str(ctx.exception), "contention factor is required for textPreview algorithm"
|
||||
)
|
||||
|
||||
|
||||
def start_mongocryptd(port) -> None:
|
||||
args = ["mongocryptd", f"--port={port}", "--idleShutdownTimeoutSecs=60"]
|
||||
_spawn_daemon(args)
|
||||
|
||||
@ -86,7 +86,7 @@ from bson.json_util import JSONOptions
|
||||
from bson.son import SON
|
||||
from pymongo import ReadPreference
|
||||
from pymongo.cursor_shared import CursorType
|
||||
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts, RangeOpts
|
||||
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts, RangeOpts, TextOpts
|
||||
from pymongo.errors import (
|
||||
AutoReconnect,
|
||||
BulkWriteError,
|
||||
@ -3425,6 +3425,261 @@ class TestAutomaticDecryptionKeys(EncryptionIntegrationTest):
|
||||
self.assertIsInstance(exc.exception.encrypted_fields["fields"][0]["keyId"], Binary)
|
||||
|
||||
|
||||
# https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#27-text-explicit-encryption
|
||||
class TestExplicitTextEncryptionProse(EncryptionIntegrationTest):
|
||||
@client_context.require_no_standalone
|
||||
@client_context.require_version_min(8, 2, -1)
|
||||
@client_context.require_libmongocrypt_min(1, 15, 1)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Load the file key1-document.json as key1Document.
|
||||
self.key1_document = json_data("etc", "data", "keys", "key1-document.json")
|
||||
# Read the "_id" field of key1Document as key1ID.
|
||||
self.key1_id = self.key1_document["_id"]
|
||||
# Drop and create the collection keyvault.datakeys.
|
||||
# Insert key1Document in keyvault.datakeys with majority write concern.
|
||||
self.key_vault = create_key_vault(self.client.keyvault.datakeys, self.key1_document)
|
||||
self.addCleanup(self.key_vault.drop)
|
||||
# Create a ClientEncryption object named clientEncryption with these options.
|
||||
self.kms_providers = {"local": {"key": LOCAL_MASTER_KEY}}
|
||||
self.client_encryption = self.create_client_encryption(
|
||||
self.kms_providers,
|
||||
self.key_vault.full_name,
|
||||
self.client,
|
||||
OPTS,
|
||||
)
|
||||
# Create a MongoClient named encryptedClient with these AutoEncryptionOpts.
|
||||
opts = AutoEncryptionOpts(
|
||||
self.kms_providers,
|
||||
"keyvault.datakeys",
|
||||
bypass_query_analysis=True,
|
||||
)
|
||||
self.client_encrypted = self.rs_or_single_client(auto_encryption_opts=opts)
|
||||
|
||||
# Using QE CreateCollection() and Collection.Drop(), drop and create the following collections with majority write concern:
|
||||
# db.prefix-suffix using the encryptedFields option set to the contents of encryptedFields-prefix-suffix.json.
|
||||
db = self.client_encrypted.db
|
||||
db.drop_collection("prefix-suffix")
|
||||
encrypted_fields = json_data("etc", "data", "encryptedFields-prefix-suffix.json")
|
||||
self.client_encryption.create_encrypted_collection(
|
||||
db, "prefix-suffix", kms_provider="local", encrypted_fields=encrypted_fields
|
||||
)
|
||||
# db.substring using the encryptedFields option set to the contents of encryptedFields-substring.json.
|
||||
db.drop_collection("substring")
|
||||
encrypted_fields = json_data("etc", "data", "encryptedFields-substring.json")
|
||||
self.client_encryption.create_encrypted_collection(
|
||||
db, "substring", kms_provider="local", encrypted_fields=encrypted_fields
|
||||
)
|
||||
|
||||
# Use clientEncryption to encrypt the string "foobarbaz" with the following EncryptOpts.
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
suffix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"foobarbaz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to insert the following document into db.prefix-suffix with majority write concern.
|
||||
coll = self.client_encrypted.db["prefix-suffix"].with_options(
|
||||
write_concern=WriteConcern(w="majority")
|
||||
)
|
||||
coll.insert_one({"_id": 0, "encryptedText": encrypted_value})
|
||||
|
||||
# Use clientEncryption to encrypt the string "foobarbaz" with the following EncryptOpts.
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
substring=dict(strMaxLength=10, strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"foobarbaz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to insert the following document into db.substring with majority write concern.
|
||||
coll = self.client_encrypted.db["substring"].with_options(
|
||||
write_concern=WriteConcern(w="majority")
|
||||
)
|
||||
coll.insert_one({"_id": 0, "encryptedText": encrypted_value})
|
||||
|
||||
def test_01_can_find_a_document_by_prefix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts.
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"foo",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.PREFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter.
|
||||
value = self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrStartsWith": {"input": "$encryptedText", "prefix": encrypted_value}}}
|
||||
)
|
||||
# Assert the following document is returned.
|
||||
expected = {"_id": 0, "encryptedText": "foobarbaz"}
|
||||
value.pop("__safeContent__", None)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
def test_02_can_find_a_document_by_suffix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "baz" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
suffix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"baz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUFFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter:
|
||||
value = self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrEndsWith": {"input": "$encryptedText", "suffix": encrypted_value}}}
|
||||
)
|
||||
# Assert the following document is returned.
|
||||
expected = {"_id": 0, "encryptedText": "foobarbaz"}
|
||||
value.pop("__safeContent__", None)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
def test_03_no_document_found_by_prefix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "baz" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"baz",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.PREFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter:
|
||||
value = self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrStartsWith": {"input": "$encryptedText", "prefix": encrypted_value}}}
|
||||
)
|
||||
# Assert that no documents are returned.
|
||||
self.assertIsNone(value)
|
||||
|
||||
def test_04_no_document_found_by_suffix(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
suffix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"foo",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUFFIXPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.prefix-suffix collection with the following filter:
|
||||
value = self.client_encrypted.db["prefix-suffix"].find_one(
|
||||
{"$expr": {"$encStrEndsWith": {"input": "$encryptedText", "suffix": encrypted_value}}}
|
||||
)
|
||||
# Assert that no documents are returned.
|
||||
self.assertIsNone(value)
|
||||
|
||||
def test_05_can_find_a_document_by_substring(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "bar" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
substring=dict(strMaxLength=10, strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"bar",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUBSTRINGPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.substring collection with the following filter:
|
||||
value = self.client_encrypted.db["substring"].find_one(
|
||||
{
|
||||
"$expr": {
|
||||
"$encStrContains": {"input": "$encryptedText", "substring": encrypted_value}
|
||||
}
|
||||
}
|
||||
)
|
||||
# Assert the following document is returned:
|
||||
expected = {"_id": 0, "encryptedText": "foobarbaz"}
|
||||
value.pop("__safeContent__", None)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
def test_06_no_document_found_by_substring(self):
|
||||
# Use clientEncryption.encrypt() to encrypt the string "qux" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
substring=dict(strMaxLength=10, strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
encrypted_value = self.client_encryption.encrypt(
|
||||
"qux",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.SUBSTRINGPREVIEW,
|
||||
contention_factor=0,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Use encryptedClient to run a "find" operation on the db.substring collection with the following filter:
|
||||
value = self.client_encrypted.db["substring"].find_one(
|
||||
{
|
||||
"$expr": {
|
||||
"$encStrContains": {"input": "$encryptedText", "substring": encrypted_value}
|
||||
}
|
||||
}
|
||||
)
|
||||
# Assert that no documents are returned.
|
||||
self.assertIsNone(value)
|
||||
|
||||
def test_07_contentionFactor_is_required(self):
|
||||
from pymongocrypt.errors import MongoCryptError
|
||||
|
||||
# Use clientEncryption.encrypt() to encrypt the string "foo" with the following EncryptOpts:
|
||||
text_opts = TextOpts(
|
||||
case_sensitive=True,
|
||||
diacritic_sensitive=True,
|
||||
prefix=dict(strMaxQueryLength=10, strMinQueryLength=2),
|
||||
)
|
||||
with self.assertRaises(EncryptionError) as ctx:
|
||||
self.client_encryption.encrypt(
|
||||
"foo",
|
||||
key_id=self.key1_id,
|
||||
algorithm=Algorithm.TEXTPREVIEW,
|
||||
query_type=QueryType.PREFIXPREVIEW,
|
||||
text_opts=text_opts,
|
||||
)
|
||||
# Expect an error from libmongocrypt with a message containing the string: "contention factor is required for textPreview algorithm".
|
||||
self.assertIsInstance(ctx.exception.cause, MongoCryptError)
|
||||
self.assertEqual(
|
||||
str(ctx.exception), "contention factor is required for textPreview algorithm"
|
||||
)
|
||||
|
||||
|
||||
def start_mongocryptd(port) -> None:
|
||||
args = ["mongocryptd", f"--port={port}", "--idleShutdownTimeoutSecs=60"]
|
||||
_spawn_daemon(args)
|
||||
|
||||
4
uv.lock
generated
4
uv.lock
generated
@ -1350,8 +1350,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pymongocrypt"
|
||||
version = "1.14.2.dev0"
|
||||
source = { git = "https://github.com/mongodb/libmongocrypt?subdirectory=bindings%2Fpython&rev=master#56048cf426bfeffa0805934b668a7af5ed8e907c" }
|
||||
version = "1.16.0"
|
||||
source = { git = "https://github.com/mongodb/libmongocrypt?subdirectory=bindings%2Fpython&rev=master#63d2591b84a9d4348cbe1c74556e266cd560ac5b" }
|
||||
dependencies = [
|
||||
{ name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.14.*'" },
|
||||
{ name = "cffi", version = "2.0.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.14.*'" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user