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 compression should be disabled by default by the
system SSL library. system SSL library.
``--prompt``
Prompt user to enter a password which overrides any password supplied via
``--key``, ``--os-password`` or environment variables.
Authentication Authentication
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@@ -17,11 +17,13 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse import argparse
import getpass
import io import io
import json import json
import logging import logging
import signal import signal
import socket import socket
import warnings
from os import environ, walk, _exit as os_exit from os import environ, walk, _exit as os_exit
from os.path import isfile, isdir, join from os.path import isfile, isdir, join
@@ -1410,6 +1412,30 @@ class HelpFormatter(argparse.HelpFormatter):
return action.dest 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): def parse_args(parser, args, enforce_requires=True):
options, args = parser.parse_known_args(args or ['-h']) options, args = parser.parse_known_args(args or ['-h'])
options = vars(options) options = vars(options)
@@ -1435,6 +1461,10 @@ def parse_args(parser, args, enforce_requires=True):
if args and args[0] == 'tempurl': if args and args[0] == 'tempurl':
return options, args 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 # Massage auth version; build out os_options subdict
process_options(options) process_options(options)
@@ -1506,6 +1536,7 @@ def main(arguments=None):
[--os-key <client-certificate-key-file>] [--os-key <client-certificate-key-file>]
[--no-ssl-compression] [--no-ssl-compression]
[--force-auth-retry] [--force-auth-retry]
[--prompt]
<subcommand> [--help] [<subcommand options>] <subcommand> [--help] [<subcommand options>]
Command-line interface to the OpenStack Swift API. Command-line interface to the OpenStack Swift API.
@@ -1620,6 +1651,12 @@ Examples:
default=False, default=False,
help='Force a re-auth attempt on ' help='Force a re-auth attempt on '
'any error other than 401 unauthorized') '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 = parser.add_argument_group("OpenStack authentication options")
os_grp.add_argument('--os-username', os_grp.add_argument('--os-username',

View File

@@ -13,8 +13,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import unicode_literals from __future__ import unicode_literals
from genericpath import getmtime from genericpath import getmtime
import getpass
import hashlib import hashlib
import json import json
import logging import logging
@@ -2283,17 +2285,66 @@ class TestParsing(TestBase):
os_opts = {"password": "secret", os_opts = {"password": "secret",
"auth_url": "http://example.com:5000/v3"} "auth_url": "http://example.com:5000/v3"}
args = _make_args("stat", opts, os_opts) 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", os_opts = {"username": "user",
"auth_url": "http://example.com:5000/v3"} "auth_url": "http://example.com:5000/v3"}
args = _make_args("stat", opts, os_opts) 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", os_opts = {"username": "user",
"password": "secret"} "password": "secret"}
args = _make_args("stat", opts, os_opts) 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): def test_no_tenant_name_or_id_v2(self):
os_opts = {"password": "secret", os_opts = {"password": "secret",