From 72210d2ee607f60de801ae021291f385ccc8a815 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 5 Nov 2014 17:21:41 +0100 Subject: [PATCH] Create a discoverable plugin Move the version detection code into a plugin because it does not belong in connection. Create a new discoverable plugin that looks at the auth_url and makes a best guess if it should use v2 or v3 auth. The plugin also now accepts access_info so a cached version of the authorization can be reproduced without hitting keystone. Change-Id: Ia5d32457c90716627a75be2c99267c4ed7903197 --- examples/common.py | 7 ++ openstack/auth/identity/discoverable.py | 78 +++++++++++++++ openstack/auth/identity/v2.py | 8 ++ openstack/auth/identity/v3.py | 10 ++ openstack/connection.py | 11 +-- .../tests/auth/identity/test_discoverable.py | 97 +++++++++++++++++++ openstack/tests/auth/identity/test_v2.py | 18 ++++ openstack/tests/auth/identity/test_v3.py | 18 ++++ openstack/tests/test_connection.py | 30 +++--- setup.cfg | 1 + 10 files changed, 256 insertions(+), 22 deletions(-) create mode 100644 openstack/auth/identity/discoverable.py create mode 100644 openstack/tests/auth/identity/test_discoverable.py diff --git a/examples/common.py b/examples/common.py index 0c86030e..0b2174a0 100755 --- a/examples/common.py +++ b/examples/common.py @@ -230,6 +230,13 @@ def option_parser(): default=env('OS_PASSWORD'), help='Authentication password (Env: OS_PASSWORD)', ) + parser.add_argument( + '--os-access-info', + dest='access_info', + metavar='', + default=env('OS_ACCESS_INFO'), + help='Access info (Env: OS_ACCESS_INFO)', + ) parser.add_argument( '--os-api-name', dest='user_preferences', diff --git a/openstack/auth/identity/discoverable.py b/openstack/auth/identity/discoverable.py new file mode 100644 index 00000000..0c45d102 --- /dev/null +++ b/openstack/auth/identity/discoverable.py @@ -0,0 +1,78 @@ +# 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. + +""" +Identity discoverable authorization plugin must be constructed with an +auhorization URL and a user id, user name or token. A user id or user name +would also require a password. The arguments that apply to the selected v2 +or v3 plugin will be used. The rest of the arguments will be ignored. For +example:: + + from openstack.auth.identity import discoverable + from openstack import transport + + args = { + 'password': 'openSesame', + 'auth_url': 'https://10.1.1.1:5000/v3/', + 'user_name': 'alibaba', + } + auth = discoverable.Auth(**args) + xport = transport.Transport() + accessInfo = auth.authorize(xport) +""" + +from openstack.auth.identity import base +from openstack.auth.identity import v2 +from openstack.auth.identity import v3 +from openstack import exceptions + + +class Auth(base.BaseIdentityPlugin): + + #: Valid options for this plugin + valid_options = list(set(v2.Auth.valid_options + v3.Auth.valid_options)) + + def __init__(self, auth_url=None, **auth_args): + """Construct an Identity Authentication Plugin. + + This authorization plugin should be constructed with an auth_url + and everything needed by either a v2 or v3 identity plugin. + + :param string auth_url: Identity service endpoint for authentication. + + :raises TypeError: if a user_id, user_name or token is not provided. + """ + + super(Auth, self).__init__(auth_url=auth_url) + + if not auth_url: + msg = ("The authorization URL auth_url was not provided.") + raise exceptions.AuthorizationFailure(msg) + endpoint_version = auth_url.split('v')[-1][0] + if endpoint_version == '2': + plugin = v2.Auth + else: + plugin = v3.Auth + valid_list = plugin.valid_options + args = dict((n, auth_args[n]) for n in valid_list if n in auth_args) + self.auth_plugin = plugin(auth_url, **args) + + @property + def token_url(self): + """The full URL where we will send authentication data.""" + return self.auth_plugin.token_url + + def authorize(self, transport, **kwargs): + return self.auth_plugin.authorize(transport, **kwargs) + + def invalidate(self): + return self.auth_plugin.invalidate() diff --git a/openstack/auth/identity/v2.py b/openstack/auth/identity/v2.py index facdb93d..0440dd31 100644 --- a/openstack/auth/identity/v2.py +++ b/openstack/auth/identity/v2.py @@ -41,6 +41,7 @@ class Auth(base.BaseIdentityPlugin): #: Valid options for this plugin valid_options = [ + 'access_info', 'auth_url', 'user_name', 'user_id', @@ -53,6 +54,7 @@ class Auth(base.BaseIdentityPlugin): ] def __init__(self, auth_url, + access_info=None, user_name=None, user_id=None, password='', @@ -68,6 +70,7 @@ class Auth(base.BaseIdentityPlugin): :class:`~openstack.auth.identity.base.BaseIdentityPlugin`. :param string auth_url: Identity service endpoint for authorization. + :param string access_info: Access info from previous authentication. :param string user_name: Username for authentication. :param string user_id: User ID for authentication. :param string password: Password for authentication. @@ -86,6 +89,7 @@ class Auth(base.BaseIdentityPlugin): msg = 'You need to specify either a user_name, user_id or token' raise TypeError(msg) + self.access_info = access_info or None self.user_id = user_id self.user_name = user_name self.password = password @@ -96,6 +100,9 @@ class Auth(base.BaseIdentityPlugin): def authorize(self, transport, **kwargs): """Obtain access information from an OpenStack Identity Service.""" + if self.token and self.access_info: + return access.AccessInfoV2(**self.access_info) + headers = {'Accept': 'application/json'} url = self.auth_url.rstrip('/') + '/tokens' params = {'auth': self.get_auth_data(headers)} @@ -132,6 +139,7 @@ class Auth(base.BaseIdentityPlugin): def invalidate(self): """Invalidate the current authentication data.""" if super(Auth, self).invalidate(): + self.access_info = None self.token = None return True return False diff --git a/openstack/auth/identity/v3.py b/openstack/auth/identity/v3.py index 2bfea5e5..192615fb 100644 --- a/openstack/auth/identity/v3.py +++ b/openstack/auth/identity/v3.py @@ -44,6 +44,7 @@ class Auth(base.BaseIdentityPlugin): #: Valid options for this plugin valid_options = [ + 'access_info', 'auth_url', 'domain_id', 'domain_name', @@ -62,6 +63,7 @@ class Auth(base.BaseIdentityPlugin): ] def __init__(self, auth_url, + access_info=None, domain_id=None, domain_name=None, password='', @@ -84,6 +86,7 @@ class Auth(base.BaseIdentityPlugin): base class :class:`~openstack.auth.identity.base.BaseIdentityPlugin`. :param string auth_url: Identity service endpoint for authentication. + :param string access_info: Access info including service catalog. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string password: User password for authentication. @@ -109,6 +112,7 @@ class Auth(base.BaseIdentityPlugin): msg = 'You need to specify either a user_name, user_id or token' raise TypeError(msg) + self.access_info = access_info self.domain_id = domain_id self.domain_name = domain_name self.project_domain_id = project_domain_id @@ -128,6 +132,7 @@ class Auth(base.BaseIdentityPlugin): self.token_method = TokenMethod(token=token) self.auth_methods = [self.token_method] else: + self.token_method = None self.auth_methods = [self.password_method] @property @@ -141,6 +146,10 @@ class Auth(base.BaseIdentityPlugin): body = {'auth': {'identity': {}}} ident = body['auth']['identity'] + if self.token_method and self.access_info: + return access.AccessInfoV3(self.token_method.token, + **self.access_info) + for method in self.auth_methods: name, auth_data = method.get_auth_data(transport, self, headers) ident.setdefault('methods', []).append(name) @@ -192,6 +201,7 @@ class Auth(base.BaseIdentityPlugin): """Invalidate the current authentication data.""" if super(Auth, self).invalidate(): self.auth_methods = [self.password_method] + self.access_info = None return True return False diff --git a/openstack/connection.py b/openstack/connection.py index b778aa07..4abb7703 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -63,7 +63,6 @@ import sys from stevedore import driver -from openstack import exceptions from openstack import session from openstack import transport as xport @@ -142,15 +141,7 @@ class Connection(object): if authenticator: return authenticator if auth_plugin is None: - if 'auth_url' not in auth_args: - msg = ("auth_url was not provided.") - raise exceptions.AuthorizationFailure(msg) - auth_url = auth_args['auth_url'] - endpoint_version = auth_url.split('v')[-1][0] - if endpoint_version == '2': - auth_plugin = 'identity_v2' - else: - auth_plugin = 'identity_v3' + auth_plugin = 'identity' mgr = driver.DriverManager( namespace=self.AUTH_PLUGIN_NAMESPACE, diff --git a/openstack/tests/auth/identity/test_discoverable.py b/openstack/tests/auth/identity/test_discoverable.py new file mode 100644 index 00000000..c4f6fe68 --- /dev/null +++ b/openstack/tests/auth/identity/test_discoverable.py @@ -0,0 +1,97 @@ +# 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 mock +import testtools + +from openstack.auth.identity import discoverable +from openstack import exceptions +from openstack.tests.auth import common + + +class TestDiscoverableAuth(testtools.TestCase): + def test_valid_options(self): + expected = [ + 'access_info', + 'auth_url', + 'domain_id', + 'domain_name', + 'password', + 'project_domain_id', + 'project_domain_name', + 'project_id', + 'project_name', + 'reauthenticate', + 'token', + 'trust_id', + 'user_domain_id', + 'user_domain_name', + 'user_id', + 'user_name', + ] + self.assertEqual(expected, sorted(discoverable.Auth.valid_options)) + + def test_create2(self): + auth_args = { + 'auth_url': 'http://localhost/v2', + 'user_name': '1', + 'password': '2', + } + auth = discoverable.Auth(**auth_args) + self.assertEqual('openstack.auth.identity.v2', + auth.auth_plugin.__class__.__module__) + + def test_create3(self): + auth_args = { + 'auth_url': 'http://localhost/v3', + 'user_name': '1', + 'password': '2', + } + auth = discoverable.Auth(**auth_args) + self.assertEqual('openstack.auth.identity.v3', + auth.auth_plugin.__class__.__module__) + + def test_create_who_knows(self): + auth_args = { + 'auth_url': 'http://localhost:5000/', + 'user_name': '1', + 'password': '2', + } + auth = discoverable.Auth(**auth_args) + self.assertEqual('openstack.auth.identity.v3', + auth.auth_plugin.__class__.__module__) + + def test_create_authenticator_no_nothing(self): + self.assertRaises( + exceptions.AuthorizationFailure, + discoverable.Auth, + ) + + def test_methods(self): + auth_args = { + 'auth_url': 'http://localhost:5000/', + 'user_name': '1', + 'password': '2', + } + auth = discoverable.Auth(**auth_args) + self.assertEqual('http://localhost:5000/auth/tokens', auth.token_url) + xport = mock.MagicMock() + xport.post = mock.Mock() + response = mock.Mock() + response.json = mock.Mock() + response.json.return_value = common.TEST_RESPONSE_DICT_V3 + response.headers = {'X-Subject-Token': common.TEST_SUBJECT} + xport.post.return_value = response + + result = auth.authorize(xport) + self.assertEqual(common.TEST_SUBJECT, result.auth_token) + self.assertEqual(True, auth.invalidate()) diff --git a/openstack/tests/auth/identity/test_v2.py b/openstack/tests/auth/identity/test_v2.py index 9821c48b..2ecb659a 100644 --- a/openstack/tests/auth/identity/test_v2.py +++ b/openstack/tests/auth/identity/test_v2.py @@ -170,6 +170,20 @@ class TestV2Auth(testtools.TestCase): ecatalog['version'] = 'v2.0' self.assertEqual(ecatalog, resp._info) + def test_authorize_token_access_info(self): + ecatalog = TEST_RESPONSE_DICT['access'].copy() + ecatalog['version'] = 'v2.0' + kargs = { + 'access_info': ecatalog, + 'token': common.TEST_TOKEN, + } + sot = v2.Auth(TEST_URL, **kargs) + xport = self.create_mock_transport(TEST_RESPONSE_DICT) + + resp = sot.authorize(xport) + + self.assertEqual(ecatalog, resp._info) + def test_authorize_bad_response(self): kargs = {'token': common.TEST_TOKEN} sot = v2.Auth(TEST_URL, **kargs) @@ -179,6 +193,7 @@ class TestV2Auth(testtools.TestCase): def test_invalidate(self): kargs = { + 'access_info': {'a': 'b'}, 'password': common.TEST_PASS, 'token': common.TEST_TOKEN, 'user_name': common.TEST_USER, @@ -194,11 +209,14 @@ class TestV2Auth(testtools.TestCase): expected = {'passwordCredentials': {'password': common.TEST_PASS, 'username': common.TEST_USER}} headers = {} + self.assertEqual(None, sot.token) + self.assertEqual(None, sot.access_info) self.assertEqual(expected, sot.get_auth_data(headers)) self.assertEqual({}, headers) def test_valid_options(self): expected = [ + 'access_info', 'auth_url', 'user_name', 'user_id', diff --git a/openstack/tests/auth/identity/test_v3.py b/openstack/tests/auth/identity/test_v3.py index 8477b014..0d2548ea 100644 --- a/openstack/tests/auth/identity/test_v3.py +++ b/openstack/tests/auth/identity/test_v3.py @@ -138,6 +138,21 @@ class TestV3Auth(testtools.TestCase): ecatalog['version'] = 'v3' self.assertEqual(ecatalog, resp._info) + def test_authorize_token_access_info(self): + ecatalog = common.TEST_RESPONSE_DICT_V3['token'].copy() + kargs = { + 'access_info': ecatalog, + 'token': common.TEST_TOKEN, + } + sot = v3.Auth(TEST_URL, **kargs) + xport = self.create_mock_transport(common.TEST_RESPONSE_DICT_V3) + + resp = sot.authorize(xport) + + ecatalog['auth_token'] = common.TEST_TOKEN + ecatalog['version'] = 'v3' + self.assertEqual(ecatalog, resp._info) + def test_authorize_token_domain_id(self): kargs = { 'domain_id': common.TEST_DOMAIN_ID, @@ -317,6 +332,7 @@ class TestV3Auth(testtools.TestCase): 'user_name': common.TEST_USER, 'password': common.TEST_PASS, 'token': common.TEST_TOKEN, + 'access_info': {}, } sot = v3.Auth(TEST_URL, **kargs) self.assertEqual(1, len(sot.auth_methods)) @@ -325,6 +341,7 @@ class TestV3Auth(testtools.TestCase): self.assertEqual(True, sot.invalidate()) + self.assertEqual(None, sot.access_info) self.assertEqual(1, len(sot.auth_methods)) auther = sot.auth_methods[0] self.assertEqual(common.TEST_USER, auther.user_name) @@ -332,6 +349,7 @@ class TestV3Auth(testtools.TestCase): def test_valid_options(self): expected = [ + 'access_info', 'auth_url', 'domain_id', 'domain_name', diff --git a/openstack/tests/test_connection.py b/openstack/tests/test_connection.py index e6d36527..c6b36ca7 100644 --- a/openstack/tests/test_connection.py +++ b/openstack/tests/test_connection.py @@ -62,24 +62,30 @@ class TestConnection(base.TestCase): self.assertEqual('1', conn.authenticator.password_method.user_name) self.assertEqual('2', conn.authenticator.password_method.password) - def test_create_authenticator_no_name_2(self): + def test_create_authenticator_discoverable(self): + auth_args = { + 'auth_url': '0', + 'user_name': '1', + 'password': '2', + } + conn = connection.Connection(transport='0', auth_plugin='identity', + **auth_args) + self.assertEqual('0', conn.authenticator.auth_url) + self.assertEqual( + '1', + conn.authenticator.auth_plugin.password_method.user_name) + self.assertEqual( + '2', + conn.authenticator.auth_plugin.password_method.password) + + def test_create_authenticator_no_name(self): auth_args = { 'auth_url': 'http://localhost/v2', 'user_name': '1', 'password': '2', } conn = connection.Connection(transport='0', **auth_args) - self.assertEqual('openstack.auth.identity.v2', - conn.authenticator.__class__.__module__) - - def test_create_authenticator_no_name_3(self): - auth_args = { - 'auth_url': 'http://localhost/v3', - 'user_name': '1', - 'password': '2', - } - conn = connection.Connection(transport='0', **auth_args) - self.assertEqual('openstack.auth.identity.v3', + self.assertEqual('openstack.auth.identity.discoverable', conn.authenticator.__class__.__module__) def test_create_authenticator_no_nothing(self): diff --git a/setup.cfg b/setup.cfg index b15744c5..f582e81c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,3 +52,4 @@ universal = 1 openstack.auth.plugin = identity_v2 = openstack.auth.identity.v2:Auth identity_v3 = openstack.auth.identity.v3:Auth + identity = openstack.auth.identity.discoverable:Auth