Skip to content
Closed
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
- 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"
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
- 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()
56 changes: 56 additions & 0 deletions django_mongodb_backend/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.contrib import admin
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.views.main import ChangeList
from django.core.paginator import InvalidPage, Paginator
from django.utils.functional import cached_property


class EncryptedPaginator(Paginator):
@cached_property
def count(self):
return len(self.object_list)


class EncryptedChangeList(ChangeList):
def get_results(self, request):
"""
This is django.contrib.admin.views.main.ChangeList.get_results with
a single modification to avoid COUNT queries.
"""
paginator = self.model_admin.get_paginator(request, self.queryset, self.list_per_page)
result_count = paginator.count
if self.model_admin.show_full_result_count:
# Modification: avoid COUNT query by using len() on the root queryset
full_result_count = len(self.root_queryset)
else:
full_result_count = None
can_show_all = result_count <= self.list_max_show_all
multi_page = result_count > self.list_per_page
if (self.show_all and can_show_all) or not multi_page:
result_list = self.queryset._clone()
else:
try:
result_list = paginator.page(self.page_num).object_list
except InvalidPage as err:
raise IncorrectLookupParameters from err
self.result_count = result_count
self.show_full_result_count = self.model_admin.show_full_result_count
self.show_admin_actions = not self.show_full_result_count or bool(full_result_count)
self.full_result_count = full_result_count
self.result_list = result_list
self.can_show_all = can_show_all
self.multi_page = multi_page
self.paginator = paginator


class EncryptedModelAdmin(admin.ModelAdmin):
"""
A ModelAdmin that uses EncryptedPaginator and EncryptedChangeList
to avoid COUNT queries in the admin changelist.
"""

def get_paginator(self, request, queryset, per_page):
return EncryptedPaginator(queryset, per_page)

def get_changelist(self, request, **kwargs):
return EncryptedChangeList
13 changes: 12 additions & 1 deletion 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 Expand Up @@ -325,7 +336,7 @@ def cursor(self):

def get_database_version(self):
"""Return a tuple of the database's version."""
return tuple(self.connection.server_info()["versionArray"])
return tuple(self.connection.admin.command("buildInfo")["versionArray"])

## Transaction API for django_mongodb_backend.transaction.atomic()
@async_unsafe
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) :]
25 changes: 24 additions & 1 deletion django_mongodb_backend/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,17 @@ 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_8_0(self):
return self.mongodb_version >= (8, 0)

@cached_property
def supports_atlas_search(self):
Expand Down Expand Up @@ -620,3 +628,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