Skip to content

Commit 7ef18af

Browse files
authored
PYTHON-4580 Add key_expiration_ms option for DEK cache lifetime (#2186)
1 parent 61d4354 commit 7ef18af

File tree

13 files changed

+549
-24
lines changed

13 files changed

+549
-24
lines changed

doc/changelog.rst

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
Changelog
22
=========
33

4-
Changes in Version 4.11.2 (YYYY/MM/DD)
4+
Changes in Version 4.12.0 (YYYY/MM/DD)
5+
--------------------------------------
6+
7+
PyMongo 4.12 brings a number of changes including:
8+
9+
- Support for configuring DEK cache lifetime via the ``key_expiration_ms`` argument to
10+
:class:`~pymongo.encryption_options.AutoEncryptionOpts`.
11+
12+
Issues Resolved
13+
...............
14+
15+
See the `PyMongo 4.12 release notes in JIRA`_ for the list of resolved issues
16+
in this release.
17+
18+
.. _PyMongo 4.12 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=41916
19+
20+
Changes in Version 4.11.2 (2025/03/05)
521
--------------------------------------
622

723
Version 4.11.2 is a bug fix release.

pymongo/asynchronous/encryption.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ def _get_internal_client(
445445
bypass_encryption=opts._bypass_auto_encryption,
446446
encrypted_fields_map=encrypted_fields_map,
447447
bypass_query_analysis=opts._bypass_query_analysis,
448+
key_expiration_ms=opts._key_expiration_ms,
448449
),
449450
)
450451
self._closed = False
@@ -547,11 +548,10 @@ class QueryType(str, enum.Enum):
547548

548549

549550
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
550-
opts = MongoCryptOptions(**kwargs)
551-
# Opt into range V2 encryption.
552-
if hasattr(opts, "enable_range_v2"):
553-
opts.enable_range_v2 = True
554-
return opts
551+
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
552+
if kwargs.get("key_expiration_ms") is None:
553+
kwargs.pop("key_expiration_ms", None)
554+
return MongoCryptOptions(**kwargs)
555555

556556

