From e063246b97a7f31a47aca0a5eb36d571f5df7236 Mon Sep 17 00:00:00 2001
From: Dean Troyer <dtroyer@gmail.com>
Date: Mon, 20 Oct 2014 18:53:10 -0500
Subject: [PATCH] Clean up shell authentication

* Remove the auth option checks as the auth plugins will validate
  their own options
* Move the initialization of client_manager to the end of
  initialize_app() so it is always called.  Note that no attempts
  to actually authenticate occur until the first use of one of the
  client attributes in client_manager.  This leaves
  initialize_clientmanager() (formerly uathenticate_user()) empty
  so remove it.
* Remove interact() as the client_manager has already been created
  And there is nothing left.
* prepare_to_run_command() is reduced to trigger an authentication
  attempt for the best_effort auth commands, currently the only
  one is 'complete'.
* Add prompt_for_password() to ask the user to enter a password
  when necessary.  Passed to ClientManager in a new kward pw_func.

Bug: 1355838
Change-Id: I9fdec9144c4c84f65aed1cf91ce41fe1895089b2
---
 openstackclient/api/auth.py             |   2 +-
 openstackclient/common/clientmanager.py |  46 +++++--
 openstackclient/shell.py                | 152 ++++++------------------
 3 files changed, 74 insertions(+), 126 deletions(-)

diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py
index e33b72d575..f6e99cdcf0 100644
--- a/openstackclient/api/auth.py
+++ b/openstackclient/api/auth.py
@@ -62,7 +62,7 @@ def select_auth_plugin(options):
     if options.os_url and options.os_token:
         # service token authentication
         auth_plugin = 'token_endpoint'
-    elif options.os_password:
+    elif options.os_username:
         if options.os_identity_api_version == '3':
             auth_plugin = 'v3password'
         elif options.os_identity_api_version == '2.0':
diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
index febcedf4b1..ae38f16077 100644
--- a/openstackclient/common/clientmanager.py
+++ b/openstackclient/common/clientmanager.py
@@ -55,17 +55,46 @@ class ClientManager(object):
                     for o in auth.OPTIONS_LIST]:
             return self._auth_params[name[1:]]
 
-    def __init__(self, auth_options, api_version=None, verify=True):
+    def __init__(
+        self,
+        auth_options,
+        api_version=None,
+        verify=True,
+        pw_func=None,
+    ):
+        """Set up a ClientManager
+
+        :param auth_options:
+            Options collected from the command-line, environment, or wherever
+        :param api_version:
+            Dict of API versions: key is API name, value is the version
+        :param verify:
+            TLS certificate verification; may be a boolean to enable or disable
+            server certificate verification, or a filename of a CA certificate
+            bundle to be used in verification (implies True)
+        :param pw_func:
+            Callback function for asking the user for a password.  The function
+            takes an optional string for the prompt ('Password: ' on None) and
+            returns a string containig the password
+        """
+
         # If no plugin is named by the user, select one based on
         # the supplied options
         if not auth_options.os_auth_plugin:
             auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options)
-
         self._auth_plugin = auth_options.os_auth_plugin
+
+        # Horrible hack alert...must handle prompt for null password if
+        # password auth is requested.
+        if (self._auth_plugin.endswith('password') and
+                not auth_options.os_password):
+            auth_options.os_password = pw_func()
+
         self._url = auth_options.os_url
         self._auth_params = auth.build_auth_params(auth_options)
         self._region_name = auth_options.os_region_name
         self._api_version = api_version
+        self._auth_ref = None
         self.timing = auth_options.timing
 
         # For compatibility until all clients can be updated
@@ -99,13 +128,16 @@ class ClientManager(object):
             verify=verify,
         )
 
-        self.auth_ref = None
-        if 'token' not in self._auth_params:
-            LOG.debug("Get service catalog")
-            self.auth_ref = self.auth.get_auth_ref(self.session)
-
         return
 
+    @property
+    def auth_ref(self):
+        """Dereference will trigger an auth if it hasn't already"""
+        if not self._auth_ref:
+            LOG.debug("Get auth_ref")
+            self._auth_ref = self.auth.get_auth_ref(self.session)
+        return self._auth_ref
+
     def get_endpoint_for_service_type(self, service_type, region_name=None):
         """Return the endpoint URL for the service type."""
         # See if we are using password flow auth, i.e. we have a
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index 668e48b5fc..e671ecc3dc 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -36,6 +36,30 @@ from openstackclient.common import utils
 DEFAULT_DOMAIN = 'default'
 
 
+def prompt_for_password(prompt=None):
+    """Prompt user for a password
+
+    Propmpt for a password if stdin is a tty.
+    """
+
+    if not prompt:
+        prompt = 'Password: '
+    pw = 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:
+            pw = getpass.getpass(prompt)
+        except EOFError:
+            pass
+    # No password because we did't have a tty or nothing was entered
+    if not pw:
+        raise exc.CommandError(
+            "No password entered, or found via --os-password or OS_PASSWORD",
+        )
+    return pw
+
+
 class OpenStackShell(app.App):
 
     CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
@@ -206,112 +230,6 @@ class OpenStackShell(app.App):
 
         return clientmanager.build_plugin_option_parser(parser)
 
