diff --git a/doc/changelog.rst b/doc/changelog.rst index 9ea2f05c2e..6494e2bfe8 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -29,7 +29,9 @@ PyMongo 4.19 brings a number of changes including: :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient`, an alternative to ``srvAllowedHostsSuffix`` for deployments whose acceptable SRV hosts cannot be expressed as a single suffix. The callback is invoked once per SRV-returned - host and returns ``True`` to accept it. It is mutually exclusive with + host and must return a boolean: ``True`` to accept the host and ``False`` to + reject it. During initial DNS discovery a non-boolean return raises an error; + during SRV polling it is treated as a rejection. It is mutually exclusive with ``srvAllowedHostsSuffix`` and, because it takes a callable, cannot be set in a connection string. See the :class:`~pymongo.synchronous.mongo_client.MongoClient` and diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 65aecce446..df390a218e 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -476,7 +476,9 @@ def __init__( argument, and must return ``True`` to accept the host or ``False`` to reject it. During initial DNS discovery, rejecting a host raises :exc:`~pymongo.errors.ConfigurationError`, as does an exception raised by - the callback itself. Use this when the set of acceptable hosts cannot be + the callback itself or a return value that is not a boolean. During SRV + polling, a rejection or a non-boolean return is treated as ``False`` and no + error is raised. Use this when the set of acceptable hosts cannot be expressed as a single suffix:: def validator(host: str) -> bool: diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 554d226acf..262e9107ce 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -148,6 +148,11 @@ def _validate_host(self, srv_host: str) -> None: raise ConfigurationError( f"srv_host_validator raised an exception for SRV host {srv_host}: {exc}" ) from exc + if not isinstance(allowed, bool): + raise ConfigurationError( + f"srv_host_validator must return a bool for SRV host {srv_host}, " + f"not {type(allowed).__name__}" + ) if not allowed: raise ConfigurationError( f"Invalid SRV host: {srv_host} was rejected by srv_host_validator" diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index df09b03b20..6aaaaed20a 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -477,7 +477,9 @@ def __init__( argument, and must return ``True`` to accept the host or ``False`` to reject it. During initial DNS discovery, rejecting a host raises :exc:`~pymongo.errors.ConfigurationError`, as does an exception raised by - the callback itself. Use this when the set of acceptable hosts cannot be + the callback itself or a return value that is not a boolean. During SRV + polling, a rejection or a non-boolean return is treated as ``False`` and no + error is raised. Use this when the set of acceptable hosts cannot be expressed as a single suffix:: def validator(host: str) -> bool: diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 18083c8a0c..25fc6f7ad6 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -148,6 +148,11 @@ def _validate_host(self, srv_host: str) -> None: raise ConfigurationError( f"srv_host_validator raised an exception for SRV host {srv_host}: {exc}" ) from exc + if not isinstance(allowed, bool): + raise ConfigurationError( + f"srv_host_validator must return a bool for SRV host {srv_host}, " + f"not {type(allowed).__name__}" + ) if not allowed: raise ConfigurationError( f"Invalid SRV host: {srv_host} was rejected by srv_host_validator" diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index befa119b46..26398ed866 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -402,6 +402,20 @@ async def test_13_throw_when_srv_host_validator_is_used_with_a_non_srv_uri(self) connect=False, ) + async def test_14_throw_when_srv_host_validator_returns_a_non_boolean_value(self): + with self.assertRaisesRegex(ConfigurationError, "srv_host_validator must return a bool"): + await self._parse( + "blogs.mongodb.com", "cluster.mongodb.com", srv_host_validator=lambda host: "true" + ) + + async def test_15_accept_an_underscore_in_srv_allowed_hosts_suffix(self): + res = await self._parse( + "blogs.my_domain.net", + "cluster.my_domain.net", + srv_allowed_hosts_suffix=".my_domain.net", + ) + self.assertEqual(["cluster.my_domain.net"], [node[0] for node in res["nodelist"]]) + async def test_srv_hostname_with_three_or_more_parts_may_equal_the_returned_hostname( self, ): diff --git a/test/test_dns.py b/test/test_dns.py index 89dee6f3a7..9d34455b45 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -398,6 +398,20 @@ def test_13_throw_when_srv_host_validator_is_used_with_a_non_srv_uri(self): connect=False, ) + def test_14_throw_when_srv_host_validator_returns_a_non_boolean_value(self): + with self.assertRaisesRegex(ConfigurationError, "srv_host_validator must return a bool"): + self._parse( + "blogs.mongodb.com", "cluster.mongodb.com", srv_host_validator=lambda host: "true" + ) + + def test_15_accept_an_underscore_in_srv_allowed_hosts_suffix(self): + res = self._parse( + "blogs.my_domain.net", + "cluster.my_domain.net", + srv_allowed_hosts_suffix=".my_domain.net", + ) + self.assertEqual(["cluster.my_domain.net"], [node[0] for node in res["nodelist"]]) + def test_srv_hostname_with_three_or_more_parts_may_equal_the_returned_hostname( self, ):