Skip to content

Commit d6ab11e

Browse files
committed
Code review fixes (2/x)
1 parent a566237 commit d6ab11e

File tree

11 files changed

+234
-195
lines changed

11 files changed

+234
-195
lines changed

django_mongodb_backend/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,9 @@ def cursor(self):
250250

251251
def get_database_version(self):
252252
"""Return a tuple of the database's version."""
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
253+
# This can use PyMongo's
254+
# `tuple(self.connection.server_info()["versionArray"])` on
255+
# pymongocrypt>=1.14.2. See: https://jira.mongodb.org/browse/PYTHON-5429
256256
return tuple(self.connection.admin.command("buildInfo")["versionArray"])
257257

258258
## Transaction API for django_mongodb_backend.transaction.atomic()

django_mongodb_backend/management/commands/showencryptedfieldsmap.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ def add_arguments(self, parser):
2626

2727
def handle(self, *args, **options):
2828
db = options["database"]
29-
create_data_keys = options.get("create_new_keys", False)
29+
create_new_keys = options.get("`create_new_keys`", False)
3030
connection = connections[db]
3131
client = connection.connection
3232
encrypted_fields_map = {}
33-
auto_encryption_opts = getattr(client._options, "auto_encryption_opts", None)
34-
for app_config in apps.get_app_configs():
35-
for model in router.get_migratable_models(app_config, db):
36-
if model_has_encrypted_fields(model):
37-
fields = connection.schema_editor()._get_encrypted_fields_map(
38-
model, client, auto_encryption_opts, create_data_keys=create_data_keys
39-
)
40-
encrypted_fields_map[model._meta.db_table] = fields
33+
with connection.schema_editor() as editor:
34+
for app_config in apps.get_app_configs():
35+
for model in router.get_migratable_models(app_config, db):
36+
if model_has_encrypted_fields(model):
37+
fields = editor._get_encrypted_fields_map(
38+
model, client, create_new_keys=create_new_keys
39+
)
40+
encrypted_fields_map[model._meta.db_table] = fields
4141
self.stdout.write(json_util.dumps(encrypted_fields_map, indent=2))

django_mongodb_backend/schema.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -450,16 +450,16 @@ def _create_collection(self, model):
450450
else:
451451
# If the encrypted fields map is provided, get the map for the
452452
# specific collection.
453-
encrypted_fields_map = encrypted_fields_map.get(db_table, None)
453+
encrypted_fields_map = encrypted_fields_map.get(db_table)
454454
db.create_collection(db_table, encryptedFields=encrypted_fields_map)
455455
else:
456456
db.create_collection(db_table)
457457