557557
class AsyncClientEncryption(Generic[_DocumentType]):
@@ -564,6 +564,7 @@ def __init__(
564564
key_vault_client: AsyncMongoClient[_DocumentTypeArg],
565565
codec_options: CodecOptions[_DocumentTypeArg],
566566
kms_tls_options: Optional[Mapping[str, Any]] = None,
567+
key_expiration_ms: Optional[int] = None,
567568
) -> None:
568569
"""Explicit client-side field level encryption.
569570
@@ -630,7 +631,12 @@ def __init__(
630631
Or to supply a client certificate::
631632
632633
kms_tls_options={'kmip': {'tlsCertificateKeyFile': 'client.pem'}}
634+
:param key_expiration_ms: The cache expiration time for data encryption keys.
635+
Defaults to ``None`` which defers to libmongocrypt's default which is currently 60000.
636+
Set to 0 to disable key expiration.
633637
638+
.. versionchanged:: 4.12
639+
Added the `key_expiration_ms` parameter.
634640
.. versionchanged:: 4.0
635641
Added the `kms_tls_options` parameter and the "kmip" KMS provider.
636642
@@ -666,14 +672,19 @@ def __init__(
666672
key_vault_coll = key_vault_client[db][coll]
667673

668674
opts = AutoEncryptionOpts(
669-
kms_providers, key_vault_namespace, kms_tls_options=kms_tls_options
675+
kms_providers,
676+
key_vault_namespace,
677+
kms_tls_options=kms_tls_options,
678+
key_expiration_ms=key_expiration_ms,
670679
)
671680
self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO(
672681
None, key_vault_coll, None, opts
673682
)
674683
self._encryption = AsyncExplicitEncrypter(
675684
self._io_callbacks,
676-
_create_mongocrypt_options(kms_providers=kms_providers, schema_map=None),
685+
_create_mongocrypt_options(
686+
kms_providers=kms_providers, schema_map=None, key_expiration_ms=key_expiration_ms
687+
),
677688
)
678689
# Use the same key vault collection as the callback.
679690
assert self._io_callbacks.key_vault_coll is not None
@@ -700,6 +711,7 @@ async def create_encrypted_collection(
700711
creation. :class:`~pymongo.errors.EncryptionError` will be
701712
raised if the collection already exists.
702713
714+
:param database: the database to create the collection
703715
:param name: the name of the collection to create
704716
:param encrypted_fields: Document that describes the encrypted fields for
705717
Queryable Encryption. The "keyId" may be set to ``None`` to auto-generate the data keys. For example:

pymongo/encryption_options.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(
5757
crypt_shared_lib_required: bool = False,
5858
bypass_query_analysis: bool = False,
5959
encrypted_fields_map: Optional[Mapping[str, Any]] = None,
60+
key_expiration_ms: Optional[int] = None,
6061
) -> None:
6162
"""Options to configure automatic client-side field level encryption.
6263
@@ -191,9 +192,14 @@ def __init__(
191192
]
192193
}
193194
}
195+
:param key_expiration_ms: The cache expiration time for data encryption keys.
196+
Defaults to ``None`` which defers to libmongocrypt's default which is currently 60000.
197+
Set to 0 to disable key expiration.
194198
199+
.. versionchanged:: 4.12
200+
Added the `key_expiration_ms` parameter.
195201
.. versionchanged:: 4.2
196-
Added `encrypted_fields_map` `crypt_shared_lib_path`, `crypt_shared_lib_required`,
202+
Added the `encrypted_fields_map`, `crypt_shared_lib_path`, `crypt_shared_lib_required`,
197203
and `bypass_query_analysis` parameters.
198204
199205
.. versionchanged:: 4.0
@@ -210,7 +216,6 @@ def __init__(
210216
if encrypted_fields_map:
211217
validate_is_mapping("encrypted_fields_map", encrypted_fields_map)
212218
self._encrypted_fields_map = encrypted_fields_map
213-
self._bypass_query_analysis = bypass_query_analysis
214219
self._crypt_shared_lib_path = crypt_shared_lib_path
215220
self._crypt_shared_lib_required = crypt_shared_lib_required
216221
self._kms_providers = kms_providers
@@ -233,6 +238,7 @@ def __init__(
233238
# Maps KMS provider name to a SSLContext.
234239
self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options)
235240
self._bypass_query_analysis = bypass_query_analysis
241+
self._key_expiration_ms = key_expiration_ms
236242

237243

238244
class RangeOpts:

pymongo/synchronous/encryption.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ def _get_internal_client(
442442
bypass_encryption=opts._bypass_auto_encryption,
443443
encrypted_fields_map=encrypted_fields_map,
444444
bypass_query_analysis=opts._bypass_query_analysis,
445+
key_expiration_ms=opts._key_expiration_ms,
445446
),
446447
)
447448
self._closed = False
@@ -544,11 +545,10 @@ class QueryType(str, enum.Enum):
544545

545546

546547
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
547-
opts = MongoCryptOptions(**kwargs)
548-
# Opt into range V2 encryption.
549-
if hasattr(opts, "enable_range_v2"):
550-
opts.enable_range_v2 = True
551-
return opts
548+
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
549+
if kwargs.get("key_expiration_ms") is None:
550+
kwargs.pop("key_expiration_ms", None)
551+
return MongoCryptOptions(**kwargs)
552552

553553

