Skip to content

Commit 6d8c1ce

Browse files
committed
PYTHON-1882 Add AutoEncryptionOpts
1 parent 7c13667 commit 6d8c1ce

File tree

8 files changed

+272
-1
lines changed

8 files changed

+272
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:mod:`encryption_options` -- Options to configure client side encryption
2+
========================================================================
3+
4+
.. automodule:: pymongo.encryption_options
5+
:members:

doc/api/pymongo/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Sub-modules:
4242
database
4343
driver_info
4444
errors
45+
encryption_options
4546
message
4647
mongo_client
4748
mongo_replica_set_client

pymongo/client_options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ def __init__(self, username, password, database, options):
167167
self.__retry_reads = options.get('retryreads', common.RETRY_READS)
168168
self.__server_selector = options.get(
169169
'server_selector', any_server_selector)
170+
self.__auto_encryption_opts = options.get('auto_encryption_opts')
170171

171172
@property
172173
def _options(self):
@@ -241,3 +242,8 @@ def retry_writes(self):
241242
def retry_reads(self):
242243
"""If this instance should retry supported read operations."""
243244
return self.__retry_reads
245+
246+
@property
247+
def auto_encryption_opts(self):
248+
"""A :class:`~pymongo.encryption.AutoEncryptionOpts` or None."""
249+
return self.__auto_encryption_opts

pymongo/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from pymongo.compression_support import (validate_compressors,
2929
validate_zlib_compression_level)
3030
from pymongo.driver_info import DriverInfo
31+
from pymongo.encryption_options import validate_auto_encryption_opts_or_none
3132
from pymongo.errors import ConfigurationError
3233
from pymongo.monitoring import _validate_event_listeners
3334
from pymongo.read_concern import ReadConcern
@@ -638,6 +639,7 @@ def validate_tzinfo(dummy, value):
638639
'username': validate_string_or_none,
639640
'password': validate_string_or_none,
640641
'server_selector': validate_is_callable_or_none,
642+
'auto_encryption_opts': validate_auto_encryption_opts_or_none,
641643
}
642644

643645
# Dictionary where keys are any URI option name, and values are the

