Skip to content

Commit a02484f

Browse files
committed
Code review fixes
- assertion methods now use camelCase - document Django QuerySet limitations - low, high, threshold args are now required kwargs - Add $ to all shell prompts - Add patient schema to management and schema tests - Add recursive fields to schema - Reorder patient models - Add bill_amount field - Set databases for management and schema tests
1 parent 16922e6 commit a02484f

File tree

6 files changed

+154
-78
lines changed

6 files changed

+154
-78
lines changed

django_mongodb_backend/schema.py

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -483,52 +483,106 @@ def _create_collection(self, model):
483483
else:
484484
db.create_collection(db_table)
485485

486-
def _get_encrypted_fields(self, model, client, create_data_keys=False):
486+
def _get_encrypted_fields(
487+
self, model, client, create_data_keys=False, key_alt_name=None, client_encryption=None
488+
):
489+
"""
490+
Recursively collect encryption schema data for fields in a model.
491+
492+
key_alt_name is the base path for this level, typically model._meta.db_table
493+
"""
487494
connection = self.connection
488495
fields = model._meta.fields
496+
key_alt_name = key_alt_name or model._meta.db_table
497+
489498
options = client._options
490499
auto_encryption_opts = options.auto_encryption_opts
491-
kms_provider = router.kms_provider(model)
492-
master_key = self.connection.settings_dict.get("KMS_CREDENTIALS", {}).get(kms_provider)
493-
client_encryption = ClientEncryption(
494-
auto_encryption_opts._kms_providers,
495-
auto_encryption_opts._key_vault_namespace,
496-
client,
497-
client.codec_options,
498-
)
499500
key_vault_db, key_vault_coll = auto_encryption_opts._key_vault_namespace.split(".", 1)
500501
key_vault_collection = client[key_vault_db][key_vault_coll]
501-
db_table = model._meta.db_table
502+
kms_provider = router.kms_provider(model)
503+
master_key = connection.settings_dict.get("KMS_CREDENTIALS", {}).get(kms_provider)
504+
505+
# Initialize ClientEncryption once
506+
if client_encryption is None:
507+
client_encryption = ClientEncryption(
508+
auto_encryption_opts._kms_providers,
509+
auto_encryption_opts._key_vault_namespace,
510+
client,
511+
client.codec_options,
512+
)
513+
502514
field_list = []
515+
503516
for field in fields:
517+
new_path = f"{key_alt_name}.{field.column}"
518+
519+
# --- EmbeddedModelField case ---
504520
if isinstance(field, EmbeddedModelField):
505-
# Recursively get encrypted fields for the embedded model.
506-
self._get_encrypted_fields(field.embedded_model, client, create_data_keys)
521+
field_dict = {"bsonType": "object", "path": field.column}
522+
523+
if getattr(field, "encrypted", False):
524+
if create_data_keys:
525+
data_key = client_encryption.create_data_key(
526+
kms_provider=kms_provider,
527+
master_key=master_key,
528+
key_alt_names=[new_path],
529+
)
530+
else:
531+
key_doc = key_vault_collection.find_one({"keyAltNames": new_path})
532+
if not key_doc:
533+
raise ValueError(
534+
f"No key found in keyvault for keyAltName={new_path}. "
535+
"Run with '--create-data-keys' to create missing keys."
536+
)
537+
data_key = key_doc["_id"]
538+
539+
field_dict["keyId"] = data_key
540+
541+
if getattr(field, "queries", False):
542+
field_dict["queries"] = field.queries
543+
544+
field_list.append(field_dict)
545+
continue
546+
547+
# Not encrypting whole object — add object entry and recurse
548+
field_list.append(field_dict)
549+
embedded_result = self._get_encrypted_fields(
550+
field.embedded_model,
551+
client,
552+
create_data_keys=create_data_keys,
553+
key_alt_name=new_path,
554+
client_encryption=client_encryption,
555+
)
556+
field_list.extend(embedded_result["fields"])
557+
continue
558+
559+
# --- Leaf encrypted field case ---
507560
if getattr(field, "encrypted", False):
508-
key_alt_name = f"{db_table}.{field.column}"
509561
if create_data_keys:
510562
data_key = client_encryption.create_data_key(
511563
kms_provider=kms_provider,
512564
master_key=master_key,
513-
key_alt_names=[key_alt_name],
565+
key_alt_names=[new_path], # distinct per field
514566
)
515567
else:
516-
key_doc = key_vault_collection.find_one({"keyAltNames": key_alt_name})
568+
key_doc = key_vault_collection.find_one({"keyAltNames": new_path})
517569
if not key_doc:
518570
raise ValueError(
519-
f"No key found in keyvault for keyAltName={key_alt_name}. "
520-
"You may need to run the management command with "
521-
"'--create-data-keys' to create missing keys."
571+
f"No key found in keyvault for keyAltName={new_path}. "
572+
"Run with '--create-data-keys' to create missing keys."
522573
)
523574
data_key = key_doc["_id"]
575+
524576
field_dict = {
525577
"bsonType": field.db_type(connection),
526578
"path": field.column,
527579
"keyId": data_key,
528580
}
529581
if getattr(field, "queries", False):
530582
field_dict["queries"] = field.queries
583+
531584
field_list.append(field_dict)
585+
532586
return {"fields": field_list}
533587

