Skip to content

Commit 4496054

Browse files
committed
INTPYTHON-527 Add Queryable Encryption support
1 parent 37a9dd5 commit 4496054

30 files changed

+1214
-152
lines changed

.evergreen/config.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,28 @@ buildvariants:
9090
tasks:
9191
- name: run-tests
9292

93+
- name: tests-7-noauth-nossl
94+
display_name: Run Tests 7.0 NoAuth NoSSL
95+
run_on: rhel87-small
96+
expansions:
97+
MONGODB_VERSION: "7.0"
98+
TOPOLOGY: server
99+
AUTH: "noauth"
100+
SSL: "nossl"
101+
tasks:
102+
- name: run-tests
103+
104+
- name: tests-7-auth-ssl
105+
display_name: Run Tests 7.0 Auth SSL
106+
run_on: rhel87-small
107+
expansions:
108+
MONGODB_VERSION: "7.0"
109+
TOPOLOGY: server
110+
AUTH: "auth"
111+
SSL: "ssl"
112+
tasks:
113+
- name: run-tests
114+
93115
- name: tests-8-noauth-nossl
94116
display_name: Run Tests 8.0 NoAuth NoSSL
95117
run_on: rhel87-small

.evergreen/run-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ set -eux
66
/opt/python/3.10/bin/python3 -m venv venv
77
. venv/bin/activate
88
python -m pip install -U pip
9+
pip install ".[encryption]"
910
pip install -e .
1011

1112
# Install django and test dependencies

.github/workflows/mongodb_settings.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22

3+
from pymongo.encryption_options import AutoEncryptionOpts
4+
35
from django_mongodb_backend import parse_uri
46

57
if mongodb_uri := os.getenv("MONGODB_URI"):
@@ -27,6 +29,36 @@
2729
},
2830
}
2931

32+
DATABASES["encrypted"] = {
33+
"ENGINE": "django_mongodb_backend",
34+
"NAME": "djangotests-encrypted",
35+
"OPTIONS": {
36+
"auto_encryption_opts": AutoEncryptionOpts(
37+
key_vault_namespace="my_encrypted_database.keyvault",
38+
kms_providers={"local": {"key": os.urandom(96)}},
39+
),
40+
"directConnection": True,
41+
},
42+
"KMS_PROVIDERS": {},
43+
"KMS_CREDENTIALS": {},
44+
}
45+
46+
47+
class EncryptedRouter:
48+
def allow_migrate(self, db, app_label, model_name=None, **hints):
49+
# The encryption_ app's models are only created in the encrypted database.
50+
if app_label == "encryption_":
51+
return db == "encrypted"
52+
# Don't create other app's models in the encrypted database.
53+
if db == "encrypted":
54+
return False
55+
return None
56+
57+
def kms_provider(self, model, **hints):
58+
return "local"
59+
60+
61+
DATABASE_ROUTERS = [EncryptedRouter()]
3062
DEFAULT_AUTO_FIELD = "django_mongodb_backend.fields.ObjectIdAutoField"
3163
PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",)
3264
SECRET_KEY = "django_tests_secret_key"

.github/workflows/runtests.py

Lines changed: 0 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -6,151 +6,6 @@
66
from django.core.exceptions import ImproperlyConfigured
77

