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
This commit is contained in:
Alistair Coles 2018-06-11 13:19:05 +01:00
parent 2312182241
commit 33ad9fd4cc
3 changed files with 95 additions and 3 deletions

View File

@ -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
~~~~~~~~~~~~~~

View File

@ -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',

View File

@ -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",