534588

docs/howto/queryable-encryption.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ the :djadmin:`showencryptedfieldsmap` command.
185185
To see the keys created by Django MongoDB Backend in the above scenario, you can
186186
run the following command::
187187

188-
python manage.py showencryptedfieldsmap --database encrypted
188+
$ python manage.py showencryptedfieldsmap --database encrypted
189189

190190
You can then use the output of the :djadmin:`showencryptedfieldsmap` command
191191
to set the ``encrypted_fields_map`` in
@@ -202,7 +202,7 @@ pre-defined encrypted fields map.
202202
If you do not want to use the data keys created by Django MongoDB Backend (when
203203
``python manage.py migrate`` is run), you can generate new data keys with::
204204

205-
python manage.py showencryptedfieldsmap --database encrypted \
205+
$ python manage.py showencryptedfieldsmap --database encrypted \
206206
--create-data-keys
207207

208208
In this scenario, Django MongoDB Backend will use the newly created data keys

tests/encryption_/models.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,27 @@
2626
from django_mongodb_backend.models import EmbeddedModel
2727

2828

29-
class Billing(EmbeddedModel):
30-
cc_type = models.CharField(max_length=50)
31-
cc_number = models.CharField(max_length=20)
32-
33-
34-
class PatientRecord(EmbeddedModel):
35-
ssn = EncryptedCharField(max_length=11, queries={"queryType": "equality"})
36-
billing = EncryptedEmbeddedModelField(Billing)
37-
38-
3929
class Patient(models.Model):
4030
patient_name = models.CharField(max_length=255)
4131
patient_id = models.BigIntegerField()
42-
patient_record = EmbeddedModelField(PatientRecord)
32+
patient_record = EmbeddedModelField("PatientRecord")
4333

4434
def __str__(self):
4535
return f"{self.patient_name} ({self.patient_id})"
4636

4737

