blob: 830dca9bf1cd750f922f33ca1525b4b1bab24dca [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 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010046 credentials = self._convert_credentials(credentials)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000047 if self.check_credentials(credentials):
48 self.credentials = credentials
49 else:
50 raise TypeError("Invalid credentials")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000051 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
Andrea Frittoli7d707a52014-04-06 11:46:32 +010059 def _convert_credentials(self, credentials):
60 # Support dict credentials for backwards compatibility
61 if isinstance(credentials, dict):
62 return get_credentials(**credentials)
63 else:
64 return credentials
65
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000066 def __str__(self):
67 return "Creds :{creds}, client type: {client_type}, interface: " \
68 "{interface}, cached auth data: {cache}".format(
69 creds=self.credentials, client_type=self.client_type,
70 interface=self.interface, cache=self.cache
71 )
72
73 def _decorate_request(self, filters, method, url, headers=None, body=None,
74 auth_data=None):
75 """
76 Decorate request with authentication data
77 """
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +000078 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000079
80 def _get_auth(self):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +000081 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000082
Andrea Frittoli2095d242014-03-20 08:36:23 +000083 def _fill_credentials(self, auth_data_body):
84 raise NotImplementedError
85
86 def fill_credentials(self):
87 """
88 Fill credentials object with data from auth
89 """
90 auth_data = self.get_auth()
91 self._fill_credentials(auth_data[1])
92 return self.credentials
93
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000094 @classmethod
95 def check_credentials(cls, credentials):
96 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010097 Verify credentials are valid.
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000098 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010099 return isinstance(credentials, Credentials) and credentials.is_valid()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000100
101 @property
102 def auth_data(self):
Andrea Frittoli2095d242014-03-20 08:36:23 +0000103 return self.get_auth()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000104
105 @auth_data.deleter
106 def auth_data(self):
107 self.clear_auth()
108
Andrea Frittoli2095d242014-03-20 08:36:23 +0000109 def get_auth(self):
110 """
111 Returns auth from cache if available, else auth first
112 """
113 if self.cache is None or self.is_expired(self.cache):
114 self.set_auth()
115 return self.cache
116
117 def set_auth(self):
118 """
119 Forces setting auth, ignores cache if it exists.
120 Refills credentials
121 """
122 self.cache = self._get_auth()
123 self._fill_credentials(self.cache[1])
124
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000125 def clear_auth(self):
126 """
127 Can be called to clear the access cache so that next request
128 will fetch a new token and base_url.
129 """
130 self.cache = None
Andrea Frittoli2095d242014-03-20 08:36:23 +0000131 self.credentials.reset()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000132
133 def is_expired(self, auth_data):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000134 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000135
136 def auth_request(self, method, url, headers=None, body=None, filters=None):
137 """
138 Obtains auth data and decorates a request with that.
139 :param method: HTTP method of the request
140 :param url: relative URL of the request (path)
141 :param headers: HTTP headers of the request
142 :param body: HTTP body in case of POST / PUT
143 :param filters: select a base URL out of the catalog
144 :returns a Tuple (url, headers, body)
145 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000146 orig_req = dict(url=url, headers=headers, body=body)
147
148 auth_url, auth_headers, auth_body = self._decorate_request(
149 filters, method, url, headers, body)
150 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
151
152 # Overwrite part if the request if it has been requested
153 if self.alt_part is not None:
154 if self.alt_auth_data is not None:
155 alt_url, alt_headers, alt_body = self._decorate_request(
156 filters, method, url, headers, body,
157 auth_data=self.alt_auth_data)
158 alt_auth_req = dict(url=alt_url, headers=alt_headers,
159 body=alt_body)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000160 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
161
162 else:
163 # If alt auth data is None, skip auth in the requested part
164 auth_req[self.alt_part] = orig_req[self.alt_part]
165
166 # Next auth request will be normal, unless otherwise requested
167 self.reset_alt_auth_data()
168
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000169 return auth_req['url'], auth_req['headers'], auth_req['body']
170
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000171 def reset_alt_auth_data(self):
172 """
173 Configure auth provider to provide valid authentication data
174 """
175 self.alt_part = None
176 self.alt_auth_data = None
177
178 def set_alt_auth_data(self, request_part, auth_data):
179 """
180 Configure auth provider to provide alt authentication data
181 on a part of the *next* auth_request. If credentials are None,
182 set invalid data.
183 :param request_part: request part to contain invalid auth: url,
184 headers, body
185 :param auth_data: alternative auth_data from which to get the
186 invalid data to be injected
187 """
188 self.alt_part = request_part
189 self.alt_auth_data = auth_data
190
191 def base_url(self, filters, auth_data=None):
192 """
193 Extracts the base_url based on provided filters
194 """
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000195 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000196
197
198class KeystoneAuthProvider(AuthProvider):
199
Andrea Frittolidbd02512014-03-21 10:06:19 +0000200 token_expiry_threshold = datetime.timedelta(seconds=60)
201
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000202 def __init__(self, credentials, client_type='tempest', interface=None):
203 super(KeystoneAuthProvider, self).__init__(credentials, client_type,
204 interface)
205 self.auth_client = self._auth_client()
206
207 def _decorate_request(self, filters, method, url, headers=None, body=None,
208 auth_data=None):
209 if auth_data is None:
210 auth_data = self.auth_data
211 token, _ = auth_data
212 base_url = self.base_url(filters=filters, auth_data=auth_data)
213 # build authenticated request
214 # returns new request, it does not touch the original values
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500215 _headers = copy.deepcopy(headers) if headers is not None else {}
Daisuke Morita02f840b2014-03-19 20:51:01 +0900216 _headers['X-Auth-Token'] = str(token)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000217 if url is None or url == "":
218 _url = base_url
219 else:
220 # Join base URL and url, and remove multiple contiguous slashes
221 _url = "/".join([base_url, url])
222 parts = [x for x in urlparse.urlparse(_url)]
223 parts[2] = re.sub("/{2,}", "/", parts[2])
224 _url = urlparse.urlunparse(parts)
225 # no change to method or body
Daisuke Morita02f840b2014-03-19 20:51:01 +0900226 return str(_url), _headers, body
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000227
228 def _auth_client(self):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000229 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000230
231 def _auth_params(self):
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000232 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000233
234 def _get_auth(self):
235 # Bypasses the cache
236 if self.client_type == 'tempest':
237 auth_func = getattr(self.auth_client, 'get_token')
238 auth_params = self._auth_params()
239
240 # returns token, auth_data
241 token, auth_data = auth_func(**auth_params)
242 return token, auth_data
243 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000244 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000245
246 def get_token(self):
247 return self.auth_data[0]
248
249
250class KeystoneV2AuthProvider(KeystoneAuthProvider):
251
252 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
253
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000254 def _auth_client(self):
255 if self.client_type == 'tempest':
256 if self.interface == 'json':
257 return json_id.TokenClientJSON()
258 else:
259 return xml_id.TokenClientXML()
260 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000261 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000262
263 def _auth_params(self):
264 if self.client_type == 'tempest':
265 return dict(
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100266 user=self.credentials.username,
267 password=self.credentials.password,
268 tenant=self.credentials.tenant_name,
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000269 auth_data=True)
270 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000271 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000272
Andrea Frittoli2095d242014-03-20 08:36:23 +0000273 def _fill_credentials(self, auth_data_body):
274 tenant = auth_data_body['token']['tenant']
275 user = auth_data_body['user']
276 if self.credentials.tenant_name is None:
277 self.credentials.tenant_name = tenant['name']
278 if self.credentials.tenant_id is None:
279 self.credentials.tenant_id = tenant['id']
280 if self.credentials.username is None:
281 self.credentials.username = user['name']
282 if self.credentials.user_id is None:
283 self.credentials.user_id = user['id']
284
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000285 def base_url(self, filters, auth_data=None):
286 """
287 Filters can be:
288 - service: compute, image, etc
289 - region: the service region
290 - endpoint_type: adminURL, publicURL, internalURL
291 - api_version: replace catalog version with this
292 - skip_path: take just the base URL
293 """
294 if auth_data is None:
295 auth_data = self.auth_data
296 token, _auth_data = auth_data
297 service = filters.get('service')
298 region = filters.get('region')
299 endpoint_type = filters.get('endpoint_type', 'publicURL')
300
301 if service is None:
302 raise exceptions.EndpointNotFound("No service provided")
303
304 _base_url = None
305 for ep in _auth_data['serviceCatalog']:
306 if ep["type"] == service:
307 for _ep in ep['endpoints']:
308 if region is not None and _ep['region'] == region:
309 _base_url = _ep.get(endpoint_type)
310 if not _base_url:
311 # No region matching, use the first
312 _base_url = ep['endpoints'][0].get(endpoint_type)
313 break
314 if _base_url is None:
315 raise exceptions.EndpointNotFound(service)
316
317 parts = urlparse.urlparse(_base_url)
318 if filters.get('api_version', None) is not None:
319 path = "/" + filters['api_version']
320 noversion_path = "/".join(parts.path.split("/")[2:])
321 if noversion_path != "":
Zhi Kun Liube8bdbc2014-02-08 11:40:57 +0800322 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000323 _base_url = _base_url.replace(parts.path, path)
324 if filters.get('skip_path', None) is not None:
325 _base_url = _base_url.replace(parts.path, "/")
326
327 return _base_url
328
329 def is_expired(self, auth_data):
330 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900331 expiry = datetime.datetime.strptime(access['token']['expires'],
332 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000333 return expiry - self.token_expiry_threshold <= \
334 datetime.datetime.utcnow()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000335
336
337class KeystoneV3AuthProvider(KeystoneAuthProvider):
338
339 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
340
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000341 def _auth_client(self):
342 if self.client_type == 'tempest':
343 if self.interface == 'json':
344 return json_v3id.V3TokenClientJSON()
345 else:
346 return xml_v3id.V3TokenClientXML()
347 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000348 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000349
350 def _auth_params(self):
351 if self.client_type == 'tempest':
352 return dict(
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100353 user=self.credentials.username,
354 password=self.credentials.password,
355 tenant=self.credentials.tenant_name,
356 domain=self.credentials.user_domain_name,
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000357 auth_data=True)
358 else:
Mauro S. M. Rodriguesd35386b2014-02-10 22:00:05 +0000359 raise NotImplementedError
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000360
Andrea Frittoli2095d242014-03-20 08:36:23 +0000361 def _fill_credentials(self, auth_data_body):
362 # project or domain, depending on the scope
363 project = auth_data_body.get('project', None)
364 domain = auth_data_body.get('domain', None)
365 # user is always there
366 user = auth_data_body['user']
367 # Set project fields
368 if project is not None:
369 if self.credentials.project_name is None:
370 self.credentials.project_name = project['name']
371 if self.credentials.project_id is None:
372 self.credentials.project_id = project['id']
373 if self.credentials.project_domain_id is None:
374 self.credentials.project_domain_id = project['domain']['id']
375 if self.credentials.project_domain_name is None:
376 self.credentials.project_domain_name = \
377 project['domain']['name']
378 # Set domain fields
379 if domain is not None:
380 if self.credentials.domain_id is None:
381 self.credentials.domain_id = domain['id']
382 if self.credentials.domain_name is None:
383 self.credentials.domain_name = domain['name']
384 # Set user fields
385 if self.credentials.username is None:
386 self.credentials.username = user['name']
387 if self.credentials.user_id is None:
388 self.credentials.user_id = user['id']
389 if self.credentials.user_domain_id is None:
390 self.credentials.user_domain_id = user['domain']['id']
391 if self.credentials.user_domain_name is None:
392 self.credentials.user_domain_name = user['domain']['name']
393
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000394 def base_url(self, filters, auth_data=None):
395 """
396 Filters can be:
397 - service: compute, image, etc
398 - region: the service region
399 - endpoint_type: adminURL, publicURL, internalURL
400 - api_version: replace catalog version with this
401 - skip_path: take just the base URL
402 """
403 if auth_data is None:
404 auth_data = self.auth_data
405 token, _auth_data = auth_data
406 service = filters.get('service')
407 region = filters.get('region')
408 endpoint_type = filters.get('endpoint_type', 'public')
409
410 if service is None:
411 raise exceptions.EndpointNotFound("No service provided")
412
413 if 'URL' in endpoint_type:
414 endpoint_type = endpoint_type.replace('URL', '')
415 _base_url = None
416 catalog = _auth_data['catalog']
417 # Select entries with matching service type
418 service_catalog = [ep for ep in catalog if ep['type'] == service]
419 if len(service_catalog) > 0:
420 service_catalog = service_catalog[0]['endpoints']
421 else:
422 # No matching service
423 raise exceptions.EndpointNotFound(service)
424 # Filter by endpoint type (interface)
425 filtered_catalog = [ep for ep in service_catalog if
426 ep['interface'] == endpoint_type]
427 if len(filtered_catalog) == 0:
428 # No matching type, keep all and try matching by region at least
429 filtered_catalog = service_catalog
430 # Filter by region
431 filtered_catalog = [ep for ep in filtered_catalog if
432 ep['region'] == region]
433 if len(filtered_catalog) == 0:
434 # No matching region, take the first endpoint
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500435 filtered_catalog = [service_catalog[0]]
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000436 # There should be only one match. If not take the first.
437 _base_url = filtered_catalog[0].get('url', None)
438 if _base_url is None:
439 raise exceptions.EndpointNotFound(service)
440
441 parts = urlparse.urlparse(_base_url)
442 if filters.get('api_version', None) is not None:
443 path = "/" + filters['api_version']
444 noversion_path = "/".join(parts.path.split("/")[2:])
445 if noversion_path != "":
Mauro S. M. Rodriguesb67129e2014-02-24 15:14:40 -0500446 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000447 _base_url = _base_url.replace(parts.path, path)
448 if filters.get('skip_path', None) is not None:
449 _base_url = _base_url.replace(parts.path, "/")
450
451 return _base_url
452
453 def is_expired(self, auth_data):
454 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900455 expiry = datetime.datetime.strptime(access['expires_at'],
456 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000457 return expiry - self.token_expiry_threshold <= \
458 datetime.datetime.utcnow()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100459
460
Andrea Frittoli2095d242014-03-20 08:36:23 +0000461def get_default_credentials(credential_type, fill_in=True):
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100462 """
463 Returns configured credentials of the specified type
464 based on the configured auth_version
465 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000466 return get_credentials(fill_in=fill_in, credential_type=credential_type)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100467
468
Andrea Frittoli2095d242014-03-20 08:36:23 +0000469def get_credentials(credential_type=None, fill_in=True, **kwargs):
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100470 """
471 Builds a credentials object based on the configured auth_version
472
473 :param credential_type (string): requests credentials from tempest
474 configuration file. Valid values are defined in
475 Credentials.TYPE.
476 :param kwargs (dict): take into account only if credential_type is
477 not specified or None. Dict of credential key/value pairs
478
479 Examples:
480
481 Returns credentials from the provided parameters:
482 >>> get_credentials(username='foo', password='bar')
483
484 Returns credentials from tempest configuration:
485 >>> get_credentials(credential_type='user')
486 """
487 if CONF.identity.auth_version == 'v2':
488 credential_class = KeystoneV2Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000489 auth_provider_class = KeystoneV2AuthProvider
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100490 elif CONF.identity.auth_version == 'v3':
491 credential_class = KeystoneV3Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000492 auth_provider_class = KeystoneV3AuthProvider
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100493 else:
494 raise exceptions.InvalidConfiguration('Unsupported auth version')
495 if credential_type is not None:
496 creds = credential_class.get_default(credential_type)
497 else:
498 creds = credential_class(**kwargs)
Andrea Frittoli2095d242014-03-20 08:36:23 +0000499 # Fill in the credentials fields that were not specified
500 if fill_in:
501 auth_provider = auth_provider_class(creds)
502 creds = auth_provider.fill_credentials()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100503 return creds
504
505
506class Credentials(object):
507 """
508 Set of credentials for accessing OpenStack services
509
510 ATTRIBUTES: list of valid class attributes representing credentials.
511
512 TYPES: types of credentials available in the configuration file.
513 For each key there's a tuple (section, prefix) to match the
514 configuration options.
515 """
516
517 ATTRIBUTES = []
518 TYPES = {
519 'identity_admin': ('identity', 'admin'),
520 'compute_admin': ('compute_admin', None),
521 'user': ('identity', None),
522 'alt_user': ('identity', 'alt')
523 }
524
525 def __init__(self, **kwargs):
526 """
527 Enforce the available attributes at init time (only).
528 Additional attributes can still be set afterwards if tests need
529 to do so.
530 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000531 self._initial = kwargs
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100532 self._apply_credentials(kwargs)
533
534 def _apply_credentials(self, attr):
535 for key in attr.keys():
536 if key in self.ATTRIBUTES:
537 setattr(self, key, attr[key])
538 else:
539 raise exceptions.InvalidCredentials
540
541 def __str__(self):
542 """
543 Represent only attributes included in self.ATTRIBUTES
544 """
545 _repr = dict((k, getattr(self, k)) for k in self.ATTRIBUTES)
546 return str(_repr)
547
548 def __eq__(self, other):
549 """
550 Credentials are equal if attributes in self.ATTRIBUTES are equal
551 """
552 return str(self) == str(other)
553
554 def __getattr__(self, key):
555 # If an attribute is set, __getattr__ is not invoked
556 # If an attribute is not set, and it is a known one, return None
557 if key in self.ATTRIBUTES:
558 return None
559 else:
560 raise AttributeError
561
562 def __delitem__(self, key):
563 # For backwards compatibility, support dict behaviour
564 if key in self.ATTRIBUTES:
565 delattr(self, key)
566 else:
567 raise AttributeError
568
569 def get(self, item, default):
570 # In this patch act as dict for backward compatibility
571 try:
572 return getattr(self, item)
573 except AttributeError:
574 return default
575
576 @classmethod
577 def get_default(cls, credentials_type):
578 if credentials_type not in cls.TYPES:
579 raise exceptions.InvalidCredentials()
580 creds = cls._get_default(credentials_type)
581 if not creds.is_valid():
582 raise exceptions.InvalidConfiguration()
583 return creds
584
585 @classmethod
586 def _get_default(cls, credentials_type):
587 raise NotImplementedError
588
589 def is_valid(self):
590 raise NotImplementedError
591
Andrea Frittoli2095d242014-03-20 08:36:23 +0000592 def reset(self):
593 # First delete all known attributes
594 for key in self.ATTRIBUTES:
595 if getattr(self, key) is not None:
596 delattr(self, key)
597 # Then re-apply initial setup
598 self._apply_credentials(self._initial)
599
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100600
601class KeystoneV2Credentials(Credentials):
602
603 CONF_ATTRIBUTES = ['username', 'password', 'tenant_name']
604 ATTRIBUTES = ['user_id', 'tenant_id']
605 ATTRIBUTES.extend(CONF_ATTRIBUTES)
606
607 @classmethod
608 def _get_default(cls, credentials_type='user'):
609 params = {}
610 section, prefix = cls.TYPES[credentials_type]
611 for attr in cls.CONF_ATTRIBUTES:
612 _section = getattr(CONF, section)
613 if prefix is None:
614 params[attr] = getattr(_section, attr)
615 else:
616 params[attr] = getattr(_section, prefix + "_" + attr)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100617 return cls(**params)
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100618
619 def is_valid(self):
620 """
621 Minimum set of valid credentials, are username and password.
622 Tenant is optional.
623 """
624 return None not in (self.username, self.password)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100625
626
627class KeystoneV3Credentials(KeystoneV2Credentials):
628 """
629 Credentials suitable for the Keystone Identity V3 API
630 """
631
632 CONF_ATTRIBUTES = ['domain_name', 'password', 'tenant_name', 'username']
633 ATTRIBUTES = ['project_domain_id', 'project_domain_name', 'project_id',
634 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
635 'user_domain_name', 'user_id']
636 ATTRIBUTES.extend(CONF_ATTRIBUTES)
637
638 def __init__(self, **kwargs):
639 """
640 If domain is not specified, load the one configured for the
641 identity manager.
642 """
643 domain_fields = set(x for x in self.ATTRIBUTES if 'domain' in x)
644 if not domain_fields.intersection(kwargs.keys()):
645 kwargs['user_domain_name'] = CONF.identity.admin_domain_name
646 super(KeystoneV3Credentials, self).__init__(**kwargs)
647
648 def __setattr__(self, key, value):
649 parent = super(KeystoneV3Credentials, self)
650 # for tenant_* set both project and tenant
651 if key == 'tenant_id':
652 parent.__setattr__('project_id', value)
653 elif key == 'tenant_name':
654 parent.__setattr__('project_name', value)
655 # for project_* set both project and tenant
656 if key == 'project_id':
657 parent.__setattr__('tenant_id', value)
658 elif key == 'project_name':
659 parent.__setattr__('tenant_name', value)
660 # for *_domain_* set both user and project if not set yet
661 if key == 'user_domain_id':
662 if self.project_domain_id is None:
663 parent.__setattr__('project_domain_id', value)
664 if key == 'project_domain_id':
665 if self.user_domain_id is None:
666 parent.__setattr__('user_domain_id', value)
667 if key == 'user_domain_name':
668 if self.project_domain_name is None:
669 parent.__setattr__('project_domain_name', value)
670 if key == 'project_domain_name':
671 if self.user_domain_name is None:
672 parent.__setattr__('user_domain_name', value)
673 # support domain_name coming from config
674 if key == 'domain_name':
675 parent.__setattr__('user_domain_name', value)
676 parent.__setattr__('project_domain_name', value)
677 # finally trigger default behaviour for all attributes
678 parent.__setattr__(key, value)
679
680 def is_valid(self):
681 """
682 Valid combinations of v3 credentials (excluding token, scope)
683 - User id, password (optional domain)
684 - User name, password and its domain id/name
685 For the scope, valid combinations are:
686 - None
687 - Project id (optional domain)
688 - Project name and its domain id/name
689 """
690 valid_user_domain = any(
691 [self.user_domain_id is not None,
692 self.user_domain_name is not None])
693 valid_project_domain = any(
694 [self.project_domain_id is not None,
695 self.project_domain_name is not None])
696 valid_user = any(
697 [self.user_id is not None,
698 self.username is not None and valid_user_domain])
699 valid_project = any(
700 [self.project_name is None and self.project_id is None,
701 self.project_id is not None,
702 self.project_name is not None and valid_project_domain])
703 return all([self.password is not None, valid_user, valid_project])