Skip to content

Commit 5105c14

Browse files
authored
Merge pull request #638 from tableau/minimum-tls-config
Add TabPy Config Parameter for Minimum TLS Version
2 parents bc88276 + 9c0a3bf commit 5105c14

File tree

13 files changed

+119
-12
lines changed

13 files changed

+119
-12
lines changed

Diff for: .github/workflows/pull_request.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ jobs:
1111
matrix:
1212
# TODO: Add 3.7 to python-versions after GitHub action regression is resolved.
1313
# https://github.com/actions/setup-python/issues/682
14+
# TODO: switch macos-13 to macos-latest@arm64
1415
python-version: ['3.8', '3.9', '3.10']
15-
os: [ubuntu-latest, windows-latest, macos-latest]
16+
os: [ubuntu-latest, windows-latest, macos-13]
1617

1718
steps:
1819
- uses: actions/checkout@v1

Diff for: .github/workflows/push.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ jobs:
1111
matrix:
1212
# TODO: Add 3.7 to python-versions after GitHub action regression is resolved.
1313
# https://github.com/actions/setup-python/issues/682
14+
# TODO: switch macos-13 to macos-latest@arm64
1415
python-version: ['3.8', '3.9', '3.10']
15-
os: [ubuntu-latest, windows-latest, macos-latest]
16-
16+
os: [ubuntu-latest, windows-latest, macos-13]
17+
1718
steps:
1819
- uses: actions/checkout@v1
1920

Diff for: .scrutinizer.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ build:
1010
tests:
1111
override:
1212
- command: 'pytest tests --cov=tabpy --cov-config=setup.cfg'
13+
idle_timeout: 600
1314
coverage:
1415
file: '.coverage'
1516
config_file: 'setup.cfg'
@@ -26,7 +27,9 @@ build:
2627
tests:
2728
before:
2829
- pip install -r requirements.txt
29-
override: [pytest]
30+
override:
31+
pytest:
32+
idle_timeout: 600
3033
checks:
3134
python:
3235
code_rating: true

Diff for: CHANGELOG

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## v2.10.0
4+
5+
### Improvements
6+
7+
- Add TabPy parameter (TABPY_MINIMUM_TLS_VERSION) to specify the minimum TLS
8+
version that the server will accept for secure connections. Default is
9+
set to TLSv1_2.
10+
311
## v2.9.0
412

513
### Improvements

