diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index bac8f295..bed2fc9f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,9 @@ jobs: strategy: matrix: - python-version: [3.7, 3.8, 3.9] + # TODO: Add 3.7 to python-versions after GitHub action regression is resolved. + # https://github.com/actions/setup-python/issues/682 + python-version: [3.8, 3.9] os: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 1b9e0c71..86c2c391 100755 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -9,7 +9,9 @@ jobs: strategy: matrix: - python-version: [3.7, 3.8, 3.9] + # TODO: Add 3.7 to python-versions after GitHub action regression is resolved. + # https://github.com/actions/setup-python/issues/682 + python-version: [3.8, 3.9] os: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/CHANGELOG b/CHANGELOG index 6033c151..6cfda689 100755 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ # Changelog +## v2.9.0 + +### Improvements + +- Require confirmation to continue when starting TabPy without authentication, + with a warning that this is an insecure state and not recommended. + ## v2.8.0 ### Improvements diff --git a/tabpy/VERSION b/tabpy/VERSION index 834f2629..c8e38b61 100755 --- a/tabpy/VERSION +++ b/tabpy/VERSION @@ -1 +1 @@ -2.8.0 +2.9.0 diff --git a/tabpy/tabpy.py b/tabpy/tabpy.py index 50acf799..32feb532 100755 --- a/tabpy/tabpy.py +++ b/tabpy/tabpy.py @@ -3,11 +3,12 @@ Usage: tabpy [-h] | [--help] - tabpy [--config ] + tabpy [--config ] [--disable-auth-warning] Options: - -h --help Show this screen. - --config Path to a config file. + -h --help Show this screen. + --config Path to a config file. + --disable-auth-warning Disable authentication warning. """ import docopt @@ -38,9 +39,13 @@ def main(): args = docopt.docopt(__doc__) config = args["--config"] or None + disable_auth_warning = False + if args["--disable-auth-warning"]: + disable_auth_warning = True + from tabpy.tabpy_server.app.app import TabPyApp - app = TabPyApp(config) + app = TabPyApp(config, disable_auth_warning) app.run() diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index e0c74d13..18b83391 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -67,7 +67,8 @@ class TabPyApp: arrow_server = None max_request_size = None - def __init__(self, config_file): + def __init__(self, config_file, disable_auth_warning=True): + self.disable_auth_warning = disable_auth_warning if config_file is None: config_file = os.path.join( os.path.dirname(__file__), os.path.pardir, "common", "default.conf" @@ -394,9 +395,7 @@ def _parse_config(self, config_file): logger.critical(msg) raise RuntimeError(msg) else: - logger.info( - "Password file is not specified: " "Authentication is not enabled" - ) + self._handle_configuration_without_authentication() features = self._get_features() self.settings[SettingsParameters.ApiVersions] = {"v1": {"features": features}} @@ -471,6 +470,31 @@ def _parse_pwd_file(self): return succeeded + def _handle_configuration_without_authentication(self): + std_no_auth_msg = "Password file is not specified: Authentication is not enabled" + + if self.disable_auth_warning == True: + logger.info(std_no_auth_msg) + return + + confirm_no_auth_msg = "\nWARNING: This TabPy server is not currently configured for username/password authentication. " + + if self.settings[SettingsParameters.EvaluateEnabled]: + confirm_no_auth_msg += ("This means that, because the TABPY_EVALUATE_ENABLE feature is enabled, there is " + "the potential that unauthenticated individuals may be able to remotely execute code on this machine. ") + + confirm_no_auth_msg += ("We strongly advise against proceeding without authentication as it poses a significant security risk.\n\n" + "Do you wish to proceed without authentication? (y/N): ") + + confirm_no_auth_input = input(confirm_no_auth_msg) + + if confirm_no_auth_input == 'y': + logger.info(std_no_auth_msg) + else: + print("\nAborting start up. To enable authentication for your TabPy server, see " + "https://github.com/tableau/TabPy/blob/master/docs/server-config.md#authentication.") + exit() + def _get_features(self): features = {} diff --git a/tests/integration/integ_test_base.py b/tests/integration/integ_test_base.py index 111a0d89..330fbe84 100755 --- a/tests/integration/integ_test_base.py +++ b/tests/integration/integ_test_base.py @@ -226,7 +226,7 @@ def setUp(self): # Platform specific - for integration tests we want to engage # startup script with open(self.tmp_dir + "/output.txt", "w") as outfile: - cmd = ["tabpy", "--config=" + self.config_file_name] + cmd = ["tabpy", "--config=" + self.config_file_name, "--disable-auth-warning"] preexec_fn = None if platform.system() == "Windows": self.py = "python" diff --git a/tests/unit/server_tests/test_config.py b/tests/unit/server_tests/test_config.py index dbc1d578..4090a7af 100644 --- a/tests/unit/server_tests/test_config.py +++ b/tests/unit/server_tests/test_config.py @@ -68,6 +68,26 @@ def test_no_state_ini_file_or_state_dir( TabPyApp(None) self.assertEqual(len(mock_os.makedirs.mock_calls), 1) + @patch('builtins.input', return_value='y') + @patch("tabpy.tabpy_server.app.app.os") + @patch("tabpy.tabpy_server.app.app.os.path.exists", return_value=False) + @patch("tabpy.tabpy_server.app.app.PythonServiceHandler") + @patch("tabpy.tabpy_server.app.app._get_state_from_file") + @patch("tabpy.tabpy_server.app.app.TabPyState") + def test_handle_configuration_without_authentication( + self, + mock_tabpy_state, + mock_get_state_from_file, + mock_psws, + mock_os_path_exists, + mock_os, + mock_input, + ): + TabPyApp(None) + mock_input.assert_not_called() + + TabPyApp(None, False) + mock_input.assert_called() class TestPartialConfigFile(unittest.TestCase): def setUp(self):