Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TabPy Config Parameter for Minimum TLS Version #638

Merged
merged 29 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
532713e
Add TABPY_MINIMUM_TLS_VERSION.
jakeichikawasalesforce Jun 19, 2024
86a4aef
Bump versiont to 2.10.0.
jakeichikawasalesforce Jun 19, 2024
b7fef84
Add _initialize_ssl_context with min_tls.
jakeichikawasalesforce Jun 19, 2024
29575c2
Fix cert validation date comparison.
jakeichikawasalesforce Jun 19, 2024
f0f8bed
Update server-config docs.
jakeichikawasalesforce Jun 19, 2024
6fcc381
Update changelog for 2.10.0.
jakeichikawasalesforce Jun 19, 2024
d2f0fea
Fix trailing spaces.
jakeichikawasalesforce Jun 19, 2024
f2305e9
Fix trailing whitespace.
jakeichikawasalesforce Jun 19, 2024
4314bb2
Increase scrutinizer timeout.
jakeichikawasalesforce Jun 19, 2024
3b8e143
Fix scrutinizer.
jakeichikawasalesforce Jun 19, 2024
2c293fb
Fix scrutinizer.
jakeichikawasalesforce Jun 19, 2024
45c4cf2
Fix macos actions.
jakeichikawasalesforce Jun 19, 2024
088686d
Fix macos actions.
jakeichikawasalesforce Jun 19, 2024
52d27ce
Fix scrutinizer.
jakeichikawasalesforce Jun 19, 2024
029aa15
Update ssl_context to CLIENT_AUTH.
jakeichikawasalesforce Jun 19, 2024
ae04884
Update CHANGELOG.
jakeichikawasalesforce Jun 19, 2024
7e0c71d
Default to TLSv1_2 if unrecognized.
jakeichikawasalesforce Jun 19, 2024
8adedbd
Add unit test for TABPY_MINIMUM_TLS_VERSION config.
jakeichikawasalesforce Jun 19, 2024
5b77032
Minor format change.
jakeichikawasalesforce Jun 19, 2024
4c816f7
Add log_file_path property.
jakeichikawasalesforce Jun 19, 2024
e618d05
Add integration tests.
jakeichikawasalesforce Jun 19, 2024
c90c4ae
Increase scrutinzer timeout.
jakeichikawasalesforce Jun 19, 2024
2889902
Clean up test files.
jakeichikawasalesforce Jun 19, 2024
031de56
Update changelog.
jakeichikawasalesforce Jun 19, 2024
2434b09
Consolidate integration tests.
jakeichikawasalesforce Jun 19, 2024
371c11f
Update integration test names.
jakeichikawasalesforce Jun 19, 2024
cd50e9e
Add integration test for TABPY_MINIMUM_TLS_VERSION not set.
jakeichikawasalesforce Jun 20, 2024
0962234
Fix typo in integration test.
jakeichikawasalesforce Jun 20, 2024
9c0a3bf
Use macos-13 in GitHub action.
jakeichikawasalesforce Jun 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
jakeichikawasalesforce marked this conversation as resolved.
Show resolved Hide resolved
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
Loading