summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlistair Coles <alistairncoles@gmail.com>2018-06-11 13:19:05 +0100
committerAlistair Coles <alistairncoles@gmail.com>2018-06-11 17:25:21 +0100
commit33ad9fd4cc0ade9f0800a2815ee0ef514ae8f264 (patch)
tree54ee62d50f13e91a19eeab7bbc81f5fecc6b339b
parent2312182241d36c716e624a23acd51f2b0252e4aa (diff)
downloadpython-swiftclient-33ad9fd4cc0ade9f0800a2815ee0ef514ae8f264.tar.gz
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
-rw-r--r--doc/source/cli/index.rst4
-rwxr-xr-xswiftclient/shell.py37
-rw-r--r--tests/unit/test_shell.py57
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 <client-certificate-key-file>]
[--no-ssl-compression]
[--force-auth-retry]
+ [--prompt]
<subcommand> [--help] [<subcommand options>]
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",