PYTHON-1882 Add AutoEncryptionOpts

This commit is contained in:
Shane Harvey 2019-07-23 15:24:09 -07:00
parent 7c13667727
commit 6d8c1ced70
8 changed files with 272 additions and 1 deletions

View File

@ -0,0 +1,5 @@
:mod:`encryption_options` -- Options to configure client side encryption
========================================================================
.. automodule:: pymongo.encryption_options
:members:

View File

@ -42,6 +42,7 @@ Sub-modules:
database
driver_info
errors
encryption_options
message
mongo_client
mongo_replica_set_client

View File

@ -167,6 +167,7 @@ class ClientOptions(object):
self.__retry_reads = options.get('retryreads', common.RETRY_READS)
self.__server_selector = options.get(
'server_selector', any_server_selector)
self.__auto_encryption_opts = options.get('auto_encryption_opts')
@property
def _options(self):
@ -241,3 +242,8 @@ class ClientOptions(object):
def retry_reads(self):
"""If this instance should retry supported read operations."""
return self.__retry_reads
@property
def auto_encryption_opts(self):
"""A :class:`~pymongo.encryption.AutoEncryptionOpts` or None."""
return self.__auto_encryption_opts

View File

@ -28,6 +28,7 @@ from pymongo.auth import MECHANISMS
from pymongo.compression_support import (validate_compressors,
validate_zlib_compression_level)
from pymongo.driver_info import DriverInfo
from pymongo.encryption_options import validate_auto_encryption_opts_or_none
from pymongo.errors import ConfigurationError
from pymongo.monitoring import _validate_event_listeners
from pymongo.read_concern import ReadConcern
@ -638,6 +639,7 @@ KW_VALIDATORS = {
'username': validate_string_or_none,
'password': validate_string_or_none,
'server_selector': validate_is_callable_or_none,
'auto_encryption_opts': validate_auto_encryption_opts_or_none,
}
# Dictionary where keys are any URI option name, and values are the

View File

