Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 31 additions & 8 deletions tabcmd/commands/auth/session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import getpass
import json
import logging
import os

import requests
Expand Down Expand Up @@ -104,13 +105,21 @@ def timeout_as_integer(logger, option_1, option_2):
return result or 0

@staticmethod
def _read_password_from_file(filename):
def _read_password_from_file(filename, logger=None):
_logger = logger or logging.getLogger(__name__)
credential = None
with open(str(filename), "r") as file_contents:
reader = file_contents.readlines()
for row in reader:
credential = row
return credential
try:
with open(str(filename), "r") as file_contents:
reader = file_contents.readlines()
for row in reader:
credential = row
except IOError as e:
Errors.exit_with_error(
_logger,
message=_("session.errors.password_file_not_found").format(filename),
exception=e,
)
return credential

def _allow_prompt(self):
try:
Expand All @@ -121,7 +130,14 @@ def _allow_prompt(self):
def _create_new_credential(self, password, credential_type):
if password is None:
if self.password_file:
password = Session._read_password_from_file(self.password_file)
if not os.path.isfile(self.password_file):
Errors.exit_with_error(
self.logger,
message=_("session.errors.password_file_not_found").format(self.password_file),
)
password = Session._read_password_from_file(self.password_file, self.logger)
if not password:
Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format(""))
elif self._allow_prompt():
password = getpass.getpass(_("session.password"))
else:
Expand All @@ -141,7 +157,14 @@ def _create_new_token_credential(self):
if self.token_value:
token = self.token_value
elif self.token_file:
token = Session._read_password_from_file(self.token_file)
if not os.path.isfile(self.token_file):
Errors.exit_with_error(
self.logger,
message=_("session.errors.token_file_not_found").format(self.token_file),
)
token = Session._read_password_from_file(self.token_file, self.logger)
if not token:
Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("token"))
elif self._allow_prompt():
token = getpass.getpass("Token:")
else:
Expand Down
Binary file modified tabcmd/locales/en/LC_MESSAGES/tabcmd.mo
Binary file not shown.
2 changes: 2 additions & 0 deletions tabcmd/locales/en/tabcmd_messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ session.errors.invalid_redirect=Cannot redirect to invalid URL: {0}
session.errors.keystore_pwdfile_create=Cannot create keystore password file, error: {0}
session.errors.missing_arguments=Cannot sign in because of missing arguments: {0}
session.errors.no_redirect_found=No redirection address in redirect response
session.errors.password_file_not_found=Password file not found: '{0}'. Check the path passed to --password-file.
session.errors.token_file_not_found=Token file not found: '{0}'. Check the path passed to --token-file.
session.errors.no_sslcafile_found=Could not find a Certificate Authority (CA) file in these locations: ''{0}''. The CA file is used to help ensure secure communication with Tableau Server. For information on creating a CA file, see the Tableau Server help. To connect to Tableau Server without validating its SSL certificate, use the --no-certcheck flag.
session.errors.script_no_password=Tabcmd was run from a script or IDE but no password was provided. A password must be provided as an argument to tabcmd in order to log in.
session.errors.session_expired=Your session has expired
Expand Down
72 changes: 72 additions & 0 deletions tests/commands/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,78 @@ def test_create_session_username_and_password_no_prompt(self, mock_tsc, mock_pas
mock_pass.assert_not_called()


@mock.patch("tabcmd.commands.auth.session.json")
@mock.patch("tabcmd.commands.auth.session.os.path")
@mock.patch("builtins.open")
class MissingCredentialFileTests(unittest.TestCase):
"""Tests that missing --password-file / --token-file paths exit with an actionable error."""

def _assert_missing_file_error(self, mock_exit, expected_key_fragment, expected_path):
"""Assert exit_with_error was called with a message referencing the missing file."""
call_kwargs = mock_exit.call_args
message = call_kwargs[1].get("message", "") or str(call_kwargs)
# Depending on whether locale .mo files are loaded, _() returns either the
# translated string (containing the path) or the raw i18n key.
self.assertTrue(
expected_path in message or expected_key_fragment in message,
"Expected path '{}' or key '{}' in error message, got: {}".format(
expected_path, expected_key_fragment, message
),
)

def test_password_file_not_found_exits_with_actionable_error(self, mock_file, mock_path, mock_json):
mock_path.isfile.return_value = False
mock_path.exists.return_value = False
new_session = Session()
new_session.username = "uuuu"
new_session.password_file = "/nonexistent/cred.txt"
with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit:
with self.assertRaises(SystemExit):
new_session._create_new_credential(None, Session.PASSWORD_CRED_TYPE)
self._assert_missing_file_error(mock_exit, "password_file_not_found", "/nonexistent/cred.txt")

def test_token_file_not_found_exits_with_actionable_error(self, mock_file, mock_path, mock_json):
mock_path.isfile.return_value = False
mock_path.exists.return_value = False
new_session = Session()
new_session.token_name = "mytoken"
new_session.token_file = "/nonexistent/token.txt"
with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit:
with self.assertRaises(SystemExit):
new_session._create_new_token_credential()
self._assert_missing_file_error(mock_exit, "token_file_not_found", "/nonexistent/token.txt")

@mock.patch("tableauserverclient.Server")
def test_create_session_with_missing_password_file_exits(self, mock_tsc, mock_file, mock_path, mock_json):
mock_path.isfile.return_value = False
mock_path.exists.return_value = False
new_session = Session()
new_session.tableau_server = mock_tsc()
test_args = Namespace(**vars(args_to_mock))
test_args.username = "uuuu"
test_args.server = "https://fake.server.com"
test_args.password_file = "/nonexistent/cred.txt"
with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit:
with self.assertRaises(SystemExit):
new_session.create_session(test_args, None)
self._assert_missing_file_error(mock_exit, "password_file_not_found", "/nonexistent/cred.txt")

@mock.patch("tableauserverclient.Server")
def test_create_session_with_missing_token_file_exits(self, mock_tsc, mock_file, mock_path, mock_json):
mock_path.isfile.return_value = False
mock_path.exists.return_value = False
new_session = Session()
new_session.tableau_server = mock_tsc()
test_args = Namespace(**vars(args_to_mock))
test_args.token_name = "mytoken"
test_args.server = "https://fake.server.com"
test_args.token_file = "/nonexistent/token.txt"
with mock.patch("tabcmd.commands.auth.session.Errors.exit_with_error", side_effect=SystemExit) as mock_exit:
with self.assertRaises(SystemExit):
new_session.create_session(test_args, None)
self._assert_missing_file_error(mock_exit, "token_file_not_found", "/nonexistent/token.txt")


def _set_mock_tsc_not_signed_in(mock_tsc):
tsc_in_test = mock.MagicMock(name="manually mocking tsc")
tsc_in_test.is_signed_in.return_value = False # CreateSessionTests.return_False
Expand Down
Loading