Skip to content

INTPYTHON-527 Add queryable encryption support #319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

aclark4life
Copy link
Collaborator

@aclark4life aclark4life commented Jun 19, 2025

Another WIP

  • Completed most setup in qe.py
  • Completed some setup in Django
    • Refactored get_auto_encryption_options to support ephemeral local KMS and allow crypt_shared_lib_path to be passed in
    • Add check for presence of libmongocrypt to supports_queryable_encryption feature check
  • Started thinking about models, fields, migrations and planning an issubclassof check for EncryptedModel to call create_encrypted_collection instead of create_collection but need to pass encrypted_client to ClientEncryption somewhere in Django.

TODO

  • Decide on a configurable way to support local KMS so we can add other KMS later using the same configuration framework.
  • Decide on how to configure crypt_shared_lib_path. I don't like setting an environment variable for this configuration task so I settled on passing to get_auto_encryption_options in the short term but maybe a Django setting or something else in long term.
  • Decide on EncryptedModel and EncryptedField (not EncryptedModelField or EncryptedEmbeddedModelField yet!) implementation to support this:
from django_mongodb_backend.models import EncryptedModel
from django_mongodb_backend.models import EncryptedField
from django import models

class Person(EncryptedModel):
  ssn = EncryptedField(models.CharField)

Question

Lastly, I'm not sure why PyMongo is trying to do explicit here or at least that is what it appears to be doing despite the auto config:

% j q
python qe.py
Traceback (most recent call last):
  File "/Users/alexclark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 124, in _wrap_encryption_errors
    yield
  File "/Users/alexclark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 861, in create_data_key
    self._encryption.create_data_key(
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        kms_provider,
        ^^^^^^^^^^^^^
    ...<2 lines>...
        key_material=key_material,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
    ),
    ^
  File "/Users/alexclark/Developer/django-mongodb-cli/.venv/lib/python3.13/site-packages/pymongocrypt/synchronous/explicit_encrypter.py", line 66, in create_data_key
    with self.mongocrypt.data_key_context(kms_provider, opts) as ctx:
         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/Users/alexclark/Developer/django-mongodb-cli/.venv/lib/python3.13/site-packages/pymongocrypt/mongocrypt.py", line 284, in data_key_context
    return DataKeyContext(
        self._create_context(),
    ...<3 lines>...
        self.__callback,
    )
  File "/Users/alexclark/Developer/django-mongodb-cli/.venv/lib/python3.13/site-packages/pymongocrypt/mongocrypt.py", line 560, in __init__
    raise ValueError(f"unknown kms_provider: {kms_provider}")
ValueError: unknown kms_provider: None

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/alexclark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 762, in create_encrypted_collection
    encrypted_fields["fields"][i]["keyId"] = self.create_data_key(
                                             ~~~~~~~~~~~~~~~~~~~~^
        kms_provider=kms_provider,  # type:ignore[arg-type]
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        master_key=master_key,
        ^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/alexclark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 858, in create_data_key
    with _wrap_encryption_errors():
         ~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/alexclark/.pyenv/versions/3.13.3/lib/python3.13/contextlib.py", line 162, in __exit__
    self.gen.throw(value)
    ~~~~~~~~~~~~~~^^^^^^^
  File "/Users/alexclark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 130, in _wrap_encryption_errors
    raise EncryptionError(exc) from exc
pymongo.errors.EncryptionError: unknown kms_provider: None

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/alexclark/Developer/django-mongodb-cli/qe.py", line 34, in <module>
    encrypted_collection = client_encryption.create_encrypted_collection(
        encrypted_database, "encrypted_collection", encrypted_fields
    )
  File "/Users/alexclark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 767, in create_encrypted_collection
    raise EncryptedCollectionError(exc, encrypted_fields) from exc
pymongo.errors.EncryptedCollectionError: unknown kms_provider: None

Seems like I may have missed passing kms_providers somewhere.

I ruled out crypted_shared_lib_path being the cause. If you configure an invalid /path/to/libmongocrypt.so you get:

pymongocrypt.errors.MongoCryptError: A crypt_shared override path was specified [/path/to/libmongocrypt.so], but we failed to open a dynamic library at that location. Load error: [Error while opening candidate for crypt_shared dynamic library [/path/to/libmongocrypt.so]: dlopen(/path/to/libmongocrypt.so, 0x0005): tried: '/path/to/libmongocrypt.so' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/path/to/libmongocrypt.so' (no such file), '/path/to/libmongocrypt.so' (no such file)]


So… yeah.

@@ -94,3 +94,11 @@ def test_invalid_credentials(self):
def test_no_scheme(self):
with self.assertRaisesMessage(pymongo.errors.InvalidURI, "Invalid URI scheme"):
parse_uri("cluster0.example.mongodb.net")


# TODO: This can be moved to `test_features` once transaction support is merged.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_features.py would only be for testing the logic of DatabaseFeatures.supports_queryable_encryption.

Comment on lines +591 to +596
try:
import pymongocrypt # noqa: F401

has_pymongocrypt = True
except ImportError:
has_pymongocrypt = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's inappropriate to check for this package here. Instead the ImportError should be surfaced to the user if they try to use a feature that requires it. I can imagine a couple of ways to do so, but I think this concern should be deferred until the implementation is ironed out.

Comment on lines +33 to +42
def get_auto_encryption_options(crypt_shared_lib_path=None):
key_vault_database_name = "encryption"
key_vault_collection_name = "__keyVault"
key_vault_namespace = f"{key_vault_database_name}.{key_vault_collection_name}"
kms_providers = {"local": {"key": os.urandom(96)}}
return AutoEncryptionOpts(
key_vault_namespace=key_vault_namespace,
kms_providers=kms_providers,
crypt_shared_lib_path=crypt_shared_lib_path,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still obvious to me from the design doc to what extent it's appropriate for this library to provide helpers to generate AutoEncryptionOpts.

)


def parse_uri(uri, *, auto_encryption_options=None, db_name=None, test=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather than options=None than auto_encryption_options=None so that we're not blowing up the signature of parse_uri() with every kwarg of MongoClient. (If it's even in scope to continually expand parse_uri() rather than to guide more advanced users toward a dictionary DATABASES). I think all the changes in this file could be discussed/implemented in a follow up.


# TODO: This can be moved to `test_features` once transaction support is merged.
class ParseUriOptionsTests(TestCase):
@skipUnlessDBFeature("supports_queryable_encryption")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we just make installing pymongo[encryption] a required part of running our test suite. In that case, this test wouldn't need a skip since it doesn't interact with the server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants