From bc52c8e75e83c60d9556c128baa90a5eee0f9158 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 25 Jun 2025 15:20:54 -0400 Subject: [PATCH 1/8] INTPYTHON-527 Add Queryable Encryption support --- django_mongodb_backend/base.py | 28 ++++++++- django_mongodb_backend/encryption.py | 66 +++++++++++++++++++++ django_mongodb_backend/features.py | 15 +++++ django_mongodb_backend/fields/__init__.py | 2 + django_mongodb_backend/fields/encryption.py | 7 +++ django_mongodb_backend/models.py | 23 +++++++ django_mongodb_backend/schema.py | 51 +++++++++++++++- docs/source/topics/encrypted-models.rst | 22 +++++++ docs/source/topics/index.rst | 1 + tests/encryption_/__init__.py | 0 tests/encryption_/models.py | 9 +++ tests/encryption_/tests.py | 30 ++++++++++ 12 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 django_mongodb_backend/encryption.py create mode 100644 django_mongodb_backend/fields/encryption.py create mode 100644 docs/source/topics/encrypted-models.rst create mode 100644 tests/encryption_/__init__.py create mode 100644 tests/encryption_/models.py create mode 100644 tests/encryption_/tests.py diff --git a/django_mongodb_backend/base.py b/django_mongodb_backend/base.py index fc21fa5b..8c8b1bbf 100644 --- a/django_mongodb_backend/base.py +++ b/django_mongodb_backend/base.py @@ -1,9 +1,10 @@ import contextlib +import copy import os from django.core.exceptions import ImproperlyConfigured from django.db import DEFAULT_DB_ALIAS -from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.base.base import NO_DB_ALIAS, BaseDatabaseWrapper from django.db.backends.utils import debug_transaction from django.utils.asyncio import async_unsafe from django.utils.functional import cached_property @@ -156,6 +157,9 @@ def _isnull_operator(a, b): def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS): super().__init__(settings_dict, alias=alias) self.session = None + # Cache the `settings_dict` in case we need to check for + # auto_encryption_opts later. + self.__dict__["_settings_dict"] = copy.deepcopy(settings_dict) def get_collection(self, name, **kwargs): collection = Collection(self.database, name, **kwargs) @@ -287,3 +291,25 @@ def validate_no_broken_transaction(self): def get_database_version(self): """Return a tuple of the database's version.""" return tuple(self.connection.server_info()["versionArray"]) + + @contextlib.contextmanager + def _nodb_cursor(self): + """ + Returns a cursor from an unencrypted connection for operations + that do not support encryption. + + Encryption is only supported on encrypted models. + """ + + # Remove auto_encryption_opts from OPTIONS + if self.settings_dict.get("OPTIONS", {}).get("auto_encryption_opts"): + self.settings_dict["OPTIONS"].pop("auto_encryption_opts") + + # Create a new connection without OPTIONS["auto_encryption_opts": …] + conn = self.__class__({**self.settings_dict}, alias=NO_DB_ALIAS) + + try: + with conn.cursor() as cursor: + yield cursor + finally: + conn.close() diff --git a/django_mongodb_backend/encryption.py b/django_mongodb_backend/encryption.py new file mode 100644 index 00000000..87b39add --- /dev/null +++ b/django_mongodb_backend/encryption.py @@ -0,0 +1,66 @@ +# Queryable Encryption helpers +# +# TODO: Decide if these helpers should even exist, and if so, find a permanent +# place for them. + +from bson.binary import STANDARD +from bson.codec_options import CodecOptions +from pymongo.encryption import AutoEncryptionOpts, ClientEncryption + + +def get_encrypted_client(auto_encryption_opts, encrypted_connection): + """ + Returns a `ClientEncryption` instance for MongoDB Client-Side Field Level + Encryption (CSFLE) that can be used to create an encrypted collection. + """ + + key_vault_namespace = auto_encryption_opts._key_vault_namespace + kms_providers = auto_encryption_opts._kms_providers + codec_options = CodecOptions(uuid_representation=STANDARD) + return ClientEncryption(kms_providers, key_vault_namespace, encrypted_connection, codec_options) + + +def get_auto_encryption_opts(crypt_shared_lib_path=None, kms_providers=None): + """ + Returns an `AutoEncryptionOpts` instance for MongoDB Client-Side Field + Level Encryption (CSFLE) that can be used to create an encrypted connection. + """ + key_vault_database_name = "encryption" + key_vault_collection_name = "__keyVault" + key_vault_namespace = f"{key_vault_database_name}.{key_vault_collection_name}" + return AutoEncryptionOpts( + key_vault_namespace=key_vault_namespace, + kms_providers=kms_providers, + crypt_shared_lib_path=crypt_shared_lib_path, + ) + + +def get_customer_master_key(): + """ + Returns a 96-byte local master key for use with MongoDB Client-Side Field Level + Encryption (CSFLE). For local testing purposes only. In production, use a secure KMS + like AWS, Azure, GCP, or KMIP. + Returns: + bytes: A 96-byte key. + """ + # WARNING: This is a static key for testing only. + # Generate with: os.urandom(96) + return bytes.fromhex( + "000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f" + "202122232425262728292a2b2c2d2e2f" + "303132333435363738393a3b3c3d3e3f" + "404142434445464748494a4b4c4d4e4f" + "505152535455565758595a5b5c5d5e5f" + ) + + +def get_kms_providers(): + """ + Return supported KMS providers for MongoDB Client-Side Field Level Encryption (CSFLE). + """ + return { + "local": { + "key": get_customer_master_key(), + }, + } diff --git a/django_mongodb_backend/features.py b/django_mongodb_backend/features.py index 3e9cc292..1feef98e 100644 --- a/django_mongodb_backend/features.py +++ b/django_mongodb_backend/features.py @@ -624,3 +624,18 @@ def supports_transactions(self): hello = client.command("hello") # a replica set or a sharded cluster return "setName" in hello or hello.get("msg") == "isdbgrid" + + @cached_property + def supports_encryption(self): + """ + Encryption is supported if the server is Atlas or Enterprise + and is configured as a replica set or sharded cluster. + """ + self.connection.ensure_connection() + client = self.connection.connection.admin + build_info = client.command("buildInfo") + is_enterprise = "enterprise" in build_info.get("modules") + # `supports_transactions` already checks if the server is a + # replica set or sharded cluster. + is_not_single = self.supports_transactions + return is_enterprise and is_not_single diff --git a/django_mongodb_backend/fields/__init__.py b/django_mongodb_backend/fields/__init__.py index be95fa5e..ced7fa2b 100644 --- a/django_mongodb_backend/fields/__init__.py +++ b/django_mongodb_backend/fields/__init__.py @@ -3,6 +3,7 @@ from .duration import register_duration_field from .embedded_model import EmbeddedModelField from .embedded_model_array import EmbeddedModelArrayField +from .encryption import EncryptedCharField from .json import register_json_field from .objectid import ObjectIdField @@ -11,6 +12,7 @@ "ArrayField", "EmbeddedModelArrayField", "EmbeddedModelField", + "EncryptedCharField", "ObjectIdAutoField", "ObjectIdField", ] diff --git a/django_mongodb_backend/fields/encryption.py b/django_mongodb_backend/fields/encryption.py new file mode 100644 index 00000000..7fb80a02 --- /dev/null +++ b/django_mongodb_backend/fields/encryption.py @@ -0,0 +1,7 @@ +from django.db import models + + +class EncryptedCharField(models.CharField): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.encrypted = True diff --git a/django_mongodb_backend/models.py b/django_mongodb_backend/models.py index adeba21e..822c744b 100644 --- a/django_mongodb_backend/models.py +++ b/django_mongodb_backend/models.py @@ -14,3 +14,26 @@ def delete(self, *args, **kwargs): def save(self, *args, **kwargs): raise NotSupportedError("EmbeddedModels cannot be saved.") + + +class EncryptedModelBase(models.base.ModelBase): + def __new__(cls, name, bases, attrs, **kwargs): + new_class = super().__new__(cls, name, bases, attrs, **kwargs) + + # Build a map of encrypted fields + encrypted_fields = { + "fields": { + field.name: field.__class__.__name__ + for field in new_class._meta.fields + if getattr(field, "encrypted", False) + } + } + + # Store it as a class-level attribute + new_class.encrypted_fields_map = encrypted_fields + return new_class + + +class EncryptedModel(models.Model, metaclass=EncryptedModelBase): + class Meta: + abstract = True diff --git a/django_mongodb_backend/schema.py b/django_mongodb_backend/schema.py index da3ec961..b2afd486 100644 --- a/django_mongodb_backend/schema.py +++ b/django_mongodb_backend/schema.py @@ -1,10 +1,13 @@ +import contextlib + from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.models import Index, UniqueConstraint +from pymongo.encryption import EncryptedCollectionError from pymongo.operations import SearchIndexModel -from django_mongodb_backend.indexes import SearchIndex - +from .encryption import get_encrypted_client from .fields import EmbeddedModelField +from .indexes import SearchIndex from .query import wrap_database_errors from .utils import OperationCollector @@ -41,7 +44,7 @@ def get_database(self): @wrap_database_errors @ignore_embedded_models def create_model(self, model): - self.get_database().create_collection(model._meta.db_table) + self._create_collection(model) self._create_model_indexes(model) # Make implicit M2M tables. for field in model._meta.local_many_to_many: @@ -418,3 +421,45 @@ def _field_should_have_unique(self, field): db_type = field.db_type(self.connection) # The _id column is automatically unique. return db_type and field.unique and field.column != "_id" + + def _supports_encryption(self, model): + """ + Check for `supports_encryption` feature and `auto_encryption_opts` + and `embedded_fields_map`. If `supports_encryption` is True and + `auto_encryption_opts` is in the cached connection settings and + the model has an embedded_fields_map property, then encryption + is supported. + """ + return ( + self.connection.features.supports_encryption + and self.connection._settings_dict.get("OPTIONS", {}).get("auto_encryption_opts") + and hasattr(model, "encrypted_fields_map") + ) + + def _create_collection(self, model): + """ + Create a collection or, if encryption is supported, create + an encrypted connection then use it to create an encrypted + client then use that to create an encrypted collection. + """ + + if self._supports_encryption(model): + auto_encryption_opts = self.connection._settings_dict.get("OPTIONS", {}).get( + "auto_encryption_opts" + ) + # Use the cached settings dict to create a new connection + encrypted_connection = self.connection.get_new_connection( + self.connection._settings_dict + ) + # Use the encrypted connection and auto_encryption_opts to create an encrypted client + encrypted_client = get_encrypted_client(auto_encryption_opts, encrypted_connection) + + with contextlib.suppress(EncryptedCollectionError): + encrypted_client.create_encrypted_collection( + encrypted_connection[self.connection.database.name], + model._meta.db_table, + model.encrypted_fields_map, + "local", # TODO: KMS provider should be configurable + ) + else: + self.get_database().create_collection(model._meta.db_table) diff --git a/docs/source/topics/encrypted-models.rst b/docs/source/topics/encrypted-models.rst new file mode 100644 index 00000000..4e40bc48 --- /dev/null +++ b/docs/source/topics/encrypted-models.rst @@ -0,0 +1,22 @@ +Encrypted models +================ + +``EncryptedCharField`` +---------------------- + +The basics +~~~~~~~~~~ + +Let's consider this example:: + + from django.db import models + + from django_mongodb_backend.fields import EncryptedCharField + from django_mongodb_backend.models import EncryptedModel + + + class Person(EncryptedModel): + ssn = EncryptedCharField("ssn", max_length=11) + + def __str__(self): + return self.ssn diff --git a/docs/source/topics/index.rst b/docs/source/topics/index.rst index 47e0c6dc..285fd718 100644 --- a/docs/source/topics/index.rst +++ b/docs/source/topics/index.rst @@ -10,4 +10,5 @@ know: cache embedded-models + encrypted-models known-issues diff --git a/tests/encryption_/__init__.py b/tests/encryption_/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/encryption_/models.py b/tests/encryption_/models.py new file mode 100644 index 00000000..597208f9 --- /dev/null +++ b/tests/encryption_/models.py @@ -0,0 +1,9 @@ +from django_mongodb_backend.fields import EncryptedCharField +from django_mongodb_backend.models import EncryptedModel + + +class Person(EncryptedModel): + ssn = EncryptedCharField("ssn", max_length=11) + + def __str__(self): + return self.ssn diff --git a/tests/encryption_/tests.py b/tests/encryption_/tests.py new file mode 100644 index 00000000..380cb6e2 --- /dev/null +++ b/tests/encryption_/tests.py @@ -0,0 +1,30 @@ +from django.test import TestCase + +from .models import Person + + +class EncryptedModelTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.objs = [Person.objects.create()] + + def test_encrypted_fields_map_on_class(self): + expected = { + "fields": { + "ssn": "EncryptedCharField", + } + } + self.assertEqual(Person.encrypted_fields_map, expected) + + def test_encrypted_fields_map_on_instance(self): + instance = Person(ssn="123-45-6789") + expected = { + "fields": { + "ssn": "EncryptedCharField", + } + } + self.assertEqual(instance.encrypted_fields_map, expected) + + def test_non_encrypted_fields_not_included(self): + encrypted_field_names = Person.encrypted_fields_map.keys() + self.assertNotIn("ssn", encrypted_field_names) From 38fb110865279898fd7cf9f61ed9d418770604d1 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 27 Jun 2025 07:17:22 -0400 Subject: [PATCH 2/8] Fix test for unencrypted field not in field map --- tests/encryption_/models.py | 3 +++ tests/encryption_/tests.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/encryption_/models.py b/tests/encryption_/models.py index 597208f9..5e1a6201 100644 --- a/tests/encryption_/models.py +++ b/tests/encryption_/models.py @@ -1,9 +1,12 @@ +from django.db import models + from django_mongodb_backend.fields import EncryptedCharField from django_mongodb_backend.models import EncryptedModel class Person(EncryptedModel): ssn = EncryptedCharField("ssn", max_length=11) + name = models.CharField("name", max_length=100) def __str__(self): return self.ssn diff --git a/tests/encryption_/tests.py b/tests/encryption_/tests.py index 380cb6e2..04bf4531 100644 --- a/tests/encryption_/tests.py +++ b/tests/encryption_/tests.py @@ -26,5 +26,5 @@ def test_encrypted_fields_map_on_instance(self): self.assertEqual(instance.encrypted_fields_map, expected) def test_non_encrypted_fields_not_included(self): - encrypted_field_names = Person.encrypted_fields_map.keys() - self.assertNotIn("ssn", encrypted_field_names) + encrypted_field_names = Person.encrypted_fields_map.get("fields").keys() + self.assertNotIn("name", encrypted_field_names) From 65bd15a88c3dd429cef41f4fccae09d29b9da3d5 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 27 Jun 2025 13:59:32 -0400 Subject: [PATCH 3/8] Fix test for unencrypted field not in field map --- django_mongodb_backend/base.py | 1 + django_mongodb_backend/schema.py | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/django_mongodb_backend/base.py b/django_mongodb_backend/base.py index 8c8b1bbf..7935d180 100644 --- a/django_mongodb_backend/base.py +++ b/django_mongodb_backend/base.py @@ -160,6 +160,7 @@ def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS): # Cache the `settings_dict` in case we need to check for # auto_encryption_opts later. self.__dict__["_settings_dict"] = copy.deepcopy(settings_dict) + self.encrypted_connection = None def get_collection(self, name, **kwargs): collection = Collection(self.database, name, **kwargs) diff --git a/django_mongodb_backend/schema.py b/django_mongodb_backend/schema.py index b2afd486..9d7a2bf3 100644 --- a/django_mongodb_backend/schema.py +++ b/django_mongodb_backend/schema.py @@ -447,16 +447,17 @@ def _create_collection(self, model): auto_encryption_opts = self.connection._settings_dict.get("OPTIONS", {}).get( "auto_encryption_opts" ) - # Use the cached settings dict to create a new connection - encrypted_connection = self.connection.get_new_connection( - self.connection._settings_dict - ) + if not self.connection.encrypted_connection: + # Use the cached settings dict to create a new connection + self.encrypted_connection = self.connection.get_new_connection( + self.connection._settings_dict + ) # Use the encrypted connection and auto_encryption_opts to create an encrypted client - encrypted_client = get_encrypted_client(auto_encryption_opts, encrypted_connection) + encrypted_client = get_encrypted_client(auto_encryption_opts, self.encrypted_connection) with contextlib.suppress(EncryptedCollectionError): encrypted_client.create_encrypted_collection( - encrypted_connection[self.connection.database.name], + self.encrypted_connection[self.connection.database.name], model._meta.db_table, model.encrypted_fields_map, "local", # TODO: KMS provider should be configurable From e08945b2366c94144683facb2408e15c995ad1ac Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 27 Jun 2025 14:16:55 -0400 Subject: [PATCH 4/8] Add comment about suppressing EncryptedCollectionError --- django_mongodb_backend/schema.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django_mongodb_backend/schema.py b/django_mongodb_backend/schema.py index 9d7a2bf3..afa1a62e 100644 --- a/django_mongodb_backend/schema.py +++ b/django_mongodb_backend/schema.py @@ -455,6 +455,8 @@ def _create_collection(self, model): # Use the encrypted connection and auto_encryption_opts to create an encrypted client encrypted_client = get_encrypted_client(auto_encryption_opts, self.encrypted_connection) + # If the collection exists, `create_encrypted_collection` will raise an + # EncryptedCollectionError. with contextlib.suppress(EncryptedCollectionError): encrypted_client.create_encrypted_collection( self.encrypted_connection[self.connection.database.name], From 7b34b44abe43a05c3222c53037d0bf9a9b6b2d92 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 27 Jun 2025 18:12:12 -0400 Subject: [PATCH 5/8] Don't rely on features to fall back to unencrypted --- django_mongodb_backend/schema.py | 16 +--------------- tests/encryption_/models.py | 4 ++-- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/django_mongodb_backend/schema.py b/django_mongodb_backend/schema.py index afa1a62e..5a1d5877 100644 --- a/django_mongodb_backend/schema.py +++ b/django_mongodb_backend/schema.py @@ -422,20 +422,6 @@ def _field_should_have_unique(self, field): # The _id column is automatically unique. return db_type and field.unique and field.column != "_id" - def _supports_encryption(self, model): - """ - Check for `supports_encryption` feature and `auto_encryption_opts` - and `embedded_fields_map`. If `supports_encryption` is True and - `auto_encryption_opts` is in the cached connection settings and - the model has an embedded_fields_map property, then encryption - is supported. - """ - return ( - self.connection.features.supports_encryption - and self.connection._settings_dict.get("OPTIONS", {}).get("auto_encryption_opts") - and hasattr(model, "encrypted_fields_map") - ) - def _create_collection(self, model): """ Create a collection or, if encryption is supported, create @@ -443,7 +429,7 @@ def _create_collection(self, model): client then use that to create an encrypted collection. """ - if self._supports_encryption(model): + if hasattr(model, "encrypted_fields_map"): auto_encryption_opts = self.connection._settings_dict.get("OPTIONS", {}).get( "auto_encryption_opts" ) diff --git a/tests/encryption_/models.py b/tests/encryption_/models.py index 5e1a6201..8adbf1a0 100644 --- a/tests/encryption_/models.py +++ b/tests/encryption_/models.py @@ -5,8 +5,8 @@ class Person(EncryptedModel): - ssn = EncryptedCharField("ssn", max_length=11) name = models.CharField("name", max_length=100) + ssn = EncryptedCharField("ssn", max_length=11) def __str__(self): - return self.ssn + return self.name From 8e83adab00e347300238de73fcebfb75658ca769 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 27 Jun 2025 20:03:07 -0400 Subject: [PATCH 6/8] Remove _nodb_cursor and disable version check --- django_mongodb_backend/base.py | 33 ++++---------------------------- django_mongodb_backend/schema.py | 13 ++++--------- 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/django_mongodb_backend/base.py b/django_mongodb_backend/base.py index 7935d180..ad60588d 100644 --- a/django_mongodb_backend/base.py +++ b/django_mongodb_backend/base.py @@ -1,10 +1,9 @@ import contextlib -import copy import os from django.core.exceptions import ImproperlyConfigured from django.db import DEFAULT_DB_ALIAS -from django.db.backends.base.base import NO_DB_ALIAS, BaseDatabaseWrapper +from django.db.backends.base.base import BaseDatabaseWrapper from django.db.backends.utils import debug_transaction from django.utils.asyncio import async_unsafe from django.utils.functional import cached_property @@ -157,10 +156,6 @@ def _isnull_operator(a, b): def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS): super().__init__(settings_dict, alias=alias) self.session = None - # Cache the `settings_dict` in case we need to check for - # auto_encryption_opts later. - self.__dict__["_settings_dict"] = copy.deepcopy(settings_dict) - self.encrypted_connection = None def get_collection(self, name, **kwargs): collection = Collection(self.database, name, **kwargs) @@ -291,26 +286,6 @@ def validate_no_broken_transaction(self): def get_database_version(self): """Return a tuple of the database's version.""" - return tuple(self.connection.server_info()["versionArray"]) - - @contextlib.contextmanager - def _nodb_cursor(self): - """ - Returns a cursor from an unencrypted connection for operations - that do not support encryption. - - Encryption is only supported on encrypted models. - """ - - # Remove auto_encryption_opts from OPTIONS - if self.settings_dict.get("OPTIONS", {}).get("auto_encryption_opts"): - self.settings_dict["OPTIONS"].pop("auto_encryption_opts") - - # Create a new connection without OPTIONS["auto_encryption_opts": …] - conn = self.__class__({**self.settings_dict}, alias=NO_DB_ALIAS) - - try: - with conn.cursor() as cursor: - yield cursor - finally: - conn.close() + return (8, 1, 1) + # TODO: provide an unencrypted connection for this method. + # return tuple(self.connection.server_info()["versionArray"]) diff --git a/django_mongodb_backend/schema.py b/django_mongodb_backend/schema.py index 5a1d5877..1ba82c34 100644 --- a/django_mongodb_backend/schema.py +++ b/django_mongodb_backend/schema.py @@ -430,22 +430,17 @@ def _create_collection(self, model): """ if hasattr(model, "encrypted_fields_map"): - auto_encryption_opts = self.connection._settings_dict.get("OPTIONS", {}).get( + auto_encryption_opts = self.connection.settings_dict.get("OPTIONS", {}).get( "auto_encryption_opts" ) - if not self.connection.encrypted_connection: - # Use the cached settings dict to create a new connection - self.encrypted_connection = self.connection.get_new_connection( - self.connection._settings_dict - ) - # Use the encrypted connection and auto_encryption_opts to create an encrypted client - encrypted_client = get_encrypted_client(auto_encryption_opts, self.encrypted_connection) + client = self.connection.connection + encrypted_client = get_encrypted_client(auto_encryption_opts, client) # If the collection exists, `create_encrypted_collection` will raise an # EncryptedCollectionError. with contextlib.suppress(EncryptedCollectionError): encrypted_client.create_encrypted_collection( - self.encrypted_connection[self.connection.database.name], + client.database, model._meta.db_table, model.encrypted_fields_map, "local", # TODO: KMS provider should be configurable From 4da895c8d28fb27e7af1f64edfce0dcc5fdc8650 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 27 Jun 2025 20:15:08 -0400 Subject: [PATCH 7/8] Don't surpress encrypted error --- django_mongodb_backend/schema.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/django_mongodb_backend/schema.py b/django_mongodb_backend/schema.py index 1ba82c34..c074708a 100644 --- a/django_mongodb_backend/schema.py +++ b/django_mongodb_backend/schema.py @@ -1,8 +1,5 @@ -import contextlib - from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.models import Index, UniqueConstraint -from pymongo.encryption import EncryptedCollectionError from pymongo.operations import SearchIndexModel from .encryption import get_encrypted_client @@ -425,8 +422,8 @@ def _field_should_have_unique(self, field): def _create_collection(self, model): """ Create a collection or, if encryption is supported, create - an encrypted connection then use it to create an encrypted - client then use that to create an encrypted collection. + an encrypted client then use that to create an encrypted + collection. """ if hasattr(model, "encrypted_fields_map"): @@ -435,15 +432,11 @@ def _create_collection(self, model): ) client = self.connection.connection encrypted_client = get_encrypted_client(auto_encryption_opts, client) - - # If the collection exists, `create_encrypted_collection` will raise an - # EncryptedCollectionError. - with contextlib.suppress(EncryptedCollectionError): - encrypted_client.create_encrypted_collection( - client.database, - model._meta.db_table, - model.encrypted_fields_map, - "local", # TODO: KMS provider should be configurable - ) + encrypted_client.create_encrypted_collection( + client.database, + model._meta.db_table, + model.encrypted_fields_map, + "local", # TODO: KMS provider should be configurable + ) else: self.get_database().create_collection(model._meta.db_table) From ed54a9bdfb05e5ffc22180fd74d7a3ab7a949013 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Fri, 27 Jun 2025 20:27:25 -0400 Subject: [PATCH 8/8] Rename get_encrypted_client -> get_client_encryption --- django_mongodb_backend/encryption.py | 2 +- django_mongodb_backend/schema.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/django_mongodb_backend/encryption.py b/django_mongodb_backend/encryption.py index 87b39add..2921f343 100644 --- a/django_mongodb_backend/encryption.py +++ b/django_mongodb_backend/encryption.py @@ -8,7 +8,7 @@ from pymongo.encryption import AutoEncryptionOpts, ClientEncryption -def get_encrypted_client(auto_encryption_opts, encrypted_connection): +def get_client_encryption(auto_encryption_opts, encrypted_connection): """ Returns a `ClientEncryption` instance for MongoDB Client-Side Field Level Encryption (CSFLE) that can be used to create an encrypted collection. diff --git a/django_mongodb_backend/schema.py b/django_mongodb_backend/schema.py index c074708a..96311c93 100644 --- a/django_mongodb_backend/schema.py +++ b/django_mongodb_backend/schema.py @@ -2,7 +2,7 @@ from django.db.models import Index, UniqueConstraint from pymongo.operations import SearchIndexModel -from .encryption import get_encrypted_client +from .encryption import get_client_encryption from .fields import EmbeddedModelField from .indexes import SearchIndex from .query import wrap_database_errors @@ -421,9 +421,7 @@ def _field_should_have_unique(self, field): def _create_collection(self, model): """ - Create a collection or, if encryption is supported, create - an encrypted client then use that to create an encrypted - collection. + Create a collection or encrypted collection for the model. """ if hasattr(model, "encrypted_fields_map"): @@ -431,8 +429,8 @@ def _create_collection(self, model): "auto_encryption_opts" ) client = self.connection.connection - encrypted_client = get_encrypted_client(auto_encryption_opts, client) - encrypted_client.create_encrypted_collection( + client_encryption = get_client_encryption(auto_encryption_opts, client) + client_encryption.create_encrypted_collection( client.database, model._meta.db_table, model.encrypted_fields_map,