@ -0,0 +1,145 @@
# Copyright 2019-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Options to configure client side encryption."""
import copy
import socket
try:
import pymongocrypt
_HAVE_PYMONGOCRYPT = True
except ImportError:
_HAVE_PYMONGOCRYPT = False
from pymongo.errors import ConfigurationError
class AutoEncryptionOpts(object):
"""Options to configure automatic encryption."""
def __init__(self, kms_providers, key_vault_namespace,
key_vault_client=None, schema_map=None,
bypass_auto_encryption=False,
mongocryptd_uri=None,
mongocryptd_bypass_spawn=False,
mongocryptd_spawn_path='mongocryptd',
mongocryptd_spawn_args=None):
"""Options to configure automatic encryption.
Automatic encryption is an enterprise only feature that only
applies to operations on a collection. Automatic encryption is not
supported for operations on a database or view and will result in
error. To bypass automatic encryption (but enable automatic
decryption), set ``bypass_auto_encryption=True`` in
AutoEncryptionOpts.
Explicit encryption/decryption and automatic decryption is a
community feature. A MongoClient configured with
bypassAutoEncryption=true will still automatically decrypt.
:Parameters:
- `kms_providers`: Map of KMS provider options. Two KMS providers
are supported: "aws" and "local". The kmsProviders map values
differ by provider:
- `aws`: Map with "accessKeyId" and "secretAccessKey" as strings.
These are the AWS access key ID and AWS secret access key used
to generate KMS messages.
- `local`: Map with "key" as a 96-byte array or string. "key"
is the master key used to encrypt/decrypt data keys. This key
should be generated and stored as securely as possible.
- `key_vault_namespace`: The namespace for the key vault collection.
The key vault collection contains all data keys used for encryption
and decryption. Data keys are stored as documents in this MongoDB
collection. Data keys are protected with encryption by a KMS
provider.
- `key_vault_client` (optional): By default the key vault collection
is assumed to reside in the same MongoDB cluster as the encrypted
MongoClient. Use this option to route data key queries to a
separate MongoDB cluster.
- `schema_map` (optional): Map of collection namespace ("db.coll") to
JSON Schema. By default, a collection's JSONSchema is periodically
polled with the listCollections command. But a JSONSchema may be
specified locally with the schemaMap option.
**Supplying a `schema_map` provides more security than relying on
JSON Schemas obtained from the server. It protects against a
malicious server advertising a false JSON Schema, which could trick
the client into sending unencrypted data that should be
encrypted.**
Schemas supplied in the schemaMap only apply to configuring
automatic encryption for client side encryption. Other validation
rules in the JSON schema will not be enforced by the driver and
will result in an error.
- `bypass_auto_encryption` (optional): If ``True``, automatic
encryption will be disabled but automatic decryption will still be
enabled. Defaults to ``False``.
- `mongocryptd_uri` (optional): The MongoDB URI used to connect
to the *local* mongocryptd process. Defaults to
``"mongodb://%2Ftmp%2Fmongocryptd.sock"`` if domain sockets are
available or ``"mongodb://localhost:27020"`` otherwise.
- `mongocryptd_bypass_spawn` (optional): If ``True``, the encrypted
MongoClient will not attempt to spawn the mongocryptd process.
Defaults to ``False``.
- `mongocryptd_spawn_path` (optional): Used for spawning the
mongocryptd process. Defaults to ``'mongocryptd'`` and spawns
mongocryptd from the system path.
- `mongocryptd_spawn_args` (optional): A list of string arguments to
use when spawning the mongocryptd process. Defaults to
``['--idleShutdownTimeoutSecs=60']``. If the list does not include
the ``idleShutdownTimeoutSecs`` option then
``'--idleShutdownTimeoutSecs=60'`` will be added.
.. versionadded:: 3.9
"""
if not _HAVE_PYMONGOCRYPT:
raise ConfigurationError(
"client side encryption requires the pymongocrypt library: "
"install a compatible version with: "
"python -m pip install pymongo['encryption']")
self._kms_providers = kms_providers
self._key_vault_namespace = key_vault_namespace
self._key_vault_client = key_vault_client
self._schema_map = schema_map
self._bypass_auto_encryption = bypass_auto_encryption
if mongocryptd_uri is None:
if hasattr(socket, 'AF_UNIX'):
mongocryptd_uri = 'mongodb://%2Ftmp%2Fmongocryptd.sock'
else:
mongocryptd_uri = 'mongodb://localhost:27020'
self._mongocryptd_uri = mongocryptd_uri
self._mongocryptd_bypass_spawn = mongocryptd_bypass_spawn
self._mongocryptd_spawn_path = mongocryptd_spawn_path
self._mongocryptd_spawn_args = (copy.copy(mongocryptd_spawn_args) or
['--idleShutdownTimeoutSecs=60'])
if not isinstance(self._mongocryptd_spawn_args, list):
raise TypeError('mongocryptd_spawn_args must be a list')
if not any('idleShutdownTimeoutSecs' in s
for s in self._mongocryptd_spawn_args):
self._mongocryptd_spawn_args.append('--idleShutdownTimeoutSecs=60')
def validate_auto_encryption_opts_or_none(option, value):
"""Validate the driver keyword arg."""
if value is None:
return value
if not isinstance(value, AutoEncryptionOpts):
raise TypeError("%s must be an instance of AutoEncryptionOpts" % (
option,))
return value

View File

@ -475,6 +475,14 @@ class MongoClient(common.BaseObject):
return data that has been written to a majority of nodes. If the
level is left unspecified, the server default will be used.
| **Client side encryption options:**
| (If not set explicitly, client side encryption will not be enabled.)
- `auto_encryption_opts`: A
:class:`~pymongo.encryption.AutoEncryptionOpts` which configures
this client to automatically encrypt collection commands and
automatically decrypt results.
.. mongodoc:: connections
.. versionchanged:: 3.9

