blob: b1ead29adefcb7169390aae137bb91eca91a554c [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
29from tempest.services.identity.v3.xml import identity_client as xml_v3id
30from tempest.services.identity.xml import identity_client as xml_id
31
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000032
33CONF = config.CONF
34LOG = logging.getLogger(__name__)
35
36
Marc Koderer235e4f52014-07-22 10:15:08 +020037@six.add_metaclass(abc.ABCMeta)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000038class AuthProvider(object):
39 """
40 Provide authentication
41 """
42
Andrea Frittoli455e8442014-09-25 12:00:19 +010043 def __init__(self, credentials, interface=None):
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000044 """
45 :param credentials: credentials for authentication
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000046 :param interface: 'json' or 'xml'. Applicable for tempest client only
47 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010048 credentials = self._convert_credentials(credentials)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000049 if self.check_credentials(credentials):
50 self.credentials = credentials
51 else:
52 raise TypeError("Invalid credentials")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000053 self.interface = interface
Andrea Frittoli455e8442014-09-25 12:00:19 +010054 if self.interface is None:
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000055 self.interface = 'json'
56 self.cache = None
57 self.alt_auth_data = None
58 self.alt_part = None
59
Andrea Frittoli7d707a52014-04-06 11:46:32 +010060 def _convert_credentials(self, credentials):
61 # Support dict credentials for backwards compatibility
62 if isinstance(credentials, dict):
63 return get_credentials(**credentials)
64 else:
65 return credentials
66
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000067 def __str__(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +010068 return "Creds :{creds}, interface: {interface}, " \
69 "cached auth data: {cache}".format(
70 creds=self.credentials, interface=self.interface,
71 cache=self.cache)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000072
Marc Koderer235e4f52014-07-22 10:15:08 +020073 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000074 def _decorate_request(self, filters, method, url, headers=None, body=None,
75 auth_data=None):
76 """
77 Decorate request with authentication data
78 """
Marc Koderer235e4f52014-07-22 10:15:08 +020079 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000080
Marc Koderer235e4f52014-07-22 10:15:08 +020081 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000082 def _get_auth(self):
Marc Koderer235e4f52014-07-22 10:15:08 +020083 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000084
Marc Koderer235e4f52014-07-22 10:15:08 +020085 @abc.abstractmethod
Andrea Frittoli2095d242014-03-20 08:36:23 +000086 def _fill_credentials(self, auth_data_body):
Marc Koderer235e4f52014-07-22 10:15:08 +020087 return
Andrea Frittoli2095d242014-03-20 08:36:23 +000088
89 def fill_credentials(self):
90 """
91 Fill credentials object with data from auth
92 """
93 auth_data = self.get_auth()
94 self._fill_credentials(auth_data[1])
95 return self.credentials
96
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000097 @classmethod
98 def check_credentials(cls, credentials):
99 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100100 Verify credentials are valid.
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000101 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100102 return isinstance(credentials, Credentials) and credentials.is_valid()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000103
104 @property
105 def auth_data(self):
Andrea Frittoli2095d242014-03-20 08:36:23 +0000106 return self.get_auth()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000107
108 @auth_data.deleter
109 def auth_data(self):
110 self.clear_auth()
111
Andrea Frittoli2095d242014-03-20 08:36:23 +0000112 def get_auth(self):
113 """
114 Returns auth from cache if available, else auth first
115 """
116 if self.cache is None or self.is_expired(self.cache):
117 self.set_auth()
118 return self.cache
119
120 def set_auth(self):
121 """
122 Forces setting auth, ignores cache if it exists.
123 Refills credentials
124 """
125 self.cache = self._get_auth()
126 self._fill_credentials(self.cache[1])
127
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000128 def clear_auth(self):
129 """
130 Can be called to clear the access cache so that next request
131 will fetch a new token and base_url.
132 """
133 self.cache = None
Andrea Frittoli2095d242014-03-20 08:36:23 +0000134 self.credentials.reset()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000135
Marc Koderer235e4f52014-07-22 10:15:08 +0200136 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000137 def is_expired(self, auth_data):
Marc Koderer235e4f52014-07-22 10:15:08 +0200138 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000139
140 def auth_request(self, method, url, headers=None, body=None, filters=None):
141 """
142 Obtains auth data and decorates a request with that.
143 :param method: HTTP method of the request
144 :param url: relative URL of the request (path)
145 :param headers: HTTP headers of the request
146 :param body: HTTP body in case of POST / PUT
147 :param filters: select a base URL out of the catalog
148 :returns a Tuple (url, headers, body)
149 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000150 orig_req = dict(url=url, headers=headers, body=body)
151
152 auth_url, auth_headers, auth_body = self._decorate_request(
153 filters, method, url, headers, body)
154 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
155
156 # Overwrite part if the request if it has been requested
157 if self.alt_part is not None:
158 if self.alt_auth_data is not None:
159 alt_url, alt_headers, alt_body = self._decorate_request(
160 filters, method, url, headers, body,
161 auth_data=self.alt_auth_data)
162 alt_auth_req = dict(url=alt_url, headers=alt_headers,
163 body=alt_body)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000164 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
165
166 else:
167 # If alt auth data is None, skip auth in the requested part
168 auth_req[self.alt_part] = orig_req[self.alt_part]
169
170 # Next auth request will be normal, unless otherwise requested
171 self.reset_alt_auth_data()
172
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000173 return auth_req['url'], auth_req['headers'], auth_req['body']
174
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000175 def reset_alt_auth_data(self):
176 """
177 Configure auth provider to provide valid authentication data
178 """
179 self.alt_part = None
180 self.alt_auth_data = None
181
182 def set_alt_auth_data(self, request_part, auth_data):
183 """
184 Configure auth provider to provide alt authentication data
185 on a part of the *next* auth_request. If credentials are None,
186 set invalid data.
187 :param request_part: request part to contain invalid auth: url,
188 headers, body
189 :param auth_data: alternative auth_data from which to get the
190 invalid data to be injected
191 """
192 self.alt_part = request_part
193 self.alt_auth_data = auth_data
194
Marc Koderer235e4f52014-07-22 10:15:08 +0200195 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000196 def base_url(self, filters, auth_data=None):
197 """
198 Extracts the base_url based on provided filters
199 """
Marc Koderer235e4f52014-07-22 10:15:08 +0200200 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000201
202
203class KeystoneAuthProvider(AuthProvider):
204
Andrea Frittolidbd02512014-03-21 10:06:19 +0000205 token_expiry_threshold = datetime.timedelta(seconds=60)
206
Andrea Frittoli455e8442014-09-25 12:00:19 +0100207 def __init__(self, credentials, interface=None):
208 super(KeystoneAuthProvider, self).__init__(credentials, interface)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000209 self.auth_client = self._auth_client()
210
211 def _decorate_request(self, filters, method, url, headers=None, body=None,
212 auth_data=None):
213 if auth_data is None:
214 auth_data = self.auth_data
215 token, _ = auth_data
216 base_url = self.base_url(filters=filters, auth_data=auth_data)
217 # build authenticated request
218 # returns new request, it does not touch the original values
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500219 _headers = copy.deepcopy(headers) if headers is not None else {}
Daisuke Morita02f840b2014-03-19 20:51:01 +0900220 _headers['X-Auth-Token'] = str(token)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000221 if url is None or url == "":
222 _url = base_url
223 else:
224 # Join base URL and url, and remove multiple contiguous slashes
225 _url = "/".join([base_url, url])
226 parts = [x for x in urlparse.urlparse(_url)]
227 parts[2] = re.sub("/{2,}", "/", parts[2])
228 _url = urlparse.urlunparse(parts)
229 # no change to method or body
Daisuke Morita02f840b2014-03-19 20:51:01 +0900230 return str(_url), _headers, body
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000231
Marc Koderer235e4f52014-07-22 10:15:08 +0200232 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000233 def _auth_client(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200234 return
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_params(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200238 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000239
240 def _get_auth(self):
241 # Bypasses the cache
Andrea Frittoli455e8442014-09-25 12:00:19 +0100242 auth_func = getattr(self.auth_client, 'get_token')
243 auth_params = self._auth_params()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000244
Andrea Frittoli455e8442014-09-25 12:00:19 +0100245 # returns token, auth_data
246 token, auth_data = auth_func(**auth_params)
247 return token, auth_data
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000248
249 def get_token(self):
250 return self.auth_data[0]
251
252
253class KeystoneV2AuthProvider(KeystoneAuthProvider):
254
255 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
256
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000257 def _auth_client(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100258 if self.interface == 'json':
259 return json_id.TokenClientJSON()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000260 else:
Andrea Frittoli455e8442014-09-25 12:00:19 +0100261 return xml_id.TokenClientXML()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000262
263 def _auth_params(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100264 return dict(
265 user=self.credentials.username,
266 password=self.credentials.password,
267 tenant=self.credentials.tenant_name,
268 auth_data=True)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000269
Andrea Frittoli2095d242014-03-20 08:36:23 +0000270 def _fill_credentials(self, auth_data_body):
271 tenant = auth_data_body['token']['tenant']
272 user = auth_data_body['user']
273 if self.credentials.tenant_name is None:
274 self.credentials.tenant_name = tenant['name']
275 if self.credentials.tenant_id is None:
276 self.credentials.tenant_id = tenant['id']
277 if self.credentials.username is None:
278 self.credentials.username = user['name']
279 if self.credentials.user_id is None:
280 self.credentials.user_id = user['id']
281
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000282 def base_url(self, filters, auth_data=None):
283 """
284 Filters can be:
285 - service: compute, image, etc
286 - region: the service region
287 - endpoint_type: adminURL, publicURL, internalURL
288 - api_version: replace catalog version with this
289 - skip_path: take just the base URL
290 """
291 if auth_data is None:
292 auth_data = self.auth_data
293 token, _auth_data = auth_data
294 service = filters.get('service')
295 region = filters.get('region')
296 endpoint_type = filters.get('endpoint_type', 'publicURL')
297
298 if service is None:
299 raise exceptions.EndpointNotFound("No service provided")
300
301 _base_url = None
302 for ep in _auth_data['serviceCatalog']:
303 if ep["type"] == service:
304 for _ep in ep['endpoints']:
305 if region is not None and _ep['region'] == region:
306 _base_url = _ep.get(endpoint_type)
307 if not _base_url:
308 # No region matching, use the first
309 _base_url = ep['endpoints'][0].get(endpoint_type)
310 break
311 if _base_url is None:
312 raise exceptions.EndpointNotFound(service)
313
314 parts = urlparse.urlparse(_base_url)
315 if filters.get('api_version', None) is not None:
316 path = "/" + filters['api_version']
317 noversion_path = "/".join(parts.path.split("/")[2:])
318 if noversion_path != "":
Zhi Kun Liube8bdbc2014-02-08 11:40:57 +0800319 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000320 _base_url = _base_url.replace(parts.path, path)
David Kranz5a2cb452014-07-29 13:51:26 -0400321 if filters.get('skip_path', None) is not None and parts.path != '':
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000322 _base_url = _base_url.replace(parts.path, "/")
323
324 return _base_url
325
326 def is_expired(self, auth_data):
327 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900328 expiry = datetime.datetime.strptime(access['token']['expires'],
329 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000330 return expiry - self.token_expiry_threshold <= \
331 datetime.datetime.utcnow()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000332
333
334class KeystoneV3AuthProvider(KeystoneAuthProvider):
335
336 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
337
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000338 def _auth_client(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100339 if self.interface == 'json':
340 return json_v3id.V3TokenClientJSON()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000341 else:
Andrea Frittoli455e8442014-09-25 12:00:19 +0100342 return xml_v3id.V3TokenClientXML()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000343
344 def _auth_params(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100345 return dict(
346 user=self.credentials.username,
347 password=self.credentials.password,
348 tenant=self.credentials.tenant_name,
349 domain=self.credentials.user_domain_name,
350 auth_data=True)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000351
Andrea Frittoli2095d242014-03-20 08:36:23 +0000352 def _fill_credentials(self, auth_data_body):
353 # project or domain, depending on the scope
354 project = auth_data_body.get('project', None)
355 domain = auth_data_body.get('domain', None)
356 # user is always there
357 user = auth_data_body['user']
358 # Set project fields
359 if project is not None:
360 if self.credentials.project_name is None:
361 self.credentials.project_name = project['name']
362 if self.credentials.project_id is None:
363 self.credentials.project_id = project['id']
364 if self.credentials.project_domain_id is None:
365 self.credentials.project_domain_id = project['domain']['id']
366 if self.credentials.project_domain_name is None:
367 self.credentials.project_domain_name = \
368 project['domain']['name']
369 # Set domain fields
370 if domain is not None:
371 if self.credentials.domain_id is None:
372 self.credentials.domain_id = domain['id']
373 if self.credentials.domain_name is None:
374 self.credentials.domain_name = domain['name']
375 # Set user fields
376 if self.credentials.username is None:
377 self.credentials.username = user['name']
378 if self.credentials.user_id is None:
379 self.credentials.user_id = user['id']
380 if self.credentials.user_domain_id is None:
381 self.credentials.user_domain_id = user['domain']['id']
382 if self.credentials.user_domain_name is None:
383 self.credentials.user_domain_name = user['domain']['name']
384
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000385 def base_url(self, filters, auth_data=None):
386 """
387 Filters can be:
388 - service: compute, image, etc
389 - region: the service region
390 - endpoint_type: adminURL, publicURL, internalURL
391 - api_version: replace catalog version with this
392 - skip_path: take just the base URL
393 """
394 if auth_data is None:
395 auth_data = self.auth_data
396 token, _auth_data = auth_data
397 service = filters.get('service')
398 region = filters.get('region')
399 endpoint_type = filters.get('endpoint_type', 'public')
400
401 if service is None:
402 raise exceptions.EndpointNotFound("No service provided")
403
404 if 'URL' in endpoint_type:
405 endpoint_type = endpoint_type.replace('URL', '')
406 _base_url = None
407 catalog = _auth_data['catalog']
408 # Select entries with matching service type
409 service_catalog = [ep for ep in catalog if ep['type'] == service]
410 if len(service_catalog) > 0:
411 service_catalog = service_catalog[0]['endpoints']
412 else:
413 # No matching service
414 raise exceptions.EndpointNotFound(service)
415 # Filter by endpoint type (interface)
416 filtered_catalog = [ep for ep in service_catalog if
417 ep['interface'] == endpoint_type]
418 if len(filtered_catalog) == 0:
419 # No matching type, keep all and try matching by region at least
420 filtered_catalog = service_catalog
421 # Filter by region
422 filtered_catalog = [ep for ep in filtered_catalog if
423 ep['region'] == region]
424 if len(filtered_catalog) == 0:
425 # No matching region, take the first endpoint
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500426 filtered_catalog = [service_catalog[0]]
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000427 # There should be only one match. If not take the first.
428 _base_url = filtered_catalog[0].get('url', None)
429 if _base_url is None:
430 raise exceptions.EndpointNotFound(service)
431
432 parts = urlparse.urlparse(_base_url)
433 if filters.get('api_version', None) is not None:
434 path = "/" + filters['api_version']
435 noversion_path = "/".join(parts.path.split("/")[2:])
436 if noversion_path != "":
Mauro S. M. Rodriguesb67129e2014-02-24 15:14:40 -0500437 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000438 _base_url = _base_url.replace(parts.path, path)
439 if filters.get('skip_path', None) is not None:
440 _base_url = _base_url.replace(parts.path, "/")
441
442 return _base_url
443
444 def is_expired(self, auth_data):
445 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900446 expiry = datetime.datetime.strptime(access['expires_at'],
447 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000448 return expiry - self.token_expiry_threshold <= \
449 datetime.datetime.utcnow()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100450
451
Andrea Frittoli2095d242014-03-20 08:36:23 +0000452def get_default_credentials(credential_type, fill_in=True):
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100453 """
454 Returns configured credentials of the specified type
455 based on the configured auth_version
456 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000457 return get_credentials(fill_in=fill_in, credential_type=credential_type)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100458
459
Andrea Frittoli2095d242014-03-20 08:36:23 +0000460def get_credentials(credential_type=None, fill_in=True, **kwargs):
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100461 """
462 Builds a credentials object based on the configured auth_version
463
464 :param credential_type (string): requests credentials from tempest
465 configuration file. Valid values are defined in
466 Credentials.TYPE.
467 :param kwargs (dict): take into account only if credential_type is
468 not specified or None. Dict of credential key/value pairs
469
470 Examples:
471
472 Returns credentials from the provided parameters:
473 >>> get_credentials(username='foo', password='bar')
474
475 Returns credentials from tempest configuration:
476 >>> get_credentials(credential_type='user')
477 """
478 if CONF.identity.auth_version == 'v2':
479 credential_class = KeystoneV2Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000480 auth_provider_class = KeystoneV2AuthProvider
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100481 elif CONF.identity.auth_version == 'v3':
482 credential_class = KeystoneV3Credentials
Andrea Frittoli2095d242014-03-20 08:36:23 +0000483 auth_provider_class = KeystoneV3AuthProvider
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100484 else:
485 raise exceptions.InvalidConfiguration('Unsupported auth version')
486 if credential_type is not None:
487 creds = credential_class.get_default(credential_type)
488 else:
489 creds = credential_class(**kwargs)
Andrea Frittoli2095d242014-03-20 08:36:23 +0000490 # Fill in the credentials fields that were not specified
491 if fill_in:
492 auth_provider = auth_provider_class(creds)
493 creds = auth_provider.fill_credentials()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100494 return creds
495
496
497class Credentials(object):
498 """
499 Set of credentials for accessing OpenStack services
500
501 ATTRIBUTES: list of valid class attributes representing credentials.
502
503 TYPES: types of credentials available in the configuration file.
504 For each key there's a tuple (section, prefix) to match the
505 configuration options.
506 """
507
508 ATTRIBUTES = []
509 TYPES = {
510 'identity_admin': ('identity', 'admin'),
511 'compute_admin': ('compute_admin', None),
512 'user': ('identity', None),
513 'alt_user': ('identity', 'alt')
514 }
515
516 def __init__(self, **kwargs):
517 """
518 Enforce the available attributes at init time (only).
519 Additional attributes can still be set afterwards if tests need
520 to do so.
521 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000522 self._initial = kwargs
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100523 self._apply_credentials(kwargs)
524
525 def _apply_credentials(self, attr):
526 for key in attr.keys():
527 if key in self.ATTRIBUTES:
528 setattr(self, key, attr[key])
529 else:
530 raise exceptions.InvalidCredentials
531
532 def __str__(self):
533 """
534 Represent only attributes included in self.ATTRIBUTES
535 """
536 _repr = dict((k, getattr(self, k)) for k in self.ATTRIBUTES)
537 return str(_repr)
538
539 def __eq__(self, other):
540 """
541 Credentials are equal if attributes in self.ATTRIBUTES are equal
542 """
543 return str(self) == str(other)
544
545 def __getattr__(self, key):
546 # If an attribute is set, __getattr__ is not invoked
547 # If an attribute is not set, and it is a known one, return None
548 if key in self.ATTRIBUTES:
549 return None
550 else:
551 raise AttributeError
552
553 def __delitem__(self, key):
554 # For backwards compatibility, support dict behaviour
555 if key in self.ATTRIBUTES:
556 delattr(self, key)
557 else:
558 raise AttributeError
559
560 def get(self, item, default):
561 # In this patch act as dict for backward compatibility
562 try:
563 return getattr(self, item)
564 except AttributeError:
565 return default
566
567 @classmethod
568 def get_default(cls, credentials_type):
569 if credentials_type not in cls.TYPES:
570 raise exceptions.InvalidCredentials()
571 creds = cls._get_default(credentials_type)
572 if not creds.is_valid():
573 raise exceptions.InvalidConfiguration()
574 return creds
575
576 @classmethod
577 def _get_default(cls, credentials_type):
578 raise NotImplementedError
579
580 def is_valid(self):
581 raise NotImplementedError
582
Andrea Frittoli2095d242014-03-20 08:36:23 +0000583 def reset(self):
584 # First delete all known attributes
585 for key in self.ATTRIBUTES:
586 if getattr(self, key) is not None:
587 delattr(self, key)
588 # Then re-apply initial setup
589 self._apply_credentials(self._initial)
590
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100591
592class KeystoneV2Credentials(Credentials):
593
594 CONF_ATTRIBUTES = ['username', 'password', 'tenant_name']
595 ATTRIBUTES = ['user_id', 'tenant_id']
596 ATTRIBUTES.extend(CONF_ATTRIBUTES)
597
598 @classmethod
599 def _get_default(cls, credentials_type='user'):
600 params = {}
601 section, prefix = cls.TYPES[credentials_type]
602 for attr in cls.CONF_ATTRIBUTES:
603 _section = getattr(CONF, section)
604 if prefix is None:
605 params[attr] = getattr(_section, attr)
606 else:
607 params[attr] = getattr(_section, prefix + "_" + attr)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100608 return cls(**params)
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100609
610 def is_valid(self):
611 """
612 Minimum set of valid credentials, are username and password.
613 Tenant is optional.
614 """
615 return None not in (self.username, self.password)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100616
617
618class KeystoneV3Credentials(KeystoneV2Credentials):
619 """
620 Credentials suitable for the Keystone Identity V3 API
621 """
622
623 CONF_ATTRIBUTES = ['domain_name', 'password', 'tenant_name', 'username']
624 ATTRIBUTES = ['project_domain_id', 'project_domain_name', 'project_id',
625 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
626 'user_domain_name', 'user_id']
627 ATTRIBUTES.extend(CONF_ATTRIBUTES)
628
629 def __init__(self, **kwargs):
630 """
631 If domain is not specified, load the one configured for the
632 identity manager.
633 """
634 domain_fields = set(x for x in self.ATTRIBUTES if 'domain' in x)
635 if not domain_fields.intersection(kwargs.keys()):
636 kwargs['user_domain_name'] = CONF.identity.admin_domain_name
637 super(KeystoneV3Credentials, self).__init__(**kwargs)
638
639 def __setattr__(self, key, value):
640 parent = super(KeystoneV3Credentials, self)
641 # for tenant_* set both project and tenant
642 if key == 'tenant_id':
643 parent.__setattr__('project_id', value)
644 elif key == 'tenant_name':
645 parent.__setattr__('project_name', value)
646 # for project_* set both project and tenant
647 if key == 'project_id':
648 parent.__setattr__('tenant_id', value)
649 elif key == 'project_name':
650 parent.__setattr__('tenant_name', value)
651 # for *_domain_* set both user and project if not set yet
652 if key == 'user_domain_id':
653 if self.project_domain_id is None:
654 parent.__setattr__('project_domain_id', value)
655 if key == 'project_domain_id':
656 if self.user_domain_id is None:
657 parent.__setattr__('user_domain_id', value)
658 if key == 'user_domain_name':
659 if self.project_domain_name is None:
660 parent.__setattr__('project_domain_name', value)
661 if key == 'project_domain_name':
662 if self.user_domain_name is None:
663 parent.__setattr__('user_domain_name', value)
664 # support domain_name coming from config
665 if key == 'domain_name':
666 parent.__setattr__('user_domain_name', value)
667 parent.__setattr__('project_domain_name', value)
668 # finally trigger default behaviour for all attributes
669 parent.__setattr__(key, value)
670
671 def is_valid(self):
672 """
673 Valid combinations of v3 credentials (excluding token, scope)
674 - User id, password (optional domain)
675 - User name, password and its domain id/name
676 For the scope, valid combinations are:
677 - None
678 - Project id (optional domain)
679 - Project name and its domain id/name
680 """
681 valid_user_domain = any(
682 [self.user_domain_id is not None,
683 self.user_domain_name is not None])
684 valid_project_domain = any(
685 [self.project_domain_id is not None,
686 self.project_domain_name is not None])
687 valid_user = any(
688 [self.user_id is not None,
689 self.username is not None and valid_user_domain])
690 valid_project = any(
691 [self.project_name is None and self.project_id is None,
692 self.project_id is not None,
693 self.project_name is not None and valid_project_domain])
694 return all([self.password is not None, valid_user, valid_project])