Skip to content

PYTHON-5309 Ensure AsyncMongoClient doesn't use PyOpenSSL #2286

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

Merged
merged 62 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
efe494d
add is_sync param
sleepyStick Apr 15, 2025
b29d1ba
update contributing
sleepyStick Apr 15, 2025
d9dfb99
add vars for pyopenssl and test
sleepyStick Apr 15, 2025
c847f25
update evergreen config to run this pyopenssl on async as well (not s…
sleepyStick Apr 15, 2025
ae8ecc4
fix typing
sleepyStick Apr 15, 2025
03f4ba1
fix tests
sleepyStick Apr 16, 2025
5349164
fix test pt2
sleepyStick Apr 16, 2025
67100fc
edit evergreen config
sleepyStick Apr 16, 2025
88ae345
fix test
sleepyStick Apr 16, 2025
e451ceb
fix test errors
sleepyStick Apr 18, 2025
0312acb
fix typo...
sleepyStick Apr 18, 2025
4e85024
fix typing
sleepyStick Apr 18, 2025
dccd96a
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 18, 2025
c86a85f
maybe this works?
sleepyStick Apr 21, 2025
12ef993
fix typing
sleepyStick Apr 21, 2025
bc76aae
fix tests
sleepyStick Apr 21, 2025
a9c63c8
fix typing again
sleepyStick Apr 21, 2025
67c6738
fix tests pt 2?
sleepyStick Apr 21, 2025
3ea4de7
fix test pt3
sleepyStick Apr 21, 2025
38ad677
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 21, 2025
c57aed2
add test_name
sleepyStick Apr 21, 2025
2591169
address review
sleepyStick Apr 22, 2025
06a710d
update changelog
sleepyStick Apr 22, 2025
ef4111e
undo whitespace changes
sleepyStick Apr 22, 2025
0b3c6bb
fix failures
sleepyStick Apr 22, 2025
9336f58
fix typing
sleepyStick Apr 22, 2025
23b7cbe
fix typing?
sleepyStick Apr 22, 2025
760fa97
fix error?
sleepyStick Apr 22, 2025
4b8a4ed
undo whitespace changes
sleepyStick Apr 22, 2025
5807ba1
more whitespace changes
sleepyStick Apr 22, 2025
683ba33
move kms_ssl_contexts
sleepyStick Apr 22, 2025
d007c5f
fix import
sleepyStick Apr 22, 2025
05c061a
fix test failures
sleepyStick Apr 22, 2025
350f103
fix test failure pt2
sleepyStick Apr 22, 2025
5fa117f
change changelog line ft noah's suggestion
sleepyStick Apr 23, 2025
56c9662
_ssl -> _stdssl and ssl_in_use -> _ssl
sleepyStick Apr 23, 2025
a7324e5
make combined error type
sleepyStick Apr 23, 2025
af83d81
jk cant do _stdssl
sleepyStick Apr 23, 2025
f6b17dd
_pysslConn back to _sslConn
sleepyStick Apr 23, 2025
74ca8be
fix _ssl and has_sni
sleepyStick Apr 23, 2025
536f189
fix test
sleepyStick Apr 23, 2025
24354b4
_ssl -> ssl
sleepyStick Apr 23, 2025
4178fcc
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 23, 2025
b2324e3
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 23, 2025
17cf61d
Update pymongo/asynchronous/pool.py
sleepyStick Apr 24, 2025
257f8fe
Update doc/changelog.rst
sleepyStick Apr 24, 2025
74f98c5
Update pymongo/asynchronous/pool.py
sleepyStick Apr 24, 2025
6752a67
address review
sleepyStick Apr 24, 2025
750a9aa
fix error
sleepyStick Apr 24, 2025
e7e36b4
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 24, 2025
8af8f09
fix typing
sleepyStick Apr 24, 2025
16d3cc3
Merge branch 'PYTHON-5309' of github.com:sleepyStick/mongo-python-dri…
sleepyStick Apr 24, 2025
6971fed
back to tuple type
sleepyStick Apr 24, 2025
4d12c59
remove added section
sleepyStick Apr 24, 2025
a0fe2e5
modify encryption tests
sleepyStick Apr 24, 2025
7b4ae9c
fix test
sleepyStick Apr 24, 2025
f02a791
change test
sleepyStick Apr 24, 2025
4ed055e
close the client
sleepyStick Apr 24, 2025
981a046
fix the tests pt3
sleepyStick Apr 24, 2025
c20623f
fix typing
sleepyStick Apr 24, 2025
bdaf87a
fix import
sleepyStick Apr 24, 2025
c2b2cc3
fix typing and add comment in encryption
sleepyStick Apr 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .evergreen/generated_configs/variants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -620,17 +620,19 @@ buildvariants:
- macos-14
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3
- name: pyopenssl-rhel8-python3.10
tasks:
- name: .replica_set .auth .ssl .sync
- name: .7.0 .auth .ssl .sync
- name: .replica_set .auth .ssl .sync_async
- name: .7.0 .auth .ssl .sync_async
display_name: PyOpenSSL RHEL8 Python3.10
run_on:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.10/bin/python3
- name: pyopenssl-rhel8-python3.11
Expand All @@ -642,6 +644,7 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.11/bin/python3
- name: pyopenssl-rhel8-python3.12
Expand All @@ -653,17 +656,19 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.12/bin/python3
- name: pyopenssl-win64-python3.13
tasks:
- name: .replica_set .auth .ssl .sync
- name: .7.0 .auth .ssl .sync
- name: .replica_set .auth .ssl .sync_async
- name: .7.0 .auth .ssl .sync_async
display_name: PyOpenSSL Win64 Python3.13
run_on:
- windows-64-vsMulti-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: C:/python/Python313/python.exe
- name: pyopenssl-rhel8-pypy3.10
Expand All @@ -675,6 +680,7 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3

Expand Down
29 changes: 20 additions & 9 deletions .evergreen/scripts/generate_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def create_enterprise_auth_variants():
def create_pyopenssl_variants():
base_name = "PyOpenSSL"
batchtime = BATCHTIME_WEEK
expansions = dict(SUB_TEST_NAME="pyopenssl")
expansions = dict(TEST_NAME="default", SUB_TEST_NAME="pyopenssl")
variants = []

for python in ALL_PYTHONS:
Expand All @@ -265,14 +265,25 @@ def create_pyopenssl_variants():
host = DEFAULT_HOST

display_name = get_variant_name(base_name, host, python=python)
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
# only need to run some on async
if python in (CPYTHONS[1], CPYTHONS[-1]):
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync_async", f".7.0 .{auth} .{ssl} .sync_async"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
else:
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
variants.append(variant)

return variants
Expand Down
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,21 @@ partially-converted asynchronous version of the same name to the `test/asynchron
Use this generated file as a starting point for the completed conversion.

The script is used like so: `python tools/convert_test_to_async.py [test_file.py]`

## Running PyMongo with SSL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for adding this section?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh uh, I was struggling to get it working and asked Noah a bunch of questions, so he suggested I add a section here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about people seeing this section and misinterpreting it to mean that they should be using tlsAllowInvalidCertificates. Can we remove it or make it very clear that this is only for local testing (using our test certificates) and not production?

Note that `AsyncMongoClient` does not support PyOpenSSL.
Assuming all required packages are installed, set the `tls` and `tlsAllowInvalidCertificates` flags in the URI to enable
the driver to connect with SSL, like so:
```python
from pymongo import MongoClient

client = MongoClient(
"mongodb://localhost:27017?tls=true&tlsAllowInvalidCertificates=true"
)
```
Another way of doing this would be to pass these options in as parameters to the MongoClient, like so:
```python
client = MongoClient(
"mongodb://localhost:27017", tls=True, tlsAllowInvalidCertificates=True
)
```
7 changes: 5 additions & 2 deletions pymongo/asynchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
)
from pymongo.read_concern import ReadConcern
from pymongo.results import BulkWriteResult, DeleteResult
from pymongo.ssl_support import BLOCKING_IO_ERRORS, get_ssl_context
from pymongo.ssl_support import BLOCKING_IO_ERRORS, PYBLOCKING_IO_ERRORS, get_ssl_context
from pymongo.typings import _DocumentType, _DocumentTypeArg
from pymongo.uri_parser_shared import parse_host
from pymongo.write_concern import WriteConcern
Expand Down Expand Up @@ -165,6 +165,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:

:return: None
"""
self.opts._parse_kms_tls_options(_IS_SYNC)
endpoint = kms_context.endpoint
message = kms_context.message
provider = kms_context.kms_provider
Expand All @@ -180,6 +181,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
False, # allow_invalid_certificates
False, # allow_invalid_hostnames
False, # disable_ocsp_endpoint_check
_IS_SYNC,
)
# CSOT: set timeout for socket creation.
connect_timeout = max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001)
Expand Down Expand Up @@ -215,7 +217,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
raise # Propagate MongoCryptError errors directly.
except Exception as exc:
# Wrap I/O errors in PyMongo exceptions.
if isinstance(exc, BLOCKING_IO_ERRORS):
if isinstance(exc, (BLOCKING_IO_ERRORS, PYBLOCKING_IO_ERRORS)):
exc = socket.timeout("timed out")
# Async raises an OSError instead of returning empty bytes.
if isinstance(exc, OSError):
Expand Down Expand Up @@ -675,6 +677,7 @@ def __init__(
kms_tls_options=kms_tls_options,
key_expiration_ms=key_expiration_ms,
)
opts._parse_kms_tls_options(_IS_SYNC)
self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO(
None, key_vault_coll, None, opts
)
Expand Down
6 changes: 3 additions & 3 deletions pymongo/asynchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
from pymongo.server_api import _add_to_command
from pymongo.server_type import SERVER_TYPE
from pymongo.socket_checker import SocketChecker
from pymongo.ssl_support import SSLError
from pymongo.ssl_support import PYSSLError, SSLError

if TYPE_CHECKING:
from bson import CodecOptions
Expand Down Expand Up @@ -637,7 +637,7 @@ async def _raise_connection_failure(self, error: BaseException) -> NoReturn:
reason = ConnectionClosedReason.ERROR
await self.close_conn(reason)
# SSLError from PyOpenSSL inherits directly from Exception.
if isinstance(error, (IOError, OSError, SSLError)):
if isinstance(error, (IOError, OSError, SSLError, PYSSLError)):
details = _get_timeout_details(self.opts)
_raise_connection_failure(self.address, error, timeout_details=details)
else:
Expand Down Expand Up @@ -1033,7 +1033,7 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
error=ConnectionClosedReason.ERROR,
)
if isinstance(error, (IOError, OSError, SSLError)):
if isinstance(error, (IOError, OSError, SSLError, PYSSLError)):
details = _get_timeout_details(self.opts)
_raise_connection_failure(self.address, error, timeout_details=details)

Expand Down
7 changes: 5 additions & 2 deletions pymongo/client_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ def _parse_read_concern(options: Mapping[str, Any]) -> ReadConcern:
return ReadConcern(concern)


def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext], bool]:
def _parse_ssl_options(
options: Mapping[str, Any], is_sync: bool
) -> tuple[Optional[SSLContext], bool]:
"""Parse ssl options."""
use_tls = options.get("tls")
if use_tls is not None:
Expand Down Expand Up @@ -138,6 +140,7 @@ def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext]
allow_invalid_certificates,
allow_invalid_hostnames,
disable_ocsp_endpoint_check,
is_sync,
)
return ctx, allow_invalid_hostnames
return None, allow_invalid_hostnames
Expand Down Expand Up @@ -167,7 +170,7 @@ def _parse_pool_options(
compression_settings = CompressionSettings(
options.get("compressors", []), options.get("zlibcompressionlevel", -1)
)
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options)
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options, is_sync)
load_balanced = options.get("loadbalanced")
max_connecting = options.get("maxconnecting", common.MAX_CONNECTING)
return PoolOptions(
Expand Down
7 changes: 6 additions & 1 deletion pymongo/encryption_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from pymongo.uri_parser_shared import _parse_kms_tls_options

if TYPE_CHECKING:
from pymongo.pyopenssl_context import SSLContext
from pymongo.typings import _AgnosticMongoClient, _DocumentTypeArg


Expand Down Expand Up @@ -236,10 +237,14 @@ def __init__(
if not any("idleShutdownTimeoutSecs" in s for s in self._mongocryptd_spawn_args):
self._mongocryptd_spawn_args.append("--idleShutdownTimeoutSecs=60")
# Maps KMS provider name to a SSLContext.
self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options)
self._kms_tls_options = kms_tls_options
self._kms_ssl_contexts: dict[str, SSLContext] = {}
self._bypass_query_analysis = bypass_query_analysis
self._key_expiration_ms = key_expiration_ms

def _parse_kms_tls_options(self, is_sync: bool) -> None:
self._kms_ssl_contexts = _parse_kms_tls_options(self._kms_tls_options, is_sync)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is problematic because a single AutoEncryptionOpts can be shared between multiple clients (even sync + async) at the same time. Currently the _kms_ssl_contexts will clash.

Another problem is that _parse_kms_tls_options is expensive and should only be called at most once for sync and async.

What if we do this at construction time?:

        # Maps KMS provider name to a SSLContext.
        self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options, True)
        if pyopenssl_enabled:
            self._async_kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options, False)
        else:
            self._async_kms_ssl_contexts = self._kms_ssl_contexts

Hmm actually, I thought of a better idea. _EncryptionIO can parse and store the contexts:

class _EncryptionIO(AsyncMongoCryptCallback):
    def __init__(
        self,
        client: Optional[AsyncMongoClient[_DocumentTypeArg]],
        key_vault_coll: AsyncCollection[_DocumentTypeArg],
        mongocryptd_client: Optional[AsyncMongoClient[_DocumentTypeArg]],
        opts: AutoEncryptionOpts,
    ):
...
        self.opts = opts
        self._kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, IS_SYNC)
...



class RangeOpts:
"""Options to configure encrypted queries using the range algorithm."""
Expand Down
Loading
Loading