From a4d481076db7b0c65b6a5508374c1baae9d25732 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Sat, 2 May 2015 14:06:08 +1000 Subject: [PATCH] A Default CLI plugin A plugin that can be used by default by any CLI application. This would allow us to convert the other service CLIs to a consistent set of options. Closes-Bug: #1459478 Change-Id: I9ce6c439d530040e9375f7fd26a9ec2e0ba8b2a4 --- keystoneclient/auth/identity/generic/cli.py | 83 +++++++++++++++++ .../tests/unit/auth/test_default_cli.py | 88 +++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 keystoneclient/auth/identity/generic/cli.py create mode 100644 keystoneclient/tests/unit/auth/test_default_cli.py diff --git a/keystoneclient/auth/identity/generic/cli.py b/keystoneclient/auth/identity/generic/cli.py new file mode 100644 index 000000000..c4938503d --- /dev/null +++ b/keystoneclient/auth/identity/generic/cli.py @@ -0,0 +1,83 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + +from keystoneclient.auth.identity.generic import password +from keystoneclient import exceptions as exc +from keystoneclient.i18n import _ +from keystoneclient import utils + + +class DefaultCLI(password.Password): + """A Plugin that provides typical authentication options for CLIs. + + This plugin provides standard username and password authentication options + as well as allowing users to override with a custom token and endpoint. + """ + + @utils.positional() + def __init__(self, endpoint=None, token=None, **kwargs): + super(DefaultCLI, self).__init__(**kwargs) + + self._token = token + self._endpoint = endpoint + + @classmethod + def get_options(cls): + options = super(DefaultCLI, cls).get_options() + options.extend([cfg.StrOpt('endpoint', + help='A URL to use instead of a catalog'), + cfg.StrOpt('token', + help='Always use the specified token')]) + return options + + def get_token(self, *args, **kwargs): + if self._token: + return self._token + + return super(DefaultCLI, self).get_token(*args, **kwargs) + + def get_endpoint(self, *args, **kwargs): + if self._endpoint: + return self._endpoint + + return super(DefaultCLI, self).get_endpoint(*args, **kwargs) + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + token = kwargs.get('token') or namespace.os_token + endpoint = kwargs.get('endpoint') or namespace.os_endpoint + auth_url = kwargs.get('auth_url') or namespace.os_auth_url + + if token and not endpoint: + # if a user provides a token then they must also provide an + # endpoint because we aren't fetching a token to get a catalog from + msg = _('A service URL must be provided with a token') + raise exc.CommandError(msg) + elif (not token) and (not auth_url): + # if you don't provide a token you are going to provide at least an + # auth_url with which to authenticate. + raise exc.CommandError(_('Expecting an auth URL via either ' + '--os-auth-url or env[OS_AUTH_URL]')) + + plugin = super(DefaultCLI, cls).load_from_argparse_arguments(namespace, + **kwargs) + + if (not token) and (not plugin._password): + # we do this after the load so that the base plugin has an + # opportunity to prompt the user for a password + raise exc.CommandError(_('Expecting a password provided via ' + 'either --os-password, env[OS_PASSWORD], ' + 'or prompted response')) + + return plugin diff --git a/keystoneclient/tests/unit/auth/test_default_cli.py b/keystoneclient/tests/unit/auth/test_default_cli.py new file mode 100644 index 000000000..2a9bad1f9 --- /dev/null +++ b/keystoneclient/tests/unit/auth/test_default_cli.py @@ -0,0 +1,88 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import uuid + +import mock + +from keystoneclient.auth.identity.generic import cli +from keystoneclient import exceptions +from keystoneclient.tests.unit import utils + + +class DefaultCliTests(utils.TestCase): + + def new_plugin(self, argv): + parser = argparse.ArgumentParser() + cli.DefaultCLI.register_argparse_arguments(parser) + opts = parser.parse_args(argv) + return cli.DefaultCLI.load_from_argparse_arguments(opts) + + def test_endpoint_override(self): + password = uuid.uuid4().hex + url = uuid.uuid4().hex + + p = self.new_plugin(['--os-auth-url', 'url', + '--os-endpoint', url, + '--os-password', password]) + + self.assertEqual(url, p.get_endpoint(None)) + self.assertEqual(password, p._password) + + def test_token_only_override(self): + self.assertRaises(exceptions.CommandError, + self.new_plugin, + ['--os-token', uuid.uuid4().hex]) + + def test_token_endpoint_override(self): + token = uuid.uuid4().hex + endpoint = uuid.uuid4().hex + + p = self.new_plugin(['--os-endpoint', endpoint, + '--os-token', token]) + + self.assertEqual(endpoint, p.get_endpoint(None)) + self.assertEqual(token, p.get_token(None)) + + def test_no_auth_url(self): + exc = self.assertRaises(exceptions.CommandError, + self.new_plugin, + ['--os-username', uuid.uuid4().hex]) + + self.assertIn('auth-url', str(exc)) + + @mock.patch('sys.stdin', autospec=True) + @mock.patch('getpass.getpass') + def test_prompt_password(self, mock_getpass, mock_stdin): + password = uuid.uuid4().hex + + mock_stdin.isatty = lambda: True + mock_getpass.return_value = password + + p = self.new_plugin(['--os-auth-url', uuid.uuid4().hex, + '--os-username', uuid.uuid4().hex]) + + self.assertEqual(password, p._password) + + @mock.patch('sys.stdin', autospec=True) + @mock.patch('getpass.getpass') + def test_prompt_no_password(self, mock_getpass, mock_stdin): + mock_stdin.isatty = lambda: True + mock_getpass.return_value = '' + + exc = self.assertRaises(exceptions.CommandError, + self.new_plugin, + ['--os-auth-url', uuid.uuid4().hex, + '--os-username', uuid.uuid4().hex]) + + self.assertIn('password', str(exc))