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
This commit is contained in:
Dean Troyer 2014-10-20 18:53:10 -05:00
parent 08ada52d6f
commit f766bca5f9
3 changed files with 74 additions and 126 deletions

View File

@ -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':

View File

@ -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

View File

@ -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)