summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-06-30 00:51:05 +0000
committerGerrit Code Review <review@openstack.org>2018-06-30 00:51:05 +0000
commitc2c5af603f8ae25be052a20b02dc109b0f8f014a (patch)
tree6f9d8be81dda245fe6d2f80ba31963b48a5de105
parenteb97c69a3cada69845a1136661646851ec4a5ce0 (diff)
parent33ad9fd4cc0ade9f0800a2815ee0ef514ae8f264 (diff)
downloadpython-swiftclient-c2c5af603f8ae25be052a20b02dc109b0f8f014a.tar.gz
Merge "Add option for user to enter password"
-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",