PYTHON-1882 Add AutoEncryptionOpts
This commit is contained in:
parent
7c13667727
commit
6d8c1ced70
5
doc/api/pymongo/encryption_options.rst
Normal file
5
doc/api/pymongo/encryption_options.rst
Normal file
@ -0,0 +1,5 @@
|
||||
:mod:`encryption_options` -- Options to configure client side encryption
|
||||
========================================================================
|
||||
|
||||
.. automodule:: pymongo.encryption_options
|
||||
:members:
|
||||
@ -42,6 +42,7 @@ Sub-modules:
|
||||
database
|
||||
driver_info
|
||||
errors
|
||||
encryption_options
|
||||
message
|
||||
mongo_client
|
||||
mongo_replica_set_client
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
145
pymongo/encryption_options.py
Normal file
145
pymongo/encryption_options.py
Normal 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
|
||||
@ -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
|
||||
|
||||
6
setup.py
6
setup.py
@ -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
100
test/test_encryption.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user