458-
def _get_encrypted_fields_map(
459-
self, model, client, auto_encryption_opts, create_data_keys=False
460-
):
458+
def _get_encrypted_fields_map(self, model, client, create_new_keys=False):
461459
connection = self.connection
462460
fields = model._meta.fields
461+
options = client._options
462+
auto_encryption_opts = options.auto_encryption_opts
463463
kms_provider = router.kms_provider(model)
464464
master_key = self.connection.settings_dict.get("KMS_CREDENTIALS").get(kms_provider)
465465
client_encryption = ClientEncryption(
@@ -475,7 +475,7 @@ def _get_encrypted_fields_map(
475475
for field in fields:
476476
if getattr(field, "encrypted", False):
477477
key_alt_name = f"{db_table}_{field.column}"
478-
if not create_data_keys:
478+
if not create_new_keys:
479479
key_doc = key_vault_collection.find_one({"keyAltNames": key_alt_name})
480480
if not key_doc:
481481
raise ValueError(f"No key found in keyvault for keyAltName={key_alt_name}")

docs/source/howto/queryable-encryption.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,11 @@ In addition to the :ref:`settings described in the how-to guide
122122
<server-side-queryable-encryption-settings>` you will need to provide a
123123
``encrypted_fields_map`` to the ``AutoEncryptionOpts``.
124124

125-
You can use the ``showencryptedfieldsmap`` management command to generate the
126-
schema map for your encrypted fields and then use the results in your settings::
125+
You can use the :djadmin:`showencryptedfieldsmap` management command to generate
126+
the schema map for your encrypted fields and then use the results in your
127+
settings::
127128

128-
python manage.py showencryptedfieldsmap
129+
python manage.py showencryptedfieldsmap --database=encrypted
129130

130131
.. admonition:: Didn't work?
131132

docs/source/topics/queryable-encryption.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
====================
12
Queryable Encryption
23
====================
34

@@ -10,7 +11,7 @@ data in MongoDB.
1011
.. _encrypted-field-example:
1112

1213
The basics
13-
----------
14+
==========
1415

1516
Let's consider this example::
1617

@@ -28,7 +29,7 @@ Let's consider this example::
2829
return self.ssn
2930

3031
Querying encrypted fields
31-
-------------------------
32+
=========================
3233

3334
The ``ssn`` field is only visible from an encrypted client connection. From an
3435
unencrypted client connection, the patient data looks like this:
@@ -40,8 +41,8 @@ unencrypted client connection, the patient data looks like this:
4041
ssn: Binary.createFromBase64('DkrbD67ejkt2u…', 6),
4142
}
4243
43-
You can query encrypted fields using a
44-
:ref:`manual:qe-supported-query-operators` which must be specified in the
44+
You can query encrypted fields using the :ref:`supported query type options
45+
<manual:qe-fundamentals-encrypt-query>` which must be specified in the
4546
field definition. For example, to query the ``ssn`` field for equality, you
4647
can use the ``{"queryType": "equality"}`` operator as shown in the example
4748
above.

tests/encryption_/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
EQUALITY_QUERY = {"queryType": "equality"}
2424
RANGE_QUERY = {"queryType": "range"}
25-
RANGE_QUERY_MIN_MAX = {"queryType": "range", "min": 0, "max": 100}
2625

2726

2827
class Appointment(models.Model):
@@ -53,7 +52,9 @@ class PatientRecord(models.Model):
5352
ssn = EncryptedCharField(max_length=11, queries=EQUALITY_QUERY)
5453
birth_date = EncryptedDateField(queries=RANGE_QUERY)
5554
profile_picture = EncryptedBinaryField(queries=EQUALITY_QUERY)
56-
patient_age = EncryptedIntegerField("patient_age", queries=RANGE_QUERY_MIN_MAX)
55+
patient_age = EncryptedIntegerField(
56+
"patient_age", queries={**RANGE_QUERY, "min": 0, "max": 100}
57+
)
5758
weight = EncryptedFloatField(queries=RANGE_QUERY)
5859

5960
# TODO: Embed Billing model

tests/encryption_/routers.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ class TestEncryptedRouter:
88
DATABASE_ROUTERS=[TestEncryptedRouter()]) takes effect.
99
"""
1010

11-
def allow_migrate(self, db, app_label, model_name=None, model=None, **hints):
12-
if model:
13-
return db == ("encrypted" if model_has_encrypted_fields(model) else "default")
14-
return db == "default"
15-
1611
def db_for_read(self, model, **hints):
1712
if model_has_encrypted_fields(model):
1813
return "encrypted"

tests/encryption_/test_base.py

Lines changed: 2 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,19 @@
1-
from datetime import datetime, time
1+
from datetime import datetime
22

3-
import pymongo
4-
from bson.binary import Binary
5-
from django.conf import settings
6-
from django.db import connections, models
73
from django.test import TransactionTestCase, override_settings
84

9-
from django_mongodb_backend.fields import EncryptedFieldMixin
10-
115
from .models import (
126
Appointment,
137
Billing,
14-
EncryptedNumbers,
158
Patient,
169
PatientPortalUser,
1710
PatientRecord,
1811
)
1912
from .routers import TestEncryptedRouter
2013

2114

22-
class EncryptedDurationField(EncryptedFieldMixin, models.DurationField):
23-
"""
24-
Unsupported by MongoDB when used with Queryable Encryption.
25-
Included in tests until fix or wontfix.
26-
"""
27-
28-
29-
class EncryptedSlugField(EncryptedFieldMixin, models.SlugField):
30-
"""
31-
Unsupported by MongoDB when used with Queryable Encryption.
32-
Included in tests until fix or wontfix.
33-
"""
34-
35-
3615
@override_settings(DATABASE_ROUTERS=[TestEncryptedRouter()])
37-
class EncryptedFieldTests(TransactionTestCase):
16+
class QueryableEncryptionTestCase(TransactionTestCase):
3817
databases = {"default", "encrypted"}
3918
available_apps = ["encryption_"]
4019

@@ -65,142 +44,3 @@ def setUp(self):
6544

6645
# TODO: Embed billing and patient_record models in patient model
6746
# then add tests
68-
69-
def test_get_encrypted_fields_map(self):
70-
"""Test class method called by schema editor
71-
and management command to get encrypted fields map for
72-
`create_encrypted_collection` and `auto_encryption_opts` respectively.
73-
There are no data keys in the results.
74-
75-
Data keys for the schema editor are created by
76-
`create_encrypted_collection` and data keys for the
77-
management command are created by the management command
78-
using code similar to the code in `create_encrypted_collection`
79-
in Pymongo.
80-
"""
81-
expected_encrypted_fields_map = {
82-
"fields": [
83-
{
84-
"bsonType": "int",
85-
"path": "patient_id",
86-
"queries": {"queryType": "equality"},
87-
},
88-
{
89-
"bsonType": "string",
90-
"path": "patient_name",
91-
},
92-
{
93-
"bsonType": "string",
94-
"path": "patient_notes",
95-
"queries": {"queryType": "equality"},
96-
},
97-
{
98-
"bsonType": "date",
99-
"path": "registration_date",
100-
"queries": {"queryType": "equality"},
101-
},
102-
{
103-
"bsonType": "bool",
104-
"path": "is_active",
105-
"queries": {"queryType": "equality"},
106-
},
107-
{
108-
"bsonType": "string",
109-
"path": "email",
110-
"queries": {"queryType": "equality"},
111-
},
112-
]
113-
}
114-
connection = connections["encrypted"]
115-
auto_encryption_opts = getattr(connection.connection._options, "auto_encryption_opts", None)
116-
with connection.schema_editor() as editor:
117-
client = connection.connection
118-
encrypted_fields_map = editor._get_encrypted_fields_map(
119-
self.patient, client, auto_encryption_opts
120-
)
121-
for field in encrypted_fields_map["fields"]:
122-
# Remove data keys from the output; they are expected to differ
123-
field.pop("keyId", None)
124-
self.assertEqual(
125-
encrypted_fields_map,
126-
expected_encrypted_fields_map,
127-
)
128-
129-
def test_appointment(self):
130-
self.assertEqual(Appointment.objects.get(time="8:00").time, time(8, 0))
131-
132-
def test_billing(self):
133-
self.assertEqual(
134-
Billing.objects.get(cc_number=1234567890123456).cc_number, 1234567890123456
135-
)
136-
self.assertEqual(Billing.objects.get(cc_type="Visa").cc_type, "Visa")
137-
self.assertTrue(Billing.objects.filter(account_balance__gte=100.0).exists())
138-
139-
def test_patientportaluser(self):
140-
self.assertEqual(
141-
PatientPortalUser.objects.get(ip_address="127.0.0.1").ip_address, "127.0.0.1"
142-
)
143-
144-
def test_patientrecord(self):
145-
self.assertEqual(PatientRecord.objects.get(ssn="123-45-6789").ssn, "123-45-6789")
146-
with self.assertRaises(PatientRecord.DoesNotExist):
147-
PatientRecord.objects.get(ssn="000-00-0000")
148-
self.assertTrue(PatientRecord.objects.filter(birth_date__gte="1969-01-01").exists())
149-
self.assertEqual(
150-
PatientRecord.objects.get(ssn="123-45-6789").profile_picture, b"image data"
151-
)
152-
self.assertTrue(PatientRecord.objects.filter(patient_age__gte=40).exists())
153-
self.assertFalse(PatientRecord.objects.filter(patient_age__gte=80).exists())
154-
self.assertTrue(PatientRecord.objects.filter(weight__gte=175.0).exists())
155-
156-
# Test encrypted patient record in unencrypted database.
157-
conn_params = connections["encrypted"].get_connection_params()
158-
db_name = settings.DATABASES["encrypted"]["NAME"]
159-
if conn_params.pop("auto_encryption_opts", False):
160-
# Call MongoClient instead of get_new_connection because
161-
# get_new_connection will return the encrypted connection
162-
# from the connection pool.
163-
with pymongo.MongoClient(**conn_params) as new_connection:
164-
patientrecords = new_connection[db_name].encryption__patientrecord.find()
165-
ssn = patientrecords[0]["ssn"]
166-
self.assertTrue(isinstance(ssn, Binary))
167-
168-
def test_patient(self):
169-
self.assertEqual(
170-
Patient.objects.get(patient_notes="patient notes " * 25).patient_notes,
171-
"patient notes " * 25,
172-
)
173-
self.assertEqual(
174-
Patient.objects.get(
175-
registration_date=datetime(2023, 10, 1, 12, 0, 0)
176-
).registration_date,
177-
datetime(2023, 10, 1, 12, 0, 0),
178-
)
179-
self.assertTrue(Patient.objects.get(patient_id=1).is_active)
180-
self.assertEqual(
181-
Patient.objects.get(email="[email protected]").email, "[email protected]"
182-
)
183-
184-
# Test decrypted patient record in encrypted database.
185-
patients = connections["encrypted"].database.encryption__patient.find()
186-
self.assertEqual(len(list(patients)), 1)
187-
records = connections["encrypted"].database.encryption__patientrecord.find()
188-
self.assertTrue("__safeContent__" in records[0])
189-
190-
def test_numeric_fields(self):
191-
"""
192-
Fields that have not been tested elsewhere.
193-
"""
194-
EncryptedNumbers.objects.create(
195-
pos_bigint=1000000,
196-
pos_smallint=12345,
197-
smallint=-12345,
198-
)
199-
200-
obj = EncryptedNumbers.objects.get(pos_bigint=1000000)
201-
obj = EncryptedNumbers.objects.get(pos_smallint=12345)
202-
obj = EncryptedNumbers.objects.get(smallint=-12345)
203-
204-
self.assertEqual(obj.pos_bigint, 1000000)
205-
self.assertEqual(obj.pos_smallint, 12345)
206-
self.assertEqual(obj.smallint, -12345)

0 commit comments

Comments
 (0)