Skip to content

Commit b492263

Browse files
authored
PYTHON-3357 Automatically create Queryable Encryption keys (mongodb#1145)
1 parent b3099c6 commit b492263

File tree

3 files changed

+348
-28
lines changed

3 files changed

+348
-28
lines changed

pymongo/database.py

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""Database level operations."""
16+
from copy import deepcopy
1617
from typing import (
1718
TYPE_CHECKING,
1819
Any,
@@ -292,6 +293,28 @@ def get_collection(
292293
read_concern,
293294
)
294295

296+
def _get_encrypted_fields(self, kwargs, coll_name, ask_db):
297+
encrypted_fields = kwargs.get("encryptedFields")
298+
if encrypted_fields:
299+
return deepcopy(encrypted_fields)
300+
if (
301+
self.client.options.auto_encryption_opts
302+
and self.client.options.auto_encryption_opts._encrypted_fields_map
303+
and self.client.options.auto_encryption_opts._encrypted_fields_map.get(
304+
f"{self.name}.{coll_name}"
305+
)
306+
):
307+
return deepcopy(
308+
self.client.options.auto_encryption_opts._encrypted_fields_map[
309+
f"{self.name}.{coll_name}"
310+
]
311+
)
312+
if ask_db and self.client.options.auto_encryption_opts:
313+
options = self[coll_name].options()
314+
if options.get("encryptedFields"):
315+
return deepcopy(options["encryptedFields"])
316+
return None
317+
295318
@_csot.apply
296319
def create_collection(
297320
self,
@@ -419,19 +442,10 @@ def create_collection(
419442
.. _create collection command:
420443
https://mongodb.com/docs/manual/reference/command/create
421444
"""
422-
encrypted_fields = kwargs.get("encryptedFields")
423-
if (
424-
not encrypted_fields
425-
and self.client.options.auto_encryption_opts
426-
and self.client.options.auto_encryption_opts._encrypted_fields_map
427-
):
428-
encrypted_fields = self.client.options.auto_encryption_opts._encrypted_fields_map.get(
429-
"%s.%s" % (self.name, name)
430-
)
431-
kwargs["encryptedFields"] = encrypted_fields
432-
445+
encrypted_fields = self._get_encrypted_fields(kwargs, name, False)
433446
if encrypted_fields:
434447
common.validate_is_mapping("encryptedFields", encrypted_fields)
448+
kwargs["encryptedFields"] = encrypted_fields
435449

436450
clustered_index = kwargs.get("clusteredIndex")
437451
if clustered_index:
@@ -1038,21 +1052,11 @@ def drop_collection(
10381052

10391053
if not isinstance(name, str):
10401054
raise TypeError("name_or_collection must be an instance of str")
1041-
full_name = "%s.%s" % (self.name, name)
1042-
if (
1043-
not encrypted_fields
1044-
and self.client.options.auto_encryption_opts
1045-
and self.client.options.auto_encryption_opts._encrypted_fields_map
1046-
):
1047-
encrypted_fields = self.client.options.auto_encryption_opts._encrypted_fields_map.get(
1048-
full_name
1049-
)
1050-
if not encrypted_fields and self.client.options.auto_encryption_opts:
1051-
colls = list(
1052-
self.list_collections(filter={"name": name}, session=session, comment=comment)
1053-
)
1054-
if colls and colls[0]["options"].get("encryptedFields"):
1055-
encrypted_fields = colls[0]["options"]["encryptedFields"]
1055+
encrypted_fields = self._get_encrypted_fields(
1056+
{"encryptedFields": encrypted_fields},
1057+
name,
1058+
True,
1059+
)
10561060
if encrypted_fields:
10571061
common.validate_is_mapping("encrypted_fields", encrypted_fields)
10581062
self._drop_helper(

pymongo/encryption.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
import enum
1919
import socket
2020
import weakref
21-
from typing import Any, Generic, Mapping, Optional, Sequence
21+
from copy import deepcopy
22+
from typing import Any, Generic, Mapping, Optional, Sequence, Tuple
2223

2324
try:
2425
from pymongocrypt.auto_encrypter import AutoEncrypter
@@ -39,8 +40,10 @@
3940
from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument, _inflate_bson
4041
from bson.son import SON
4142
from pymongo import _csot
43+
from pymongo.collection import Collection
4244
from pymongo.cursor import Cursor
4345
from pymongo.daemon import _spawn_daemon
46+
from pymongo.database import Database
4447
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
4548
from pymongo.errors import (
4649
ConfigurationError,
@@ -552,6 +555,107 @@ def __init__(
552555
# Use the same key vault collection as the callback.
553556
self._key_vault_coll = self._io_callbacks.key_vault_coll
554557

558+
def create_encrypted_collection(
559+
self,
560+
database: Database,
561+
name: str,
562+
encrypted_fields: Mapping[str, Any],
563+
kms_provider: Optional[str] = None,
564+
master_key: Optional[Mapping[str, Any]] = None,
565+
key_alt_names: Optional[Sequence[str]] = None,
566+
key_material: Optional[bytes] = None,
567+
**kwargs: Any,
568+
) -> Tuple[Collection[_DocumentType], Mapping[str, Any]]:
569+
"""Create a collection with encryptedFields.
570+
571+
.. warning::
572+
This function does not update the encryptedFieldsMap in the client's
573+
AutoEncryptionOpts, thus the user must create a new client after calling this function with
574+
the encryptedFields returned.
575+
576+
Normally collection creation is automatic. This method should
577+
only be used to specify options on
578+
creation. :class:`~pymongo.errors.EncryptionError` will be
579+
raised if the collection already exists.
580+
581+
:Parameters:
582+
- `name`: the name of the collection to create
583+
- `encrypted_fields` (dict): **(BETA)** Document that describes the encrypted fields for
584+
Queryable Encryption. For example::
585+
586+
{
587+
"escCollection": "enxcol_.encryptedCollection.esc",
588+
"eccCollection": "enxcol_.encryptedCollection.ecc",
589+
"ecocCollection": "enxcol_.encryptedCollection.ecoc",
590+
"fields": [
591+
{
592+
"path": "firstName",
593+
"keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')),
594+
"bsonType": "string",
595+
"queries": {"queryType": "equality"}
596+
},
597+
{
598+
"path": "ssn",
599+
"keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')),
600+
"bsonType": "string"
601+
}
602+
]
603+
}
604+
605+
The "keyId" may be set to ``None`` to auto-generate the data keys.
606+
- `kms_provider` (optional): the KMS provider to be used
607+
- `master_key` (optional): Identifies a KMS-specific key used to encrypt the
608+
new data key. If the kmsProvider is "local" the `master_key` is
609+
not applicable and may be omitted.
610+
- `key_alt_names` (optional): An optional list of string alternate
611+
names used to reference a key. If a key is created with alternate
612+
names, then encryption may refer to the key by the unique alternate
613+
name instead of by ``key_id``.
614+
- `key_material` (optional): Sets the custom key material to be used
615+
by the data key for encryption and decryption.
616+
- `**kwargs` (optional): additional keyword arguments are the same as "create_collection".
617+
618+
All optional `create collection command`_ parameters should be passed
619+
as keyword arguments to this method.
620+
See the documentation for :meth:`~pymongo.database.Database.create_collection` for all valid options.
621+
622+
.. versionadded:: 4.4
623+
624+
.. _create collection command:
625+
https://mongodb.com/docs/manual/reference/command/create
626+
627+
"""
628+
encrypted_fields = deepcopy(encrypted_fields)
629+
for i, field in enumerate(encrypted_fields["fields"]):
630+
if isinstance(field, dict) and field.get("keyId") is None:
631+
try:
632+
encrypted_fields["fields"][i]["keyId"] = self.create_data_key(
633+
kms_provider=kms_provider, # type:ignore[arg-type]
634+
master_key=master_key,
635+
key_alt_names=key_alt_names,
636+
key_material=key_material,
637+
)
638+
except EncryptionError as exc:
639+
raise EncryptionError(
640+
Exception(
641+
"Error occurred while creating data key for field %s with encryptedFields=%s"
642+
% (field["path"], encrypted_fields)
643+
)
644+
) from exc
645+
kwargs["encryptedFields"] = encrypted_fields
646+
kwargs["check_exists"] = False
647+
try:
648+
return (
649+
database.create_collection(name=name, **kwargs),
650+
encrypted_fields,
651+
)
652+
except Exception as exc:
653+
raise EncryptionError(
654+
Exception(
655+
f"Error: {str(exc)} occurred while creating collection with encryptedFields={str(encrypted_fields)}"
656+
)
657+
) from exc
658+
555659
def create_data_key(
556660
self,
557661
kms_provider: str,

0 commit comments

Comments
 (0)