Skip to content

Commit

Permalink
Merge pull request #638 from tableau/minimum-tls-config
Browse files Browse the repository at this point in the history
Add TabPy Config Parameter for Minimum TLS Version
  • Loading branch information
swethabez authored Jun 20, 2024
2 parents bc88276 + 9c0a3bf commit 5105c14
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 12 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ jobs:
matrix:
# TODO: Add 3.7 to python-versions after GitHub action regression is resolved.
# https://github.com/actions/setup-python/issues/682
# TODO: switch macos-13 to macos-latest@arm64
python-version: ['3.8', '3.9', '3.10']
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest, macos-13]

steps:
- uses: actions/checkout@v1
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ jobs:
matrix:
# TODO: Add 3.7 to python-versions after GitHub action regression is resolved.
# https://github.com/actions/setup-python/issues/682
# TODO: switch macos-13 to macos-latest@arm64
python-version: ['3.8', '3.9', '3.10']
os: [ubuntu-latest, windows-latest, macos-latest]

os: [ubuntu-latest, windows-latest, macos-13]
steps:
- uses: actions/checkout@v1

Expand Down
5 changes: 4 additions & 1 deletion .scrutinizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ build:
tests:
override:
- command: 'pytest tests --cov=tabpy --cov-config=setup.cfg'
idle_timeout: 600
coverage:
file: '.coverage'
config_file: 'setup.cfg'
Expand All @@ -26,7 +27,9 @@ build:
tests:
before:
- pip install -r requirements.txt
override: [pytest]
override:
pytest:
idle_timeout: 600
checks:
python:
code_rating: true
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v2.10.0

### Improvements

- Add TabPy parameter (TABPY_MINIMUM_TLS_VERSION) to specify the minimum TLS
version that the server will accept for secure connections. Default is
set to TLSv1_2.

## v2.9.0

