Skip to content

Commit 8229407

Browse files
authored
Fix deprecated SSL options (#809)
* Fix usage of deprecated ssl options * Add SSLContext tests to config tests * TestKit Docker: install tools for all python versions * Improve TestKit glue * add option to choose interpreter version * make Python invocations more strict (warnings as errors) * refactor and simplify code
1 parent f43e95c commit 8229407

File tree

8 files changed

+194
-42
lines changed

8 files changed

+194
-42
lines changed

neo4j/_conf.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,9 +423,8 @@ def get_ssl_context(self):
423423
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
424424

425425
# For recommended security options see
426-
# https://docs.python.org/3.7/library/ssl.html#protocol-versions
427-
ssl_context.options |= ssl.OP_NO_TLSv1 # Python 3.2
428-
ssl_context.options |= ssl.OP_NO_TLSv1_1 # Python 3.4
426+
# https://docs.python.org/3.10/library/ssl.html#protocol-versions
427+
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
429428

430429
if isinstance(self.trusted_certificates, TrustAll):
431430
# trust any certificate

testkit/Dockerfile

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,20 @@ RUN git clone https://github.com/pyenv/pyenv.git .pyenv
4141
ENV PYENV_ROOT /.pyenv
4242
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
4343

44-
# Set minimum supported Python version
45-
RUN pyenv install 3.7:latest
46-
RUN pyenv install 3.8:latest
47-
RUN pyenv install 3.9:latest
48-
RUN pyenv install 3.10:latest
44+
# Setup python version
45+
ENV PYTHON_VERSIONS 3.7 3.8 3.9 3.10
46+
47+
RUN for version in $PYTHON_VERSIONS; do \
48+
pyenv install $version:latest; \
49+
done
4950
RUN pyenv rehash
5051
RUN pyenv global $(pyenv versions --bare --skip-aliases)
5152

52-
# Install Latest pip for each environment
53+
# Install Latest pip and setuptools for each environment
54+
# + tox and tools for starting the tests
5355
# https://pip.pypa.io/en/stable/news/
54-
RUN python -m pip install --upgrade pip
55-
56-
# Install Python Testing Tools
57-
RUN python -m pip install coverage tox tox-factor
56+
RUN for version in 3.7 3.8 3.9 3.10; do \
57+
python$version -m pip install -U pip && \
58+
python$version -m pip install -U setuptools && \
59+
python$version -m pip install -U coverage tox tox-factor; \
60+
done

testkit/_common.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
import subprocess
3+
import sys
4+
5+
6+
TEST_BACKEND_VERSION = os.getenv("TEST_BACKEND_VERSION", "python")
7+
8+
9+
def run(args, env=None):
10+
return subprocess.run(
11+
args, universal_newlines=True, stdout=sys.stdout, stderr=sys.stderr,
12+
check=True, env=env
13+
)
14+
15+
16+
def run_python(args, env=None):
17+
run([TEST_BACKEND_VERSION, "-W", "error", *args], env=env)

testkit/backend.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919

2020

2121
import os
22-
import subprocess
23-
import sys
22+
23+
from _common import run_python
2424

2525

2626
if __name__ == "__main__":
27-
cmd = ["python", "-W", "error", "-m", "testkitbackend"]
27+
cmd = ["-m", "testkitbackend"]
2828
if "TEST_BACKEND_SERVER" in os.environ:
2929
cmd.append(os.environ["TEST_BACKEND_SERVER"])
30-
subprocess.check_call(cmd, stdout=sys.stdout, stderr=sys.stderr)
30+
run_python(cmd)

testkit/build.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,11 @@
2424
"""
2525

2626

27-
import subprocess
28-
import sys
29-
30-
31-
def run(args, env=None):
32-
subprocess.run(args, universal_newlines=True, stdout=sys.stdout,
33-
stderr=sys.stderr, check=True, env=env)
27+
from _common import run_python
3428

3529

3630
if __name__ == "__main__":
37-
run(["python", "setup.py", "build"])
38-
run(["python", "-m", "pip", "install", "-U", "pip"])
39-
run(["python", "-m", "pip", "install", "-Ur",
40-
"testkitbackend/requirements.txt"])
31+
run_python(["setup.py", "build"])
32+
run_python(["-m", "pip", "install", "-U", "pip"])
33+
run_python(["-m", "pip", "install", "-Ur",
34+
"testkitbackend/requirements.txt"])

testkit/integration.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,8 @@
1818
# limitations under the License.
1919

2020

21-
import subprocess
22-
23-
24-
def run(args):
25-
subprocess.run(
26-
args, universal_newlines=True, stderr=subprocess.STDOUT, check=True)
21+
from _common import run_python
2722

2823

2924
if __name__ == "__main__":
30-
run(["python", "-W", "error", "-m", "tox", "-f", "integration"])
25+
run_python(["-m", "tox", "-f", "integration"])

testkit/unittests.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,8 @@
1818
# limitations under the License.
1919

2020

21-
import subprocess
22-
23-
24-
def run(args):
25-
subprocess.run(
26-
args, universal_newlines=True, stderr=subprocess.STDOUT, check=True)
21+
from _common import run_python
2722

2823

2924
if __name__ == "__main__":
30-
run(["python", "-W", "error", "-m", "tox", "-f", "unit"])
25+
run_python(["-m", "tox", "-f", "unit"])

tests/unit/common/test_conf.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
# limitations under the License.
1717

1818

19+
import ssl
20+
1921
import pytest
2022

2123
from neo4j import (
@@ -266,3 +268,150 @@ def test_init_session_config_with_not_valid_key():
266268
_ = SessionConfig.consume(test_config_b)
267269

268270
assert session_config.connection_acquisition_timeout == 333
271+
272+
273+
@pytest.mark.parametrize("config", (
274+
{},
275+
{"encrypted": False},
276+
{"trusted_certificates": TrustSystemCAs()},
277+
{"trusted_certificates": TrustAll()},
278+
{"trusted_certificates": TrustCustomCAs("foo", "bar")},
279+
))
280+
def test_no_ssl_mock(config, mocker):
281+
ssl_context_mock = mocker.patch("ssl.SSLContext", autospec=True)
282+
pool_config = PoolConfig.consume(config)
283+
assert pool_config.encrypted is False
284+
assert pool_config.get_ssl_context() is None
285+
ssl_context_mock.assert_not_called()
286+
287+
288+
@pytest.mark.parametrize("config", (
289+
{"encrypted": True},
290+
{"encrypted": True, "trusted_certificates": TrustSystemCAs()},
291+
))
292+
def test_trust_system_cas_mock(config, mocker):
293+
ssl_context_mock = mocker.patch("ssl.SSLContext", autospec=True)
294+
pool_config = PoolConfig.consume(config)
295+
assert pool_config.encrypted is True
296+
ssl_context = pool_config.get_ssl_context()
297+
_assert_mock_tls_1_2(ssl_context_mock)
298+
assert ssl_context.minimum_version == ssl.TLSVersion.TLSv1_2
299+
ssl_context_mock.return_value.load_default_certs.assert_called_once_with()
300+
ssl_context_mock.return_value.load_verify_locations.assert_not_called()
301+
assert ssl_context.check_hostname is True
302+
assert ssl_context.verify_mode == ssl.CERT_REQUIRED
303+
304+
305+
@pytest.mark.parametrize("config", (
306+
{"encrypted": True, "trusted_certificates": TrustCustomCAs("foo", "bar")},
307+
{"encrypted": True, "trusted_certificates": TrustCustomCAs()},
308+
))
309+
def test_trust_custom_cas_mock(config, mocker):
310+
ssl_context_mock = mocker.patch("ssl.SSLContext", autospec=True)
311+
certs = config["trusted_certificates"].certs
312+
pool_config = PoolConfig.consume(config)
313+
assert pool_config.encrypted is True
314+
ssl_context = pool_config.get_ssl_context()
315+
_assert_mock_tls_1_2(ssl_context_mock)
316+
assert ssl_context.minimum_version == ssl.TLSVersion.TLSv1_2
317+
ssl_context_mock.return_value.load_default_certs.assert_not_called()
318+
assert (
319+
ssl_context_mock.return_value.load_verify_locations.call_args_list
320+
== [((cert,), {}) for cert in certs]
321+
)
322+
assert ssl_context.check_hostname is True
323+
assert ssl_context.verify_mode == ssl.CERT_REQUIRED
324+
325+
326+
@pytest.mark.parametrize("config", (
327+
{"encrypted": True, "trusted_certificates": TrustAll()},
328+
))
329+
def test_trust_all_mock(config, mocker):
330+
ssl_context_mock = mocker.patch("ssl.SSLContext", autospec=True)
331+
pool_config = PoolConfig.consume(config)
332+
assert pool_config.encrypted is True
333+
ssl_context = pool_config.get_ssl_context()
334+
_assert_mock_tls_1_2(ssl_context_mock)
335+
assert ssl_context.minimum_version == ssl.TLSVersion.TLSv1_2
336+
ssl_context_mock.return_value.load_default_certs.assert_not_called()
337+
ssl_context_mock.return_value.load_verify_locations.assert_not_called()
338+
assert ssl_context.check_hostname is False
339+
assert ssl_context.verify_mode is ssl.CERT_NONE
340+
341+
342+
def _assert_mock_tls_1_2(mock):
343+
mock.assert_called_once_with(ssl.PROTOCOL_TLS_CLIENT)
344+
assert mock.return_value.minimum_version == ssl.TLSVersion.TLSv1_2
345+
346+
347+
@pytest.mark.parametrize("config", (
348+
{},
349+
{"encrypted": False},
350+
{"trusted_certificates": TrustSystemCAs()},
351+
{"trusted_certificates": TrustAll()},
352+
{"trusted_certificates": TrustCustomCAs("foo", "bar")},
353+
))
354+
def test_no_ssl(config):
355+
pool_config = PoolConfig.consume(config)
356+
assert pool_config.encrypted is False
357+
assert pool_config.get_ssl_context() is None
358+
359+
360+
@pytest.mark.parametrize("config", (
361+
{"encrypted": True},
362+
{"encrypted": True, "trusted_certificates": TrustSystemCAs()},
363+
))
364+
def test_trust_system_cas(config):
365+
pool_config = PoolConfig.consume(config)
366+
assert pool_config.encrypted is True
367+
ssl_context = pool_config.get_ssl_context()
368+
assert isinstance(ssl_context, ssl.SSLContext)
369+
_assert_context_tls_1_2(ssl_context)
370+
assert ssl_context.check_hostname is True
371+
assert ssl_context.verify_mode == ssl.CERT_REQUIRED
372+
373+
374+
@pytest.mark.parametrize("config", (
375+
{"encrypted": True, "trusted_certificates": TrustCustomCAs()},
376+
))
377+
def test_trust_custom_cas(config):
378+
pool_config = PoolConfig.consume(config)
379+
assert pool_config.encrypted is True
380+
ssl_context = pool_config.get_ssl_context()
381+
assert isinstance(ssl_context, ssl.SSLContext)
382+
_assert_context_tls_1_2(ssl_context)
383+
assert ssl_context.check_hostname is True
384+
assert ssl_context.verify_mode == ssl.CERT_REQUIRED
385+
386+
387+
@pytest.mark.parametrize("config", (
388+
{"encrypted": True, "trusted_certificates": TrustAll()},
389+
))
390+
def test_trust_all(config):
391+
pool_config = PoolConfig.consume(config)
392+
assert pool_config.encrypted is True
393+
ssl_context = pool_config.get_ssl_context()
394+
assert isinstance(ssl_context, ssl.SSLContext)
395+
_assert_context_tls_1_2(ssl_context)
396+
assert ssl_context.check_hostname is False
397+
assert ssl_context.verify_mode is ssl.CERT_NONE
398+
399+
400+
def _assert_context_tls_1_2(ctx):
401+
assert ctx.protocol == ssl.PROTOCOL_TLS_CLIENT
402+
assert ctx.minimum_version == ssl.TLSVersion.TLSv1_2
403+
404+
405+
@pytest.mark.parametrize("encrypted", (True, False))
406+
@pytest.mark.parametrize("trusted_certificates", (
407+
TrustSystemCAs(), TrustAll(), TrustCustomCAs()
408+
))
409+
def test_custom_ssl_context(encrypted, trusted_certificates):
410+
custom_ssl_context = object()
411+
pool_config = PoolConfig.consume({
412+
"encrypted": encrypted,
413+
"trusted_certificates": trusted_certificates,
414+
"ssl_context": custom_ssl_context,
415+
})
416+
assert pool_config.encrypted is encrypted
417+
assert pool_config.get_ssl_context() is custom_ssl_context

0 commit comments

Comments
 (0)