Skip to content

Commit 72210d2

Browse files
committed
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
1 parent 5edaf87 commit 72210d2

File tree

10 files changed

+253
-19
lines changed

10 files changed

+253
-19
lines changed

examples/common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ def option_parser():
230230
default=env('OS_PASSWORD'),
231231
help='Authentication password (Env: OS_PASSWORD)',
232232
)
233+
parser.add_argument(
234+
'--os-access-info',
235+
dest='access_info',
236+
metavar='<access-info>',
237+
default=env('OS_ACCESS_INFO'),
238+
help='Access info (Env: OS_ACCESS_INFO)',
239+
)
233240
parser.add_argument(
234241
'--os-api-name',
235242
dest='user_preferences',
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
"""
14+
Identity discoverable authorization plugin must be constructed with an
15+
auhorization URL and a user id, user name or token. A user id or user name
16+
would also require a password. The arguments that apply to the selected v2
17+
or v3 plugin will be used. The rest of the arguments will be ignored. For
18+
example::
19+
20+
from openstack.auth.identity import discoverable
21+
from openstack import transport
22+
23+
args = {
24+
'password': 'openSesame',
25+
'auth_url': 'https://10.1.1.1:5000/v3/',
26+
'user_name': 'alibaba',
27+
}
28+
auth = discoverable.Auth(**args)
29+
xport = transport.Transport()
30+
accessInfo = auth.authorize(xport)
31+
"""
32+
33+
from openstack.auth.identity import base
34+
from openstack.auth.identity import v2
35+
from openstack.auth.identity import v3
36+
from openstack import exceptions
37+
38+
39+
class Auth(base.BaseIdentityPlugin):
40+
41+
#: Valid options for this plugin
42+
valid_options = list(set(v2.Auth.valid_options + v3.Auth.valid_options))
43+
44+
def __init__(self, auth_url=None, **auth_args):
45+
"""Construct an Identity Authentication Plugin.
46+
47+
This authorization plugin should be constructed with an auth_url
48+
and everything needed by either a v2 or v3 identity plugin.
49+
50+
:param string auth_url: Identity service endpoint for authentication.
51+
52+
:raises TypeError: if a user_id, user_name or token is not provided.
53+
"""
54+
55+
super(Auth, self).__init__(auth_url=auth_url)
56+
57+
if not auth_url:
58+
msg = ("The authorization URL auth_url was not provided.")
59+
raise exceptions.AuthorizationFailure(msg)
60+
endpoint_version = auth_url.split('v')[-1][0]
61+
if endpoint_version == '2':
62+
plugin = v2.Auth
63+
else:
64+
plugin = v3.Auth
65+
valid_list = plugin.valid_options
66+
args = dict((n, auth_args[n]) for n in valid_list if n in auth_args)
67+
self.auth_plugin = plugin(auth_url, **args)
68+
69+
@property
70+
def token_url(self):
71+
"""The full URL where we will send authentication data."""
72+
return self.auth_plugin.token_url
73+
74+
def authorize(self, transport, **kwargs):
75+
return self.auth_plugin.authorize(transport, **kwargs)
76+
77+
def invalidate(self):
78+
return self.auth_plugin.invalidate()

openstack/auth/identity/v2.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Auth(base.BaseIdentityPlugin):
4141

4242
#: Valid options for this plugin
4343
valid_options = [
44+
'access_info',
4445
'auth_url',
4546
'user_name',
4647
'user_id',
@@ -53,6 +54,7 @@ class Auth(base.BaseIdentityPlugin):
5354
]
5455

5556
def __init__(self, auth_url,
57+
access_info=None,
5658
user_name=None,
5759
user_id=None,
5860
password='',
@@ -68,6 +70,7 @@ def __init__(self, auth_url,
6870
:class:`~openstack.auth.identity.base.BaseIdentityPlugin`.
6971
7072
:param string auth_url: Identity service endpoint for authorization.
73+
:param string access_info: Access info from previous authentication.
7174
:param string user_name: Username for authentication.
7275
:param string user_id: User ID for authentication.
7376
:param string password: Password for authentication.
@@ -86,6 +89,7 @@ def __init__(self, auth_url,
8689
msg = 'You need to specify either a user_name, user_id or token'
8790
raise TypeError(msg)
8891

92+
self.access_info = access_info or None
8993
self.user_id = user_id
9094
self.user_name = user_name
9195
self.password = password
@@ -96,6 +100,9 @@ def __init__(self, auth_url,
96100

97101
def authorize(self, transport, **kwargs):
98102
"""Obtain access information from an OpenStack Identity Service."""
103+
if self.token and self.access_info:
104+
return access.AccessInfoV2(**self.access_info)
105+
99106
headers = {'Accept': 'application/json'}
100107
url = self.auth_url.rstrip('/') + '/tokens'
101108
params = {'auth': self.get_auth_data(headers)}
@@ -132,6 +139,7 @@ def get_auth_data(self, headers):
132139
def invalidate(self):
133140
"""Invalidate the current authentication data."""
134141
if super(Auth, self).invalidate():
142+
self.access_info = None
135143
self.token = None
136144
return True
137145
return False

openstack/auth/identity/v3.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Auth(base.BaseIdentityPlugin):
4444

4545
#: Valid options for this plugin
4646
valid_options = [
47+
'access_info',
4748
'auth_url',
4849
'domain_id',
4950
'domain_name',
@@ -62,6 +63,7 @@ class Auth(base.BaseIdentityPlugin):
6263
]
6364

6465
def __init__(self, auth_url,
66+
access_info=None,
6567
domain_id=None,
6668
domain_name=None,
6769
password='',
@@ -84,6 +86,7 @@ def __init__(self, auth_url,
8486
base class :class:`~openstack.auth.identity.base.BaseIdentityPlugin`.
8587
8688
:param string auth_url: Identity service endpoint for authentication.
89+
:param string access_info: Access info including service catalog.
8790
:param string domain_id: Domain ID for domain scoping.
8891
:param string domain_name: Domain name for domain scoping.
8992
:param string password: User password for authentication.
@@ -109,6 +112,7 @@ def __init__(self, auth_url,
109112
msg = 'You need to specify either a user_name, user_id or token'
110113
raise TypeError(msg)
111114

115+
self.access_info = access_info
112116
self.domain_id = domain_id
113117
self.domain_name = domain_name
114118
self.project_domain_id = project_domain_id
@@ -128,6 +132,7 @@ def __init__(self, auth_url,
128132
self.token_method = TokenMethod(token=token)
129133
self.auth_methods = [self.token_method]
130134
else:
135+
self.token_method = None
131136
self.auth_methods = [self.password_method]
132137

133138
@property
@@ -141,6 +146,10 @@ def authorize(self, transport, **kwargs):
141146
body = {'auth': {'identity': {}}}
142147
ident = body['auth']['identity']
143148

149+
if self.token_method and self.access_info:
150+
return access.AccessInfoV3(self.token_method.token,
151+
**self.access_info)
152+
144153
for method in self.auth_methods:
145154
name, auth_data = method.get_auth_data(transport, self, headers)
146155
ident.setdefault('methods', []).append(name)
@@ -192,6 +201,7 @@ def invalidate(self):
192201
"""Invalidate the current authentication data."""
193202
if super(Auth, self).invalidate():
194203
self.auth_methods = [self.password_method]
204+
self.access_info = None
195205
return True
196206
return False
197207

openstack/connection.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363

6464
from stevedore import driver
6565

66-
from openstack import exceptions
6766
from openstack import session
6867
from openstack import transport as xport
6968

@@ -142,15 +141,7 @@ def _create_authenticator(self, authenticator, auth_plugin, **auth_args):
142141
if authenticator:
143142
return authenticator
144143
if auth_plugin is None:
145-
if 'auth_url' not in auth_args:
146-
msg = ("auth_url was not provided.")
147-
raise exceptions.AuthorizationFailure(msg)
148-
auth_url = auth_args['auth_url']
149-
endpoint_version = auth_url.split('v')[-1][0]
150-
if endpoint_version == '2':
151-
auth_plugin = 'identity_v2'
152-
else:
153-
auth_plugin = 'identity_v3'
144+
auth_plugin = 'identity'
154145

155146
mgr = driver.DriverManager(
156147
namespace=self.AUTH_PLUGIN_NAMESPACE,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import mock
14+
import testtools
15+
16+
from openstack.auth.identity import discoverable
17+
from openstack import exceptions
18+
from openstack.tests.auth import common
19+
20+
21+
class TestDiscoverableAuth(testtools.TestCase):
22+
def test_valid_options(self):
23+
expected = [
24+
'access_info',
25+
'auth_url',
26+
'domain_id',
27+
'domain_name',
28+
'password',
29+
'project_domain_id',
30+
'project_domain_name',
31+
'project_id',
32+
'project_name',
33+
'reauthenticate',
34+
'token',
35+
'trust_id',
36+
'user_domain_id',
37+
'user_domain_name',
38+
'user_id',
39+
'user_name',
40+
]
41+
self.assertEqual(expected, sorted(discoverable.Auth.valid_options))
42+
43+
def test_create2(self):
44+
auth_args = {
45+
'auth_url': 'http://localhost/v2',
46+
'user_name': '1',
47+
'password': '2',
48+
}
49+
auth = discoverable.Auth(**auth_args)
50+
self.assertEqual('openstack.auth.identity.v2',
51+
auth.auth_plugin.__class__.__module__)
52+
53+
def test_create3(self):
54+
auth_args = {
55+
'auth_url': 'http://localhost/v3',
56+
'user_name': '1',
57+
'password': '2',
58+
}
59+
auth = discoverable.Auth(**auth_args)
60+
self.assertEqual('openstack.auth.identity.v3',
61+
auth.auth_plugin.__class__.__module__)
62+
63+
def test_create_who_knows(self):
64+
auth_args = {
65+
'auth_url': 'http://localhost:5000/',
66+
'user_name': '1',
67+
'password': '2',
68+
}
69+
auth = discoverable.Auth(**auth_args)
70+
self.assertEqual('openstack.auth.identity.v3',
71+
auth.auth_plugin.__class__.__module__)
72+
73+
def test_create_authenticator_no_nothing(self):
74+
self.assertRaises(
75+
exceptions.AuthorizationFailure,
76+
discoverable.Auth,
77+
)
78+
79+
def test_methods(self):
80+
auth_args = {
81+
'auth_url': 'http://localhost:5000/',
82+
'user_name': '1',
83+
'password': '2',
84+
}
85+
auth = discoverable.Auth(**auth_args)
86+
self.assertEqual('http://localhost:5000/auth/tokens', auth.token_url)
87+
xport = mock.MagicMock()
88+
xport.post = mock.Mock()
89+
response = mock.Mock()
90+
response.json = mock.Mock()
91+
response.json.return_value = common.TEST_RESPONSE_DICT_V3
92+
response.headers = {'X-Subject-Token': common.TEST_SUBJECT}
93+
xport.post.return_value = response
94+
95+
result = auth.authorize(xport)
96+
self.assertEqual(common.TEST_SUBJECT, result.auth_token)
97+
self.assertEqual(True, auth.invalidate())

openstack/tests/auth/identity/test_v2.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ def test_authorize_token_only(self):
170170
ecatalog['version'] = 'v2.0'
171171
self.assertEqual(ecatalog, resp._info)
172172

173+
def test_authorize_token_access_info(self):
174+
ecatalog = TEST_RESPONSE_DICT['access'].copy()
175+
ecatalog['version'] = 'v2.0'
176+
kargs = {
177+
'access_info': ecatalog,
178+
'token': common.TEST_TOKEN,
179+
}
180+
sot = v2.Auth(TEST_URL, **kargs)
181+
xport = self.create_mock_transport(TEST_RESPONSE_DICT)
182+
183+
resp = sot.authorize(xport)
184+
185+
self.assertEqual(ecatalog, resp._info)
186+
173187
def test_authorize_bad_response(self):
174188
kargs = {'token': common.TEST_TOKEN}
175189
sot = v2.Auth(TEST_URL, **kargs)
@@ -179,6 +193,7 @@ def test_authorize_bad_response(self):
179193

180194
def test_invalidate(self):
181195
kargs = {
196+
'access_info': {'a': 'b'},
182197
'password': common.TEST_PASS,
183198
'token': common.TEST_TOKEN,
184199
'user_name': common.TEST_USER,
@@ -194,11 +209,14 @@ def test_invalidate(self):
194209
expected = {'passwordCredentials': {'password': common.TEST_PASS,
195210
'username': common.TEST_USER}}
196211
headers = {}
212+
self.assertEqual(None, sot.token)
213+
self.assertEqual(None, sot.access_info)
197214
self.assertEqual(expected, sot.get_auth_data(headers))
198215
self.assertEqual({}, headers)
199216

200217
def test_valid_options(self):
201218
expected = [
219+
'access_info',
202220
'auth_url',
203221
'user_name',
204222
'user_id',

0 commit comments

Comments
 (0)