-    def initialize_clientmanager(self):
-        """Validating authentication options and generate a clientmanager"""
-
-        if self.client_manager:
-            self.log.debug('The clientmanager has been initialized already')
-            return
-
-        self.log.debug("validating authentication options")
-
-        # Assuming all auth plugins will be named in the same fashion,
-        # ie vXpluginName
-        if (not self.options.os_url and
-           self.options.os_auth_plugin.startswith('v') and
-           self.options.os_auth_plugin[1] !=
-           self.options.os_identity_api_version[0]):
-            raise exc.CommandError(
-                "Auth plugin %s not compatible"
-                " with requested API version" % self.options.os_auth_plugin
-            )
-        # TODO(mhu) All these checks should be exposed at the plugin level
-        # or just dropped altogether, as the client instantiation will fail
-        # anyway
-        if self.options.os_url and not self.options.os_token:
-            # service token needed
-            raise exc.CommandError(
-                "You must provide a service token via"
-                " either --os-token or env[OS_TOKEN]")
-
-        if (self.options.os_auth_plugin.endswith('token') and
-           (self.options.os_token or self.options.os_auth_url)):
-            # Token flow auth takes priority
-            if not self.options.os_token:
-                raise exc.CommandError(
-                    "You must provide a token via"
-                    " either --os-token or env[OS_TOKEN]")
-
-            if not self.options.os_auth_url:
-                raise exc.CommandError(
-                    "You must provide a service URL via"
-                    " either --os-auth-url or env[OS_AUTH_URL]")
-
-        if (not self.options.os_url and
-           not self.options.os_auth_plugin.endswith('token')):
-            # Validate password flow auth
-            if not self.options.os_username:
-                raise exc.CommandError(
-                    "You must provide a username via"
-                    " either --os-username or env[OS_USERNAME]")
-
-            if not self.options.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:
-                        self.options.os_password = getpass.getpass()
-                    except EOFError:
-                        pass
-                # No password because we did't have a tty or the
-                # user Ctl-D when prompted?
-                if not self.options.os_password:
-                    raise exc.CommandError(
-                        "You must provide a password via"
-                        " either --os-password, or env[OS_PASSWORD], "
-                        " or prompted response")
-
-            if not ((self.options.os_project_id
-                    or self.options.os_project_name) or
-                    (self.options.os_domain_id
-                    or self.options.os_domain_name) or
-                    self.options.os_trust_id):
-                if self.options.os_auth_plugin.endswith('password'):
-                    raise exc.CommandError(
-                        "You must provide authentication scope as a project "
-                        "or a domain via --os-project-id "
-                        "or env[OS_PROJECT_ID], "
-                        "--os-project-name or env[OS_PROJECT_NAME], "
-                        "--os-domain-id or env[OS_DOMAIN_ID], or"
-                        "--os-domain-name or env[OS_DOMAIN_NAME], or "
-                        "--os-trust-id or env[OS_TRUST_ID].")
-
-            if not self.options.os_auth_url:
-                raise exc.CommandError(
-                    "You must provide an auth url via"
-                    " either --os-auth-url or via env[OS_AUTH_URL]")
-
-            if (self.options.os_trust_id and
-               self.options.os_identity_api_version != '3'):
-                raise exc.CommandError(
-                    "Trusts can only be used with Identity API v3")
-
-            if (self.options.os_trust_id and
-               ((self.options.os_project_id
-                 or self.options.os_project_name) or
-                (self.options.os_domain_id
-                 or self.options.os_domain_name))):
-                raise exc.CommandError(
-                    "Authentication cannot be scoped to multiple targets. "
-                    "Pick one of project, domain or trust.")
-
-        self.client_manager = clientmanager.ClientManager(
-            auth_options=self.options,
-            verify=self.verify,
-            api_version=self.api_version,
-        )
-        return
-
     def initialize_app(self, argv):
         """Global app init bits:
 
@@ -368,19 +286,23 @@ class OpenStackShell(app.App):
         else:
             self.verify = not self.options.insecure
 
+        self.client_manager = clientmanager.ClientManager(
+            auth_options=self.options,
+            verify=self.verify,
+            api_version=self.api_version,
+            pw_func=prompt_for_password,
+        )
+
     def prepare_to_run_command(self, cmd):
         """Set up auth and API versions"""
         self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__)
 
-        if not cmd.auth_required:
-            return
-        if cmd.best_effort:
+        if cmd.auth_required and cmd.best_effort:
             try:
-                self.initialize_clientmanager()
+                # Trigger the Identity client to initialize
+                self.client_manager.auth_ref
             except Exception:
                 pass
-        else:
-            self.initialize_clientmanager()
         return
 
     def clean_up(self, cmd, result, err):
@@ -412,12 +334,6 @@ class OpenStackShell(app.App):
             targs = tparser.parse_args(['-f', format])
             tcmd.run(targs)
 
-    def interact(self):
-        # NOTE(dtroyer): Maintain the old behaviour for interactive use as
-        #                this path does not call prepare_to_run_command()
-        self.initialize_clientmanager()
-        super(OpenStackShell, self).interact()
-
 
 def main(argv=sys.argv[1:]):
     return OpenStackShell().run(argv)