blob: 5df6224b52a58f69193b5e79657e79058ea837a7 [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
Marc Koderer235e4f52014-07-22 10:15:08 +020016import abc
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000017import copy
Masayuki Igawa1edf94f2014-03-04 18:34:16 +090018import datetime
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000019import exceptions
20import re
Marc Koderer235e4f52014-07-22 10:15:08 +020021import six
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000022import urlparse
23
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000024from tempest import config
25from tempest.services.identity.json import identity_client as json_id
26from tempest.services.identity.v3.json import identity_client as json_v3id
27from tempest.services.identity.v3.xml import identity_client as xml_v3id
28from tempest.services.identity.xml import identity_client as xml_id
29
30from tempest.openstack.common import log as logging
31
32CONF = config.CONF
33LOG = logging.getLogger(__name__)
34
35
Marc Koderer235e4f52014-07-22 10:15:08 +020036@six.add_metaclass(abc.ABCMeta)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000037class AuthProvider(object):
38 """
39 Provide authentication
40 """
41
42 def __init__(self, credentials, client_type='tempest',
43 interface=None):
44 """
45 :param credentials: credentials for authentication
46 :param client_type: 'tempest' or 'official'
47 :param interface: 'json' or 'xml'. Applicable for tempest client only
48 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010049 credentials = self._convert_credentials(credentials)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000050 if self.check_credentials(credentials):
51 self.credentials = credentials
52 else:
53 raise TypeError("Invalid credentials")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000054 self.client_type = client_type
55 self.interface = interface
56 if self.client_type == 'tempest' and self.interface is None:
57 self.interface = 'json'
58 self.cache = None
59 self.alt_auth_data = None
60 self.alt_part = None
61
Andrea Frittoli7d707a52014-04-06 11:46:32 +010062 def _convert_credentials(self, credentials):
63 # Support dict credentials for backwards compatibility
64 if isinstance(credentials, dict):
65 return get_credentials(**credentials)
66 else:
67 return credentials
68
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000069 def __str__(self):
70 return "Creds :{creds}, client type: {client_type}, interface: " \
71 "{interface}, cached auth data: {cache}".format(
72 creds=self.credentials, client_type=self.client_type,
73 interface=self.interface, cache=self.cache
74 )
75
Marc Koderer235e4f52014-07-22 10:15:08 +020076 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000077 def _decorate_request(self, filters, method, url, headers=None, body=None,
78 auth_data=None):
79 """
80 Decorate request with authentication data
81 """
Marc Koderer235e4f52014-07-22 10:15:08 +020082 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000083
Marc Koderer235e4f52014-07-22 10:15:08 +020084 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000085 def _get_auth(self):
Marc Koderer235e4f52014-07-22 10:15:08 +020086 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000087
Marc Koderer235e4f52014-07-22 10:15:08 +020088 @abc.abstractmethod
Andrea Frittoli2095d242014-03-20 08:36:23 +000089 def _fill_credentials(self, auth_data_body):
Marc Koderer235e4f52014-07-22 10:15:08 +020090 return
Andrea Frittoli2095d242014-03-20 08:36:23 +000091
92 def fill_credentials(self):
93 """
94 Fill credentials object with data from auth
95 """
96 auth_data = self.get_auth()
97 self._fill_credentials(auth_data[1])
98 return self.credentials
99
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000100 @classmethod
101 def check_credentials(cls, credentials):
102 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100103 Verify credentials are valid.
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000104 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100105 return isinstance(credentials, Credentials) and credentials.is_valid()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000106
107 @property
108 def auth_data(self):
Andrea Frittoli2095d242014-03-20 08:36:23 +0000109 return self.get_auth()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000110
111 @auth_data.deleter
112 def auth_data(self):
113 self.clear_auth()
114
Andrea Frittoli2095d242014-03-20 08:36:23 +0000115 def get_auth(self):
116 """
117 Returns auth from cache if available, else auth first
118 """
119 if self.cache is None or self.is_expired(self.cache):
120 self.set_auth()
121 return self.cache
122
123 def set_auth(self):
124 """
125 Forces setting auth, ignores cache if it exists.
126 Refills credentials
127 """
128 self.cache = self._get_auth()
129 self._fill_credentials(self.cache[1])
130
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000131 def clear_auth(self):
132 """
133 Can be called to clear the access cache so that next request
134 will fetch a new token and base_url.
135 """
136 self.cache = None
Andrea Frittoli2095d242014-03-20 08:36:23 +0000137 self.credentials.reset()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000138
Marc Koderer235e4f52014-07-22 10:15:08 +0200139 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000140 def is_expired(self, auth_data):
Marc Koderer235e4f52014-07-22 10:15:08 +0200141 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000142
143 def auth_request(self, method, url, headers=None, body=None, filters=None):
144 """
145 Obtains auth data and decorates a request with that.
146 :param method: HTTP method of the request
147 :param url: relative URL of the request (path)
148 :param headers: HTTP headers of the request
149 :param body: HTTP body in case of POST / PUT
150 :param filters: select a base URL out of the catalog
151 :returns a Tuple (url, headers, body)
152 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000153 orig_req = dict(url=url, headers=headers, body=body)
154
155 auth_url, auth_headers, auth_body = self._decorate_request(
156 filters, method, url, headers, body)
157 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
158
159 # Overwrite part if the request if it has been requested
160 if self.alt_part is not None:
161 if self.alt_auth_data is not None:
162 alt_url, alt_headers, alt_body = self._decorate_request(
163 filters, method, url, headers, body,
164 auth_data=self.alt_auth_data)
165 alt_auth_req = dict(url=alt_url, headers=alt_headers,
166 body=alt_body)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000167 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
168
169 else:
170 # If alt auth data is None, skip auth in the requested part
171 auth_req[self.alt_part] = orig_req[self.alt_part]
172
173 # Next auth request will be normal, unless otherwise requested
174 self.reset_alt_auth_data()
175
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000176 return auth_req['url'], auth_req['headers'], auth_req['body']
177
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000178 def reset_alt_auth_data(self):
179 """
180 Configure auth provider to provide valid authentication data
181 """
182 self.alt_part = None
183 self.alt_auth_data = None
184
185 def set_alt_auth_data(self, request_part, auth_data):
186 """
187 Configure auth provider to provide alt authentication data
188 on a part of the *next* auth_request. If credentials are None,
189 set invalid data.
190 :param request_part: request part to contain invalid auth: url,
191 headers, body
192 :param auth_data: alternative auth_data from which to get the
193 invalid data to be injected
194 """
195 self.alt_part = request_part
196 self.alt_auth_data = auth_data
197
Marc Koderer235e4f52014-07-22 10:15:08 +0200198 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000199 def base_url(self, filters, auth_data=None):
200 """
201 Extracts the base_url based on provided filters
202 """
Marc Koderer235e4f52014-07-22 10:15:08 +0200203 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000204
205
206class KeystoneAuthProvider(AuthProvider):
207
Andrea Frittolidbd02512014-03-21 10:06:19 +0000208 token_expiry_threshold = datetime.timedelta(seconds=60)
209
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000210 def __init__(self, credentials, client_type='tempest', interface=None):
211 super(KeystoneAuthProvider, self).__init__(credentials, client_type,
212 interface)
213 self.auth_client = self._auth_client()
214
215 def _decorate_request(self, filters, method, url, headers=None, body=None,
216 auth_data=None):
217 if auth_data is None:
218 auth_data = self.auth_data
219 token, _ = auth_data
220 base_url = self.base_url(filters=filters, auth_data=auth_data)
221 # build authenticated request
222 # returns new request, it does not touch the original values
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500223 _headers = copy.deepcopy(headers) if headers is not None else {}
Daisuke Morita02f840b2014-03-19 20:51:01 +0900224 _headers['X-Auth-Token'] = str(token)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000225 if url is None or url == "":
226 _url = base_url
227 else:
228 # Join base URL and url, and remove multiple contiguous slashes
229 _url = "/".join([base_url, url])
230 parts = [x for x in urlparse.urlparse(_url)]
231 parts[2] = re.sub("/{2,}", "/", parts[2])
232 _url = urlparse.urlunparse(parts)
233 # no change to method or body
Daisuke Morita02f840b2014-03-19 20:51:01 +0900234 return str(_url), _headers, body
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000235
Marc Koderer235e4f52014-07-22 10:15:08 +0200236 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000237 def _auth_client(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200238 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000239
Marc Koderer235e4f52014-07-22 10:15:08 +0200240 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000241 def _auth_params(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200242 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000243
244 def _get_auth(self):
245 # Bypasses the cache
246 if self.client_type == 'tempest':
247 auth_func = getattr(self.auth_client, 'get_token')
248 auth_params = self._auth_params()
249
250 # returns token, auth_data
251 token, auth_data = auth_func(**auth_params)
252 return token, auth_data
253 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000254 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000255
256 def get_token(self):
257 return self.auth_data[0]
258
259
260class KeystoneV2AuthProvider(KeystoneAuthProvider):
261
262 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
263
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000264 def _auth_client(self):
265 if self.client_type == 'tempest':
266 if self.interface == 'json':
267 return json_id.TokenClientJSON()
268 else:
269 return xml_id.TokenClientXML()
270 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000271 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000272
273 def _auth_params(self):
274 if self.client_type == 'tempest':
275 return dict(
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100276 user=self.credentials.username,
277 password=self.credentials.password,
278 tenant=self.credentials.tenant_name,
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000279 auth_data=True)
280 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000281 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000282
Andrea Frittoli2095d242014-03-20 08:36:23 +0000283 def _fill_credentials(self, auth_data_body):
284 tenant = auth_data_body['token']['tenant']
285 user = auth_data_body['user']
286 if self.credentials.tenant_name is None:
287 self.credentials.tenant_name = tenant['name']
288 if self.credentials.tenant_id is None:
289 self.credentials.tenant_id = tenant['id']
290 if self.credentials.username is None:
291 self.credentials.username = user['name']
292 if self.credentials.user_id is None:
293 self.credentials.user_id = user['id']
294
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000295 def base_url(self, filters, auth_data=None):
296 """
297 Filters can be:
298 - service: compute, image, etc
299 - region: the service region
300 - endpoint_type: adminURL, publicURL, internalURL
301 - api_version: replace catalog version with this
302 - skip_path: take just the base URL
303 """
304 if auth_data is None:
305 auth_data = self.auth_data
306 token, _auth_data = auth_data
307 service = filters.get('service')
308 region = filters.get('region')
309 endpoint_type = filters.get('endpoint_type', 'publicURL')
310
311 if service is None:
312 raise exceptions.EndpointNotFound("No service provided")
313
314 _base_url = None
315 for ep in _auth_data['serviceCatalog']:
316 if ep["type"] == service:
317 for _ep in ep['endpoints']:
318 if region is not None and _ep['region'] == region:
319 _base_url = _ep.get(endpoint_type)
320 if not _base_url:
321 # No region matching, use the first
322 _base_url = ep['endpoints'][0].get(endpoint_type)
323 break
324 if _base_url is None:
325 raise exceptions.EndpointNotFound(service)
326
327 parts = urlparse.urlparse(_base_url)
328 if filters.get('api_version', None) is not None:
329 path = "/" + filters['api_version']
330 noversion_path = "/".join(parts.path.split("/")[2:])
331 if noversion_path != "":
Zhi Kun Liube8bdbc2014-02-08 11:40:57 +0800332 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000333 _base_url = _base_url.replace(parts.path, path)
David Kranz5a2cb452014-07-29 13:51:26 -0400334 if filters.get('skip_path', None) is not None and parts.path != '':
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000335 _base_url = _base_url.replace(parts.path, "/")
336
337 return _base_url
338
339 def is_expired(self, auth_data):
340 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900341 expiry = datetime.datetime.strptime(access['token']['expires'],
342 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000343 return expiry - self.token_expiry_threshold <= \
344 datetime.datetime.utcnow()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000345
346
347class KeystoneV3AuthProvider(KeystoneAuthProvider):
348
349 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
350
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000351 def _auth_client(self):
352 if self.client_type == 'tempest':
353 if self.interface == 'json':
354 return json_v3id.V3TokenClientJSON()
355 else:
356 return xml_v3id.V3TokenClientXML()
357 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000358 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000359
360 def _auth_params(self):
361 if self.client_type == 'tempest':
362 return dict(
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100363 user=self.credentials.username,
364 password=self.credentials.password,
365 tenant=self.credentials.tenant_name,
366 domain=self.credentials.user_domain_name,
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000367 auth_data=True)
368 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000369 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000370
Andrea Frittoli2095d242014-03-20 08:36:23 +0000371 def _fill_credentials(self, auth_data_body):
372 # project or domain, depending on the scope
373 project = auth_data_body.get('project', None)
374 domain = auth_data_body.get('domain', None)
375 # user is always there
376 user = auth_data_body['user']
377 # Set project fields
378 if project is not None:
379 if self.credentials.project_name is None:
380 self.credentials.project_name = project['name']
381 if self.credentials.project_id is None:
382 self.credentials.project_id = project['id']
383 if self.credentials.project_domain_id is None:
384 self.credentials.project_domain_id = project['domain']['id']
385 if self.credentials.project_domain_name is None:
386 self.credentials.project_domain_name = \
387 project['domain']['name']
388 # Set domain fields
389 if domain is not None:
390 if self.credentials.domain_id is None:
391 self.credentials.domain_id = domain['id']
392 if self.credentials.domain_name is None:
393 self.credentials.domain_name = domain['name']
394 # Set user fields
395 if self.credentials.username is None:
396 self.credentials.username = user['name']
397 if self.credentials.user_id is None:
398 self.credentials.user_id = user['id']
399 if self.credentials.user_domain_id is None:
400 self.credentials.user_domain_id = user['domain']['id']
401 if self.credentials.user_domain_name is None:
402 self.credentials.user_domain_name = user['domain']['name']
403
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000404 def base_url(self, filters, auth_data=None):
405 """
406 Filters can be:
407 - service: compute, image, etc
408 - region: the service region
409 - endpoint_type: adminURL, publicURL, internalURL
410 - api_version: replace catalog version with this
411 - skip_path: take just the base URL
412 """
413 if auth_data is None:
414 auth_data = self.auth_data
415 token, _auth_data = auth_data
416 service = filters.get('service')
417 region = filters.get('region')
418 endpoint_type = filters.get('endpoint_type', 'public')
419
420 if service is None:
421 raise exceptions.EndpointNotFound("No service provided")
422
423 if 'URL' in endpoint_type:
424 endpoint_type = endpoint_type.replace('URL', '')
425 _base_url = None
426 catalog = _auth_data['catalog']
427 # Select entries with matching service type
428 service_catalog = [ep for ep in catalog if ep['type'] == service]
429 if len(service_catalog) > 0:
430 service_catalog = service_catalog[0]['endpoints']
431 else:
432 # No matching service
433 raise exceptions.EndpointNotFound(service)
434 # Filter by endpoint type (interface)
435 filtered_catalog = [ep for ep in service_catalog if
436 ep['interface'] == endpoint_type]
437 if len(filtered_catalog) == 0:
438 # No matching type, keep all and try matching by region at least
439 filtered_catalog = service_catalog
440 # Filter by region
441 filtered_catalog = [ep for ep in filtered_catalog if
442 ep['region'] == region]
443 if len(filtered_catalog) == 0:
444 # No matching region, take the first endpoint
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500445 filtered_catalog = [service_catalog[0]]
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000446 # There should be only one match. If not take the first.
447 _base_url = filtered_catalog[0].get('url', None)
448 if _base_url is None:
449 raise exceptions.EndpointNotFound(service)
450
451 parts = urlparse.urlparse(_base_url)
452 if filters.get('api_version', None) is not None:
453 path = "/" + filters['api_version']
454 noversion_path = "/".join(parts.path.split("/")[2:])
455 if noversion_path != "":
Mauro S. M. Rodriguesb67129e2014-02-24 15:14:40 -0500456 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000457 _base_url = _base_url.replace(parts.path, path)
458 if filters.get('skip_path', None) is not None:
459 _base_url = _base_url.replace(parts.path, "/")
460
461 return _base_url
462
463 def is_expired(self, auth_data):
464 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900465 expiry = datetime.datetime.strptime(access['expires_at'],
466 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000467 return expiry - self.token_expiry_threshold <= \
468 datetime.datetime.utcnow()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100469
470
Andrea Frittoli2095d242014-03-20 08:36:23 +0000471def get_default_credentials(credential_type, fill_in=True):
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100472 """
473 Returns configured credentials of the specified type
474 based on the configured auth_version
475 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000476 return get_credentials(fill_in=fill_in, credential_type=credential_type)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100477
478
Andrea Frittoli2095d242014-03-20 08:36:23 +0000479def get_credentials(credential_type=None, fill_in=True, **kwargs):
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100480 """
481 Builds a credentials object based on the configured auth_version
482
483 :param credential_type (string): requests credentials from tempest
484 configuration file. Valid values are defined in
485 Credentials.TYPE.
486 :param kwargs (dict): take into account only if credential_type is
487 not specified or None. Dict of credential key/value pairs
488
489 Examples:
490
491 Returns credentials from the provided parameters:
492 >>> get_credentials(username='foo', password='bar')
493
494 Returns credentials from tempest configuration:
495 >>> get_credentials(credential_type='user')
496 """
497 if CONF.identity.auth_version == 'v2':
498 credential_class = KeystoneV2Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000499 auth_provider_class = KeystoneV2AuthProvider
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100500 elif CONF.identity.auth_version == 'v3':
501 credential_class = KeystoneV3Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000502 auth_provider_class = KeystoneV3AuthProvider
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100503 else:
504 raise exceptions.InvalidConfiguration('Unsupported auth version')
505 if credential_type is not None:
506 creds = credential_class.get_default(credential_type)
507 else:
508 creds = credential_class(**kwargs)
Andrea Frittoli2095d242014-03-20 08:36:23 +0000509 # Fill in the credentials fields that were not specified
510 if fill_in:
511 auth_provider = auth_provider_class(creds)
512 creds = auth_provider.fill_credentials()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100513 return creds
514
515
516class Credentials(object):
517 """
518 Set of credentials for accessing OpenStack services
519
520 ATTRIBUTES: list of valid class attributes representing credentials.
521
522 TYPES: types of credentials available in the configuration file.
523 For each key there's a tuple (section, prefix) to match the
524 configuration options.
525 """
526
527 ATTRIBUTES = []
528 TYPES = {
529 'identity_admin': ('identity', 'admin'),
530 'compute_admin': ('compute_admin', None),
531 'user': ('identity', None),
532 'alt_user': ('identity', 'alt')
533 }
534
535 def __init__(self, **kwargs):
536 """
537 Enforce the available attributes at init time (only).
538 Additional attributes can still be set afterwards if tests need
539 to do so.
540 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000541 self._initial = kwargs
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100542 self._apply_credentials(kwargs)
543
544 def _apply_credentials(self, attr):
545 for key in attr.keys():
546 if key in self.ATTRIBUTES:
547 setattr(self, key, attr[key])
548 else:
549 raise exceptions.InvalidCredentials
550
551 def __str__(self):
552 """
553 Represent only attributes included in self.ATTRIBUTES
554 """
555 _repr = dict((k, getattr(self, k)) for k in self.ATTRIBUTES)
556 return str(_repr)
557
558 def __eq__(self, other):
559 """
560 Credentials are equal if attributes in self.ATTRIBUTES are equal
561 """
562 return str(self) == str(other)
563
564 def __getattr__(self, key):
565 # If an attribute is set, __getattr__ is not invoked
566 # If an attribute is not set, and it is a known one, return None
567 if key in self.ATTRIBUTES:
568 return None
569 else:
570 raise AttributeError
571
572 def __delitem__(self, key):
573 # For backwards compatibility, support dict behaviour
574 if key in self.ATTRIBUTES:
575 delattr(self, key)
576 else:
577 raise AttributeError
578
579 def get(self, item, default):
580 # In this patch act as dict for backward compatibility
581 try:
582 return getattr(self, item)
583 except AttributeError:
584 return default
585
586 @classmethod
587 def get_default(cls, credentials_type):
588 if credentials_type not in cls.TYPES:
589 raise exceptions.InvalidCredentials()
590 creds = cls._get_default(credentials_type)
591 if not creds.is_valid():
592 raise exceptions.InvalidConfiguration()
593 return creds
594
595 @classmethod
596 def _get_default(cls, credentials_type):
597 raise NotImplementedError
598
599 def is_valid(self):
600 raise NotImplementedError
601
Andrea Frittoli2095d242014-03-20 08:36:23 +0000602 def reset(self):
603 # First delete all known attributes
604 for key in self.ATTRIBUTES:
605 if getattr(self, key) is not None:
606 delattr(self, key)
607 # Then re-apply initial setup
608 self._apply_credentials(self._initial)
609
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100610
611class KeystoneV2Credentials(Credentials):
612
613 CONF_ATTRIBUTES = ['username', 'password', 'tenant_name']
614 ATTRIBUTES = ['user_id', 'tenant_id']
615 ATTRIBUTES.extend(CONF_ATTRIBUTES)
616
617 @classmethod
618 def _get_default(cls, credentials_type='user'):
619 params = {}
620 section, prefix = cls.TYPES[credentials_type]
621 for attr in cls.CONF_ATTRIBUTES:
622 _section = getattr(CONF, section)
623 if prefix is None:
624 params[attr] = getattr(_section, attr)
625 else:
626 params[attr] = getattr(_section, prefix + "_" + attr)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100627 return cls(**params)
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100628
629 def is_valid(self):
630 """
631 Minimum set of valid credentials, are username and password.
632 Tenant is optional.
633 """
634 return None not in (self.username, self.password)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100635
636
637class KeystoneV3Credentials(KeystoneV2Credentials):
638 """
639 Credentials suitable for the Keystone Identity V3 API
640 """
641
642 CONF_ATTRIBUTES = ['domain_name', 'password', 'tenant_name', 'username']
643 ATTRIBUTES = ['project_domain_id', 'project_domain_name', 'project_id',
644 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
645 'user_domain_name', 'user_id']
646 ATTRIBUTES.extend(CONF_ATTRIBUTES)
647
648 def __init__(self, **kwargs):
649 """
650 If domain is not specified, load the one configured for the
651 identity manager.
652 """
653 domain_fields = set(x for x in self.ATTRIBUTES if 'domain' in x)
654 if not domain_fields.intersection(kwargs.keys()):
655 kwargs['user_domain_name'] = CONF.identity.admin_domain_name
656 super(KeystoneV3Credentials, self).__init__(**kwargs)
657
658 def __setattr__(self, key, value):
659 parent = super(KeystoneV3Credentials, self)
660 # for tenant_* set both project and tenant
661 if key == 'tenant_id':
662 parent.__setattr__('project_id', value)
663 elif key == 'tenant_name':
664 parent.__setattr__('project_name', value)
665 # for project_* set both project and tenant
666 if key == 'project_id':
667 parent.__setattr__('tenant_id', value)
668 elif key == 'project_name':
669 parent.__setattr__('tenant_name', value)
670 # for *_domain_* set both user and project if not set yet
671 if key == 'user_domain_id':
672 if self.project_domain_id is None:
673 parent.__setattr__('project_domain_id', value)
674 if key == 'project_domain_id':
675 if self.user_domain_id is None:
676 parent.__setattr__('user_domain_id', value)
677 if key == 'user_domain_name':
678 if self.project_domain_name is None:
679 parent.__setattr__('project_domain_name', value)
680 if key == 'project_domain_name':
681 if self.user_domain_name is None:
682 parent.__setattr__('user_domain_name', value)
683 # support domain_name coming from config
684 if key == 'domain_name':
685 parent.__setattr__('user_domain_name', value)
686 parent.__setattr__('project_domain_name', value)
687 # finally trigger default behaviour for all attributes
688 parent.__setattr__(key, value)
689
690 def is_valid(self):
691 """
692 Valid combinations of v3 credentials (excluding token, scope)
693 - User id, password (optional domain)
694 - User name, password and its domain id/name
695 For the scope, valid combinations are:
696 - None
697 - Project id (optional domain)
698 - Project name and its domain id/name
699 """
700 valid_user_domain = any(
701 [self.user_domain_id is not None,
702 self.user_domain_name is not None])
703 valid_project_domain = any(
704 [self.project_domain_id is not None,
705 self.project_domain_name is not None])
706 valid_user = any(
707 [self.user_id is not None,
708 self.username is not None and valid_user_domain])
709 valid_project = any(
710 [self.project_name is None and self.project_id is None,
711 self.project_id is not None,
712 self.project_name is not None and valid_project_domain])
713 return all([self.password is not None, valid_user, valid_project])