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
This commit is contained in:
Terry Howe 2014-11-05 17:21:41 +01:00
parent 5edaf879ee
commit 72210d2ee6
10 changed files with 256 additions and 22 deletions

View File

@ -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='<access-info>',
default=env('OS_ACCESS_INFO'),
help='Access info (Env: OS_ACCESS_INFO)',
)
parser.add_argument(
'--os-api-name',
dest='user_preferences',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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