Diff for: docs/server-config.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
* [Configuration File Content](#configuration-file-content)
99
* [Configuration File Example](#configuration-file-example)
1010
- [Configuring HTTP vs HTTPS](#configuring-http-vs-https)
11-
- [Configuring TPS](#configuring-http-vs-https)
1211
- [Authentication](#authentication)
1312
* [Enabling Authentication](#enabling-authentication)
1413
* [Password File](#password-file)
@@ -83,6 +82,10 @@ at [`logging.config` documentation page](https://docs.python.org/3.6/library/log
8382
- `TABPY_KEY_FILE` - absolute path to private key file to run TabPy with.
8483
Only used with `TABPY_TRANSFER_PROTOCOL` set to `https`. Default value -
8584
not set.
85+
- `TABPY_MINIMUM_TLS_VERSION` - set the minimum TLS version that the server
86+
will accept for secure connections (`TLSv1_2`, `TLSv1_3`, etc). Refer to
87+
[docs.python.org](https://docs.python.org/3/library/ssl.html#ssl.TLSVersion.MINIMUM_SUPPORTED)
88+
for acceptable values. Default value - `TLSv1_2`.
8689
- `TABPY_LOG_DETAILS` - when set to `true` additional call information
8790
(caller IP, URL, client info, etc.) is logged. Default value - `false`.
8891
- `TABPY_MAX_REQUEST_SIZE_MB` - maximal request size supported by TabPy server
@@ -124,6 +127,7 @@ settings._
124127
# TABPY_TRANSFER_PROTOCOL = https
125128
# TABPY_CERTIFICATE_FILE = /path/to/certificate/file.crt
126129
# TABPY_KEY_FILE = /path/to/key/file.key
130+
# TABPY_MINIMUM_TLS_VERSION = TLSv1_2
127131

128132
# Log additional request details including caller IP, full URL, client
129133
# end user info if provided.

Diff for: tabpy/VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.9.0
1+
2.10.0

Diff for: tabpy/tabpy_server/app/app.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import shutil
77
import signal
8+
import ssl
89
import sys
910
import _thread
1011

@@ -83,6 +84,24 @@ def __init__(self, config_file, disable_auth_warning=True):
8384

8485
self._parse_config(config_file)
8586

87+
def _initialize_ssl_context(self):
88+
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
89+
90+
ssl_context.load_cert_chain(
91+
certfile=self.settings[SettingsParameters.CertificateFile],
92+
keyfile=self.settings[SettingsParameters.KeyFile]
93+
)
94+
95+
min_tls = self.settings[SettingsParameters.MinimumTLSVersion]
96+
if not hasattr(ssl.TLSVersion, min_tls):
97+
logger.warning(f"Unrecognized value for TABPY_MINIMUM_TLS_VERSION: {min_tls}")
98+
min_tls = "TLSv1_2"
99+
100+
logger.info(f"Setting minimum TLS version to {min_tls}")
101+
ssl_context.minimum_version = ssl.TLSVersion[min_tls]
102+
103+
return ssl_context
104+
86105
def _get_tls_certificates(self, config):
87106
tls_certificates = []
88107
cert = config[SettingsParameters.CertificateFile]
@@ -127,10 +146,7 @@ def run(self):
127146
protocol = self.settings[SettingsParameters.TransferProtocol]
128147
ssl_options = None
129148
if protocol == "https":
130-
ssl_options = {
131-
"certfile": self.settings[SettingsParameters.CertificateFile],
132-
"keyfile": self.settings[SettingsParameters.KeyFile],
133-
}
149+
ssl_options = self._initialize_ssl_context()
134150
elif protocol != "http":
135151
msg = f"Unsupported transfer protocol {protocol}."
136152
logger.critical(msg)
@@ -328,6 +344,8 @@ def _parse_config(self, config_file):
328344
(SettingsParameters.CertificateFile, ConfigParameters.TABPY_CERTIFICATE_FILE,
329345
None, None),
330346
(SettingsParameters.KeyFile, ConfigParameters.TABPY_KEY_FILE, None, None),
347+
(SettingsParameters.MinimumTLSVersion, ConfigParameters.TABPY_MINIMUM_TLS_VERSION,
348+
"TLSv1_2", None),
331349
(SettingsParameters.StateFilePath, ConfigParameters.TABPY_STATE_PATH,
332350
os.path.join(pkg_path, "tabpy_server"), None),
333351
(SettingsParameters.StaticPath, ConfigParameters.TABPY_STATIC_PATH,

Diff for: tabpy/tabpy_server/app/app_parameters.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class ConfigParameters:
1010
TABPY_TRANSFER_PROTOCOL = "TABPY_TRANSFER_PROTOCOL"
1111
TABPY_CERTIFICATE_FILE = "TABPY_CERTIFICATE_FILE"
1212
TABPY_KEY_FILE = "TABPY_KEY_FILE"
13+
TABPY_MINIMUM_TLS_VERSION = "TABPY_MINIMUM_TLS_VERSION"
1314
TABPY_LOG_DETAILS = "TABPY_LOG_DETAILS"
1415
TABPY_STATIC_PATH = "TABPY_STATIC_PATH"
1516
TABPY_MAX_REQUEST_SIZE_MB = "TABPY_MAX_REQUEST_SIZE_MB"
@@ -33,6 +34,7 @@ class SettingsParameters:
3334
UploadDir = "upload_dir"
3435
CertificateFile = "certificate_file"
3536
KeyFile = "key_file"
37+
MinimumTLSVersion = "minimum_tls_version"
3638
StateFilePath = "state_file_path"
3739
ApiVersions = "versions"
3840
LogRequestContext = "log_request_context"

Diff for: tabpy/tabpy_server/app/util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def validate_cert(cert_file_path):
1717
date_format, encoding = "%Y%m%d%H%M%SZ", "ascii"
1818
not_before = datetime.strptime(cert.get_notBefore().decode(encoding), date_format)
1919
not_after = datetime.strptime(cert.get_notAfter().decode(encoding), date_format)
20-
now = datetime.now()
20+
now = datetime.utcnow()
2121

2222
https_error = "Error using HTTPS: "
2323
if now < not_before:

Diff for: tabpy/tabpy_server/common/default.conf

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# TABPY_TRANSFER_PROTOCOL = https
1616
# TABPY_CERTIFICATE_FILE = /path/to/certificate/file.crt
1717
# TABPY_KEY_FILE = /path/to/key/file.key
18+
# TABPY_MINIMUM_TLS_VERSION = TLSv1_2
1819

1920
# Log additional request details including caller IP, full URL, client
2021
# end user info if provided.

Diff for: tests/integration/integ_test_base.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ def setUp(self):
225225

226226
# Platform specific - for integration tests we want to engage
227227
# startup script
228-
with open(self.tmp_dir + "/output.txt", "w") as outfile:
228+
self.log_file_path = os.path.join(self.tmp_dir, "output.txt")
229+
with open(self.log_file_path, "w") as outfile:
229230
cmd = ["tabpy", "--config=" + self.config_file_name, "--disable-auth-warning"]
230231
preexec_fn = None
231232
if platform.system() == "Windows":

Diff for: tests/integration/test_minimum_tls_version.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from . import integ_test_base
2+
import os
3+
4+
class TestMinimumTLSVersion(integ_test_base.IntegTestBase):
5+
def _get_log_contents(self):
6+
with open(self.log_file_path, 'r') as f:
7+
return f.read()
8+
9+
def _get_config_file_name(self, tls_version: str) -> str:
10+
config_file = open(os.path.join(self.tmp_dir, "test.conf"), "w+")
11+
config_file.write(
12+
"[TabPy]\n"
13+
"TABPY_PORT = 9005\n"
14+
"TABPY_TRANSFER_PROTOCOL = https\n"
15+
"TABPY_CERTIFICATE_FILE = ./tests/integration/resources/2019_04_24_to_3018_08_25.crt\n"
16+
"TABPY_KEY_FILE = ./tests/integration/resources/2019_04_24_to_3018_08_25.key\n"
17+
)
18+
19+
if tls_version is not None:
20+
config_file.write(f"TABPY_MINIMUM_TLS_VERSION = {tls_version}")
21+
22+
pwd_file = self._get_pwd_file()
23+
if pwd_file is not None:
24+
pwd_file = os.path.abspath(pwd_file)
25+
config_file.write(f"TABPY_PWD_FILE = {pwd_file}\n")
26+
27+
config_file.close()
28+
self.delete_config_file = True
29+
return config_file.name
30+
31+
class TestMinimumTLSVersionValid(TestMinimumTLSVersion):
32+
def _get_config_file_name(self) -> str:
33+
return super()._get_config_file_name("TLSv1_3")
34+
35+
def test_minimum_tls_version_valid(self):
36+
log_contents = self._get_log_contents()
37+
self.assertIn("Setting minimum TLS version to TLSv1_3", log_contents)
38+
39+
class TestMinimumTLSVersionInvalid(TestMinimumTLSVersion):
40+
def _get_config_file_name(self) -> str:
41+
return super()._get_config_file_name("TLSv-1.3")
42+
43+
def test_minimum_tls_version_invalid(self):
44+
log_contents = self._get_log_contents()
45+
self.assertIn("Unrecognized value for TABPY_MINIMUM_TLS_VERSION", log_contents)
46+
self.assertIn("Setting minimum TLS version to TLSv1_2", log_contents)
47+
48+
class TestMinimumTLSVersionNotSpecified(TestMinimumTLSVersion):
49+
def _get_config_file_name(self) -> str:
50+
return super()._get_config_file_name(None)
51+
52+
def test_minimum_tls_version_not_specified(self):
53+
log_contents = self._get_log_contents()
54+
self.assertIn("Setting minimum TLS version to TLSv1_2", log_contents)

Diff for: tests/unit/server_tests/test_config.py

+14
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,20 @@ def test_gzip_setting_off_valid(
209209
app = TabPyApp(self.config_file.name)
210210
self.assertEqual(app.settings["gzip_enabled"], False)
211211

212+
@patch("tabpy.tabpy_server.app.app.os.path.exists", return_value=True)
213+
@patch("tabpy.tabpy_server.app.app._get_state_from_file")
214+
@patch("tabpy.tabpy_server.app.app.TabPyState")
215+
def test_min_tls_setting_valid(
216+
self, mock_state, mock_get_state_from_file, mock_path_exists
217+
):
218+
self.assertTrue(self.config_file is not None)
219+
config_file = self.config_file
220+
config_file.write("[TabPy]\n" "TABPY_MINIMUM_TLS_VERSION = TLSv1_3".encode())
221+
config_file.close()
222+
223+
app = TabPyApp(self.config_file.name)
224+
self.assertEqual(app.settings["minimum_tls_version"], "TLSv1_3")
225+
212226
class TestTransferProtocolValidation(unittest.TestCase):
213227
def assertTabPyAppRaisesRuntimeError(self, expected_message):
214228
with self.assertRaises(RuntimeError) as err:

0 commit comments

Comments
 (0)