From 17d51f771ea9b6210c00c946d16d94fedc3f9cc1 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 15 Apr 2015 09:36:10 +1000 Subject: [PATCH] Prompt for password on CLI if not provided load_from_argparse_arguments is very specifically for use with argparse. We can therefore safely prompt for a password from the user if none is provided and it won't affect config options or other loading mechanisms. Change-Id: Ib76743b768c5f0eef756184f1da49613423298f0 --- .../auth/identity/generic/password.py | 8 +++++ keystoneclient/auth/identity/v2.py | 8 +++++ keystoneclient/auth/identity/v3/password.py | 9 ++++++ keystoneclient/shell.py | 10 ++---- .../tests/unit/auth/test_identity_v2.py | 28 ++++++++++++++++ .../tests/unit/auth/test_identity_v3.py | 32 +++++++++++++++++++ .../tests/unit/auth/test_password.py | 31 ++++++++++++++++++ keystoneclient/utils.py | 18 +++++++++++ 8 files changed, 136 insertions(+), 8 deletions(-) diff --git a/keystoneclient/auth/identity/generic/password.py b/keystoneclient/auth/identity/generic/password.py index 6790fe22c..f1c8aa66b 100644 --- a/keystoneclient/auth/identity/generic/password.py +++ b/keystoneclient/auth/identity/generic/password.py @@ -82,3 +82,11 @@ class Password(base.BaseGenericPlugin): options = super(Password, cls).get_options() options.extend(get_options()) return options + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + if not (kwargs.get('password') or namespace.os_password): + kwargs['password'] = utils.prompt_user_password() + + return super(Password, cls).load_from_argparse_arguments(namespace, + **kwargs) diff --git a/keystoneclient/auth/identity/v2.py b/keystoneclient/auth/identity/v2.py index 8eaa9c59c..bed958b20 100644 --- a/keystoneclient/auth/identity/v2.py +++ b/keystoneclient/auth/identity/v2.py @@ -144,6 +144,14 @@ class Password(Auth): return {'passwordCredentials': auth} + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + if not (kwargs.get('password') or namespace.os_password): + kwargs['password'] = utils.prompt_user_password() + + return super(Password, cls).load_from_argparse_arguments(namespace, + **kwargs) + @classmethod def get_options(cls): options = super(Password, cls).get_options() diff --git a/keystoneclient/auth/identity/v3/password.py b/keystoneclient/auth/identity/v3/password.py index 7e432faaf..d9cfa4a14 100644 --- a/keystoneclient/auth/identity/v3/password.py +++ b/keystoneclient/auth/identity/v3/password.py @@ -13,6 +13,7 @@ from oslo_config import cfg from keystoneclient.auth.identity.v3 import base +from keystoneclient import utils __all__ = ['PasswordMethod', 'Password'] @@ -86,3 +87,11 @@ class Password(base.AuthConstructor): ]) return options + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + if not (kwargs.get('password') or namespace.os_password): + kwargs['password'] = utils.prompt_user_password() + + return super(Password, cls).load_from_argparse_arguments(namespace, + **kwargs) diff --git a/keystoneclient/shell.py b/keystoneclient/shell.py index 1221e57a0..4a6dbd26b 100644 --- a/keystoneclient/shell.py +++ b/keystoneclient/shell.py @@ -19,7 +19,6 @@ from __future__ import print_function import argparse -import getpass import logging import os import sys @@ -296,13 +295,8 @@ class OpenStackIdentityShell(object): '--os-username or env[OS_USERNAME]') if not args.os_password: - # No password, If we've got a tty, try prompting for it - if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): - # Check for Ctl-D - try: - args.os_password = getpass.getpass('OS Password: ') - except EOFError: - pass + args.os_password = utils.prompt_user_password() + # No password because we didn't have a tty or the # user Ctl-D when prompted? if not args.os_password: diff --git a/keystoneclient/tests/unit/auth/test_identity_v2.py b/keystoneclient/tests/unit/auth/test_identity_v2.py index 4c05ee234..8eaae5450 100644 --- a/keystoneclient/tests/unit/auth/test_identity_v2.py +++ b/keystoneclient/tests/unit/auth/test_identity_v2.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import copy import uuid +import mock + from keystoneclient.auth.identity import v2 from keystoneclient import exceptions from keystoneclient import session @@ -294,3 +297,28 @@ class V2IdentityPlugin(utils.TestCase): def test_password_with_no_user_id_or_name(self): self.assertRaises(TypeError, v2.Password, self.TEST_URL, password=self.TEST_PASS) + + @mock.patch('sys.stdin', autospec=True) + def test_prompt_password(self, mock_stdin): + parser = argparse.ArgumentParser() + v2.Password.register_argparse_arguments(parser) + + username = uuid.uuid4().hex + auth_url = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + password = uuid.uuid4().hex + + opts = parser.parse_args(['--os-username', username, + '--os-auth-url', auth_url, + '--os-tenant-id', tenant_id]) + + with mock.patch('getpass.getpass') as mock_getpass: + mock_getpass.return_value = password + mock_stdin.isatty = lambda: True + + plugin = v2.Password.load_from_argparse_arguments(opts) + + self.assertEqual(auth_url, plugin.auth_url) + self.assertEqual(username, plugin.username) + self.assertEqual(tenant_id, plugin.tenant_id) + self.assertEqual(password, plugin.password) diff --git a/keystoneclient/tests/unit/auth/test_identity_v3.py b/keystoneclient/tests/unit/auth/test_identity_v3.py index 077ebf53f..b90409aef 100644 --- a/keystoneclient/tests/unit/auth/test_identity_v3.py +++ b/keystoneclient/tests/unit/auth/test_identity_v3.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import copy import uuid +import mock + from keystoneclient import access from keystoneclient.auth.identity import v3 from keystoneclient.auth.identity.v3 import base as v3_base @@ -531,3 +534,32 @@ class V3IdentityPlugin(utils.TestCase): s = session.Session() self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) + + @mock.patch('sys.stdin', autospec=True) + def test_prompt_password(self, mock_stdin): + parser = argparse.ArgumentParser() + v3.Password.register_argparse_arguments(parser) + + username = uuid.uuid4().hex + user_domain_id = uuid.uuid4().hex + auth_url = uuid.uuid4().hex + project_id = uuid.uuid4().hex + password = uuid.uuid4().hex + + opts = parser.parse_args(['--os-username', username, + '--os-auth-url', auth_url, + '--os-user-domain-id', user_domain_id, + '--os-project-id', project_id]) + + with mock.patch('getpass.getpass') as mock_getpass: + mock_getpass.return_value = password + mock_stdin.isatty = lambda: True + + plugin = v3.Password.load_from_argparse_arguments(opts) + + self.assertEqual(auth_url, plugin.auth_url) + self.assertEqual(username, plugin.auth_methods[0].username) + self.assertEqual(project_id, plugin.project_id) + self.assertEqual(user_domain_id, + plugin.auth_methods[0].user_domain_id) + self.assertEqual(password, plugin.auth_methods[0].password) diff --git a/keystoneclient/tests/unit/auth/test_password.py b/keystoneclient/tests/unit/auth/test_password.py index 2891d8f6b..7926a22e8 100644 --- a/keystoneclient/tests/unit/auth/test_password.py +++ b/keystoneclient/tests/unit/auth/test_password.py @@ -10,8 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import uuid +import mock + from keystoneclient.auth.identity.generic import password from keystoneclient.auth.identity import v2 from keystoneclient.auth.identity import v3 @@ -66,3 +69,31 @@ class PasswordTests(utils.GenericPluginTestCase): def test_symbols(self): self.assertIs(v3.Password, v3_password.Password) self.assertIs(v3.PasswordMethod, v3_password.PasswordMethod) + + @mock.patch('sys.stdin', autospec=True) + def test_prompt_password(self, mock_stdin): + parser = argparse.ArgumentParser() + self.PLUGIN_CLASS.register_argparse_arguments(parser) + + username = uuid.uuid4().hex + user_domain_id = uuid.uuid4().hex + auth_url = uuid.uuid4().hex + project_id = uuid.uuid4().hex + password = uuid.uuid4().hex + + opts = parser.parse_args(['--os-username', username, + '--os-auth-url', auth_url, + '--os-user-domain-id', user_domain_id, + '--os-project-id', project_id]) + + with mock.patch('getpass.getpass') as mock_getpass: + mock_getpass.return_value = password + mock_stdin.isatty = lambda: True + + plugin = self.PLUGIN_CLASS.load_from_argparse_arguments(opts) + + self.assertEqual(auth_url, plugin.auth_url) + self.assertEqual(username, plugin._username) + self.assertEqual(project_id, plugin._project_id) + self.assertEqual(user_domain_id, plugin._user_domain_id) + self.assertEqual(password, plugin._password) diff --git a/keystoneclient/utils.py b/keystoneclient/utils.py index 7a2739f5b..ea921af8e 100644 --- a/keystoneclient/utils.py +++ b/keystoneclient/utils.py @@ -147,6 +147,24 @@ def hash_signed_token(signed_text, mode='md5'): return hash_.hexdigest() +def prompt_user_password(): + """Prompt user for a password + + Prompt for a password if stdin is a tty. + """ + password = None + + # If stdin is a tty, try prompting for the password + if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): + # Check for Ctl-D + try: + password = getpass.getpass('Password: ') + except EOFError: + pass + + return password + + def prompt_for_password(): """Prompt user for password if not provided so the password doesn't show up in the bash history.