pymongo/encryption_options.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Copyright 2019-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Options to configure client side encryption."""
16+
17+
import copy
18+
import socket
19+
20+
try:
21+
import pymongocrypt
22+
_HAVE_PYMONGOCRYPT = True
23+
except ImportError:
24+
_HAVE_PYMONGOCRYPT = False
25+
26+
from pymongo.errors import ConfigurationError
27+
28+
29+
class AutoEncryptionOpts(object):
30+
"""Options to configure automatic encryption."""
31+
32+
def __init__(self, kms_providers, key_vault_namespace,
33+
key_vault_client=None, schema_map=None,
34+
bypass_auto_encryption=False,
35+
mongocryptd_uri=None,
36+
mongocryptd_bypass_spawn=False,
37+
mongocryptd_spawn_path='mongocryptd',
38+
mongocryptd_spawn_args=None):
39+
"""Options to configure automatic encryption.
40+
41+
Automatic encryption is an enterprise only feature that only
42+
applies to operations on a collection. Automatic encryption is not
43+
supported for operations on a database or view and will result in
44+
error. To bypass automatic encryption (but enable automatic
45+
decryption), set ``bypass_auto_encryption=True`` in
46+
AutoEncryptionOpts.
47+
48+
Explicit encryption/decryption and automatic decryption is a
49+
community feature. A MongoClient configured with
50+
bypassAutoEncryption=true will still automatically decrypt.
51+
52+
:Parameters:
53+
- `kms_providers`: Map of KMS provider options. Two KMS providers
54+
are supported: "aws" and "local". The kmsProviders map values
55+
differ by provider:
56+
57+
- `aws`: Map with "accessKeyId" and "secretAccessKey" as strings.
58+
These are the AWS access key ID and AWS secret access key used
59+
to generate KMS messages.
60+
- `local`: Map with "key" as a 96-byte array or string. "key"
61+
is the master key used to encrypt/decrypt data keys. This key
62+
should be generated and stored as securely as possible.
63+
64+
- `key_vault_namespace`: The namespace for the key vault collection.
65+
The key vault collection contains all data keys used for encryption
66+
and decryption. Data keys are stored as documents in this MongoDB
67+
collection. Data keys are protected with encryption by a KMS
68+
provider.
69+
- `key_vault_client` (optional): By default the key vault collection
70+
is assumed to reside in the same MongoDB cluster as the encrypted
71+
MongoClient. Use this option to route data key queries to a
72+
separate MongoDB cluster.
73+
- `schema_map` (optional): Map of collection namespace ("db.coll") to
74+
JSON Schema. By default, a collection's JSONSchema is periodically
75+
polled with the listCollections command. But a JSONSchema may be
76+
specified locally with the schemaMap option.
77+
78+
**Supplying a `schema_map` provides more security than relying on
79+
JSON Schemas obtained from the server. It protects against a
80+
malicious server advertising a false JSON Schema, which could trick
81+
the client into sending unencrypted data that should be
82+
encrypted.**
83+
84+
Schemas supplied in the schemaMap only apply to configuring
85+
automatic encryption for client side encryption. Other validation
86+
rules in the JSON schema will not be enforced by the driver and
87+
will result in an error.
88+
- `bypass_auto_encryption` (optional): If ``True``, automatic
89+
encryption will be disabled but automatic decryption will still be
90+
enabled. Defaults to ``False``.
91+
- `mongocryptd_uri` (optional): The MongoDB URI used to connect
92+
to the *local* mongocryptd process. Defaults to
93+
``"mongodb://%2Ftmp%2Fmongocryptd.sock"`` if domain sockets are
94+
available or ``"mongodb://localhost:27020"`` otherwise.
95+
- `mongocryptd_bypass_spawn` (optional): If ``True``, the encrypted
96+
MongoClient will not attempt to spawn the mongocryptd process.
97+
Defaults to ``False``.
98+
- `mongocryptd_spawn_path` (optional): Used for spawning the
99+
mongocryptd process. Defaults to ``'mongocryptd'`` and spawns
100+
mongocryptd from the system path.
101+
- `mongocryptd_spawn_args` (optional): A list of string arguments to
102+
use when spawning the mongocryptd process. Defaults to
103+
``['--idleShutdownTimeoutSecs=60']``. If the list does not include
104+
the ``idleShutdownTimeoutSecs`` option then
105+
``'--idleShutdownTimeoutSecs=60'`` will be added.
106+
107+
.. versionadded:: 3.9
108+
"""
109+
if not _HAVE_PYMONGOCRYPT:
110+
raise ConfigurationError(
111+
"client side encryption requires the pymongocrypt library: "
112+
"install a compatible version with: "
113+
"python -m pip install pymongo['encryption']")
114+
115+
self._kms_providers = kms_providers
116+
self._key_vault_namespace = key_vault_namespace
117+
self._key_vault_client = key_vault_client
118+
self._schema_map = schema_map
119+
self._bypass_auto_encryption = bypass_auto_encryption
120+
if mongocryptd_uri is None:
121+
if hasattr(socket, 'AF_UNIX'):
122+
mongocryptd_uri = 'mongodb://%2Ftmp%2Fmongocryptd.sock'
123+
else:
124+
mongocryptd_uri = 'mongodb://localhost:27020'
125+
self._mongocryptd_uri = mongocryptd_uri
126+
self._mongocryptd_bypass_spawn = mongocryptd_bypass_spawn
127+
self._mongocryptd_spawn_path = mongocryptd_spawn_path
128+
self._mongocryptd_spawn_args = (copy.copy(mongocryptd_spawn_args) or
129+
['--idleShutdownTimeoutSecs=60'])
130+
if not isinstance(self._mongocryptd_spawn_args, list):
131+
raise TypeError('mongocryptd_spawn_args must be a list')
132+
if not any('idleShutdownTimeoutSecs' in s
133+
for s in self._mongocryptd_spawn_args):
134+
self._mongocryptd_spawn_args.append('--idleShutdownTimeoutSecs=60')
135+
136+
137+
def validate_auto_encryption_opts_or_none(option, value):
138+
"""Validate the driver keyword arg."""
139+
if value is None:
140+
return value
141+
if not isinstance(value, AutoEncryptionOpts):
142+
raise TypeError("%s must be an instance of AutoEncryptionOpts" % (
143+
option,))
144+
145+
return value

pymongo/mongo_client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,14 @@ def __init__(
475475
return data that has been written to a majority of nodes. If the
476476
level is left unspecified, the server default will be used.
477477
478+
| **Client side encryption options:**
479+
| (If not set explicitly, client side encryption will not be enabled.)
480+
481+
- `auto_encryption_opts`: A
482+
:class:`~pymongo.encryption.AutoEncryptionOpts` which configures
483+
this client to automatically encrypt collection commands and
484+
automatically decrypt results.
485+
478486
.. mongodoc:: connections
479487
480488
.. versionchanged:: 3.9

setup.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,11 @@ def build_extension(self, ext):
317317
sources=['pymongo/_cmessagemodule.c',
318318
'bson/buffer.c'])]
319319

320-
extras_require = {'snappy': ["python-snappy"], 'zstd': ["zstandard"]}
320+
extras_require = {
321+
'snappy': ['python-snappy'],
322+
'zstd': ['zstandard'],
323+
'encryption': ['pymongocrypt'], # For client side field level encryption.
324+
}
321325
vi = sys.version_info
322326
if vi[0] == 2:
323327
extras_require.update(

test/test_encryption.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Copyright 2019-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test client side encryption spec."""
16+
17+
import socket
18+
import sys
19+
20+
sys.path[0:0] = [""]
21+
22+
from pymongo.errors import ConfigurationError
23+
from pymongo.mongo_client import MongoClient
24+
from pymongo.encryption_options import AutoEncryptionOpts, _HAVE_PYMONGOCRYPT
25+
26+
from test import unittest, PyMongoTestCase
27+
28+
29+
def get_client_opts(client):
30+
return client._MongoClient__options
31+
32+
33+
class TestAutoEncryptionOpts(PyMongoTestCase):
34+
@unittest.skipIf(_HAVE_PYMONGOCRYPT, 'pymongocrypt is installed')
35+
def test_init_requires_pymongocrypt(self):
36+
with self.assertRaises(ConfigurationError):
37+
AutoEncryptionOpts({}, 'admin.datakeys')
38+
39+
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
40+
def test_init(self):
41+
opts = AutoEncryptionOpts({}, 'admin.datakeys')
42+
self.assertEqual(opts._kms_providers, {})
43+
self.assertEqual(opts._key_vault_namespace, 'admin.datakeys')
44+
self.assertEqual(opts._key_vault_client, None)
45+
self.assertEqual(opts._schema_map, None)
46+
self.assertEqual(opts._bypass_auto_encryption, False)
47+
48+
if hasattr(socket, 'AF_UNIX'):
49+
self.assertEqual(
50+
opts._mongocryptd_uri, 'mongodb://%2Ftmp%2Fmongocryptd.sock')
51+
else:
52+
self.assertEqual(
53+
opts._mongocryptd_uri, 'mongodb://localhost:27020')
54+
55+
self.assertEqual(opts._mongocryptd_bypass_spawn, False)
56+
self.assertEqual(opts._mongocryptd_spawn_path, 'mongocryptd')
57+
self.assertEqual(
58+
opts._mongocryptd_spawn_args, ['--idleShutdownTimeoutSecs=60'])
59+
60+
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
61+
def test_init_spawn_args(self):
62+
# User can override idleShutdownTimeoutSecs
63+
opts = AutoEncryptionOpts(
64+
{}, 'admin.datakeys',
65+
mongocryptd_spawn_args=['--idleShutdownTimeoutSecs=88'])
66+
self.assertEqual(
67+
opts._mongocryptd_spawn_args, ['--idleShutdownTimeoutSecs=88'])
68+
69+
# idleShutdownTimeoutSecs is added by default
70+
opts = AutoEncryptionOpts(
71+
{}, 'admin.datakeys', mongocryptd_spawn_args=[])
72+
self.assertEqual(
73+
opts._mongocryptd_spawn_args, ['--idleShutdownTimeoutSecs=60'])
74+
75+
# Also added when other options are given
76+
opts = AutoEncryptionOpts(
77+
{}, 'admin.datakeys',
78+
mongocryptd_spawn_args=['--quiet', '--port=27020'])
79+
self.assertEqual(
80+
opts._mongocryptd_spawn_args,
81+
['--quiet', '--port=27020', '--idleShutdownTimeoutSecs=60'])
82+
83+
84+
class TestClientOptions(PyMongoTestCase):
85+
def test_default(self):
86+
client = MongoClient(connect=False)
87+
self.assertEqual(get_client_opts(client).auto_encryption_opts, None)
88+
89+
client = MongoClient(auto_encryption_opts=None, connect=False)
90+
self.assertEqual(get_client_opts(client).auto_encryption_opts, None)
91+
92+
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
93+
def test_kwargs(self):
94+
opts = AutoEncryptionOpts({}, 'admin.datakeys')
95+
client = MongoClient(auto_encryption_opts=opts, connect=False)
96+
self.assertEqual(get_client_opts(client).auto_encryption_opts, opts)
97+
98+
99+
if __name__ == "__main__":
100+
unittest.main()

0 commit comments

Comments
 (0)