From 33ad9fd4cc0ade9f0800a2815ee0ef514ae8f264 Mon Sep 17 00:00:00 2001 From: Alistair Coles Date: Mon, 11 Jun 2018 13:19:05 +0100 Subject: Add option for user to enter password Add the --prompt option for the CLI which will cause the user to be prompted to enter a password. Any password otherwise specified by --key, --os-password or an environment variable will be ignored. The swift client will exit with a warning if the password cannot be entered without its value being echoed. Closes-Bug: #1357562 Change-Id: I513647eed460007617f129691069c6fb1bfe62d7 --- doc/source/cli/index.rst | 4 ++++ swiftclient/shell.py | 37 +++++++++++++++++++++++++++++++ tests/unit/test_shell.py | 57 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 4aa3bfc..88fafa1 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -139,6 +139,10 @@ swift optional arguments compression should be disabled by default by the system SSL library. +``--prompt`` + Prompt user to enter a password which overrides any password supplied via + ``--key``, ``--os-password`` or environment variables. + Authentication ~~~~~~~~~~~~~~ diff --git a/swiftclient/shell.py b/swiftclient/shell.py index c15d7cf..e91a16f 100755 --- a/swiftclient/shell.py +++ b/swiftclient/shell.py @@ -17,11 +17,13 @@ from __future__ import print_function, unicode_literals import argparse +import getpass import io import json import logging import signal import socket +import warnings from os import environ, walk, _exit as os_exit from os.path import isfile, isdir, join @@ -1410,6 +1412,30 @@ class HelpFormatter(argparse.HelpFormatter): return action.dest +def prompt_for_password(): + """ + Prompt the user for a password. + + :raise SystemExit: if a password cannot be entered without it being echoed + to the terminal. + :return: the entered password. + """ + with warnings.catch_warnings(): + warnings.filterwarnings('error', category=getpass.GetPassWarning, + append=True) + try: + # temporarily set signal handling back to default to avoid user + # Ctrl-c leaving terminal in weird state + signal.signal(signal.SIGINT, signal.SIG_DFL) + return getpass.getpass() + except EOFError: + return None + except getpass.GetPassWarning: + exit('Input stream incompatible with --prompt option') + finally: + signal.signal(signal.SIGINT, immediate_exit) + + def parse_args(parser, args, enforce_requires=True): options, args = parser.parse_known_args(args or ['-h']) options = vars(options) @@ -1435,6 +1461,10 @@ def parse_args(parser, args, enforce_requires=True): if args and args[0] == 'tempurl': return options, args + # do this before process_options sets default auth version + if enforce_requires and options['prompt']: + options['key'] = options['os_password'] = prompt_for_password() + # Massage auth version; build out os_options subdict process_options(options) @@ -1506,6 +1536,7 @@ def main(arguments=None): [--os-key ] [--no-ssl-compression] [--force-auth-retry] + [--prompt] [--help] [] Command-line interface to the OpenStack Swift API. @@ -1620,6 +1651,12 @@ Examples: default=False, help='Force a re-auth attempt on ' 'any error other than 401 unauthorized') + parser.add_argument('--prompt', + action='store_true', dest='prompt', + default=False, + help='Prompt user to enter a password which overrides ' + 'any password supplied via --key, --os-password ' + 'or environment variables.') os_grp = parser.add_argument_group("OpenStack authentication options") os_grp.add_argument('--os-username', diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index 110fb01..3db48a4 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -13,8 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import unicode_literals + from genericpath import getmtime +import getpass import hashlib import json import logging @@ -2283,17 +2285,66 @@ class TestParsing(TestBase): os_opts = {"password": "secret", "auth_url": "http://example.com:5000/v3"} args = _make_args("stat", opts, os_opts) - self.assertRaises(SystemExit, swiftclient.shell.main, args) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn( + 'Auth version 3 requires either OS_USERNAME or OS_USER_ID', + str(cm.exception)) os_opts = {"username": "user", "auth_url": "http://example.com:5000/v3"} args = _make_args("stat", opts, os_opts) - self.assertRaises(SystemExit, swiftclient.shell.main, args) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn('Auth version 3 requires OS_PASSWORD', str(cm.exception)) os_opts = {"username": "user", "password": "secret"} args = _make_args("stat", opts, os_opts) - self.assertRaises(SystemExit, swiftclient.shell.main, args) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn('Auth version 3 requires OS_AUTH_URL', str(cm.exception)) + + def test_password_prompt(self): + def do_test(opts, os_opts, auth_version): + args = _make_args("stat", opts, os_opts) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch('swiftclient.shell.st_stat', fake_command): + with mock.patch('getpass.getpass', + return_value='input_pwd') as mock_getpass: + swiftclient.shell.main(args) + mock_getpass.assert_called_once_with() + self.assertEqual('input_pwd', result[0]['key']) + self.assertEqual('input_pwd', result[0]['os_password']) + + # ctrl-D + with self.assertRaises(SystemExit) as cm: + with mock.patch('swiftclient.shell.st_stat', fake_command): + with mock.patch('getpass.getpass', + side_effect=EOFError) as mock_getpass: + swiftclient.shell.main(args) + mock_getpass.assert_called_once_with() + self.assertIn( + 'Auth version %s requires' % auth_version, str(cm.exception)) + + # force getpass to think it needs to use raw input + with self.assertRaises(SystemExit) as cm: + with mock.patch('getpass.getpass', getpass.fallback_getpass): + swiftclient.shell.main(args) + self.assertIn( + 'Input stream incompatible', str(cm.exception)) + + opts = {"prompt": None, "user": "bob", "key": "secret", + "auth": "http://example.com:8080/auth/v1.0"} + do_test(opts, {}, '1.0') + os_opts = {"username": "user", + "password": "secret", + "auth_url": "http://example.com:5000/v3"} + opts = {"auth_version": "2.0", "prompt": None} + do_test(opts, os_opts, '2.0') + opts = {"auth_version": "3", "prompt": None} + do_test(opts, os_opts, '3') def test_no_tenant_name_or_id_v2(self): os_opts = {"password": "secret", -- cgit v1.2.1