554554
class ClientEncryption(Generic[_DocumentType]):
@@ -561,6 +561,7 @@ def __init__(
561561
key_vault_client: MongoClient[_DocumentTypeArg],
562562
codec_options: CodecOptions[_DocumentTypeArg],
563563
kms_tls_options: Optional[Mapping[str, Any]] = None,
564+
key_expiration_ms: Optional[int] = None,
564565
) -> None:
565566
"""Explicit client-side field level encryption.
566567
@@ -627,7 +628,12 @@ def __init__(
627628
Or to supply a client certificate::
628629
629630
kms_tls_options={'kmip': {'tlsCertificateKeyFile': 'client.pem'}}
631+
:param key_expiration_ms: The cache expiration time for data encryption keys.
632+
Defaults to ``None`` which defers to libmongocrypt's default which is currently 60000.
633+
Set to 0 to disable key expiration.
630634
635+
.. versionchanged:: 4.12
636+
Added the `key_expiration_ms` parameter.
631637
.. versionchanged:: 4.0
632638
Added the `kms_tls_options` parameter and the "kmip" KMS provider.
633639
@@ -659,14 +665,19 @@ def __init__(
659665
key_vault_coll = key_vault_client[db][coll]
660666

661667
opts = AutoEncryptionOpts(
662-
kms_providers, key_vault_namespace, kms_tls_options=kms_tls_options
668+
kms_providers,
669+
key_vault_namespace,
670+
kms_tls_options=kms_tls_options,
671+
key_expiration_ms=key_expiration_ms,
663672
)
664673
self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO(
665674
None, key_vault_coll, None, opts
666675
)
667676
self._encryption = ExplicitEncrypter(
668677
self._io_callbacks,
669-
_create_mongocrypt_options(kms_providers=kms_providers, schema_map=None),
678+
_create_mongocrypt_options(
679+
kms_providers=kms_providers, schema_map=None, key_expiration_ms=key_expiration_ms
680+
),
670681
)
671682
# Use the same key vault collection as the callback.
672683
assert self._io_callbacks.key_vault_coll is not None
@@ -693,6 +704,7 @@ def create_encrypted_collection(
693704
creation. :class:`~pymongo.errors.EncryptionError` will be
694705
raised if the collection already exists.
695706
707+
:param database: the database to create the collection
696708
:param name: the name of the collection to create
697709
:param encrypted_fields: Document that describes the encrypted fields for
698710
Queryable Encryption. The "keyId" may be set to ``None`` to auto-generate the data keys. For example:

test/asynchronous/unified_format.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ async def drop(self: AsyncGridFSBucket, *args: Any, **kwargs: Any) -> None:
378378
opts["key_vault_client"],
379379
DEFAULT_CODEC_OPTIONS,
380380
opts.get("kms_tls_options", kms_tls_options),
381+
opts.get("key_expiration_ms"),
381382
)
382383
return
383384
elif entity_type == "thread":
@@ -439,7 +440,7 @@ class UnifiedSpecTestMixinV1(AsyncIntegrationTest):
439440
a class attribute ``TEST_SPEC``.
440441
"""
441442

442-
SCHEMA_VERSION = Version.from_string("1.21")
443+
SCHEMA_VERSION = Version.from_string("1.22")
443444
RUN_ON_LOAD_BALANCER = True
444445
RUN_ON_SERVERLESS = True
445446
TEST_SPEC: Any

test/asynchronous/utils_spec_runner.py

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import asyncio
1919
import functools
2020
import os
21+
import time
2122
import unittest
2223
from asyncio import iscoroutinefunction
2324
from collections import abc
@@ -314,6 +315,10 @@ async def assert_index_not_exists(self, database, collection, index):
314315
coll = self.client[database][collection]
315316
self.assertNotIn(index, [doc["name"] async for doc in await coll.list_indexes()])
316317

318+
async def wait(self, ms):
319+
"""Run the "wait" test operation."""
320+
await asyncio.sleep(ms / 1000.0)
321+
317322
def assertErrorLabelsContain(self, exc, expected_labels):
318323
labels = [l for l in expected_labels if exc.has_error_label(l)]
319324
self.assertEqual(labels, expected_labels)

test/client-side-encryption/spec/legacy/fle2v2-Rangev2-Compact.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
"replicaset",
77
"sharded",
88
"load-balanced"
9-
],
10-
"serverless": "forbid"
9+
]
1110
}
1211
],
1312
"database_name": "default",

0 commit comments

Comments
 (0)