1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5
# not use this file except in compliance with the License. You may obtain
6
# a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
# License for the specific language governing permissions and limitations
17
This auth module is intended to allow Openstack client-tools to select from a
18
variety of authentication strategies, including NoAuth (the default), and
19
Keystone (an identity management system).
21
> auth_plugin = AuthPlugin(creds)
23
> auth_plugin.authenticate()
25
> auth_plugin.auth_token
28
> auth_plugin.management_url
29
http://service_endpoint/
35
from heat.common import exception
37
from heat.openstack.common.gettextutils import _
40
class BaseStrategy(object):
42
self.auth_token = None
43
# TODO(sirp): Should expose selecting public/internal/admin URL.
44
self.management_url = None
46
def authenticate(self):
47
raise NotImplementedError
50
def is_authenticated(self):
51
raise NotImplementedError
55
raise NotImplementedError
58
class NoAuthStrategy(BaseStrategy):
59
def authenticate(self):
63
def is_authenticated(self):
71
class KeystoneStrategy(BaseStrategy):
74
def __init__(self, creds, service_type):
76
self.service_type = service_type
77
super(KeystoneStrategy, self).__init__()
79
def check_auth_params(self):
80
# Ensure that supplied credential parameters are as required
81
for required in ('username', 'password', 'auth_url',
83
if required not in self.creds:
84
raise exception.MissingCredentialError(required=required)
85
if self.creds['strategy'] != 'keystone':
86
raise exception.BadAuthStrategy(expected='keystone',
87
received=self.creds['strategy'])
88
# For v2.0 also check tenant is present
89
if self.creds['auth_url'].rstrip('/').endswith('v2.0'):
90
if 'tenant' not in self.creds:
91
raise exception.MissingCredentialError(required='tenant')
93
def authenticate(self):
94
"""Authenticate with the Keystone service.
96
There are a few scenarios to consider here:
98
1. Which version of Keystone are we using? v1 which uses headers to
99
pass the credentials, or v2 which uses a JSON encoded request body?
101
2. Keystone may respond back with a redirection using a 305 status
104
3. We may attempt a v1 auth when v2 is what's called for. In this
105
case, we rewrite the url to contain /v2.0/ and retry using the v2
108
def _authenticate(auth_url):
109
# If OS_AUTH_URL is missing a trailing slash add one
110
if not auth_url.endswith('/'):
112
token_url = urlparse.urljoin(auth_url, "tokens")
113
# 1. Check Keystone version
114
is_v2 = auth_url.rstrip('/').endswith('v2.0')
116
self._v2_auth(token_url)
118
self._v1_auth(token_url)
120
self.check_auth_params()
121
auth_url = self.creds['auth_url']
122
for x in range(self.MAX_REDIRECTS):
124
_authenticate(auth_url)
125
except exception.AuthorizationRedirect as e:
126
# 2. Keystone may redirect us
128
except exception.AuthorizationFailure:
129
# 3. In some configurations nova makes redirection to
130
# v2.0 keystone endpoint. Also, new location does not
131
# contain real endpoint, only hostname and port.
132
if 'v2.0' not in auth_url:
133
auth_url = urlparse.urljoin(auth_url, 'v2.0/')
135
# If we sucessfully auth'd, then memorize the correct auth_url
137
self.creds['auth_url'] = auth_url
140
# Guard against a redirection loop
141
raise exception.MaxRedirectsExceeded(redirects=self.MAX_REDIRECTS)
143
def _v1_auth(self, token_url):
147
headers['X-Auth-User'] = creds['username']
148
headers['X-Auth-Key'] = creds['password']
150
tenant = creds.get('tenant')
152
headers['X-Auth-Tenant'] = tenant
154
resp, resp_body = self._do_request(token_url, 'GET', headers=headers)
156
def _management_url(self, resp):
157
for url_header in ('x-heat-management-url',
158
'x-server-management-url',
161
return resp[url_header]
162
except KeyError as e:
166
if resp.status in (200, 204):
168
self.management_url = _management_url(self, resp)
169
self.auth_token = resp['x-auth-token']
171
raise exception.AuthorizationFailure()
172
elif resp.status == 305:
173
raise exception.AuthorizationRedirect(resp['location'])
174
elif resp.status == 400:
175
raise exception.AuthBadRequest(url=token_url)
176
elif resp.status == 401:
177
raise exception.NotAuthorized()
178
elif resp.status == 404:
179
raise exception.AuthUrlNotFound(url=token_url)
182
raise Exception(_('Unexpected response: %(status)s')
183
% {'status': resp.status})
185
def _v2_auth(self, token_url):
186
def get_endpoint(service_catalog):
188
Select an endpoint from the service catalog
190
We search the full service catalog for services
191
matching both type and region. If the client
192
supplied no region then any endpoint for the service
193
is considered a match. There must be one -- and
194
only one -- successful match in the catalog,
195
otherwise we will raise an exception.
197
region = self.creds.get('region')
199
service_type_matches = lambda s: s.get('type') == self.service_type
200
region_matches = lambda e: region is None or e['region'] == region
202
endpoints = [ep for s in service_catalog if service_type_matches(s)
203
for ep in s['endpoints'] if region_matches(ep)]
205
if len(endpoints) > 1:
206
raise exception.RegionAmbiguity(region=region)
208
raise exception.NoServiceEndpoint()
210
# FIXME(sirp): for now just use the public url.
211
return endpoints[0]['publicURL']
217
"tenantName": creds['tenant'],
218
"passwordCredentials": {
219
"username": creds['username'],
220
"password": creds['password']}}}
223
headers['Content-Type'] = 'application/json'
224
req_body = json.dumps(creds)
226
resp, resp_body = self._do_request(
227
token_url, 'POST', headers=headers, body=req_body)
229
if resp.status == 200:
230
resp_auth = json.loads(resp_body)['access']
231
self.management_url = get_endpoint(resp_auth['serviceCatalog'])
232
self.auth_token = resp_auth['token']['id']
233
elif resp.status == 305:
234
raise exception.RedirectException(resp['location'])
235
elif resp.status == 400:
236
raise exception.AuthBadRequest(url=token_url)
237
elif resp.status == 401:
238
raise exception.NotAuthorized()
239
elif resp.status == 404:
240
raise exception.AuthUrlNotFound(url=token_url)
243
body = json.loads(resp_body)
244
msg = body['error']['message']
245
except (ValueError, KeyError):
247
raise exception.KeystoneError(resp.status, msg)
250
def is_authenticated(self):
251
return self.auth_token is not None
258
def _do_request(url, method, headers=None, body=None):
259
headers = headers or {}
260
conn = httplib2.Http()
261
conn.force_exception_to_status_code = True
262
headers['User-Agent'] = 'heat-client'
263
resp, resp_body = conn.request(url, method, headers=headers, body=body)
264
return resp, resp_body
267
def get_plugin_from_strategy(strategy, creds=None, service_type=None):
268
if strategy == 'noauth':
269
return NoAuthStrategy()
270
elif strategy == 'keystone':
271
return KeystoneStrategy(creds, service_type)
273
raise Exception(_("Unknown auth strategy '%s'") % strategy)