### Improvements
Expand Down
6 changes: 5 additions & 1 deletion docs/server-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
* [Configuration File Content](#configuration-file-content)
* [Configuration File Example](#configuration-file-example)
- [Configuring HTTP vs HTTPS](#configuring-http-vs-https)
- [Configuring TPS](#configuring-http-vs-https)
- [Authentication](#authentication)
* [Enabling Authentication](#enabling-authentication)
* [Password File](#password-file)
Expand Down Expand Up @@ -83,6 +82,10 @@ at [`logging.config` documentation page](https://docs.python.org/3.6/library/log
- `TABPY_KEY_FILE` - absolute path to private key file to run TabPy with.
Only used with `TABPY_TRANSFER_PROTOCOL` set to `https`. Default value -
not set.
- `TABPY_MINIMUM_TLS_VERSION` - set the minimum TLS version that the server
will accept for secure connections (`TLSv1_2`, `TLSv1_3`, etc). Refer to
[docs.python.org](https://docs.python.org/3/library/ssl.html#ssl.TLSVersion.MINIMUM_SUPPORTED)
for acceptable values. Default value - `TLSv1_2`.
- `TABPY_LOG_DETAILS` - when set to `true` additional call information
(caller IP, URL, client info, etc.) is logged. Default value - `false`.
- `TABPY_MAX_REQUEST_SIZE_MB` - maximal request size supported by TabPy server
Expand Down Expand Up @@ -124,6 +127,7 @@ settings._
# TABPY_TRANSFER_PROTOCOL = https
# TABPY_CERTIFICATE_FILE = /path/to/certificate/file.crt
# TABPY_KEY_FILE = /path/to/key/file.key
# TABPY_MINIMUM_TLS_VERSION = TLSv1_2

# Log additional request details including caller IP, full URL, client
# end user info if provided.
Expand Down
2 changes: 1 addition & 1 deletion tabpy/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.9.0
2.10.0
26 changes: 22 additions & 4 deletions tabpy/tabpy_server/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import shutil
import signal
import ssl
import sys
import _thread

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

self._parse_config(config_file)

def _initialize_ssl_context(self):
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

ssl_context.load_cert_chain(
certfile=self.settings[SettingsParameters.CertificateFile],
keyfile=self.settings[SettingsParameters.KeyFile]
)

min_tls = self.settings[SettingsParameters.MinimumTLSVersion]
if not hasattr(ssl.TLSVersion, min_tls):
logger.warning(f"Unrecognized value for TABPY_MINIMUM_TLS_VERSION: {min_tls}")
min_tls = "TLSv1_2"

logger.info(f"Setting minimum TLS version to {min_tls}")
ssl_context.minimum_version = ssl.TLSVersion[min_tls]

return ssl_context

def _get_tls_certificates(self, config):
tls_certificates = []
cert = config[SettingsParameters.CertificateFile]
Expand Down Expand Up @@ -127,10 +146,7 @@ def run(self):
protocol = self.settings[SettingsParameters.TransferProtocol]
ssl_options = None
if protocol == "https":
ssl_options = {
"certfile": self.settings[SettingsParameters.CertificateFile],
"keyfile": self.settings[SettingsParameters.KeyFile],
}
ssl_options = self._initialize_ssl_context()
elif protocol != "http":
msg = f"Unsupported transfer protocol {protocol}."
logger.critical(msg)
Expand Down Expand Up @@ -328,6 +344,8 @@ def _parse_config(self, config_file):
(SettingsParameters.CertificateFile, ConfigParameters.TABPY_CERTIFICATE_FILE,
None, None),
(SettingsParameters.KeyFile, ConfigParameters.TABPY_KEY_FILE, None, None),
(SettingsParameters.MinimumTLSVersion, ConfigParameters.TABPY_MINIMUM_TLS_VERSION,
"TLSv1_2", None),
(SettingsParameters.StateFilePath, ConfigParameters.TABPY_STATE_PATH,
os.path.join(pkg_path, "tabpy_server"), None),
(SettingsParameters.StaticPath, ConfigParameters.TABPY_STATIC_PATH,
Expand Down
2 changes: 2 additions & 0 deletions tabpy/tabpy_server/app/app_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class ConfigParameters:
TABPY_TRANSFER_PROTOCOL = "TABPY_TRANSFER_PROTOCOL"
TABPY_CERTIFICATE_FILE = "TABPY_CERTIFICATE_FILE"
TABPY_KEY_FILE = "TABPY_KEY_FILE"
TABPY_MINIMUM_TLS_VERSION = "TABPY_MINIMUM_TLS_VERSION"
TABPY_LOG_DETAILS = "TABPY_LOG_DETAILS"
TABPY_STATIC_PATH = "TABPY_STATIC_PATH"
TABPY_MAX_REQUEST_SIZE_MB = "TABPY_MAX_REQUEST_SIZE_MB"
Expand All @@ -33,6 +34,7 @@ class SettingsParameters:
UploadDir = "upload_dir"
CertificateFile = "certificate_file"
KeyFile = "key_file"
MinimumTLSVersion = "minimum_tls_version"
StateFilePath = "state_file_path"
ApiVersions = "versions"
LogRequestContext = "log_request_context"
Expand Down
2 changes: 1 addition & 1 deletion tabpy/tabpy_server/app/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def validate_cert(cert_file_path):
date_format, encoding = "%Y%m%d%H%M%SZ", "ascii"
not_before = datetime.strptime(cert.get_notBefore().decode(encoding), date_format)
not_after = datetime.strptime(cert.get_notAfter().decode(encoding), date_format)
now = datetime.now()
now = datetime.utcnow()

https_error = "Error using HTTPS: "
if now < not_before:
Expand Down
1 change: 1 addition & 0 deletions tabpy/tabpy_server/common/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# TABPY_TRANSFER_PROTOCOL = https
# TABPY_CERTIFICATE_FILE = /path/to/certificate/file.crt
# TABPY_KEY_FILE = /path/to/key/file.key
# TABPY_MINIMUM_TLS_VERSION = TLSv1_2

# Log additional request details including caller IP, full URL, client
# end user info if provided.
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/integ_test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ def setUp(self):

# Platform specific - for integration tests we want to engage
# startup script
with open(self.tmp_dir + "/output.txt", "w") as outfile:
self.log_file_path = os.path.join(self.tmp_dir, "output.txt")
with open(self.log_file_path, "w") as outfile:
cmd = ["tabpy", "--config=" + self.config_file_name, "--disable-auth-warning"]
preexec_fn = None
if platform.system() == "Windows":
Expand Down
54 changes: 54 additions & 0 deletions tests/integration/test_minimum_tls_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from . import integ_test_base
import os

class TestMinimumTLSVersion(integ_test_base.IntegTestBase):
def _get_log_contents(self):
with open(self.log_file_path, 'r') as f:
return f.read()

def _get_config_file_name(self, tls_version: str) -> str:
config_file = open(os.path.join(self.tmp_dir, "test.conf"), "w+")
config_file.write(
"[TabPy]\n"
"TABPY_PORT = 9005\n"
"TABPY_TRANSFER_PROTOCOL = https\n"
"TABPY_CERTIFICATE_FILE = ./tests/integration/resources/2019_04_24_to_3018_08_25.crt\n"
"TABPY_KEY_FILE = ./tests/integration/resources/2019_04_24_to_3018_08_25.key\n"
)

if tls_version is not None:
config_file.write(f"TABPY_MINIMUM_TLS_VERSION = {tls_version}")

pwd_file = self._get_pwd_file()
if pwd_file is not None:
pwd_file = os.path.abspath(pwd_file)
config_file.write(f"TABPY_PWD_FILE = {pwd_file}\n")

config_file.close()
self.delete_config_file = True
return config_file.name

class TestMinimumTLSVersionValid(TestMinimumTLSVersion):
def _get_config_file_name(self) -> str:
return super()._get_config_file_name("TLSv1_3")

def test_minimum_tls_version_valid(self):
log_contents = self._get_log_contents()
self.assertIn("Setting minimum TLS version to TLSv1_3", log_contents)

class TestMinimumTLSVersionInvalid(TestMinimumTLSVersion):
def _get_config_file_name(self) -> str:
return super()._get_config_file_name("TLSv-1.3")

def test_minimum_tls_version_invalid(self):
log_contents = self._get_log_contents()
self.assertIn("Unrecognized value for TABPY_MINIMUM_TLS_VERSION", log_contents)
self.assertIn("Setting minimum TLS version to TLSv1_2", log_contents)

class TestMinimumTLSVersionNotSpecified(TestMinimumTLSVersion):
def _get_config_file_name(self) -> str:
return super()._get_config_file_name(None)

def test_minimum_tls_version_not_specified(self):
log_contents = self._get_log_contents()
self.assertIn("Setting minimum TLS version to TLSv1_2", log_contents)
14 changes: 14 additions & 0 deletions tests/unit/server_tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,20 @@ def test_gzip_setting_off_valid(
app = TabPyApp(self.config_file.name)
self.assertEqual(app.settings["gzip_enabled"], False)

@patch("tabpy.tabpy_server.app.app.os.path.exists", return_value=True)
@patch("tabpy.tabpy_server.app.app._get_state_from_file")
@patch("tabpy.tabpy_server.app.app.TabPyState")
def test_min_tls_setting_valid(
self, mock_state, mock_get_state_from_file, mock_path_exists
):
self.assertTrue(self.config_file is not None)
config_file = self.config_file
config_file.write("[TabPy]\n" "TABPY_MINIMUM_TLS_VERSION = TLSv1_3".encode())
config_file.close()

app = TabPyApp(self.config_file.name)
self.assertEqual(app.settings["minimum_tls_version"], "TLSv1_3")

class TestTransferProtocolValidation(unittest.TestCase):
def assertTabPyAppRaisesRuntimeError(self, expected_message):
with self.assertRaises(RuntimeError) as err:
Expand Down

0 comments on commit 5105c14

Please sign in to comment.