Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c36bfbc3f9 | ||
|
|
7604387148 | ||
|
|
398be02d23 | ||
|
|
839904f4f9 | ||
|
|
c7db2c215f | ||
|
|
358a4864c1 | ||
|
|
fa80968120 | ||
|
|
d4592b659f | ||
|
|
f38dfd672e | ||
|
|
29f4d5cf89 |
62
.github/workflows/codeql.yml
vendored
Normal file
62
.github/workflows/codeql.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "v*"]
|
||||
tags: ['*']
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '17 10 * * 2'
|
||||
|
||||
concurrency:
|
||||
group: codeql-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: "ubuntu-latest"
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: c-cpp
|
||||
build-mode: manual
|
||||
- language: python
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
queries: security-extended
|
||||
config: |
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- 'doc/**'
|
||||
- 'tools/**'
|
||||
- 'test/**'
|
||||
|
||||
- if: matrix.build-mode == 'manual'
|
||||
run: |
|
||||
pip install -e .
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
4
.github/workflows/test-python.yml
vendored
4
.github/workflows/test-python.yml
vendored
@ -53,7 +53,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
python-version: ["3.7", "3.11", "pypy-3.8"]
|
||||
python-version: ["3.7", "3.11", "pypy-3.9"]
|
||||
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -137,7 +137,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python: ["3.7", "3.11"]
|
||||
python: ["3.8", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
@ -8,6 +8,7 @@ exclude .git-blame-ignore-revs
|
||||
exclude .pre-commit-config.yaml
|
||||
exclude .readthedocs.yaml
|
||||
exclude CONTRIBUTING.md
|
||||
include sbom.json
|
||||
exclude RELEASE.md
|
||||
recursive-include doc *.rst
|
||||
recursive-include doc *.py
|
||||
|
||||
@ -1,6 +1,39 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Changes in Version 4.7.3
|
||||
-------------------------
|
||||
|
||||
Version 4.7.3 has further fixes for lazily loading modules.
|
||||
|
||||
- Use deferred imports instead of importlib lazy module loading.
|
||||
- Improve import time on Windows.
|
||||
- Reduce verbosity of "Waiting for suitable server to become available" log message from info to debug.
|
||||
|
||||
Issues Resolved
|
||||
...............
|
||||
|
||||
See the `PyMongo 4.7.3 release notes in JIRA`_ for the list of resolved issues
|
||||
in this release.
|
||||
|
||||
.. _PyMongo 4.7.3 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=39865
|
||||
|
||||
Changes in Version 4.7.2
|
||||
-------------------------
|
||||
|
||||
Version 4.7.2 fixes a bug introduced in 4.7.0:
|
||||
|
||||
- Fixed a bug where PyMongo could not be used with the Nuitka compiler.
|
||||
|
||||
Issues Resolved
|
||||
...............
|
||||
|
||||
See the `PyMongo 4.7.2 release notes in JIRA`_ for the list of resolved issues
|
||||
in this release.
|
||||
|
||||
.. _PyMongo 4.7.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=39710
|
||||
|
||||
|
||||
Changes in Version 4.7.1
|
||||
-------------------------
|
||||
|
||||
|
||||
@ -16,10 +16,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
def _get_gcp_response(resource: str, timeout: float = 5) -> dict[str, Any]:
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
|
||||
url += f"?audience={resource}"
|
||||
headers = {"Metadata-Flavor": "Google"}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
# Copyright 2024-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.
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
def lazy_import(name: str) -> ModuleType:
|
||||
"""Lazily import a module by name
|
||||
|
||||
From https://docs.python.org/3/library/importlib.html#implementing-lazy-imports
|
||||
"""
|
||||
# Workaround for PYTHON-4424.
|
||||
if "__compiled__" in globals():
|
||||
return importlib.import_module(name)
|
||||
try:
|
||||
spec = importlib.util.find_spec(name)
|
||||
except ValueError:
|
||||
# Note: this cannot be ModuleNotFoundError, see PYTHON-4424.
|
||||
raise ImportError(name=name) from None
|
||||
if spec is None:
|
||||
# Note: this cannot be ModuleNotFoundError, see PYTHON-4424.
|
||||
raise ImportError(name=name)
|
||||
assert spec is not None
|
||||
loader = importlib.util.LazyLoader(spec.loader) # type:ignore[arg-type]
|
||||
spec.loader = loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
loader.exec_module(module)
|
||||
return module
|
||||
@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Tuple, Union
|
||||
|
||||
version_tuple: Tuple[Union[int, str], ...] = (4, 8, 0, ".dev0")
|
||||
version_tuple: Tuple[Union[int, str], ...] = (4, 7, 4, ".dev0")
|
||||
|
||||
|
||||
def get_version_string() -> str:
|
||||
|
||||
@ -15,15 +15,6 @@
|
||||
"""MONGODB-AWS Authentication helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pymongo._lazy_import import lazy_import
|
||||
|
||||
try:
|
||||
pymongo_auth_aws = lazy_import("pymongo_auth_aws")
|
||||
_HAVE_MONGODB_AWS = True
|
||||
except ImportError:
|
||||
_HAVE_MONGODB_AWS = False
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Mapping, Type
|
||||
|
||||
import bson
|
||||
@ -38,11 +29,13 @@ if TYPE_CHECKING:
|
||||
|
||||
def _authenticate_aws(credentials: MongoCredential, conn: Connection) -> None:
|
||||
"""Authenticate using MONGODB-AWS."""
|
||||
if not _HAVE_MONGODB_AWS:
|
||||
try:
|
||||
import pymongo_auth_aws # type:ignore[import]
|
||||
except ImportError as e:
|
||||
raise ConfigurationError(
|
||||
"MONGODB-AWS authentication requires pymongo-auth-aws: "
|
||||
"install with: python -m pip install 'pymongo[aws]'"
|
||||
)
|
||||
) from e
|
||||
|
||||
# Delayed import.
|
||||
from pymongo_auth_aws.auth import ( # type:ignore[import]
|
||||
|
||||
@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, cast
|
||||
|
||||
from bson.codec_options import _parse_codec_options
|
||||
from pymongo import common
|
||||
from pymongo.auth import MongoCredential, _build_credentials_tuple
|
||||
from pymongo.compression_support import CompressionSettings
|
||||
from pymongo.errors import ConfigurationError
|
||||
from pymongo.monitoring import _EventListener, _EventListeners
|
||||
@ -36,6 +35,7 @@ from pymongo.write_concern import WriteConcern, validate_boolean
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bson.codec_options import CodecOptions
|
||||
from pymongo.auth import MongoCredential
|
||||
from pymongo.encryption_options import AutoEncryptionOpts
|
||||
from pymongo.pyopenssl_context import SSLContext
|
||||
from pymongo.topology_description import _ServerSelector
|
||||
@ -48,6 +48,8 @@ def _parse_credentials(
|
||||
mechanism = options.get("authmechanism", "DEFAULT" if username else None)
|
||||
source = options.get("authsource")
|
||||
if username or mechanism:
|
||||
from pymongo.auth import _build_credentials_tuple
|
||||
|
||||
return _build_credentials_tuple(mechanism, source, username, password, options, database)
|
||||
return None
|
||||
|
||||
|
||||
@ -40,8 +40,6 @@ from bson import SON
|
||||
from bson.binary import UuidRepresentation
|
||||
from bson.codec_options import CodecOptions, DatetimeConversion, TypeRegistry
|
||||
from bson.raw_bson import RawBSONDocument
|
||||
from pymongo.auth import MECHANISMS
|
||||
from pymongo.auth_oidc import OIDCCallback
|
||||
from pymongo.compression_support import (
|
||||
validate_compressors,
|
||||
validate_zlib_compression_level,
|
||||
@ -380,6 +378,8 @@ def validate_read_preference_mode(dummy: Any, value: Any) -> _ServerMode:
|
||||
|
||||
def validate_auth_mechanism(option: str, value: Any) -> str:
|
||||
"""Validate the authMechanism URI option."""
|
||||
from pymongo.auth import MECHANISMS
|
||||
|
||||
if value not in MECHANISMS:
|
||||
raise ValueError(f"{option} must be in {tuple(MECHANISMS)}")
|
||||
return value
|
||||
@ -444,6 +444,8 @@ def validate_auth_mechanism_properties(option: str, value: Any) -> dict[str, Uni
|
||||
elif key in ["ALLOWED_HOSTS"] and isinstance(value, list):
|
||||
props[key] = value
|
||||
elif key in ["OIDC_CALLBACK", "OIDC_HUMAN_CALLBACK"]:
|
||||
from pymongo.auth_oidc import OIDCCallback
|
||||
|
||||
if not isinstance(value, OIDCCallback):
|
||||
raise ValueError("callback must be an OIDCCallback object")
|
||||
props[key] = value
|
||||
|
||||
@ -16,36 +16,41 @@ from __future__ import annotations
|
||||
import warnings
|
||||
from typing import Any, Iterable, Optional, Union
|
||||
|
||||
from pymongo._lazy_import import lazy_import
|
||||
from pymongo.hello import HelloCompat
|
||||
from pymongo.monitoring import _SENSITIVE_COMMANDS
|
||||
|
||||
try:
|
||||
snappy = lazy_import("snappy")
|
||||
_HAVE_SNAPPY = True
|
||||
except ImportError:
|
||||
# python-snappy isn't available.
|
||||
_HAVE_SNAPPY = False
|
||||
|
||||
try:
|
||||
zlib = lazy_import("zlib")
|
||||
|
||||
_HAVE_ZLIB = True
|
||||
except ImportError:
|
||||
# Python built without zlib support.
|
||||
_HAVE_ZLIB = False
|
||||
|
||||
try:
|
||||
zstandard = lazy_import("zstandard")
|
||||
_HAVE_ZSTD = True
|
||||
except ImportError:
|
||||
_HAVE_ZSTD = False
|
||||
from pymongo.helpers import _SENSITIVE_COMMANDS
|
||||
|
||||
_SUPPORTED_COMPRESSORS = {"snappy", "zlib", "zstd"}
|
||||
_NO_COMPRESSION = {HelloCompat.CMD, HelloCompat.LEGACY_CMD}
|
||||
_NO_COMPRESSION.update(_SENSITIVE_COMMANDS)
|
||||
|
||||
|
||||
def _have_snappy() -> bool:
|
||||
try:
|
||||
import snappy # type:ignore[import] # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def _have_zlib() -> bool:
|
||||
try:
|
||||
import zlib # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def _have_zstd() -> bool:
|
||||
try:
|
||||
import zstandard # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def validate_compressors(dummy: Any, value: Union[str, Iterable[str]]) -> list[str]:
|
||||
try:
|
||||
# `value` is string.
|
||||
@ -58,21 +63,21 @@ def validate_compressors(dummy: Any, value: Union[str, Iterable[str]]) -> list[s
|
||||
if compressor not in _SUPPORTED_COMPRESSORS:
|
||||
compressors.remove(compressor)
|
||||
warnings.warn(f"Unsupported compressor: {compressor}", stacklevel=2)
|
||||
elif compressor == "snappy" and not _HAVE_SNAPPY:
|
||||
elif compressor == "snappy" and not _have_snappy():
|
||||
compressors.remove(compressor)
|
||||
warnings.warn(
|
||||
"Wire protocol compression with snappy is not available. "
|
||||
"You must install the python-snappy module for snappy support.",
|
||||
stacklevel=2,
|
||||
)
|
||||
elif compressor == "zlib" and not _HAVE_ZLIB:
|
||||
elif compressor == "zlib" and not _have_zlib():
|
||||
compressors.remove(compressor)
|
||||
warnings.warn(
|
||||
"Wire protocol compression with zlib is not available. "
|
||||
"The zlib module is not available.",
|
||||
stacklevel=2,
|
||||
)
|
||||
elif compressor == "zstd" and not _HAVE_ZSTD:
|
||||
elif compressor == "zstd" and not _have_zstd():
|
||||
compressors.remove(compressor)
|
||||
warnings.warn(
|
||||
"Wire protocol compression with zstandard is not available. "
|
||||
@ -117,6 +122,8 @@ class SnappyContext:
|
||||
|
||||
@staticmethod
|
||||
def compress(data: bytes) -> bytes:
|
||||
import snappy
|
||||
|
||||
return snappy.compress(data)
|
||||
|
||||
|
||||
@ -127,6 +134,8 @@ class ZlibContext:
|
||||
self.level = level
|
||||
|
||||
def compress(self, data: bytes) -> bytes:
|
||||
import zlib
|
||||
|
||||
return zlib.compress(data, self.level)
|
||||
|
||||
|
||||
@ -137,6 +146,8 @@ class ZstdContext:
|
||||
def compress(data: bytes) -> bytes:
|
||||
# ZstdCompressor is not thread safe.
|
||||
# TODO: Use a pool?
|
||||
import zstandard
|
||||
|
||||
return zstandard.ZstdCompressor().compress(data)
|
||||
|
||||
|
||||
@ -146,12 +157,18 @@ def decompress(data: bytes, compressor_id: int) -> bytes:
|
||||
# https://github.com/andrix/python-snappy/issues/65
|
||||
# This only matters when data is a memoryview since
|
||||
# id(bytes(data)) == id(data) when data is a bytes.
|
||||
import snappy
|
||||
|
||||
return snappy.uncompress(bytes(data))
|
||||
elif compressor_id == ZlibContext.compressor_id:
|
||||
import zlib
|
||||
|
||||
return zlib.decompress(data)
|
||||
elif compressor_id == ZstdContext.compressor_id:
|
||||
# ZstdDecompressor is not thread safe.
|
||||
# TODO: Use a pool?
|
||||
import zstandard
|
||||
|
||||
return zstandard.ZstdDecompressor().decompress(data)
|
||||
else:
|
||||
raise ValueError("Unknown compressorId %d" % (compressor_id,))
|
||||
|
||||
@ -93,6 +93,21 @@ _REAUTHENTICATION_REQUIRED_CODE: int = 391
|
||||
# Server code raised when authentication fails.
|
||||
_AUTHENTICATION_FAILURE_CODE: int = 18
|
||||
|
||||
# Note - to avoid bugs from forgetting which if these is all lowercase and
|
||||
# which are camelCase, and at the same time avoid having to add a test for
|
||||
# every command, use all lowercase here and test against command_name.lower().
|
||||
_SENSITIVE_COMMANDS: set = {
|
||||
"authenticate",
|
||||
"saslstart",
|
||||
"saslcontinue",
|
||||
"getnonce",
|
||||
"createuser",
|
||||
"updateuser",
|
||||
"copydbgetnonce",
|
||||
"copydbsaslstart",
|
||||
"copydb",
|
||||
}
|
||||
|
||||
|
||||
def _gen_index_name(keys: _IndexList) -> str:
|
||||
"""Generate an index name from the set of fields it is over."""
|
||||
|
||||
@ -1344,8 +1344,9 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
# always send primaryPreferred when directly connected to a repl set
|
||||
# member.
|
||||
# Thread safe: if the type is single it cannot change.
|
||||
topology = self._get_topology()
|
||||
single = topology.description.topology_type == TOPOLOGY_TYPE.Single
|
||||
# NOTE: We already opened the Topology when selecting a server so there's no need
|
||||
# to call _get_topology() again.
|
||||
single = self._topology.description.topology_type == TOPOLOGY_TYPE.Single
|
||||
|
||||
with self._checkout(server, session) as conn:
|
||||
if single:
|
||||
@ -1365,7 +1366,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
operation: str,
|
||||
) -> ContextManager[tuple[Connection, _ServerMode]]:
|
||||
assert read_preference is not None, "read_preference must not be None"
|
||||
_ = self._get_topology()
|
||||
server = self._select_server(read_preference, session, operation)
|
||||
return self._conn_from_server(read_preference, server, session)
|
||||
|
||||
|
||||
@ -191,7 +191,7 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.helpers import _handle_exception
|
||||
from pymongo.helpers import _SENSITIVE_COMMANDS, _handle_exception
|
||||
from pymongo.typings import _Address, _DocumentOut
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -507,22 +507,6 @@ def register(listener: _EventListener) -> None:
|
||||
_LISTENERS.cmap_listeners.append(listener)
|
||||
|
||||
|
||||
# Note - to avoid bugs from forgetting which if these is all lowercase and
|
||||
# which are camelCase, and at the same time avoid having to add a test for
|
||||
# every command, use all lowercase here and test against command_name.lower().
|
||||
_SENSITIVE_COMMANDS: set = {
|
||||
"authenticate",
|
||||
"saslstart",
|
||||
"saslcontinue",
|
||||
"getnonce",
|
||||
"createuser",
|
||||
"updateuser",
|
||||
"copydbgetnonce",
|
||||
"copydbsaslstart",
|
||||
"copydb",
|
||||
}
|
||||
|
||||
|
||||
# The "hello" command is also deemed sensitive when attempting speculative
|
||||
# authentication.
|
||||
def _is_speculative_authenticate(command_name: str, doc: Mapping[str, Any]) -> bool:
|
||||
|
||||
@ -41,7 +41,7 @@ from typing import (
|
||||
|
||||
import bson
|
||||
from bson import DEFAULT_CODEC_OPTIONS
|
||||
from pymongo import __version__, _csot, auth, helpers
|
||||
from pymongo import __version__, _csot, helpers
|
||||
from pymongo.client_session import _validate_session_write_concern
|
||||
from pymongo.common import (
|
||||
MAX_BSON_SIZE,
|
||||
@ -211,13 +211,14 @@ elif sys.platform == "darwin":
|
||||
"version": platform.mac_ver()[0],
|
||||
}
|
||||
elif sys.platform == "win32":
|
||||
_ver = sys.getwindowsversion()
|
||||
_METADATA["os"] = {
|
||||
"type": platform.system(),
|
||||
# "Windows XP", "Windows 7", "Windows 10", etc.
|
||||
"name": " ".join((platform.system(), platform.release())),
|
||||
"architecture": platform.machine(),
|
||||
# Windows patch level (e.g. 5.1.2600-SP3)
|
||||
"version": "-".join(platform.win32_ver()[1:3]),
|
||||
"type": "Windows",
|
||||
"name": "Windows",
|
||||
# Avoid using platform calls, see PYTHON-4455.
|
||||
"architecture": os.environ.get("PROCESSOR_ARCHITECTURE") or platform.machine(),
|
||||
# Windows patch level (e.g. 10.0.17763-SP0).
|
||||
"version": ".".join(map(str, _ver[:3])) + f"-SP{_ver[-1] or '0'}",
|
||||
}
|
||||
elif sys.platform.startswith("java"):
|
||||
_name, _ver, _arch = platform.java_ver()[-1]
|
||||
@ -859,6 +860,8 @@ class Connection:
|
||||
if creds:
|
||||
if creds.mechanism == "DEFAULT" and creds.username:
|
||||
cmd["saslSupportedMechs"] = creds.source + "." + creds.username
|
||||
from pymongo import auth
|
||||
|
||||
auth_ctx = auth._AuthContext.from_credentials(creds, self.address)
|
||||
if auth_ctx:
|
||||
speculative_authenticate = auth_ctx.speculate_command()
|
||||
@ -1090,6 +1093,8 @@ class Connection:
|
||||
if not self.ready:
|
||||
creds = self.opts._credentials
|
||||
if creds:
|
||||
from pymongo import auth
|
||||
|
||||
auth.authenticate(creds, self, reauthenticate=reauthenticate)
|
||||
self.ready = True
|
||||
if self.enabled_for_cmap:
|
||||
|
||||
@ -25,10 +25,11 @@ from errno import EINTR as _EINTR
|
||||
from ipaddress import ip_address as _ip_address
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
|
||||
|
||||
import cryptography.x509 as x509
|
||||
import service_identity
|
||||
from OpenSSL import SSL as _SSL
|
||||
from OpenSSL import crypto as _crypto
|
||||
|
||||
from pymongo._lazy_import import lazy_import
|
||||
from pymongo.errors import ConfigurationError as _ConfigurationError
|
||||
from pymongo.errors import _CertificateError # type:ignore[attr-defined]
|
||||
from pymongo.ocsp_cache import _OCSPCache
|
||||
@ -37,14 +38,9 @@ from pymongo.socket_checker import SocketChecker as _SocketChecker
|
||||
from pymongo.socket_checker import _errno_from_exception
|
||||
from pymongo.write_concern import validate_boolean
|
||||
|
||||
_x509 = lazy_import("cryptography.x509")
|
||||
_service_identity = lazy_import("service_identity")
|
||||
_service_identity_pyopenssl = lazy_import("service_identity.pyopenssl")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ssl import VerifyMode
|
||||
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
@ -184,7 +180,7 @@ class _CallbackData:
|
||||
"""Data class which is passed to the OCSP callback."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.trusted_ca_certs: Optional[list[Certificate]] = None
|
||||
self.trusted_ca_certs: Optional[list[x509.Certificate]] = None
|
||||
self.check_ocsp_endpoint: Optional[bool] = None
|
||||
self.ocsp_response_cache = _OCSPCache()
|
||||
|
||||
@ -336,11 +332,12 @@ class SSLContext:
|
||||
"""Attempt to load CA certs from Windows trust store."""
|
||||
cert_store = self._ctx.get_cert_store()
|
||||
oid = _stdlibssl.Purpose.SERVER_AUTH.oid
|
||||
|
||||
for cert, encoding, trust in _stdlibssl.enum_certificates(store): # type: ignore
|
||||
if encoding == "x509_asn":
|
||||
if trust is True or oid in trust:
|
||||
cert_store.add_cert(
|
||||
_crypto.X509.from_cryptography(_x509.load_der_x509_certificate(cert))
|
||||
_crypto.X509.from_cryptography(x509.load_der_x509_certificate(cert))
|
||||
)
|
||||
|
||||
def load_default_certs(self) -> None:
|
||||
@ -404,14 +401,16 @@ class SSLContext:
|
||||
# XXX: Do this in a callback registered with
|
||||
# SSLContext.set_info_callback? See Twisted for an example.
|
||||
if self.check_hostname and server_hostname is not None:
|
||||
from service_identity import pyopenssl
|
||||
|
||||
try:
|
||||
if _is_ip_address(server_hostname):
|
||||
_service_identity_pyopenssl.verify_ip_address(ssl_conn, server_hostname)
|
||||
pyopenssl.verify_ip_address(ssl_conn, server_hostname)
|
||||
else:
|
||||
_service_identity_pyopenssl.verify_hostname(ssl_conn, server_hostname)
|
||||
except (
|
||||
_service_identity.SICertificateError,
|
||||
_service_identity.SIVerificationError,
|
||||
pyopenssl.verify_hostname(ssl_conn, server_hostname)
|
||||
except ( # type:ignore[misc]
|
||||
service_identity.SICertificateError,
|
||||
service_identity.SIVerificationError,
|
||||
) as exc:
|
||||
raise _CertificateError(str(exc)) from None
|
||||
return ssl_conn
|
||||
|
||||
@ -17,17 +17,22 @@ from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import random
|
||||
from typing import Any, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
|
||||
from pymongo.common import CONNECT_TIMEOUT
|
||||
from pymongo.errors import ConfigurationError
|
||||
|
||||
try:
|
||||
if TYPE_CHECKING:
|
||||
from dns import resolver
|
||||
|
||||
_HAVE_DNSPYTHON = True
|
||||
except ImportError:
|
||||
_HAVE_DNSPYTHON = False
|
||||
|
||||
def _have_dnspython() -> bool:
|
||||
try:
|
||||
import dns # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
# dnspython can return bytes or str from various parts
|
||||
@ -40,6 +45,8 @@ def maybe_decode(text: Union[str, bytes]) -> str:
|
||||
|
||||
# PYTHON-2667 Lazily call dns.resolver methods for compatibility with eventlet.
|
||||
def _resolve(*args: Any, **kwargs: Any) -> resolver.Answer:
|
||||
from dns import resolver
|
||||
|
||||
if hasattr(resolver, "resolve"):
|
||||
# dnspython >= 2
|
||||
return resolver.resolve(*args, **kwargs)
|
||||
@ -81,6 +88,8 @@ class _SrvResolver:
|
||||
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,))
|
||||
|
||||
def get_options(self) -> Optional[str]:
|
||||
from dns import resolver
|
||||
|
||||
try:
|
||||
results = _resolve(self.__fqdn, "TXT", lifetime=self.__connect_timeout)
|
||||
except (resolver.NoAnswer, resolver.NXDOMAIN):
|
||||
|
||||
@ -44,7 +44,6 @@ from pymongo.lock import _create_lock
|
||||
from pymongo.logger import (
|
||||
_SERVER_SELECTION_LOGGER,
|
||||
_debug_log,
|
||||
_info_log,
|
||||
_ServerSelectionStatusMessage,
|
||||
)
|
||||
from pymongo.monitor import SrvMonitor
|
||||
@ -306,7 +305,7 @@ class Topology:
|
||||
)
|
||||
|
||||
if not logged_waiting:
|
||||
_info_log(
|
||||
_debug_log(
|
||||
_SERVER_SELECTION_LOGGER,
|
||||
message=_ServerSelectionStatusMessage.WAITING,
|
||||
selector=selector,
|
||||
|
||||
@ -40,7 +40,7 @@ from pymongo.common import (
|
||||
get_validated_options,
|
||||
)
|
||||
from pymongo.errors import ConfigurationError, InvalidURI
|
||||
from pymongo.srv_resolver import _HAVE_DNSPYTHON, _SrvResolver
|
||||
from pymongo.srv_resolver import _have_dnspython, _SrvResolver
|
||||
from pymongo.typings import _Address
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -472,7 +472,7 @@ def parse_uri(
|
||||
is_srv = False
|
||||
scheme_free = uri[SCHEME_LEN:]
|
||||
elif uri.startswith(SRV_SCHEME):
|
||||
if not _HAVE_DNSPYTHON:
|
||||
if not _have_dnspython():
|
||||
python_path = sys.executable or "python"
|
||||
raise ConfigurationError(
|
||||
'The "dnspython" module must be '
|
||||
|
||||
10
sbom.json
Normal file
10
sbom.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"metadata": {
|
||||
"timestamp": "2024-06-05T10:36:04.606968+00:00"
|
||||
},
|
||||
"serialNumber": "urn:uuid:a94f1412-ea1f-4821-b9f0-e14788b0776e",
|
||||
"version": 1,
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.5"
|
||||
}
|
||||
@ -194,7 +194,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"level": "debug",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
|
||||
@ -184,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"level": "debug",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
|
||||
@ -193,7 +193,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"level": "debug",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
|
||||
@ -211,7 +211,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"level": "debug",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
|
||||
@ -86,7 +86,7 @@ from pymongo import event_loggers, message, monitoring
|
||||
from pymongo.client_options import ClientOptions
|
||||
from pymongo.command_cursor import CommandCursor
|
||||
from pymongo.common import _UUID_REPRESENTATIONS, CONNECT_TIMEOUT
|
||||
from pymongo.compression_support import _HAVE_SNAPPY, _HAVE_ZSTD
|
||||
from pymongo.compression_support import _have_snappy, _have_zstd
|
||||
from pymongo.cursor import Cursor, CursorType
|
||||
from pymongo.database import Database
|
||||
from pymongo.driver_info import DriverInfo
|
||||
@ -1558,7 +1558,7 @@ class TestClient(IntegrationTest):
|
||||
self.assertEqual(opts.compressors, ["zlib"])
|
||||
self.assertEqual(opts.zlib_compression_level, -1)
|
||||
|
||||
if not _HAVE_SNAPPY:
|
||||
if not _have_snappy():
|
||||
uri = "mongodb://localhost:27017/?compressors=snappy"
|
||||
client = MongoClient(uri, connect=False)
|
||||
opts = compression_settings(client)
|
||||
@ -1573,7 +1573,7 @@ class TestClient(IntegrationTest):
|
||||
opts = compression_settings(client)
|
||||
self.assertEqual(opts.compressors, ["snappy", "zlib"])
|
||||
|
||||
if not _HAVE_ZSTD:
|
||||
if not _have_zstd():
|
||||
uri = "mongodb://localhost:27017/?compressors=zstd"
|
||||
client = MongoClient(uri, connect=False)
|
||||
opts = compression_settings(client)
|
||||
|
||||
@ -28,7 +28,7 @@ import pymongo
|
||||
from pymongo import common
|
||||
from pymongo.errors import ConfigurationError
|
||||
from pymongo.mongo_client import MongoClient
|
||||
from pymongo.srv_resolver import _HAVE_DNSPYTHON
|
||||
from pymongo.srv_resolver import _have_dnspython
|
||||
|
||||
WAIT_TIME = 0.1
|
||||
|
||||
@ -148,7 +148,7 @@ class TestSrvPolling(unittest.TestCase):
|
||||
return True
|
||||
|
||||
def run_scenario(self, dns_response, expect_change):
|
||||
self.assertEqual(_HAVE_DNSPYTHON, True)
|
||||
self.assertEqual(_have_dnspython(), True)
|
||||
if callable(dns_response):
|
||||
dns_resolver_response = dns_response
|
||||
else:
|
||||
|
||||
@ -27,7 +27,7 @@ sys.path[0:0] = [""]
|
||||
from test import clear_warning_registry, unittest
|
||||
|
||||
from pymongo.common import INTERNAL_URI_OPTION_NAME_MAP, validate
|
||||
from pymongo.compression_support import _HAVE_SNAPPY
|
||||
from pymongo.compression_support import _have_snappy
|
||||
from pymongo.uri_parser import SRV_SCHEME, parse_uri
|
||||
|
||||
CONN_STRING_TEST_PATH = os.path.join(
|
||||
@ -95,7 +95,7 @@ def run_scenario_in_dir(target_workdir):
|
||||
def create_test(test, test_workdir):
|
||||
def run_scenario(self):
|
||||
compressors = (test.get("options") or {}).get("compressors", [])
|
||||
if "snappy" in compressors and not _HAVE_SNAPPY:
|
||||
if "snappy" in compressors and not _have_snappy():
|
||||
self.skipTest("This test needs the snappy module.")
|
||||
valid = True
|
||||
warning = False
|
||||
|
||||
@ -39,9 +39,9 @@ from pymongo.collection import ReturnDocument
|
||||
from pymongo.cursor import CursorType
|
||||
from pymongo.errors import ConfigurationError, OperationFailure
|
||||
from pymongo.hello import HelloCompat
|
||||
from pymongo.helpers import _SENSITIVE_COMMANDS
|
||||
from pymongo.lock import _create_lock
|
||||
from pymongo.monitoring import (
|
||||
_SENSITIVE_COMMANDS,
|
||||
ConnectionCheckedInEvent,
|
||||
ConnectionCheckedOutEvent,
|
||||
ConnectionCheckOutFailedEvent,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user