-
Notifications
You must be signed in to change notification settings - Fork 25
INTPYTHON-527 Add Queryable Encryption support #329
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
base: main
Are you sure you want to change the base?
Conversation
Wrong commit message for 65bd15a and I don't want to force push yet. It should have said:
I'm aware that
|
It's not working as you think it is. As I said elsewhere, Does this fix the "command not supported for auto encryption: buildinfo" error? If so, it's perhaps because I'd suggest to use my patch is as a starting point for maintaining two connections. |
I don't disagree, but it feels a lot like
Yes it works by design, not a side effect. I'm
I'd make a few passes at it but did not get anywhere, I'll try again though. |
Your "stumble" theory of how it's working isn't correct. |
Copy that, thanks! I've removed
Still working on an unencrypted connection, but perhaps the only time we need it is for the version check. |
@ShaneHarvey @Jibola @timgraham FYI here is the
And here is the error again with some additional debug:
And the full traceback:
Test settings:
This is happening in the |
Co-authored-by: Tim Graham <[email protected]>
😮 |
Factor out field init into mixin and add int field
The main title in the left hand navigation should go to index.
tests/encryption_/tests.py
Outdated
) | ||
|
||
def test_auto_encryption_opts(self): | ||
management.call_command("get_encrypted_fields_map", "--database", "encrypted", verbosity=0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know Django tests use this format, but I think it's better to simply import call_command
rather than to use management.call_command
.
And you can add testing the output:
from io import StringIO
out = StringIO()
call_command("dance", stdout=out)
self.assertIn("I don't feel like dancing Rock'n'Roll.\n", out.getvalue())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def get_encrypted_fields_map(self, connection): | ||
return { | ||
"fields": [ | ||
field | ||
for app_config in apps.get_app_configs() | ||
for model in router.get_migratable_models( | ||
app_config, connection.alias, include_auto_created=False | ||
) | ||
if getattr(model, "encrypted", False) | ||
for field in connection.schema_editor()._get_encrypted_fields_map(model) | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This format doesn't look correct. Doesn't it have to include the database and collection information? Look at the pymongo example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. If I pass the results as schema_map
to AutoEncryptionOpts
I don't get an error, but I'll try with namespace.
django_mongodb_backend/encryption.py
Outdated
return AutoEncryptionOpts( | ||
key_vault_namespace=key_vault_namespace, | ||
kms_providers=kms_providers, | ||
crypt_shared_lib_path=crypt_shared_lib_path, | ||
schema_map=schema_map, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this will evolve, but did you notice that get_auto_encryption_opts
is currently nothing more than an alias of AutoEncryptionOpts
(passing all kwargs straight through). :-D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😂 e562718
def kms_credentials(self, model): | ||
# return KMS_CREDENTIALS.get(provider, None) | ||
return {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My impression is that kms_credentials
is more of a global setting.. it doesn't change per model, thus doesn't need a router method. Possibly it could be different for each database, so it might be appropriate to use a new setting, e.g. DATABASES["alias"]["KMS_CREDENTIALS"]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
django_mongodb_backend/encryption.py
Outdated
def kms_provider(self, model): | ||
return getattr(settings, "KMS_PROVIDER", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user will provide a value rather than it be retrieved from a setting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs/source/howto/encryption.rst
Outdated
``encryption.EncryptedRouter`` | ||
------------------------------ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagined this wouldn't be a public API. This is the global router for the Django test suite that lives in our test settings. It makes certain assumptions about database aliases that may not be true of user projects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
django_mongodb_backend/encryption.py
Outdated
Return a `ClientEncryption` instance for use with Queryable Encryption. | ||
""" | ||
|
||
codec_options = CodecOptions(uuid_representation=STANDARD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our backend doesn't use uuid_representation
. UUIDs are stored as strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
connection.features.__dict__.pop("supports_queryable_encryption", None) | ||
|
||
def tearDown(self): | ||
connection.features.__dict__.pop("supports_queryable_encryption", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can use the del ...
version since it will exist by now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can use the
del ...
version since it will exist by now.
By now? I still get an attribute error …
|
||
class EncryptedRouter: | ||
def _get_db_for_model(self, model): | ||
if getattr(model, "encrypted", False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry If I ask something that was previously discussed, but how does it know if a model was an encrypted model or a model that has a field encrypted = models.BooleanField()
. Maybe with getattr(model, "encrypted", False) is True
could save for some false positives.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, thanks. I don't think we've finalized using that conditional yet and more importantly there is one in schema._create_collection
.
return name, path, args, kwargs | ||
|
||
|
||
class EncryptedCharField(EncryptedFieldMixin, models.CharField): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't find in the docs if an encrypted collection could have an aggregate query. So my question is:
does it support all the lookups from CharField ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No aggregation stages are supported and two tests from Django's test_charfield
are failing, though only one is an aggregation stage failure:
======================================================================
ERROR: test_assignment_from_choice_enum (encryption_.test_charfield.TestEncryptedCharField.test_assignment_from_choice_enum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 124, in _wrap_encryption_errors
yield
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 466, in encrypt
encrypted_cmd = self._auto_encrypter.encrypt(database, encoded_cmd)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/.venv/lib/python3.12/site-packages/pymongocrypt/synchronous/auto_encrypter.py", line 44, in encrypt
return run_state_machine(ctx, self.callback)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/.venv/lib/python3.12/site-packages/pymongocrypt/synchronous/state_machine.py", line 136, in run_state_machine
result = callback.mark_command(ctx.database, mongocryptd_cmd)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 286, in mark_command
res = self.mongocryptd_client[database].command(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/_csot.py", line 125, in csot_wrapper
return func(self, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/database.py", line 930, in command
return self._command(
^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/database.py", line 770, in _command
return conn.command(
^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/helpers.py", line 47, in inner
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/pool.py", line 414, in command
return command(
^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/network.py", line 212, in command
helpers_shared._check_command_response(
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/helpers_shared.py", line 250, in _check_command_response
raise OperationFailure(errmsg, code, response, max_wire_version)
pymongo.errors.OperationFailure: Comparison disallowed between fields where one is randomly encrypted; field 'title' is randomly encrypted., full error: RawBSONDocument(b"\xae\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02errmsg\x00k\x00\x00\x00Comparison disallowed between fields where one is randomly encrypted; field 'title' is randomly encrypted.\x00\x10code\x00\xb6y\x00\x00\x02codeName\x00\x0e\x00\x00\x00Location31158\x00\x00", codec_options=CodecOptions(document_class=<class 'bson.raw_bson.RawBSONDocument'>, tz_aware=False, uuid_representation=UuidRepresentation.UNSPECIFIED, unicode_decode_error_handler='strict', tzinfo=None, type_registry=TypeRegistry(type_codecs=[], fallback_encoder=None), datetime_conversion=DatetimeConversion.DATETIME))
ERROR: test_lookup_integer_in_charfield (encryption_.test_charfield.TestEncryptedCharField.test_lookup_integer_in_charfield)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 124, in _wrap_encryption_errors
yield
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 466, in encrypt
encrypted_cmd = self._auto_encrypter.encrypt(database, encoded_cmd)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/.venv/lib/python3.12/site-packages/pymongocrypt/synchronous/auto_encrypter.py", line 44, in encrypt
return run_state_machine(ctx, self.callback)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/.venv/lib/python3.12/site-packages/pymongocrypt/synchronous/state_machine.py", line 136, in run_state_machine
result = callback.mark_command(ctx.database, mongocryptd_cmd)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/encryption.py", line 286, in mark_command
res = self.mongocryptd_client[database].command(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/_csot.py", line 125, in csot_wrapper
return func(self, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/database.py", line 930, in command
return self._command(
^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/database.py", line 770, in _command
return conn.command(
^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/helpers.py", line 47, in inner
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/pool.py", line 414, in command
return command(
^^^^^^^^
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/synchronous/network.py", line 212, in command
helpers_shared._check_command_response(
File "/Users/alex.clark/Developer/django-mongodb-cli/src/mongo-python-driver/pymongo/helpers_shared.py", line 250, in _check_command_response
raise OperationFailure(errmsg, code, response, max_wire_version)
pymongo.errors.OperationFailure: Aggregation stage $internalFacetTeeConsumer is not allowed or supported with automatic encryption., full error: RawBSONDocument(b'\xa6\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02errmsg\x00c\x00\x00\x00Aggregation stage $internalFacetTeeConsumer is not allowed or supported with automatic encryption.\x00\x10code\x00#y\x00\x00\x02codeName\x00\x0e\x00\x00\x00Location31011\x00\x00', codec_options=CodecOptions(document_class=<class 'bson.raw_bson.RawBSONDocument'>, tz_aware=False, uuid_representation=UuidRepresentation.UNSPECIFIED, unicode_decode_error_handler='strict', tzinfo=None, type_registry=TypeRegistry(type_codecs=[], fallback_encoder=None), datetime_conversion=DatetimeConversion.DATETIME))
Only testing EncryptedIntegerField
Both checks cannot exist in the same class else one may be interrupted by the other and fail as a result. Instead, check the version once and cache the results so subsequent checks can check the cache instead of the connection.
Regression in daa9a8e
(see previous attempts in #318, #319 and #323 for additional context)