Skip to content

Commit b5bee42

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Clean up shell authentication"
2 parents 5b7d3ba + e063246 commit b5bee42

3 files changed

Lines changed: 74 additions & 126 deletions

File tree

‎openstackclient/api/auth.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def select_auth_plugin(options):
6262
if options.os_url and options.os_token:
6363
# service token authentication
6464
auth_plugin = 'token_endpoint'
65-
elif options.os_password:
65+
elif options.os_username:
6666
if options.os_identity_api_version == '3':
6767
auth_plugin = 'v3password'
6868
elif options.os_identity_api_version == '2.0':

‎openstackclient/common/clientmanager.py‎

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,46 @@ def __getattr__(self, name):
5555
for o in auth.OPTIONS_LIST]:
5656
return self._auth_params[name[1:]]
5757

58-
def __init__(self, auth_options, api_version=None, verify=True):
58+
def __init__(
59+
self,
60+
auth_options,
61+
api_version=None,
62+
verify=True,
63+
pw_func=None,
64+
):
65+
"""Set up a ClientManager
66+
67+
:param auth_options:
68+
Options collected from the command-line, environment, or wherever
69+
:param api_version:
70+
Dict of API versions: key is API name, value is the version
71+
:param verify:
72+
TLS certificate verification; may be a boolean to enable or disable
73+
server certificate verification, or a filename of a CA certificate
74+
bundle to be used in verification (implies True)
75+
:param pw_func:
76+
Callback function for asking the user for a password. The function
77+
takes an optional string for the prompt ('Password: ' on None) and
78+
returns a string containig the password
79+
"""
80+
5981
# If no plugin is named by the user, select one based on
6082
# the supplied options
6183
if not auth_options.os_auth_plugin:
6284
auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options)
63-
6485
self._auth_plugin = auth_options.os_auth_plugin
86+
87+
# Horrible hack alert...must handle prompt for null password if
88+
# password auth is requested.
89+
if (self._auth_plugin.endswith('password') and
90+
not auth_options.os_password):
91+
auth_options.os_password = pw_func()
92+
6593
self._url = auth_options.os_url
6694
self._auth_params = auth.build_auth_params(auth_options)
6795
self._region_name = auth_options.os_region_name
6896
self._api_version = api_version
97+
self._auth_ref = None
6998
self.timing = auth_options.timing
7099

71100
# For compatibility until all clients can be updated
@@ -99,13 +128,16 @@ def __init__(self, auth_options, api_version=None, verify=True):
99128
verify=verify,
100129
)
101130

102-
self.auth_ref = None
103-
if 'token' not in self._auth_params:
104-
LOG.debug("Get service catalog")
105-
self.auth_ref = self.auth.get_auth_ref(self.session)
106-
107131
return
108132

133+
@property
134+
def auth_ref(self):
135+
"""Dereference will trigger an auth if it hasn't already"""
136+
if not self._auth_ref:
137+
LOG.debug("Get auth_ref")
138+
self._auth_ref = self.auth.get_auth_ref(self.session)
139+
return self._auth_ref
140+
109141
def get_endpoint_for_service_type(self, service_type, region_name=None):
110142
"""Return the endpoint URL for the service type."""
111143
# See if we are using password flow auth, i.e. we have a

‎openstackclient/shell.py‎

Lines changed: 34 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,30 @@
3636
DEFAULT_DOMAIN = 'default'
3737

3838

39+
def prompt_for_password(prompt=None):
40+
"""Prompt user for a password
41+
42+
Propmpt for a password if stdin is a tty.
43+
"""
44+
45+
if not prompt:
46+
prompt = 'Password: '
47+
pw = None
48+
# If stdin is a tty, try prompting for the password
49+
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
50+
# Check for Ctl-D
51+
try:
52+
pw = getpass.getpass(prompt)
53+
except EOFError:
54+
pass
55+
# No password because we did't have a tty or nothing was entered
56+
if not pw:
57+
raise exc.CommandError(
58+
"No password entered, or found via --os-password or OS_PASSWORD",
59+
)
60+
return pw
61+
62+
3963
class OpenStackShell(app.App):
4064

4165
CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
@@ -206,112 +230,6 @@ def build_option_parser(self, description, version):
206230

207231
return clientmanager.build_plugin_option_parser(parser)
208232