View File

@ -317,7 +317,11 @@ ext_modules = [Extension('bson._cbson',
sources=['pymongo/_cmessagemodule.c',
'bson/buffer.c'])]
extras_require = {'snappy': ["python-snappy"], 'zstd': ["zstandard"]}
extras_require = {
'snappy': ['python-snappy'],
'zstd': ['zstandard'],
'encryption': ['pymongocrypt'], # For client side field level encryption.
}
vi = sys.version_info
if vi[0] == 2:
extras_require.update(

100
test/test_encryption.py Normal file
View File

@ -0,0 +1,100 @@
# Copyright 2019-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test client side encryption spec."""
import socket
import sys
sys.path[0:0] = [""]
from pymongo.errors import ConfigurationError
from pymongo.mongo_client import MongoClient
from pymongo.encryption_options import AutoEncryptionOpts, _HAVE_PYMONGOCRYPT
from test import unittest, PyMongoTestCase
def get_client_opts(client):
return client._MongoClient__options
class TestAutoEncryptionOpts(PyMongoTestCase):
@unittest.skipIf(_HAVE_PYMONGOCRYPT, 'pymongocrypt is installed')
def test_init_requires_pymongocrypt(self):
with self.assertRaises(ConfigurationError):
AutoEncryptionOpts({}, 'admin.datakeys')
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
def test_init(self):
opts = AutoEncryptionOpts({}, 'admin.datakeys')
self.assertEqual(opts._kms_providers, {})
self.assertEqual(opts._key_vault_namespace, 'admin.datakeys')
self.assertEqual(opts._key_vault_client, None)
self.assertEqual(opts._schema_map, None)
self.assertEqual(opts._bypass_auto_encryption, False)
if hasattr(socket, 'AF_UNIX'):
self.assertEqual(
opts._mongocryptd_uri, 'mongodb://%2Ftmp%2Fmongocryptd.sock')
else:
self.assertEqual(
opts._mongocryptd_uri, 'mongodb://localhost:27020')
self.assertEqual(opts._mongocryptd_bypass_spawn, False)
self.assertEqual(opts._mongocryptd_spawn_path, 'mongocryptd')
self.assertEqual(
opts._mongocryptd_spawn_args, ['--idleShutdownTimeoutSecs=60'])
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
def test_init_spawn_args(self):
# User can override idleShutdownTimeoutSecs
opts = AutoEncryptionOpts(
{}, 'admin.datakeys',
mongocryptd_spawn_args=['--idleShutdownTimeoutSecs=88'])
self.assertEqual(
opts._mongocryptd_spawn_args, ['--idleShutdownTimeoutSecs=88'])
# idleShutdownTimeoutSecs is added by default
opts = AutoEncryptionOpts(
{}, 'admin.datakeys', mongocryptd_spawn_args=[])
self.assertEqual(
opts._mongocryptd_spawn_args, ['--idleShutdownTimeoutSecs=60'])
# Also added when other options are given
opts = AutoEncryptionOpts(
{}, 'admin.datakeys',
mongocryptd_spawn_args=['--quiet', '--port=27020'])
self.assertEqual(
opts._mongocryptd_spawn_args,
['--quiet', '--port=27020', '--idleShutdownTimeoutSecs=60'])
class TestClientOptions(PyMongoTestCase):
def test_default(self):
client = MongoClient(connect=False)
self.assertEqual(get_client_opts(client).auto_encryption_opts, None)
client = MongoClient(auto_encryption_opts=None, connect=False)
self.assertEqual(get_client_opts(client).auto_encryption_opts, None)
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, 'pymongocrypt is not installed')
def test_kwargs(self):
opts = AutoEncryptionOpts({}, 'admin.datakeys')
client = MongoClient(auto_encryption_opts=opts, connect=False)
self.assertEqual(get_client_opts(client).auto_encryption_opts, opts)
if __name__ == "__main__":
unittest.main()