blob: 0e451617c622273313618f1d3211db7bad6a5524 [file] [log] [blame]
Andrea Frittoli8bbdb162014-01-06 11:06:13 +00001# Copyright 2014 Hewlett-Packard Development Company, L.P.
2# All Rights Reserved.
3#
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
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
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
14# under the License.
15
16import copy
Masayuki Igawa1edf94f2014-03-04 18:34:16 +090017import datetime
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000018import exceptions
19import re
20import urlparse
21
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000022from tempest import config
23from tempest.services.identity.json import identity_client as json_id
24from tempest.services.identity.v3.json import identity_client as json_v3id
25from tempest.services.identity.v3.xml import identity_client as xml_v3id
26from tempest.services.identity.xml import identity_client as xml_id
27
28from tempest.openstack.common import log as logging
29
30CONF = config.CONF
31LOG = logging.getLogger(__name__)
32
33
34class AuthProvider(object):
35 """
36 Provide authentication
37 """
38
39 def __init__(self, credentials, client_type='tempest',
40 interface=None):
41 """
42 :param credentials: credentials for authentication
43 :param client_type: 'tempest' or 'official'
44 :param interface: 'json' or 'xml'. Applicable for tempest client only
45 """
46 if self.check_credentials(credentials):
47 self.credentials = credentials
48 else:
49 raise TypeError("Invalid credentials")
50 self.credentials = credentials
51 self.client_type = client_type
52 self.interface = interface
53 if self.client_type == 'tempest' and self.interface is None:
54 self.interface = 'json'
55 self.cache = None
56 self.alt_auth_data = None
57 self.alt_part = None
58
59 def __str__(self):
60 return "Creds :{creds}, client type: {client_type}, interface: " \
61 "{interface}, cached auth data: {cache}".format(
62 creds=self.credentials, client_type=self.client_type,
63 interface=self.interface, cache=self.cache
64 )
65
66 def _decorate_request(self, filters, method, url, headers=None, body=None,
67 auth_data=None):
68 """
69 Decorate request with authentication data
70 """
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +000071 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000072
73 def _get_auth(self):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +000074 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000075
76 @classmethod
77 def check_credentials(cls, credentials):
78 """
79 Verify credentials are valid. Subclasses can do a better check.
80 """
81 return isinstance(credentials, dict)
82
83 @property
84 def auth_data(self):
85 if self.cache is None or self.is_expired(self.cache):
86 self.cache = self._get_auth()
87 return self.cache
88
89 @auth_data.deleter
90 def auth_data(self):
91 self.clear_auth()
92
93 def clear_auth(self):
94 """
95 Can be called to clear the access cache so that next request
96 will fetch a new token and base_url.
97 """
98 self.cache = None
99
100 def is_expired(self, auth_data):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000101 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000102
103 def auth_request(self, method, url, headers=None, body=None, filters=None):
104 """
105 Obtains auth data and decorates a request with that.
106 :param method: HTTP method of the request
107 :param url: relative URL of the request (path)
108 :param headers: HTTP headers of the request
109 :param body: HTTP body in case of POST / PUT
110 :param filters: select a base URL out of the catalog
111 :returns a Tuple (url, headers, body)
112 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000113 orig_req = dict(url=url, headers=headers, body=body)
114
115 auth_url, auth_headers, auth_body = self._decorate_request(
116 filters, method, url, headers, body)
117 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
118
119 # Overwrite part if the request if it has been requested
120 if self.alt_part is not None:
121 if self.alt_auth_data is not None:
122 alt_url, alt_headers, alt_body = self._decorate_request(
123 filters, method, url, headers, body,
124 auth_data=self.alt_auth_data)
125 alt_auth_req = dict(url=alt_url, headers=alt_headers,
126 body=alt_body)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000127 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
128
129 else:
130 # If alt auth data is None, skip auth in the requested part
131 auth_req[self.alt_part] = orig_req[self.alt_part]
132
133 # Next auth request will be normal, unless otherwise requested
134 self.reset_alt_auth_data()
135
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000136 return auth_req['url'], auth_req['headers'], auth_req['body']
137
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000138 def reset_alt_auth_data(self):
139 """
140 Configure auth provider to provide valid authentication data
141 """
142 self.alt_part = None
143 self.alt_auth_data = None
144
145 def set_alt_auth_data(self, request_part, auth_data):
146 """
147 Configure auth provider to provide alt authentication data
148 on a part of the *next* auth_request. If credentials are None,
149 set invalid data.
150 :param request_part: request part to contain invalid auth: url,
151 headers, body
152 :param auth_data: alternative auth_data from which to get the
153 invalid data to be injected
154 """
155 self.alt_part = request_part
156 self.alt_auth_data = auth_data
157
158 def base_url(self, filters, auth_data=None):
159 """
160 Extracts the base_url based on provided filters
161 """
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000162 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000163
164
165class KeystoneAuthProvider(AuthProvider):
166
167 def __init__(self, credentials, client_type='tempest', interface=None):
168 super(KeystoneAuthProvider, self).__init__(credentials, client_type,
169 interface)
170 self.auth_client = self._auth_client()
171
172 def _decorate_request(self, filters, method, url, headers=None, body=None,
173 auth_data=None):
174 if auth_data is None:
175 auth_data = self.auth_data
176 token, _ = auth_data
177 base_url = self.base_url(filters=filters, auth_data=auth_data)
178 # build authenticated request
179 # returns new request, it does not touch the original values
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500180 _headers = copy.deepcopy(headers) if headers is not None else {}
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000181 _headers['X-Auth-Token'] = token
182 if url is None or url == "":
183 _url = base_url
184 else:
185 # Join base URL and url, and remove multiple contiguous slashes
186 _url = "/".join([base_url, url])
187 parts = [x for x in urlparse.urlparse(_url)]
188 parts[2] = re.sub("/{2,}", "/", parts[2])
189 _url = urlparse.urlunparse(parts)
190 # no change to method or body
191 return _url, _headers, body
192
193 def _auth_client(self):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000194 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000195
196 def _auth_params(self):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000197 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000198
199 def _get_auth(self):
200 # Bypasses the cache
201 if self.client_type == 'tempest':
202 auth_func = getattr(self.auth_client, 'get_token')
203 auth_params = self._auth_params()
204
205 # returns token, auth_data
206 token, auth_data = auth_func(**auth_params)
207 return token, auth_data
208 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000209 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000210
211 def get_token(self):
212 return self.auth_data[0]
213
214
215class KeystoneV2AuthProvider(KeystoneAuthProvider):
216
217 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
218
219 @classmethod
220 def check_credentials(cls, credentials, scoped=True):
221 # tenant_name is optional if not scoped
222 valid = super(KeystoneV2AuthProvider, cls).check_credentials(
223 credentials) and 'username' in credentials and \
224 'password' in credentials
225 if scoped:
226 valid = valid and 'tenant_name' in credentials
227 return valid
228
229 def _auth_client(self):
230 if self.client_type == 'tempest':
231 if self.interface == 'json':
232 return json_id.TokenClientJSON()
233 else:
234 return xml_id.TokenClientXML()
235 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000236 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000237
238 def _auth_params(self):
239 if self.client_type == 'tempest':
240 return dict(
241 user=self.credentials['username'],
242 password=self.credentials['password'],
243 tenant=self.credentials.get('tenant_name', None),
244 auth_data=True)
245 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000246 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000247
248 def base_url(self, filters, auth_data=None):
249 """
250 Filters can be:
251 - service: compute, image, etc
252 - region: the service region
253 - endpoint_type: adminURL, publicURL, internalURL
254 - api_version: replace catalog version with this
255 - skip_path: take just the base URL
256 """
257 if auth_data is None:
258 auth_data = self.auth_data
259 token, _auth_data = auth_data
260 service = filters.get('service')
261 region = filters.get('region')
262 endpoint_type = filters.get('endpoint_type', 'publicURL')
263
264 if service is None:
265 raise exceptions.EndpointNotFound("No service provided")
266
267 _base_url = None
268 for ep in _auth_data['serviceCatalog']:
269 if ep["type"] == service:
270 for _ep in ep['endpoints']:
271 if region is not None and _ep['region'] == region:
272 _base_url = _ep.get(endpoint_type)
273 if not _base_url:
274 # No region matching, use the first
275 _base_url = ep['endpoints'][0].get(endpoint_type)
276 break
277 if _base_url is None:
278 raise exceptions.EndpointNotFound(service)
279
280 parts = urlparse.urlparse(_base_url)
281 if filters.get('api_version', None) is not None:
282 path = "/" + filters['api_version']
283 noversion_path = "/".join(parts.path.split("/")[2:])
284 if noversion_path != "":
Zhi Kun Liube8bdbc2014-02-08 11:40:57 +0800285 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000286 _base_url = _base_url.replace(parts.path, path)
287 if filters.get('skip_path', None) is not None:
288 _base_url = _base_url.replace(parts.path, "/")
289
290 return _base_url
291
292 def is_expired(self, auth_data):
293 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900294 expiry = datetime.datetime.strptime(access['token']['expires'],
295 self.EXPIRY_DATE_FORMAT)
296 return expiry <= datetime.datetime.now()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000297
298
299class KeystoneV3AuthProvider(KeystoneAuthProvider):
300
301 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
302
303 @classmethod
304 def check_credentials(cls, credentials, scoped=True):
305 # tenant_name is optional if not scoped
306 valid = super(KeystoneV3AuthProvider, cls).check_credentials(
307 credentials) and 'username' in credentials and \
308 'password' in credentials and 'domain_name' in credentials
309 if scoped:
310 valid = valid and 'tenant_name' in credentials
311 return valid
312
313 def _auth_client(self):
314 if self.client_type == 'tempest':
315 if self.interface == 'json':
316 return json_v3id.V3TokenClientJSON()
317 else:
318 return xml_v3id.V3TokenClientXML()
319 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000320 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000321
322 def _auth_params(self):
323 if self.client_type == 'tempest':
324 return dict(
325 user=self.credentials['username'],
326 password=self.credentials['password'],
327 tenant=self.credentials.get('tenant_name', None),
328 domain=self.credentials['domain_name'],
329 auth_data=True)
330 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000331 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000332
333 def base_url(self, filters, auth_data=None):
334 """
335 Filters can be:
336 - service: compute, image, etc
337 - region: the service region
338 - endpoint_type: adminURL, publicURL, internalURL
339 - api_version: replace catalog version with this
340 - skip_path: take just the base URL
341 """
342 if auth_data is None:
343 auth_data = self.auth_data
344 token, _auth_data = auth_data
345 service = filters.get('service')
346 region = filters.get('region')
347 endpoint_type = filters.get('endpoint_type', 'public')
348
349 if service is None:
350 raise exceptions.EndpointNotFound("No service provided")
351
352 if 'URL' in endpoint_type:
353 endpoint_type = endpoint_type.replace('URL', '')
354 _base_url = None
355 catalog = _auth_data['catalog']
356 # Select entries with matching service type
357 service_catalog = [ep for ep in catalog if ep['type'] == service]
358 if len(service_catalog) > 0:
359 service_catalog = service_catalog[0]['endpoints']
360 else:
361 # No matching service
362 raise exceptions.EndpointNotFound(service)
363 # Filter by endpoint type (interface)
364 filtered_catalog = [ep for ep in service_catalog if
365 ep['interface'] == endpoint_type]
366 if len(filtered_catalog) == 0:
367 # No matching type, keep all and try matching by region at least
368 filtered_catalog = service_catalog
369 # Filter by region
370 filtered_catalog = [ep for ep in filtered_catalog if
371 ep['region'] == region]
372 if len(filtered_catalog) == 0:
373 # No matching region, take the first endpoint
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500374 filtered_catalog = [service_catalog[0]]
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000375 # There should be only one match. If not take the first.
376 _base_url = filtered_catalog[0].get('url', None)
377 if _base_url is None:
378 raise exceptions.EndpointNotFound(service)
379
380 parts = urlparse.urlparse(_base_url)
381 if filters.get('api_version', None) is not None:
382 path = "/" + filters['api_version']
383 noversion_path = "/".join(parts.path.split("/")[2:])
384 if noversion_path != "":
Mauro S. M. Rodriguesb67129e2014-02-24 15:14:40 -0500385 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000386 _base_url = _base_url.replace(parts.path, path)
387 if filters.get('skip_path', None) is not None:
388 _base_url = _base_url.replace(parts.path, "/")
389
390 return _base_url
391
392 def is_expired(self, auth_data):
393 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900394 expiry = datetime.datetime.strptime(access['expires_at'],
395 self.EXPIRY_DATE_FORMAT)
396 return expiry <= datetime.datetime.now()