88
test_apps = [
9-
"admin_changelist",
10-
"admin_checks",
11-
"admin_custom_urls",
12-
"admin_docs",
13-
"admin_filters",
14-
"admin_inlines",
15-
"admin_ordering",
16-
"admin_scripts",
17-
"admin_utils",
18-
"admin_views",
19-
"admin_widgets",
20-
"aggregation",
21-
"aggregation_regress",
22-
"annotations",
23-
"apps",
24-
"async",
25-
"auth_tests",
26-
"backends",
27-
"basic",
28-
"bulk_create",
29-
"cache",
30-
"check_framework",
31-
"constraints",
32-
"contenttypes_tests",
33-
"context_processors",
34-
"custom_columns",
35-
"custom_lookups",
36-
"custom_managers",
37-
"custom_pk",
38-
"datatypes",
39-
"dates",
40-
"datetimes",
41-
"db_functions",
42-
"defer",
43-
"defer_regress",
44-
"delete",
45-
"delete_regress",
46-
"empty",
47-
"empty_models",
48-
"expressions",
49-
"expressions_case",
50-
"field_defaults",
51-
"file_storage",
52-
"file_uploads",
53-
"fixtures",
54-
"fixtures_model_package",
55-
"fixtures_regress",
56-
"flatpages_tests",
57-
"force_insert_update",
58-
"foreign_object",
59-
"forms_tests",
60-
"from_db_value",
61-
"generic_inline_admin",
62-
"generic_relations",
63-
"generic_relations_regress",
64-
"generic_views",
65-
"get_earliest_or_latest",
66-
"get_object_or_404",
67-
"get_or_create",
68-
"i18n",
69-
"indexes",
70-
"inline_formsets",
71-
"introspection",
72-
"invalid_models_tests",
73-
"known_related_objects",
74-
"lookup",
75-
"m2m_and_m2o",
76-
"m2m_intermediary",
77-
"m2m_multiple",
78-
"m2m_recursive",
79-
"m2m_regress",
80-
"m2m_signals",
81-
"m2m_through",
82-
"m2m_through_regress",
83-
"m2o_recursive",
84-
"managers_regress",
85-
"many_to_many",
86-
"many_to_one",
87-
"many_to_one_null",
88-
"max_lengths",
89-
"messages_tests",
90-
"migrate_signals",
91-
"migration_test_data_persistence",
92-
"migrations",
93-
"model_fields",
94-
"model_forms",
95-
"model_formsets",
96-
"model_formsets_regress",
97-
"model_indexes",
98-
"model_inheritance",
99-
"model_inheritance_regress",
100-
"model_options",
101-
"model_package",
102-
"model_regress",
103-
"model_utils",
104-
"modeladmin",
105-
"multiple_database",
106-
"mutually_referential",
107-
"nested_foreign_keys",
108-
"null_fk",
109-
"null_fk_ordering",
110-
"null_queries",
111-
"one_to_one",
112-
"or_lookups",
113-
"order_with_respect_to",
114-
"ordering",
115-
"pagination",
116-
"prefetch_related",
117-
"proxy_model_inheritance",
118-
"proxy_models",
119-
"queries",
120-
"queryset_pickle",
121-
"redirects_tests",
122-
"reserved_names",
123-
"reverse_lookup",
124-
"save_delete_hooks",
125-
"schema",
126-
"select_for_update",
127-
"select_related",
128-
"select_related_onetoone",
129-
"select_related_regress",
130-
"serializers",
131-
"servers",
132-
"sessions_tests",
133-
"shortcuts",
134-
"signals",
135-
"sitemaps_tests",
136-
"sites_framework",
137-
"sites_tests",
138-
"string_lookup",
139-
"swappable_models",
140-
"syndication_tests",
141-
"test_client",
142-
"test_client_regress",
143-
"test_runner",
144-
"test_utils",
145-
"timezones",
146-
"transactions",
147-
"unmanaged_models",
148-
"update",
149-
"update_only_fields",
150-
"user_commands",
151-
"validation",
152-
"view_tests",
153-
"xor_lookups",
1549
# Add directories in django_mongodb_backend/tests
15510
*sorted(
15611
[

.github/workflows/test-python-atlas.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
- name: install django-mongodb-backend
2929
run: |
3030
pip3 install --upgrade pip
31+
pip3 install ".[encryption]"
3132
pip3 install -e .
3233
- name: Checkout Django
3334
uses: actions/checkout@v4

.github/workflows/test-python.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
- name: install django-mongodb-backend
2929
run: |
3030
pip3 install --upgrade pip
31+
pip3 install ".[encryption]"
3132
pip3 install -e .
3233
- name: Checkout Django
3334
uses: actions/checkout@v4

django_mongodb_backend/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .indexes import register_indexes # noqa: E402
1515
from .lookups import register_lookups # noqa: E402
1616
from .query import register_nodes # noqa: E402
17+
from .routers import register_routers # noqa: E402
1718

1819
__all__ = ["parse_uri"]
1920

@@ -25,3 +26,4 @@
2526
register_indexes()
2627
register_lookups()
2728
register_nodes()
29+
register_routers()

django_mongodb_backend/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ def cursor(self):
250250

251251
def get_database_version(self):
252252
"""Return a tuple of the database's version."""
253-
return tuple(self.connection.server_info()["versionArray"])
253+
# Avoid using PyMongo to check the database version or require
254+
# pymongocrypt>=1.14.2 which will contain a fix for the `buildInfo`
255+
# command. https://jira.mongodb.org/browse/PYTHON-5429
256+
return tuple(self.connection.admin.command("buildInfo")["versionArray"])
254257

255258
## Transaction API for django_mongodb_backend.transaction.atomic()
256259
@async_unsafe

django_mongodb_backend/features.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,22 @@ def django_test_expected_failures(self):
578578
}
579579

580580
@cached_property
581+
581582
def django_test_skips(self):
582583
skips = super().django_test_skips
583584
skips.update(self._django_test_skips)
584585
return skips
585586

587+
def mongodb_version(self):
588+
return self.connection.get_database_version() # e.g., (6, 3, 0)
589+
586590
@cached_property
587591
def is_mongodb_6_3(self):
588-
return self.connection.get_database_version() >= (6, 3)
592+
return self.mongodb_version >= (6, 3)
593+
594+
@cached_property
595+
def is_mongodb_7_0(self):
596+
return self.mongodb_version >= (7, 0)
589597

590598
@cached_property
591599
def supports_atlas_search(self):
@@ -615,3 +623,18 @@ def _supports_transactions(self):
615623
hello = client.command("hello")
616624
# a replica set or a sharded cluster
617625
return "setName" in hello or hello.get("msg") == "isdbgrid"
626+
627+
@cached_property
628+
def supports_queryable_encryption(self):
629+
"""
630+
Queryable Encryption requires a MongoDB 7.0 or later replica set or sharded
631+
cluster, as well as MonogDB Atlas or Enterprise.
632+
"""
633+
self.connection.ensure_connection()
634+
build_info = self.connection.connection.admin.command("buildInfo")
635+
is_enterprise = "enterprise" in build_info.get("modules")
636+
return (
637+
(is_enterprise or self.supports_atlas_search)
638+
and self._supports_transactions
639+
and self.is_mongodb_7_0
640+
)

django_mongodb_backend/fields/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@
33
from .duration import register_duration_field
44
from .embedded_model import EmbeddedModelField
55
from .embedded_model_array import EmbeddedModelArrayField
6+
from .encryption import (
7+
EncryptedBigIntegerField,
8+
EncryptedBinaryField,
9+
EncryptedBooleanField,
10+
EncryptedCharField,
11+
EncryptedDateField,
12+
EncryptedDateTimeField,
13+
EncryptedDecimalField,
14+
EncryptedEmailField,
15+
EncryptedFieldMixin,
16+
EncryptedFloatField,
17+
EncryptedGenericIPAddressField,
18+
EncryptedIntegerField,
19+
EncryptedPositiveBigIntegerField,
20+
EncryptedPositiveIntegerField,
21+
EncryptedPositiveSmallIntegerField,
22+
EncryptedSmallIntegerField,
23+
EncryptedTextField,
24+
EncryptedTimeField,
25+
EncryptedURLField,
26+
)
627
from .json import register_json_field
728
from .objectid import ObjectIdField
829
from .polymorphic_embedded_model import PolymorphicEmbeddedModelField
@@ -12,6 +33,25 @@
1233
"ArrayField",
1334
"EmbeddedModelArrayField",
1435
"EmbeddedModelField",
36+
"EncryptedBigIntegerField",
37+
"EncryptedBinaryField",
38+
"EncryptedBooleanField",
39+
"EncryptedCharField",
40+
"EncryptedDateField",
41+
"EncryptedDateTimeField",
42+
"EncryptedDecimalField",
43+
"EncryptedEmailField",
44+
"EncryptedFieldMixin",
45+
"EncryptedFloatField",
46+
"EncryptedGenericIPAddressField",
47+
"EncryptedIntegerField",
48+
"EncryptedPositiveBigIntegerField",
49+
"EncryptedPositiveIntegerField",
50+
"EncryptedPositiveSmallIntegerField",
51+
"EncryptedSmallIntegerField",
52+
"EncryptedTextField",
53+
"EncryptedTimeField",
54+
"EncryptedURLField",
1555
"ObjectIdAutoField",
1656
"ObjectIdField",
1757
"PolymorphicEmbeddedModelArrayField",

0 commit comments

Comments
 (0)