209-
def initialize_clientmanager(self):
210-
"""Validating authentication options and generate a clientmanager"""
211-
212-
if self.client_manager:
213-
self.log.debug('The clientmanager has been initialized already')
214-
return
215-
216-
self.log.debug("validating authentication options")
217-
218-
# Assuming all auth plugins will be named in the same fashion,
219-
# ie vXpluginName
220-
if (not self.options.os_url and
221-
self.options.os_auth_plugin.startswith('v') and
222-
self.options.os_auth_plugin[1] !=
223-
self.options.os_identity_api_version[0]):
224-
raise exc.CommandError(
225-
"Auth plugin %s not compatible"
226-
" with requested API version" % self.options.os_auth_plugin
227-
)
228-
# TODO(mhu) All these checks should be exposed at the plugin level
229-
# or just dropped altogether, as the client instantiation will fail
230-
# anyway
231-
if self.options.os_url and not self.options.os_token:
232-
# service token needed
233-
raise exc.CommandError(
234-
"You must provide a service token via"
235-
" either --os-token or env[OS_TOKEN]")
236-
237-
if (self.options.os_auth_plugin.endswith('token') and
238-
(self.options.os_token or self.options.os_auth_url)):
239-
# Token flow auth takes priority
240-
if not self.options.os_token:
241-
raise exc.CommandError(
242-
"You must provide a token via"
243-
" either --os-token or env[OS_TOKEN]")
244-
245-
if not self.options.os_auth_url:
246-
raise exc.CommandError(
247-
"You must provide a service URL via"
248-
" either --os-auth-url or env[OS_AUTH_URL]")
249-
250-
if (not self.options.os_url and
251-
not self.options.os_auth_plugin.endswith('token')):
252-
# Validate password flow auth
253-
if not self.options.os_username:
254-
raise exc.CommandError(
255-
"You must provide a username via"
256-
" either --os-username or env[OS_USERNAME]")
257-
258-
if not self.options.os_password:
259-
# No password, if we've got a tty, try prompting for it
260-
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
261-
# Check for Ctl-D
262-
try:
263-
self.options.os_password = getpass.getpass()
264-
except EOFError:
265-
pass
266-
# No password because we did't have a tty or the
267-
# user Ctl-D when prompted?
268-
if not self.options.os_password:
269-
raise exc.CommandError(
270-
"You must provide a password via"
271-
" either --os-password, or env[OS_PASSWORD], "
272-
" or prompted response")
273-
274-
if not ((self.options.os_project_id
275-
or self.options.os_project_name) or
276-
(self.options.os_domain_id
277-
or self.options.os_domain_name) or
278-
self.options.os_trust_id):
279-
if self.options.os_auth_plugin.endswith('password'):
280-
raise exc.CommandError(
281-
"You must provide authentication scope as a project "
282-
"or a domain via --os-project-id "
283-
"or env[OS_PROJECT_ID], "
284-
"--os-project-name or env[OS_PROJECT_NAME], "
285-
"--os-domain-id or env[OS_DOMAIN_ID], or"
286-
"--os-domain-name or env[OS_DOMAIN_NAME], or "
287-
"--os-trust-id or env[OS_TRUST_ID].")
288-
289-
if not self.options.os_auth_url:
290-
raise exc.CommandError(
291-
"You must provide an auth url via"
292-
" either --os-auth-url or via env[OS_AUTH_URL]")
293-
294-
if (self.options.os_trust_id and
295-
self.options.os_identity_api_version != '3'):
296-
raise exc.CommandError(
297-
"Trusts can only be used with Identity API v3")
298-
299-
if (self.options.os_trust_id and
300-
((self.options.os_project_id
301-
or self.options.os_project_name) or
302-
(self.options.os_domain_id
303-
or self.options.os_domain_name))):
304-
raise exc.CommandError(
305-
"Authentication cannot be scoped to multiple targets. "
306-
"Pick one of project, domain or trust.")
307-
308-
self.client_manager = clientmanager.ClientManager(
309-
auth_options=self.options,
310-
verify=self.verify,
311-
api_version=self.api_version,
312-
)
313-
return
314-
315233
def initialize_app(self, argv):
316234
"""Global app init bits:
317235
@@ -368,19 +286,23 @@ def initialize_app(self, argv):
368286
else:
369287
self.verify = not self.options.insecure
370288

289+
self.client_manager = clientmanager.ClientManager(
290+
auth_options=self.options,
291+
verify=self.verify,
292+
api_version=self.api_version,
293+
pw_func=prompt_for_password,
294+
)
295+
371296
def prepare_to_run_command(self, cmd):
372297
"""Set up auth and API versions"""
373298
self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__)
374299

375-
if not cmd.auth_required:
376-
return
377-
if cmd.best_effort:
300+
if cmd.auth_required and cmd.best_effort:
378301
try:
379-
self.initialize_clientmanager()
302+
# Trigger the Identity client to initialize
303+
self.client_manager.auth_ref
380304
except Exception:
381305
pass
382-
else:
383-
self.initialize_clientmanager()
384306
return
385307

386308
def clean_up(self, cmd, result, err):
@@ -412,12 +334,6 @@ def clean_up(self, cmd, result, err):
412334
targs = tparser.parse_args(['-f', format])
413335
tcmd.run(targs)
414336

415-
def interact(self):
416-
# NOTE(dtroyer): Maintain the old behaviour for interactive use as
417-
# this path does not call prepare_to_run_command()
418-
self.initialize_clientmanager()
419-
super(OpenStackShell, self).interact()
420-
421337

422338
def main(argv=sys.argv[1:]):
423339
return OpenStackShell().run(argv)

0 commit comments

Comments
 (0)