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:
parent
f600c0eafb
commit
e063246b97
openstackclient
@ -62,7 +62,7 @@ def select_auth_plugin(options):
|
|||||||
if options.os_url and options.os_token:
|
if options.os_url and options.os_token:
|
||||||
# service token authentication
|
# service token authentication
|
||||||
auth_plugin = 'token_endpoint'
|
auth_plugin = 'token_endpoint'
|
||||||
elif options.os_password:
|
elif options.os_username:
|
||||||
if options.os_identity_api_version == '3':
|
if options.os_identity_api_version == '3':
|
||||||
auth_plugin = 'v3password'
|
auth_plugin = 'v3password'
|
||||||
elif options.os_identity_api_version == '2.0':
|
elif options.os_identity_api_version == '2.0':
|
||||||
|
@ -55,17 +55,46 @@ class ClientManager(object):
|
|||||||
for o in auth.OPTIONS_LIST]:
|
for o in auth.OPTIONS_LIST]:
|
||||||
return self._auth_params[name[1:]]
|
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
|
# If no plugin is named by the user, select one based on
|
||||||
# the supplied options
|
# the supplied options
|
||||||
if not auth_options.os_auth_plugin:
|
if not auth_options.os_auth_plugin:
|
||||||
auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options)
|
auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options)
|
||||||
|
|
||||||
self._auth_plugin = auth_options.os_auth_plugin
|
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._url = auth_options.os_url
|
||||||
self._auth_params = auth.build_auth_params(auth_options)
|
self._auth_params = auth.build_auth_params(auth_options)
|
||||||
self._region_name = auth_options.os_region_name
|
self._region_name = auth_options.os_region_name
|
||||||
self._api_version = api_version
|
self._api_version = api_version
|
||||||
|
self._auth_ref = None
|
||||||
self.timing = auth_options.timing
|
self.timing = auth_options.timing
|
||||||
|
|
||||||
# For compatibility until all clients can be updated
|
# For compatibility until all clients can be updated
|
||||||
@ -99,13 +128,16 @@ class ClientManager(object):
|
|||||||
verify=verify,
|
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
|
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):
|
def get_endpoint_for_service_type(self, service_type, region_name=None):
|
||||||
"""Return the endpoint URL for the service type."""
|
"""Return the endpoint URL for the service type."""
|
||||||
# See if we are using password flow auth, i.e. we have a
|
# See if we are using password flow auth, i.e. we have a
|
||||||
|
@ -36,6 +36,30 @@ from openstackclient.common import utils
|
|||||||
DEFAULT_DOMAIN = 'default'
|
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):
|
class OpenStackShell(app.App):
|
||||||
|
|
||||||
CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
|
CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
|
||||||
@ -206,112 +230,6 @@ class OpenStackShell(app.App):
|
|||||||
|
|
||||||
return clientmanager.build_plugin_option_parser(parser)
|
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):
|
def initialize_app(self, argv):
|
||||||
"""Global app init bits:
|
"""Global app init bits:
|
||||||
|
|
||||||
@ -368,19 +286,23 @@ class OpenStackShell(app.App):
|
|||||||
else:
|
else:
|
||||||
self.verify = not self.options.insecure
|
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):
|
def prepare_to_run_command(self, cmd):
|
||||||
"""Set up auth and API versions"""
|
"""Set up auth and API versions"""
|
||||||
self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__)
|
self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__)
|
||||||
|
|
||||||
if not cmd.auth_required:
|
if cmd.auth_required and cmd.best_effort:
|
||||||
return
|
|
||||||
if cmd.best_effort:
|
|
||||||
try:
|
try:
|
||||||
self.initialize_clientmanager()
|
# Trigger the Identity client to initialize
|
||||||
|
self.client_manager.auth_ref
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
self.initialize_clientmanager()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def clean_up(self, cmd, result, err):
|
def clean_up(self, cmd, result, err):
|
||||||
@ -412,12 +334,6 @@ class OpenStackShell(app.App):
|
|||||||
targs = tparser.parse_args(['-f', format])
|
targs = tparser.parse_args(['-f', format])
|
||||||
tcmd.run(targs)
|
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:]):
|
def main(argv=sys.argv[1:]):
|
||||||
return OpenStackShell().run(argv)
|
return OpenStackShell().run(argv)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user