Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/encrypted_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Settings for django_mongodb_backend/tests when encryption is supported.
import os

from mongodb_settings import * # noqa: F403
from pymongo.encryption import AutoEncryptionOpts

DATABASES["encrypted"] = { # noqa: F405
"ENGINE": "django_mongodb_backend",
"NAME": "djangotests_encrypted",
"OPTIONS": {
"auto_encryption_opts": AutoEncryptionOpts(
key_vault_namespace="djangotests_encrypted.__keyVault",
kms_providers={"local": {"key": os.urandom(96)}},
),
"directConnection": True,
},
"KMS_CREDENTIALS": {},
}


class EncryptedRouter:
def db_for_read(self, model, **hints):
if model._meta.app_label == "encryption_":
return "encrypted"
return None

db_for_write = db_for_read

def allow_migrate(self, db, app_label, model_name=None, **hints):
# The encryption_ app's models are only created in the encrypted
# database.
if app_label == "encryption_":
return db == "encrypted"
# Don't create other app's models in the encrypted database.
if db == "encrypted":
return False
return None


DATABASE_ROUTERS.append(EncryptedRouter()) # noqa: F405
3 changes: 2 additions & 1 deletion .github/workflows/mongodb_settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Settings for django_mongodb_backend/tests.
# Settings for django_mongodb_backend/tests when encryption isn't supported.
from django_settings import * # noqa: F403

DATABASES["encrypted"] = {} # noqa: F405
DATABASE_ROUTERS = ["django_mongodb_backend.routers.MongoRouter"]
26 changes: 24 additions & 2 deletions .github/workflows/test-python-atlas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: install django-mongodb-backend
run: |
pip3 install --upgrade pip
pip3 install -e .
pip3 install -e .[encryption]
- name: Checkout Django
uses: actions/checkout@v5
with:
Expand All @@ -51,8 +51,30 @@ jobs:
run: cp .github/workflows/runtests.py django_repo/tests/runtests_.py
- name: Start local Atlas
working-directory: .
run: bash .github/workflows/start_local_atlas.sh mongodb/mongodb-atlas-local:7
run: bash .github/workflows/start_local_atlas.sh mongodb/mongodb-atlas-local:8.0.15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should have a separate job for Atlas 8 so we still test with Atlas 7, although it's useful to keep it like this for now so we can see the modifications.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about 7 outside of QE? If not, let's just test 8. Still thinking about going the other direction and supporting query, queryPreview, etc. But in this one case I think it's reasonable to cling to non-preview and versions that support query.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. In fact, based on the driver policy, we have to support MongoDB 6 until July 2028. I've argued that since Django 5.2 is supported until April 2028, we could make Django 5.2 the last version to support MongoDB 6. This is similar to Django's version support for its built in databases. In any case, it would be nice to finalize and document a MongoDB version support policy.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK in that case to remove as much ambiguity as possible, and to support as much MongoDB as we can, how about if we go back to 7 for QE and document rangePreview. I haven't thought about the MongoDB support policy in the context of this project, but what you propose sounds fine. Maybe open a PR to the docs with that policy defined so we can discuss there and merge.

- name: Install mongosh
run: |
wget -q https://downloads.mongodb.com/compass/mongosh-2.2.10-linux-x64.tgz
tar -xzf mongosh-*-linux-x64.tgz
sudo cp mongosh-*-linux-x64/bin/mongosh /usr/local/bin/
mongosh --version
- name: Install mongocryptd from Enterprise tarball
run: |
curl -sSL -o mongodb-enterprise.tgz "https://downloads.mongodb.com/linux/mongodb-linux-x86_64-enterprise-ubuntu2204-8.0.15.tgz"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a variable for 8.0.15 so it's not hardcoded in so many places? (I have to research to answer.) Same for ubuntu2204, probably. This action uses ubuntu-latest which is 24.04 I believe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tar -xzf mongodb-enterprise.tgz
sudo cp mongodb-linux-x86_64-enterprise-ubuntu2204-8.0.15/bin/mongocryptd /usr/local/bin/
- name: Start mongocryptd
run: |
nohup mongocryptd --logpath=/tmp/mongocryptd.log &
- name: Verify MongoDB installation
run: |
mongosh --eval 'db.runCommand({ connectionStatus: 1 })'
- name: Verify mongocryptd is running
run: |
pgrep mongocryptd
Comment on lines +69 to +74
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was your thinking here? Were these steps copied from another project? Wouldn't the start server / mongocryptd commands fail with a suitable exit code if they didn't start?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POC developed here: https://github.com/aclark4life/test-supercharge-action. The verify steps can probably come out.

- name: Run tests
run: python3 django_repo/tests/runtests_.py
permissions:
contents: read
env:
DJANGO_SETTINGS_MODULE: "encrypted_settings"
2 changes: 2 additions & 0 deletions django_mongodb_backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .indexes import register_indexes # noqa: E402
from .lookups import register_lookups # noqa: E402
from .query import register_nodes # noqa: E402
from .routers import register_routers # noqa: E402

__all__ = ["parse_uri"]

Expand All @@ -25,3 +26,4 @@
register_indexes()
register_lookups()
register_nodes()
register_routers()
11 changes: 11 additions & 0 deletions django_mongodb_backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.utils.functional import cached_property
from pymongo.collection import Collection
from pymongo.driver_info import DriverInfo
from pymongo.encryption import ClientEncryption
from pymongo.mongo_client import MongoClient
from pymongo.uri_parser import parse_uri

Expand Down Expand Up @@ -241,6 +242,16 @@ def get_database(self):
return OperationDebugWrapper(self)
return self.database

