blob: 022a450dc5c6e6c7a22770a05ad725a34b92bd16 [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
21import urlparse
22
Matthew Treinish96e9e882014-06-09 18:37:19 -040023import six
24
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000025from tempest import config
Matthew Treinish96e9e882014-06-09 18:37:19 -040026from tempest.openstack.common import log as logging
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000027from tempest.services.identity.json import identity_client as json_id
28from tempest.services.identity.v3.json import identity_client as json_v3id
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000029
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000030
31CONF = config.CONF
32LOG = logging.getLogger(__name__)
33
34
Marc Koderer235e4f52014-07-22 10:15:08 +020035@six.add_metaclass(abc.ABCMeta)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000036class AuthProvider(object):
37 """
38 Provide authentication
39 """
40
Andrea Frittoli455e8442014-09-25 12:00:19 +010041 def __init__(self, credentials, interface=None):
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000042 """
43 :param credentials: credentials for authentication
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000044 :param interface: 'json' or 'xml'. Applicable for tempest client only
Sean Daguefc072542014-11-24 11:50:25 -050045 (deprecated: only json now supported)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000046 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010047 credentials = self._convert_credentials(credentials)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000048 if self.check_credentials(credentials):
49 self.credentials = credentials
50 else:
51 raise TypeError("Invalid credentials")
Sean Daguefc072542014-11-24 11:50:25 -050052 self.interface = 'json'
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000053 self.cache = None
54 self.alt_auth_data = None
55 self.alt_part = None
56
Andrea Frittoli7d707a52014-04-06 11:46:32 +010057 def _convert_credentials(self, credentials):
58 # Support dict credentials for backwards compatibility
59 if isinstance(credentials, dict):
60 return get_credentials(**credentials)
61 else:
62 return credentials
63
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000064 def __str__(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +010065 return "Creds :{creds}, interface: {interface}, " \
66 "cached auth data: {cache}".format(
67 creds=self.credentials, interface=self.interface,
68 cache=self.cache)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000069
Marc Koderer235e4f52014-07-22 10:15:08 +020070 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000071 def _decorate_request(self, filters, method, url, headers=None, body=None,
72 auth_data=None):
73 """
74 Decorate request with authentication data
75 """
Marc Koderer235e4f52014-07-22 10:15:08 +020076 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000077
Marc Koderer235e4f52014-07-22 10:15:08 +020078 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000079 def _get_auth(self):
Marc Koderer235e4f52014-07-22 10:15:08 +020080 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000081
Marc Koderer235e4f52014-07-22 10:15:08 +020082 @abc.abstractmethod
Andrea Frittoli2095d242014-03-20 08:36:23 +000083 def _fill_credentials(self, auth_data_body):
Marc Koderer235e4f52014-07-22 10:15:08 +020084 return
Andrea Frittoli2095d242014-03-20 08:36:23 +000085
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
Marc Koderer235e4f52014-07-22 10:15:08 +0200133 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000134 def is_expired(self, auth_data):
Marc Koderer235e4f52014-07-22 10:15:08 +0200135 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000136
137 def auth_request(self, method, url, headers=None, body=None, filters=None):
138 """
139 Obtains auth data and decorates a request with that.
140 :param method: HTTP method of the request
141 :param url: relative URL of the request (path)
142 :param headers: HTTP headers of the request
143 :param body: HTTP body in case of POST / PUT
144 :param filters: select a base URL out of the catalog
145 :returns a Tuple (url, headers, body)
146 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000147 orig_req = dict(url=url, headers=headers, body=body)
148
149 auth_url, auth_headers, auth_body = self._decorate_request(
150 filters, method, url, headers, body)
151 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
152
153 # Overwrite part if the request if it has been requested
154 if self.alt_part is not None:
155 if self.alt_auth_data is not None:
156 alt_url, alt_headers, alt_body = self._decorate_request(
157 filters, method, url, headers, body,
158 auth_data=self.alt_auth_data)
159 alt_auth_req = dict(url=alt_url, headers=alt_headers,
160 body=alt_body)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000161 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
162
163 else:
164 # If alt auth data is None, skip auth in the requested part
165 auth_req[self.alt_part] = orig_req[self.alt_part]
166
167 # Next auth request will be normal, unless otherwise requested
168 self.reset_alt_auth_data()
169
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000170 return auth_req['url'], auth_req['headers'], auth_req['body']
171
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000172 def reset_alt_auth_data(self):
173 """
174 Configure auth provider to provide valid authentication data
175 """
176 self.alt_part = None
177 self.alt_auth_data = None
178
179 def set_alt_auth_data(self, request_part, auth_data):
180 """
181 Configure auth provider to provide alt authentication data
182 on a part of the *next* auth_request. If credentials are None,
183 set invalid data.
184 :param request_part: request part to contain invalid auth: url,
185 headers, body
186 :param auth_data: alternative auth_data from which to get the
187 invalid data to be injected
188 """
189 self.alt_part = request_part
190 self.alt_auth_data = auth_data
191
Marc Koderer235e4f52014-07-22 10:15:08 +0200192 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000193 def base_url(self, filters, auth_data=None):
194 """
195 Extracts the base_url based on provided filters
196 """
Marc Koderer235e4f52014-07-22 10:15:08 +0200197 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000198
199
200class KeystoneAuthProvider(AuthProvider):
201
Andrea Frittolidbd02512014-03-21 10:06:19 +0000202 token_expiry_threshold = datetime.timedelta(seconds=60)
203
Andrea Frittoli455e8442014-09-25 12:00:19 +0100204 def __init__(self, credentials, interface=None):
205 super(KeystoneAuthProvider, self).__init__(credentials, interface)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000206 self.auth_client = self._auth_client()
207
208 def _decorate_request(self, filters, method, url, headers=None, body=None,
209 auth_data=None):
210 if auth_data is None:
211 auth_data = self.auth_data
212 token, _ = auth_data
213 base_url = self.base_url(filters=filters, auth_data=auth_data)
214 # build authenticated request
215 # returns new request, it does not touch the original values
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500216 _headers = copy.deepcopy(headers) if headers is not None else {}
Daisuke Morita02f840b2014-03-19 20:51:01 +0900217 _headers['X-Auth-Token'] = str(token)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000218 if url is None or url == "":
219 _url = base_url
220 else:
221 # Join base URL and url, and remove multiple contiguous slashes
222 _url = "/".join([base_url, url])
223 parts = [x for x in urlparse.urlparse(_url)]
224 parts[2] = re.sub("/{2,}", "/", parts[2])
225 _url = urlparse.urlunparse(parts)
226 # no change to method or body
Daisuke Morita02f840b2014-03-19 20:51:01 +0900227 return str(_url), _headers, body
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000228
Marc Koderer235e4f52014-07-22 10:15:08 +0200229 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000230 def _auth_client(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200231 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000232
Marc Koderer235e4f52014-07-22 10:15:08 +0200233 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000234 def _auth_params(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200235 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000236
237 def _get_auth(self):
238 # Bypasses the cache
Andrea Frittoli455e8442014-09-25 12:00:19 +0100239 auth_func = getattr(self.auth_client, 'get_token')
240 auth_params = self._auth_params()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000241
Andrea Frittoli455e8442014-09-25 12:00:19 +0100242 # returns token, auth_data
243 token, auth_data = auth_func(**auth_params)
244 return token, auth_data
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):
Sean Daguefc072542014-11-24 11:50:25 -0500255 return json_id.TokenClientJSON()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000256
257 def _auth_params(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100258 return dict(
259 user=self.credentials.username,
260 password=self.credentials.password,
261 tenant=self.credentials.tenant_name,
262 auth_data=True)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000263
Andrea Frittoli2095d242014-03-20 08:36:23 +0000264 def _fill_credentials(self, auth_data_body):
265 tenant = auth_data_body['token']['tenant']
266 user = auth_data_body['user']
267 if self.credentials.tenant_name is None:
268 self.credentials.tenant_name = tenant['name']
269 if self.credentials.tenant_id is None:
270 self.credentials.tenant_id = tenant['id']
271 if self.credentials.username is None:
272 self.credentials.username = user['name']
273 if self.credentials.user_id is None:
274 self.credentials.user_id = user['id']
275
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000276 def base_url(self, filters, auth_data=None):
277 """
278 Filters can be:
279 - service: compute, image, etc
280 - region: the service region
281 - endpoint_type: adminURL, publicURL, internalURL
282 - api_version: replace catalog version with this
283 - skip_path: take just the base URL
284 """
285 if auth_data is None:
286 auth_data = self.auth_data
287 token, _auth_data = auth_data
288 service = filters.get('service')
289 region = filters.get('region')
290 endpoint_type = filters.get('endpoint_type', 'publicURL')
291
292 if service is None:
293 raise exceptions.EndpointNotFound("No service provided")
294
295 _base_url = None
296 for ep in _auth_data['serviceCatalog']:
297 if ep["type"] == service:
298 for _ep in ep['endpoints']:
299 if region is not None and _ep['region'] == region:
300 _base_url = _ep.get(endpoint_type)
301 if not _base_url:
302 # No region matching, use the first
303 _base_url = ep['endpoints'][0].get(endpoint_type)
304 break
305 if _base_url is None:
306 raise exceptions.EndpointNotFound(service)
307
308 parts = urlparse.urlparse(_base_url)
309 if filters.get('api_version', None) is not None:
310 path = "/" + filters['api_version']
311 noversion_path = "/".join(parts.path.split("/")[2:])
312 if noversion_path != "":
Zhi Kun Liube8bdbc2014-02-08 11:40:57 +0800313 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000314 _base_url = _base_url.replace(parts.path, path)
David Kranz5a2cb452014-07-29 13:51:26 -0400315 if filters.get('skip_path', None) is not None and parts.path != '':
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000316 _base_url = _base_url.replace(parts.path, "/")
317
318 return _base_url
319
320 def is_expired(self, auth_data):
321 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900322 expiry = datetime.datetime.strptime(access['token']['expires'],
323 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000324 return expiry - self.token_expiry_threshold <= \
325 datetime.datetime.utcnow()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000326
327
328class KeystoneV3AuthProvider(KeystoneAuthProvider):
329
330 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
331
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000332 def _auth_client(self):
Sean Daguefc072542014-11-24 11:50:25 -0500333 return json_v3id.V3TokenClientJSON()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000334
335 def _auth_params(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100336 return dict(
337 user=self.credentials.username,
338 password=self.credentials.password,
339 tenant=self.credentials.tenant_name,
340 domain=self.credentials.user_domain_name,
341 auth_data=True)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000342
Andrea Frittoli2095d242014-03-20 08:36:23 +0000343 def _fill_credentials(self, auth_data_body):
344 # project or domain, depending on the scope
345 project = auth_data_body.get('project', None)
346 domain = auth_data_body.get('domain', None)
347 # user is always there
348 user = auth_data_body['user']
349 # Set project fields
350 if project is not None:
351 if self.credentials.project_name is None:
352 self.credentials.project_name = project['name']
353 if self.credentials.project_id is None:
354 self.credentials.project_id = project['id']
355 if self.credentials.project_domain_id is None:
356 self.credentials.project_domain_id = project['domain']['id']
357 if self.credentials.project_domain_name is None:
358 self.credentials.project_domain_name = \
359 project['domain']['name']
360 # Set domain fields
361 if domain is not None:
362 if self.credentials.domain_id is None:
363 self.credentials.domain_id = domain['id']
364 if self.credentials.domain_name is None:
365 self.credentials.domain_name = domain['name']
366 # Set user fields
367 if self.credentials.username is None:
368 self.credentials.username = user['name']
369 if self.credentials.user_id is None:
370 self.credentials.user_id = user['id']
371 if self.credentials.user_domain_id is None:
372 self.credentials.user_domain_id = user['domain']['id']
373 if self.credentials.user_domain_name is None:
374 self.credentials.user_domain_name = user['domain']['name']
375
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000376 def base_url(self, filters, auth_data=None):
377 """
378 Filters can be:
379 - service: compute, image, etc
380 - region: the service region
381 - endpoint_type: adminURL, publicURL, internalURL
382 - api_version: replace catalog version with this
383 - skip_path: take just the base URL
384 """
385 if auth_data is None:
386 auth_data = self.auth_data
387 token, _auth_data = auth_data
388 service = filters.get('service')
389 region = filters.get('region')
390 endpoint_type = filters.get('endpoint_type', 'public')
391
392 if service is None:
393 raise exceptions.EndpointNotFound("No service provided")
394
395 if 'URL' in endpoint_type:
396 endpoint_type = endpoint_type.replace('URL', '')
397 _base_url = None
398 catalog = _auth_data['catalog']
399 # Select entries with matching service type
400 service_catalog = [ep for ep in catalog if ep['type'] == service]
401 if len(service_catalog) > 0:
402 service_catalog = service_catalog[0]['endpoints']
403 else:
404 # No matching service
405 raise exceptions.EndpointNotFound(service)
406 # Filter by endpoint type (interface)
407 filtered_catalog = [ep for ep in service_catalog if
408 ep['interface'] == endpoint_type]
409 if len(filtered_catalog) == 0:
410 # No matching type, keep all and try matching by region at least
411 filtered_catalog = service_catalog
412 # Filter by region
413 filtered_catalog = [ep for ep in filtered_catalog if
414 ep['region'] == region]
415 if len(filtered_catalog) == 0:
416 # No matching region, take the first endpoint
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500417 filtered_catalog = [service_catalog[0]]
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000418 # There should be only one match. If not take the first.
419 _base_url = filtered_catalog[0].get('url', None)
420 if _base_url is None:
421 raise exceptions.EndpointNotFound(service)
422
423 parts = urlparse.urlparse(_base_url)
424 if filters.get('api_version', None) is not None:
425 path = "/" + filters['api_version']
426 noversion_path = "/".join(parts.path.split("/")[2:])
427 if noversion_path != "":
Mauro S. M. Rodriguesb67129e2014-02-24 15:14:40 -0500428 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000429 _base_url = _base_url.replace(parts.path, path)
430 if filters.get('skip_path', None) is not None:
431 _base_url = _base_url.replace(parts.path, "/")
432
433 return _base_url
434
435 def is_expired(self, auth_data):
436 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900437 expiry = datetime.datetime.strptime(access['expires_at'],
438 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000439 return expiry - self.token_expiry_threshold <= \
440 datetime.datetime.utcnow()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100441
442
Andrea Frittoli2095d242014-03-20 08:36:23 +0000443def get_default_credentials(credential_type, fill_in=True):
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100444 """
445 Returns configured credentials of the specified type
446 based on the configured auth_version
447 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000448 return get_credentials(fill_in=fill_in, credential_type=credential_type)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100449
450
Andrea Frittoli2095d242014-03-20 08:36:23 +0000451def get_credentials(credential_type=None, fill_in=True, **kwargs):
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100452 """
453 Builds a credentials object based on the configured auth_version
454
455 :param credential_type (string): requests credentials from tempest
456 configuration file. Valid values are defined in
457 Credentials.TYPE.
458 :param kwargs (dict): take into account only if credential_type is
459 not specified or None. Dict of credential key/value pairs
460
461 Examples:
462
463 Returns credentials from the provided parameters:
464 >>> get_credentials(username='foo', password='bar')
465
466 Returns credentials from tempest configuration:
467 >>> get_credentials(credential_type='user')
468 """
469 if CONF.identity.auth_version == 'v2':
470 credential_class = KeystoneV2Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000471 auth_provider_class = KeystoneV2AuthProvider
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100472 elif CONF.identity.auth_version == 'v3':
473 credential_class = KeystoneV3Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000474 auth_provider_class = KeystoneV3AuthProvider
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100475 else:
476 raise exceptions.InvalidConfiguration('Unsupported auth version')
477 if credential_type is not None:
478 creds = credential_class.get_default(credential_type)
479 else:
480 creds = credential_class(**kwargs)
Andrea Frittoli2095d242014-03-20 08:36:23 +0000481 # Fill in the credentials fields that were not specified
482 if fill_in:
483 auth_provider = auth_provider_class(creds)
484 creds = auth_provider.fill_credentials()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100485 return creds
486
487
488class Credentials(object):
489 """
490 Set of credentials for accessing OpenStack services
491
492 ATTRIBUTES: list of valid class attributes representing credentials.
493
494 TYPES: types of credentials available in the configuration file.
495 For each key there's a tuple (section, prefix) to match the
496 configuration options.
497 """
498
499 ATTRIBUTES = []
500 TYPES = {
501 'identity_admin': ('identity', 'admin'),
502 'compute_admin': ('compute_admin', None),
503 'user': ('identity', None),
504 'alt_user': ('identity', 'alt')
505 }
506
507 def __init__(self, **kwargs):
508 """
509 Enforce the available attributes at init time (only).
510 Additional attributes can still be set afterwards if tests need
511 to do so.
512 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000513 self._initial = kwargs
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100514 self._apply_credentials(kwargs)
515
516 def _apply_credentials(self, attr):
517 for key in attr.keys():
518 if key in self.ATTRIBUTES:
519 setattr(self, key, attr[key])
520 else:
521 raise exceptions.InvalidCredentials
522
523 def __str__(self):
524 """
525 Represent only attributes included in self.ATTRIBUTES
526 """
527 _repr = dict((k, getattr(self, k)) for k in self.ATTRIBUTES)
528 return str(_repr)
529
530 def __eq__(self, other):
531 """
532 Credentials are equal if attributes in self.ATTRIBUTES are equal
533 """
534 return str(self) == str(other)
535
536 def __getattr__(self, key):
537 # If an attribute is set, __getattr__ is not invoked
538 # If an attribute is not set, and it is a known one, return None
539 if key in self.ATTRIBUTES:
540 return None
541 else:
542 raise AttributeError
543
544 def __delitem__(self, key):
545 # For backwards compatibility, support dict behaviour
546 if key in self.ATTRIBUTES:
547 delattr(self, key)
548 else:
549 raise AttributeError
550
551 def get(self, item, default):
552 # In this patch act as dict for backward compatibility
553 try:
554 return getattr(self, item)
555 except AttributeError:
556 return default
557
558 @classmethod
559 def get_default(cls, credentials_type):
560 if credentials_type not in cls.TYPES:
561 raise exceptions.InvalidCredentials()
562 creds = cls._get_default(credentials_type)
563 if not creds.is_valid():
564 raise exceptions.InvalidConfiguration()
565 return creds
566
567 @classmethod
568 def _get_default(cls, credentials_type):
569 raise NotImplementedError
570
571 def is_valid(self):
572 raise NotImplementedError
573
Andrea Frittoli2095d242014-03-20 08:36:23 +0000574 def reset(self):
575 # First delete all known attributes
576 for key in self.ATTRIBUTES:
577 if getattr(self, key) is not None:
578 delattr(self, key)
579 # Then re-apply initial setup
580 self._apply_credentials(self._initial)
581
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100582
583class KeystoneV2Credentials(Credentials):
584
585 CONF_ATTRIBUTES = ['username', 'password', 'tenant_name']
586 ATTRIBUTES = ['user_id', 'tenant_id']
587 ATTRIBUTES.extend(CONF_ATTRIBUTES)
588
589 @classmethod
590 def _get_default(cls, credentials_type='user'):
591 params = {}
592 section, prefix = cls.TYPES[credentials_type]
593 for attr in cls.CONF_ATTRIBUTES:
594 _section = getattr(CONF, section)
595 if prefix is None:
596 params[attr] = getattr(_section, attr)
597 else:
598 params[attr] = getattr(_section, prefix + "_" + attr)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100599 return cls(**params)
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100600
601 def is_valid(self):
602 """
603 Minimum set of valid credentials, are username and password.
604 Tenant is optional.
605 """
606 return None not in (self.username, self.password)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100607
608
609class KeystoneV3Credentials(KeystoneV2Credentials):
610 """
611 Credentials suitable for the Keystone Identity V3 API
612 """
613
614 CONF_ATTRIBUTES = ['domain_name', 'password', 'tenant_name', 'username']
615 ATTRIBUTES = ['project_domain_id', 'project_domain_name', 'project_id',
616 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
617 'user_domain_name', 'user_id']
618 ATTRIBUTES.extend(CONF_ATTRIBUTES)
619
620 def __init__(self, **kwargs):
621 """
622 If domain is not specified, load the one configured for the
623 identity manager.
624 """
625 domain_fields = set(x for x in self.ATTRIBUTES if 'domain' in x)
626 if not domain_fields.intersection(kwargs.keys()):
627 kwargs['user_domain_name'] = CONF.identity.admin_domain_name
628 super(KeystoneV3Credentials, self).__init__(**kwargs)
629
630 def __setattr__(self, key, value):
631 parent = super(KeystoneV3Credentials, self)
632 # for tenant_* set both project and tenant
633 if key == 'tenant_id':
634 parent.__setattr__('project_id', value)
635 elif key == 'tenant_name':
636 parent.__setattr__('project_name', value)
637 # for project_* set both project and tenant
638 if key == 'project_id':
639 parent.__setattr__('tenant_id', value)
640 elif key == 'project_name':
641 parent.__setattr__('tenant_name', value)
642 # for *_domain_* set both user and project if not set yet
643 if key == 'user_domain_id':
644 if self.project_domain_id is None:
645 parent.__setattr__('project_domain_id', value)
646 if key == 'project_domain_id':
647 if self.user_domain_id is None:
648 parent.__setattr__('user_domain_id', value)
649 if key == 'user_domain_name':
650 if self.project_domain_name is None:
651 parent.__setattr__('project_domain_name', value)
652 if key == 'project_domain_name':
653 if self.user_domain_name is None:
654 parent.__setattr__('user_domain_name', value)
655 # support domain_name coming from config
656 if key == 'domain_name':
657 parent.__setattr__('user_domain_name', value)
658 parent.__setattr__('project_domain_name', value)
659 # finally trigger default behaviour for all attributes
660 parent.__setattr__(key, value)
661
662 def is_valid(self):
663 """
664 Valid combinations of v3 credentials (excluding token, scope)
665 - User id, password (optional domain)
666 - User name, password and its domain id/name
667 For the scope, valid combinations are:
668 - None
669 - Project id (optional domain)
670 - Project name and its domain id/name
671 """
672 valid_user_domain = any(
673 [self.user_domain_id is not None,
674 self.user_domain_name is not None])
675 valid_project_domain = any(
676 [self.project_domain_id is not None,
677 self.project_domain_name is not None])
678 valid_user = any(
679 [self.user_id is not None,
680 self.username is not None and valid_user_domain])
681 valid_project = any(
682 [self.project_name is None and self.project_id is None,
683 self.project_id is not None,
684 self.project_name is not None and valid_project_domain])
685 return all([self.password is not None, valid_user, valid_project])