Skip to content

Commit d0a88a1

Browse files
committed
Add queryable encryption config
1 parent 2ffbaeb commit d0a88a1

File tree

4 files changed

+81
-3
lines changed

4 files changed

+81
-3
lines changed

django_mongodb_backend/__init__.py

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

33
# Check Django compatibility before other imports which may fail if the
44
# wrong version of Django is installed.
5-
from .utils import check_django_compatability, parse_uri
5+
from .utils import check_django_compatability, get_auto_encryption_options, parse_uri
66

77
check_django_compatability()
88

@@ -15,7 +15,7 @@
1515
from .lookups import register_lookups # noqa: E402
1616
from .query import register_nodes # noqa: E402
1717

18-
__all__ = ["parse_uri"]
18+
__all__ = ["get_auto_encryption_options", "parse_uri"]
1919

2020
register_aggregates()
2121
register_checks()

django_mongodb_backend/features.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,3 +629,13 @@ def supports_transactions(self):
629629
engine = client.command("serverStatus").get("storageEngine", {})
630630
return engine.get("name") == "wiredTiger"
631631
return False
632+
633+
@cached_property
634+
def supports_queryable_encryption(self):
635+
"""
636+
Queryable Encryption is available if the server is Atlas or Enterprise.
637+
"""
638+
self.connection.ensure_connection()
639+
client = self.connection.connection.admin
640+
build_info = client.command("buildInfo")
641+
return "enterprise" in build_info.get("modules")

django_mongodb_backend/utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import copy
2+
import os
23
import time
4+
from pathlib import Path
5+
from urllib.parse import urlencode
36

47
import django
58
from django.conf import settings
@@ -8,6 +11,7 @@
811
from django.utils.functional import SimpleLazyObject
912
from django.utils.text import format_lazy
1013
from django.utils.version import get_version_tuple
14+
from pymongo.encryption_options import AutoEncryptionOpts
1115
from pymongo.uri_parser import parse_uri as pymongo_parse_uri
1216

1317

@@ -28,6 +32,62 @@ def check_django_compatability():
2832
)
2933

3034

35+
# Queryable Encryption-related functions based on helpers from Python Queryable
36+
# Encryption Tutorial
37+
# https://github.com/mongodb/docs/tree/master/source/includes/qe-tutorials/python/
38+
def _get_kms_provider_credentials(kms_provider_name):
39+
"""
40+
"A KMS is a remote service that securely stores and manages your encryption keys."
41+
42+
Via https://www.mongodb.com/docs/manual/core/queryable-encryption/quick-start/
43+
44+
Here we check the provider name and return the appropriate credentials.
45+
"""
46+
# TODO: Add support for other KMS providers.
47+
if kms_provider_name == "local":
48+
if not Path("./customer-master-key.txt").exists:
49+
try:
50+
path = "customer-master-key.txt"
51+
file_bytes = os.urandom(96)
52+
with Path.open(path, "wb") as f:
53+
f.write(file_bytes)
54+
except Exception as e:
55+
raise Exception(
56+
"Unable to write Customer Master Key to file due to the following error: "
57+
) from e
58+
59+
try:
60+
path = "./customer-master-key.txt"
61+
with Path.open(path, "rb") as f:
62+
local_master_key = f.read()
63+
if len(local_master_key) != 96:
64+
raise Exception("Expected the customer master key file to be 96 bytes.")
65+
return {
66+
"local": {"key": local_master_key},
67+
}
68+
except Exception as e:
69+
raise Exception(
70+
"Unable to read Customer Master Key from file due to the following error: "
71+
) from e
72+
else:
73+
raise ValueError(
74+
"Unrecognized value for kms_provider_name encountered while retrieving KMS credentials."
75+
)
76+
77+
78+
def get_auto_encryption_options(kms_provider_name):
79+
key_vault_database_name = "encryption"
80+
key_vault_collection_name = "__keyVault"
81+
key_vault_namespace = f"{key_vault_database_name}.{key_vault_collection_name}"
82+
kms_provider_credentials = _get_kms_provider_credentials(kms_provider_name)
83+
auto_encryption_opts = AutoEncryptionOpts(
84+
kms_provider_credentials,
85+
key_vault_namespace,
86+
crypt_shared_lib_path=os.environ.get("SHARED_LIB_PATH"),
87+
)
88+
return urlencode(auto_encryption_opts)
89+
90+
3191
def parse_uri(uri, *, db_name=None, test=None):
3292
"""
3393
Convert the given uri into a dictionary suitable for Django's DATABASES

tests/backend_/utils/test_parse_uri.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from unittest.mock import patch
2+
from urllib.parse import parse_qs
23

34
import pymongo
45
from django.core.exceptions import ImproperlyConfigured
56
from django.test import SimpleTestCase
67

7-
from django_mongodb_backend import parse_uri
8+
from django_mongodb_backend import get_auto_encryption_options, parse_uri
89

910

1011
class ParseURITests(SimpleTestCase):
@@ -94,3 +95,10 @@ def test_invalid_credentials(self):
9495
def test_no_scheme(self):
9596
with self.assertRaisesMessage(pymongo.errors.InvalidURI, "Invalid URI scheme"):
9697
parse_uri("cluster0.example.mongodb.net")
98+
99+
def test_queryable_encryption_config(self):
100+
auto_encryption_options = get_auto_encryption_options("local")
101+
settings_dict = parse_uri(
102+
f"mongodb://cluster0.example.mongodb.net/myDatabase{auto_encryption_options}"
103+
)
104+
self.assertEqual(settings_dict["OPTIONS"], parse_qs(auto_encryption_options))

0 commit comments

Comments
 (0)