Skip to content

Commit fb1e120

Browse files
committed
INTPYTHON-527 Add Queryable Encryption support
1 parent d5aa1a7 commit fb1e120

30 files changed

+1309
-153
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
@@ -4,151 +4,6 @@
44
import sys
55

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

.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
@@ -229,4 +229,7 @@ def cursor(self):
229229

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

django_mongodb_backend/features.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,9 +569,17 @@ def django_test_expected_failures(self):
569569
},
570570
}
571571

572+
@cached_property
573+
def mongodb_version(self):
574+
return self.connection.get_database_version() # e.g., (6, 3, 0)
575+
572576
@cached_property
573577
def is_mongodb_6_3(self):
574-
return self.connection.get_database_version() >= (6, 3)
578+
return self.mongodb_version >= (6, 3)
579+
580+
@cached_property
581+
def is_mongodb_7_0(self):
582+
return self.mongodb_version >= (7, 0)
575583

576584
@cached_property
577585
def supports_atlas_search(self):
@@ -601,3 +609,18 @@ def _supports_transactions(self):
601609
hello = client.command("hello")
602610
# a replica set or a sharded cluster
603611
return "setName" in hello or hello.get("msg") == "isdbgrid"
612+
613+
@cached_property
614+
def supports_queryable_encryption(self):
615+
"""
616+
Queryable Encryption requires a MongoDB 7.0 or later replica set or sharded
617+
cluster, as well as MonogDB Atlas or Enterprise.
618+
"""
619+
self.connection.ensure_connection()
620+
build_info = self.connection.connection.admin.command("buildInfo")
621+
is_enterprise = "enterprise" in build_info.get("modules")
622+
return (
623+
(is_enterprise or self.supports_atlas_search)
624+
and self._supports_transactions
625+
and self.is_mongodb_7_0
626+
)

django_mongodb_backend/fields/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,30 @@
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+
EqualityQuery,
27+
RangeQuery,
28+
has_encrypted_fields,
29+
)
630
from .json import register_json_field
731
from .objectid import ObjectIdField
832
from .polymorphic_embedded_model import PolymorphicEmbeddedModelField
@@ -12,10 +36,32 @@
1236
"ArrayField",
1337
"EmbeddedModelArrayField",
1438
"EmbeddedModelField",
39+
"EncryptedBigIntegerField",
40+
"EncryptedBinaryField",
41+
"EncryptedBooleanField",
42+
"EncryptedCharField",
43+
"EncryptedDateField",
44+
"EncryptedDateTimeField",
45+
"EncryptedDecimalField",
46+
"EncryptedEmailField",
47+
"EncryptedFieldMixin",
48+
"EncryptedFloatField",
49+
"EncryptedGenericIPAddressField",
50+
"EncryptedIntegerField",
51+
"EncryptedPositiveBigIntegerField",
52+
"EncryptedPositiveIntegerField",
53+
"EncryptedPositiveSmallIntegerField",
54+
"EncryptedSmallIntegerField",
55+
"EncryptedTextField",
56+
"EncryptedTimeField",
57+
"EncryptedURLField",
58+
"EqualityQuery",
1559
"ObjectIdAutoField",
1660
"ObjectIdField",
1761
"PolymorphicEmbeddedModelArrayField",
1862
"PolymorphicEmbeddedModelField",
63+
"RangeQuery",
64+
"has_encrypted_fields",
1965
"register_fields",
2066
]
2167

0 commit comments

Comments
 (0)