48-
class EncryptedModel(models.Model):
38+
class PatientRecord(EmbeddedModel):
39+
ssn = EncryptedCharField(max_length=11, queries={"queryType": "equality"})
40+
billing = EncryptedEmbeddedModelField("Billing")
41+
bill_amount = models.DecimalField(max_digits=10, decimal_places=2)
42+
43+
44+
class Billing(EmbeddedModel):
45+
cc_type = models.CharField(max_length=50)
46+
cc_number = models.CharField(max_length=20)
47+
48+
49+
class EncryptedModelBase(models.Model):
4950
"""
5051
Abstract base model for all Encrypted models
5152
that require the 'supports_queryable_encryption' DB feature.
@@ -57,78 +58,78 @@ class Meta:
5758

5859

5960
# Equality-queryable fields
60-
class EncryptedBinaryTest(EncryptedModel):
61+
class EncryptedBinaryTest(EncryptedModelBase):
6162
value = EncryptedBinaryField(queries={"queryType": "equality"})
6263

6364

64-
class EncryptedBooleanTest(EncryptedModel):
65+
class EncryptedBooleanTest(EncryptedModelBase):
6566
value = EncryptedBooleanField(queries={"queryType": "equality"})
6667

6768

68-
class EncryptedCharTest(EncryptedModel):
69+
class EncryptedCharTest(EncryptedModelBase):
6970
value = EncryptedCharField(max_length=255, queries={"queryType": "equality"})
7071

7172

72-
class EncryptedEmailTest(EncryptedModel):
73+
class EncryptedEmailTest(EncryptedModelBase):
7374
value = EncryptedEmailField(max_length=255, queries={"queryType": "equality"})
7475

7576

76-
class EncryptedGenericIPAddressTest(EncryptedModel):
77+
class EncryptedGenericIPAddressTest(EncryptedModelBase):
7778
value = EncryptedGenericIPAddressField(queries={"queryType": "equality"})
7879

7980

80-
class EncryptedTextTest(EncryptedModel):
81+
class EncryptedTextTest(EncryptedModelBase):
8182
value = EncryptedTextField(queries={"queryType": "equality"})
8283

8384

84-
class EncryptedURLTest(EncryptedModel):
85+
class EncryptedURLTest(EncryptedModelBase):
8586
value = EncryptedURLField(max_length=500, queries={"queryType": "equality"})
8687

8788

8889
# Range-queryable fields (also support equality)
89-
class EncryptedBigIntegerTest(EncryptedModel):
90+
class EncryptedBigIntegerTest(EncryptedModelBase):
9091
value = EncryptedBigIntegerField(queries={"queryType": "range"})
9192

9293

93-
class EncryptedDateTest(EncryptedModel):
94+
class EncryptedDateTest(EncryptedModelBase):
9495
value = EncryptedDateField(queries={"queryType": "range"})
9596

9697

97-
class EncryptedDateTimeTest(EncryptedModel):
98+
class EncryptedDateTimeTest(EncryptedModelBase):
9899
value = EncryptedDateTimeField(queries={"queryType": "range"})
99100

100101

101-
class EncryptedDecimalTest(EncryptedModel):
102+
class EncryptedDecimalTest(EncryptedModelBase):
102103
value = EncryptedDecimalField(max_digits=10, decimal_places=2, queries={"queryType": "range"})
103104

104105

105-
class EncryptedDurationTest(EncryptedModel):
106+
class EncryptedDurationTest(EncryptedModelBase):
106107
value = EncryptedDurationField(queries={"queryType": "range"})
107108

108109

109-
class EncryptedFloatTest(EncryptedModel):
110+
class EncryptedFloatTest(EncryptedModelBase):
110111
value = EncryptedFloatField(queries={"queryType": "range"})
111112

112113

113-
class EncryptedIntegerTest(EncryptedModel):
114+
class EncryptedIntegerTest(EncryptedModelBase):
114115
value = EncryptedIntegerField(queries={"queryType": "range"})
115116

116117

117-
class EncryptedPositiveBigIntegerTest(EncryptedModel):
118+
class EncryptedPositiveBigIntegerTest(EncryptedModelBase):
118119
value = EncryptedPositiveBigIntegerField(queries={"queryType": "range"})
119120

120121

121-
class EncryptedPositiveIntegerTest(EncryptedModel):
122+
class EncryptedPositiveIntegerTest(EncryptedModelBase):
122123
value = EncryptedPositiveIntegerField(queries={"queryType": "range"})
123124

124125

125-
class EncryptedPositiveSmallIntegerTest(EncryptedModel):
126+
class EncryptedPositiveSmallIntegerTest(EncryptedModelBase):
126127
value = EncryptedPositiveSmallIntegerField(queries={"queryType": "range"})
127128

128129

129-
class EncryptedSmallIntegerTest(EncryptedModel):
130+
class EncryptedSmallIntegerTest(EncryptedModelBase):
130131
value = EncryptedSmallIntegerField(queries={"queryType": "range"})
131132

132133

133-
class EncryptedTimeTest(EncryptedModel):
134+
class EncryptedTimeTest(EncryptedModelBase):
134135
value = EncryptedTimeField(queries={"queryType": "range"})

0 commit comments

Comments
 (0)