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)