Skip to content

Commit 6dfa67f

Browse files
committed
Add SSL option for connecting to Tableau Server with a weaker DH key length
Fixes #1582
1 parent b81993a commit 6dfa67f

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

tableauserverclient/server/server.py

+40
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import requests
44
import urllib3
5+
import ssl
56

67
from defusedxml.ElementTree import fromstring, ParseError
78
from packaging.version import Version
@@ -91,6 +92,13 @@ class Server:
9192
and a later version of the REST API. For more information, see REST API
9293
Versions.
9394
95+
http_options : dict, optional
96+
Additional options to pass to the requests library when making HTTP requests.
97+
98+
session_factory : callable, optional
99+
A factory function that returns a requests.Session object. If not provided,
100+
requests.session is used.
101+
94102
Examples
95103
--------
96104
>>> import tableauserverclient as TSC
@@ -107,6 +115,16 @@ class Server:
107115
>>> # for example, 2.8
108116
>>> # server.version = '2.8'
109117
118+
>>> # if connecting to an older Tableau Server with weak DH keys (Python 3.12+ only)
119+
>>> server.configure_ssl(allow_weak_dh=True) # Note: reduces security
120+
121+
Notes
122+
-----
123+
When using Python 3.12 or later with older versions of Tableau Server, you may encounter
124+
SSL errors related to weak Diffie-Hellman keys. This is because newer Python versions
125+
enforce stronger security requirements. You can temporarily work around this using
126+
configure_ssl(allow_weak_dh=True), but this reduces security and should only be used
127+
as a temporary measure until the server can be upgraded.
110128
"""
111129

112130
class PublishMode:
@@ -125,6 +143,7 @@ def __init__(self, server_address, use_server_version=False, http_options=None,
125143
self._auth_token = None
126144
self._site_id = None
127145
self._user_id = None
146+
self._ssl_context = None
128147

129148
# TODO: this needs to change to default to https, but without breaking existing code
130149
if not server_address.startswith("http://") and not server_address.startswith("https://"):
@@ -313,3 +332,24 @@ def session(self):
313332

314333
def is_signed_in(self):
315334
return self._auth_token is not None
335+
336+
def configure_ssl(self, *, allow_weak_dh=False):
337+
"""Configure SSL/TLS settings for the server connection.
338+
339+
Parameters
340+
----------
341+
allow_weak_dh : bool, optional
342+
If True, allows connections to servers with DH keys that are considered too small by modern Python versions.
343+
WARNING: This reduces security and should only be used as a temporary workaround.
344+
"""
345+
if allow_weak_dh:
346+
logger.warning("WARNING: Allowing weak Diffie-Hellman keys. This reduces security and should only be used temporarily.")
347+
self._ssl_context = ssl.create_default_context()
348+
# Allow weak DH keys by setting minimum key size to 512 bits (default is 1024 in Python 3.12+)
349+
self._ssl_context.set_dh_parameters(min_key_bits=512)
350+
self.add_http_options({'verify': self._ssl_context})
351+
else:
352+
self._ssl_context = None
353+
# Remove any custom SSL context if we're reverting to default settings
354+
if 'verify' in self._http_options:
355+
del self._http_options['verify']

test/test_ssl_config.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import unittest
2+
import ssl
3+
from unittest.mock import patch, MagicMock
4+
from tableauserverclient import Server
5+
from tableauserverclient.server.endpoint import Endpoint
6+
import logging
7+
8+
9+
class TestSSLConfig(unittest.TestCase):
10+
@patch('requests.session')
11+
@patch('tableauserverclient.server.endpoint.Endpoint.set_parameters')
12+
def setUp(self, mock_set_parameters, mock_session):
13+
"""Set up test fixtures with mocked session and request validation"""
14+
# Mock the session
15+
self.mock_session = MagicMock()
16+
mock_session.return_value = self.mock_session
17+
18+
# Mock request preparation
19+
self.mock_request = MagicMock()
20+
self.mock_session.prepare_request.return_value = self.mock_request
21+
22+
# Create server instance with mocked components
23+
self.server = Server('http://test')
24+
25+
def test_default_ssl_config(self):
26+
"""Test that by default, no custom SSL context is used"""
27+
self.assertIsNone(self.server._ssl_context)
28+
self.assertNotIn('verify', self.server.http_options)
29+
30+
@patch('ssl.create_default_context')
31+
def test_weak_dh_config(self, mock_create_context):
32+
"""Test that weak DH keys can be allowed when configured"""
33+
# Setup mock SSL context
34+
mock_context = MagicMock()
35+
mock_create_context.return_value = mock_context
36+
37+
# Configure SSL with weak DH
38+
self.server.configure_ssl(allow_weak_dh=True)
39+
40+
# Verify SSL context was created and configured correctly
41+
mock_create_context.assert_called_once()
42+
mock_context.set_dh_parameters.assert_called_once_with(min_key_bits=512)
43+
44+
# Verify context was added to http options
45+
self.assertEqual(self.server.http_options['verify'], mock_context)
46+
47+
@patch('ssl.create_default_context')
48+
def test_disable_weak_dh_config(self, mock_create_context):
49+
"""Test that SSL config can be reset to defaults"""
50+
# Setup mock SSL context
51+
mock_context = MagicMock()
52+
mock_create_context.return_value = mock_context
53+
54+
# First enable weak DH
55+
self.server.configure_ssl(allow_weak_dh=True)
56+
self.assertIsNotNone(self.server._ssl_context)
57+
self.assertIn('verify', self.server.http_options)
58+
59+
# Then disable it
60+
self.server.configure_ssl(allow_weak_dh=False)
61+
self.assertIsNone(self.server._ssl_context)
62+
self.assertNotIn('verify', self.server.http_options)
63+
64+
@patch('ssl.create_default_context')
65+
def test_warning_on_weak_dh(self, mock_create_context):
66+
"""Test that a warning is logged when enabling weak DH keys"""
67+
logging.getLogger().setLevel(logging.WARNING)
68+
with self.assertLogs(level='WARNING') as log:
69+
self.server.configure_ssl(allow_weak_dh=True)
70+
self.assertTrue(
71+
any('WARNING: Allowing weak Diffie-Hellman keys' in record for record in log.output),
72+
"Expected warning about weak DH keys was not logged"
73+
)
74+
75+
if __name__ == '__main__':
76+
unittest.main()

0 commit comments

Comments
 (0)