@cached_property
def client_encryption(self):
auto_encryption_opts = self.connection._options.auto_encryption_opts
return ClientEncryption(
auto_encryption_opts._kms_providers,
auto_encryption_opts._key_vault_namespace,
self.connection,
self.connection.codec_options,
)

@cached_property
def database(self):
"""Connect to the database the first time it's accessed."""
Expand Down
16 changes: 15 additions & 1 deletion django_mongodb_backend/creation.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
from django.conf import settings
from django.db.backends.base.creation import BaseDatabaseCreation
from django.db.backends.base.creation import TEST_DATABASE_PREFIX, BaseDatabaseCreation


class DatabaseCreation(BaseDatabaseCreation):
def _execute_create_test_db(self, cursor, parameters, keepdb=False):
# Close the connection (which may point to the non-test database) so
# that a new connection to the test database can be established later.
self.connection.close_pool()
# Use a test _key_vault_namespace. This assumes the key vault database
# is the same as the encrypted database so that _destroy_test_db() can
# reset the collection by dropping it.
opts = self.connection.settings_dict["OPTIONS"].get("auto_encryption_opts")
if opts:
self.connection.settings_dict["OPTIONS"][
"auto_encryption_opts"
]._key_vault_namespace = TEST_DATABASE_PREFIX + opts._key_vault_namespace
if not keepdb:
self._destroy_test_db(parameters["dbname"], verbosity=0)

Expand All @@ -24,3 +32,9 @@ def destroy_test_db(self, old_database_name=None, verbosity=1, keepdb=False, suf
super().destroy_test_db(old_database_name, verbosity, keepdb, suffix)
# Close the connection to the test database.
self.connection.close_pool()
# Restore the original _key_vault_namespace.
opts = self.connection.settings_dict["OPTIONS"].get("auto_encryption_opts")
if opts:
self.connection.settings_dict["OPTIONS"][
"auto_encryption_opts"
]._key_vault_namespace = opts._key_vault_namespace[len(TEST_DATABASE_PREFIX) :]
29 changes: 28 additions & 1 deletion django_mongodb_backend/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,21 @@ def django_test_skips(self):
skips.update(self._django_test_skips)
return skips

@cached_property
def mongodb_version(self):
return self.connection.get_database_version() # e.g., (6, 3, 0)

@cached_property
def is_mongodb_6_3(self):
return self.connection.get_database_version() >= (6, 3)
return self.mongodb_version >= (6, 3)

@cached_property
def is_mongodb_7_0(self):
return self.mongodb_version >= (7, 0)

@cached_property
def is_mongodb_8_0(self):
return self.mongodb_version >= (8, 0)

@cached_property
def supports_atlas_search(self):
Expand Down Expand Up @@ -620,3 +632,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_queryable_encryption(self):
"""
Queryable Encryption requires a MongoDB 8.0 or later replica set or sharded
cluster, as well as MongoDB Atlas or Enterprise.
"""
self.connection.ensure_connection()
build_info = self.connection.connection.admin.command("buildInfo")
is_enterprise = "enterprise" in build_info.get("modules")
return (
(is_enterprise or self.supports_atlas_search)
and self._supports_transactions
and self.is_mongodb_8_0
)
52 changes: 52 additions & 0 deletions django_mongodb_backend/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
from .duration import register_duration_field
from .embedded_model import EmbeddedModelField
from .embedded_model_array import EmbeddedModelArrayField
from .encryption import (
EncryptedArrayField,
EncryptedBigIntegerField,
EncryptedBinaryField,
EncryptedBooleanField,
EncryptedCharField,
EncryptedDateField,
EncryptedDateTimeField,
EncryptedDecimalField,
EncryptedDurationField,
EncryptedEmailField,
EncryptedEmbeddedModelArrayField,
EncryptedEmbeddedModelField,
EncryptedFieldMixin,
EncryptedFloatField,
EncryptedGenericIPAddressField,
EncryptedIntegerField,
EncryptedObjectIdField,
EncryptedPositiveBigIntegerField,
EncryptedPositiveIntegerField,
EncryptedPositiveSmallIntegerField,
EncryptedSmallIntegerField,
EncryptedTextField,
EncryptedTimeField,
EncryptedURLField,
EncryptedUUIDField,
)
from .json import register_json_field
from .objectid import ObjectIdField
from .polymorphic_embedded_model import PolymorphicEmbeddedModelField
Expand All @@ -12,6 +39,31 @@
"ArrayField",
"EmbeddedModelArrayField",
"EmbeddedModelField",
"EncryptedArrayField",
"EncryptedBigIntegerField",
"EncryptedBinaryField",
"EncryptedBooleanField",
"EncryptedCharField",
"EncryptedDateField",
"EncryptedDateTimeField",
"EncryptedDecimalField",
"EncryptedDurationField",
"EncryptedEmailField",
"EncryptedEmbeddedModelArrayField",
"EncryptedEmbeddedModelField",
"EncryptedFieldMixin",
"EncryptedFloatField",
"EncryptedGenericIPAddressField",
"EncryptedIntegerField",
"EncryptedObjectIdField",
"EncryptedPositiveBigIntegerField",
"EncryptedPositiveIntegerField",
"EncryptedPositiveSmallIntegerField",
"EncryptedSmallIntegerField",
"EncryptedTextField",
"EncryptedTimeField",
"EncryptedURLField",
"EncryptedUUIDField",
"ObjectIdAutoField",
"ObjectIdField",
"PolymorphicEmbeddedModelArrayField",
Expand Down
Loading