From bec206fa0a0214d856259661c5e32086f33d2f62 Mon Sep 17 00:00:00 2001
From: Dean Troyer <dtroyer@gmail.com>
Date: Mon, 29 Aug 2016 11:07:49 -0500
Subject: [PATCH] Fix auth prompt brokenness

We start by fixing this in the already-present OSC_Config class so OSC
can move forward.  This change needs to get ported down into
os-client-config in the near future, maybe even soon enough to make the
client library freeze this week.

* Add the pw-func argument to the OSC_Config (or OpenStackConfig) __init__()
* When looping through the auth options from the KSA plugin look for any
  that have a prompt defined and do not have a value already, so ask for one.

Closes-bug: #1617384
Change-Id: Ic86d56b8a6844516292fb74513712b486fec4442
---
 openstackclient/common/client_config.py   | 43 +++++++++++++++++
 openstackclient/common/clientmanager.py   |  2 -
 openstackclient/shell.py                  |  2 +-
 openstackclient/tests/test_shell_integ.py | 58 +++++++++++++++++++++++
 4 files changed, 102 insertions(+), 3 deletions(-)

diff --git a/openstackclient/common/client_config.py b/openstackclient/common/client_config.py
index bbcb34eb7b..bc2314385b 100644
--- a/openstackclient/common/client_config.py
+++ b/openstackclient/common/client_config.py
@@ -25,6 +25,40 @@ LOG = logging.getLogger(__name__)
 # before auth plugins are loaded
 class OSC_Config(OpenStackConfig):
 
+    # TODO(dtroyer): Once os-client-config with pw_func argument is in
+    #                global-requirements we can remove __init()__
+    def __init__(
+        self,
+        config_files=None,
+        vendor_files=None,
+        override_defaults=None,
+        force_ipv4=None,
+        envvar_prefix=None,
+        secure_files=None,
+        pw_func=None,
+    ):
+        ret = super(OSC_Config, self).__init__(
+            config_files=config_files,
+            vendor_files=vendor_files,
+            override_defaults=override_defaults,
+            force_ipv4=force_ipv4,
+            envvar_prefix=envvar_prefix,
+            secure_files=secure_files,
+        )
+
+        # NOTE(dtroyer): This will be pushed down into os-client-config
+        #                The default is there is no callback, the calling
+        #                application must specify what to use, typically
+        #                it will be osc_lib.shell.prompt_for_password()
+        if '_pw_callback' not in vars(self):
+            # Set the default if it doesn't already exist
+            self._pw_callback = None
+        if pw_func is not None:
+            # Set the passed in value
+            self._pw_callback = pw_func
+
+        return ret
+
     def _auth_select_default_plugin(self, config):
         """Select a default plugin based on supplied arguments
 
@@ -183,4 +217,13 @@ class OSC_Config(OpenStackConfig):
                 else:
                     config['auth'][p_opt.dest] = winning_value
 
+            # See if this needs a prompting
+            if (
+                    'prompt' in vars(p_opt) and
+                    p_opt.prompt is not None and
+                    p_opt.dest not in config['auth'] and
+                    self._pw_callback is not None
+            ):
+                config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt)
+
         return config
diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
index ccfde2d0d3..9097543b14 100644
--- a/openstackclient/common/clientmanager.py
+++ b/openstackclient/common/clientmanager.py
@@ -44,12 +44,10 @@ class ClientManager(clientmanager.ClientManager):
         self,
         cli_options=None,
         api_version=None,
-        pw_func=None,
     ):
         super(ClientManager, self).__init__(
             cli_options=cli_options,
             api_version=api_version,
-            pw_func=pw_func,
         )
 
         # TODO(dtroyer): For compatibility; mark this for removal when plugin
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index da58b63bf7..26147be999 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -145,6 +145,7 @@ class OpenStackShell(shell.OpenStackShell):
                     'interface': None,
                     'auth_type': self._auth_type,
                 },
+                pw_func=shell.prompt_for_password,
             )
         except (IOError, OSError) as e:
             self.log.critical("Could not read clouds.yaml configuration file")
@@ -162,7 +163,6 @@ class OpenStackShell(shell.OpenStackShell):
         self.client_manager = clientmanager.ClientManager(
             cli_options=self.cloud,
             api_version=self.api_version,
-            pw_func=shell.prompt_for_password,
         )
 
 
diff --git a/openstackclient/tests/test_shell_integ.py b/openstackclient/tests/test_shell_integ.py
index bc5f1ae51a..d50113b2ff 100644
--- a/openstackclient/tests/test_shell_integ.py
+++ b/openstackclient/tests/test_shell_integ.py
@@ -354,6 +354,64 @@ class TestShellCliV3Integ(TestShellInteg):
         self.assertFalse(self.requests_mock.request_history[0].verify)
 
 
+class TestShellCliV3Prompt(TestShellInteg):
+
+    def setUp(self):
+        super(TestShellCliV3Prompt, self).setUp()
+        env = {
+            "OS_AUTH_URL": V3_AUTH_URL,
+            "OS_PROJECT_DOMAIN_ID": test_shell.DEFAULT_PROJECT_DOMAIN_ID,
+            "OS_USER_DOMAIN_ID": test_shell.DEFAULT_USER_DOMAIN_ID,
+            "OS_USERNAME": test_shell.DEFAULT_USERNAME,
+            "OS_IDENTITY_API_VERSION": "3",
+        }
+        self.useFixture(osc_lib_utils.EnvFixture(copy.deepcopy(env)))
+
+        self.token = ksa_fixture.V3Token(
+            project_domain_id=test_shell.DEFAULT_PROJECT_DOMAIN_ID,
+            user_domain_id=test_shell.DEFAULT_USER_DOMAIN_ID,
+            user_name=test_shell.DEFAULT_USERNAME,
+        )
+
+        # Set up the v3 auth routes
+        self.requests_mock.register_uri(
+            'GET',
+            V3_AUTH_URL,
+            json=V3_VERSION_RESP,
+            status_code=200,
+        )
+        self.requests_mock.register_uri(
+            'POST',
+            V3_AUTH_URL + 'auth/tokens',
+            json=self.token,
+            status_code=200,
+        )
+
+    @mock.patch("osc_lib.shell.prompt_for_password")
+    def test_shell_callback(self, mock_prompt):
+        mock_prompt.return_value = "qaz"
+        _shell = shell.OpenStackShell()
+        _shell.run("configuration show".split())
+
+        # Check general calls
+        self.assertEqual(len(self.requests_mock.request_history), 2)
+
+        # Check password callback set correctly
+        self.assertEqual(
+            mock_prompt,
+            _shell.cloud._openstack_config._pw_callback
+        )
+
+        # Check auth request
+        auth_req = self.requests_mock.request_history[1].json()
+
+        # Check returned password from prompt function
+        self.assertEqual(
+            "qaz",
+            auth_req['auth']['identity']['password']['user']['password'],
+        )
+
+
 class TestShellCliPrecedence(TestShellInteg):
     """Validate option precedence rules without clouds.yaml