blob: 7b00f2add38767fe4b0ad286e45af176bb630a5e [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
Matthew Treinish96e9e882014-06-09 18:37:19 -040025from tempest.openstack.common import log as logging
ghanshyamc0edda02015-02-06 15:51:40 +090026from tempest.services.identity.json import token_client as json_id
27from tempest.services.identity.v3.json import token_client as json_v3id
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000028
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000029
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000030LOG = logging.getLogger(__name__)
31
32
Marc Koderer235e4f52014-07-22 10:15:08 +020033@six.add_metaclass(abc.ABCMeta)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000034class AuthProvider(object):
35 """
36 Provide authentication
37 """
38
Andrea Frittolic0978352015-02-06 15:57:40 +000039 def __init__(self, credentials):
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000040 """
41 :param credentials: credentials for authentication
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000042 """
43 if self.check_credentials(credentials):
44 self.credentials = credentials
45 else:
46 raise TypeError("Invalid credentials")
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000047 self.cache = None
48 self.alt_auth_data = None
49 self.alt_part = None
50
51 def __str__(self):
Andrea Frittolic0978352015-02-06 15:57:40 +000052 return "Creds :{creds}, cached auth data: {cache}".format(
53 creds=self.credentials, cache=self.cache)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000054
Marc Koderer235e4f52014-07-22 10:15:08 +020055 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000056 def _decorate_request(self, filters, method, url, headers=None, body=None,
57 auth_data=None):
58 """
59 Decorate request with authentication data
60 """
Marc Koderer235e4f52014-07-22 10:15:08 +020061 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000062
Marc Koderer235e4f52014-07-22 10:15:08 +020063 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000064 def _get_auth(self):
Marc Koderer235e4f52014-07-22 10:15:08 +020065 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000066
Marc Koderer235e4f52014-07-22 10:15:08 +020067 @abc.abstractmethod
Andrea Frittoli2095d242014-03-20 08:36:23 +000068 def _fill_credentials(self, auth_data_body):
Marc Koderer235e4f52014-07-22 10:15:08 +020069 return
Andrea Frittoli2095d242014-03-20 08:36:23 +000070
71 def fill_credentials(self):
72 """
73 Fill credentials object with data from auth
74 """
75 auth_data = self.get_auth()
76 self._fill_credentials(auth_data[1])
77 return self.credentials
78
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000079 @classmethod
80 def check_credentials(cls, credentials):
81 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010082 Verify credentials are valid.
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000083 """
Andrea Frittoli7d707a52014-04-06 11:46:32 +010084 return isinstance(credentials, Credentials) and credentials.is_valid()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000085
86 @property
87 def auth_data(self):
Andrea Frittoli2095d242014-03-20 08:36:23 +000088 return self.get_auth()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000089
90 @auth_data.deleter
91 def auth_data(self):
92 self.clear_auth()
93
Andrea Frittoli2095d242014-03-20 08:36:23 +000094 def get_auth(self):
95 """
96 Returns auth from cache if available, else auth first
97 """
98 if self.cache is None or self.is_expired(self.cache):
99 self.set_auth()
100 return self.cache
101
102 def set_auth(self):
103 """
104 Forces setting auth, ignores cache if it exists.
105 Refills credentials
106 """
107 self.cache = self._get_auth()
108 self._fill_credentials(self.cache[1])
109
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000110 def clear_auth(self):
111 """
112 Can be called to clear the access cache so that next request
113 will fetch a new token and base_url.
114 """
115 self.cache = None
Andrea Frittoli2095d242014-03-20 08:36:23 +0000116 self.credentials.reset()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000117
Marc Koderer235e4f52014-07-22 10:15:08 +0200118 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000119 def is_expired(self, auth_data):
Marc Koderer235e4f52014-07-22 10:15:08 +0200120 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000121
122 def auth_request(self, method, url, headers=None, body=None, filters=None):
123 """
124 Obtains auth data and decorates a request with that.
125 :param method: HTTP method of the request
126 :param url: relative URL of the request (path)
127 :param headers: HTTP headers of the request
128 :param body: HTTP body in case of POST / PUT
129 :param filters: select a base URL out of the catalog
130 :returns a Tuple (url, headers, body)
131 """
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000132 orig_req = dict(url=url, headers=headers, body=body)
133
134 auth_url, auth_headers, auth_body = self._decorate_request(
135 filters, method, url, headers, body)
136 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
137
138 # Overwrite part if the request if it has been requested
139 if self.alt_part is not None:
140 if self.alt_auth_data is not None:
141 alt_url, alt_headers, alt_body = self._decorate_request(
142 filters, method, url, headers, body,
143 auth_data=self.alt_auth_data)
144 alt_auth_req = dict(url=alt_url, headers=alt_headers,
145 body=alt_body)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000146 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
147
148 else:
149 # If alt auth data is None, skip auth in the requested part
150 auth_req[self.alt_part] = orig_req[self.alt_part]
151
152 # Next auth request will be normal, unless otherwise requested
153 self.reset_alt_auth_data()
154
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000155 return auth_req['url'], auth_req['headers'], auth_req['body']
156
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000157 def reset_alt_auth_data(self):
158 """
159 Configure auth provider to provide valid authentication data
160 """
161 self.alt_part = None
162 self.alt_auth_data = None
163
164 def set_alt_auth_data(self, request_part, auth_data):
165 """
166 Configure auth provider to provide alt authentication data
167 on a part of the *next* auth_request. If credentials are None,
168 set invalid data.
169 :param request_part: request part to contain invalid auth: url,
170 headers, body
171 :param auth_data: alternative auth_data from which to get the
172 invalid data to be injected
173 """
174 self.alt_part = request_part
175 self.alt_auth_data = auth_data
176
Marc Koderer235e4f52014-07-22 10:15:08 +0200177 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000178 def base_url(self, filters, auth_data=None):
179 """
180 Extracts the base_url based on provided filters
181 """
Marc Koderer235e4f52014-07-22 10:15:08 +0200182 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000183
184
185class KeystoneAuthProvider(AuthProvider):
186
Andrea Frittolidbd02512014-03-21 10:06:19 +0000187 token_expiry_threshold = datetime.timedelta(seconds=60)
188
ghanshyam5ff763f2015-02-18 16:15:58 +0900189 def __init__(self, credentials, auth_url):
Andrea Frittolic0978352015-02-06 15:57:40 +0000190 super(KeystoneAuthProvider, self).__init__(credentials)
ghanshyam5ff763f2015-02-18 16:15:58 +0900191 self.auth_client = self._auth_client(auth_url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000192
193 def _decorate_request(self, filters, method, url, headers=None, body=None,
194 auth_data=None):
195 if auth_data is None:
196 auth_data = self.auth_data
197 token, _ = auth_data
198 base_url = self.base_url(filters=filters, auth_data=auth_data)
199 # build authenticated request
200 # returns new request, it does not touch the original values
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500201 _headers = copy.deepcopy(headers) if headers is not None else {}
Daisuke Morita02f840b2014-03-19 20:51:01 +0900202 _headers['X-Auth-Token'] = str(token)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000203 if url is None or url == "":
204 _url = base_url
205 else:
206 # Join base URL and url, and remove multiple contiguous slashes
207 _url = "/".join([base_url, url])
208 parts = [x for x in urlparse.urlparse(_url)]
209 parts[2] = re.sub("/{2,}", "/", parts[2])
210 _url = urlparse.urlunparse(parts)
211 # no change to method or body
Daisuke Morita02f840b2014-03-19 20:51:01 +0900212 return str(_url), _headers, body
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000213
Marc Koderer235e4f52014-07-22 10:15:08 +0200214 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000215 def _auth_client(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200216 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000217
Marc Koderer235e4f52014-07-22 10:15:08 +0200218 @abc.abstractmethod
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000219 def _auth_params(self):
Marc Koderer235e4f52014-07-22 10:15:08 +0200220 return
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000221
222 def _get_auth(self):
223 # Bypasses the cache
Andrea Frittoli455e8442014-09-25 12:00:19 +0100224 auth_func = getattr(self.auth_client, 'get_token')
225 auth_params = self._auth_params()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000226
Andrea Frittoli455e8442014-09-25 12:00:19 +0100227 # returns token, auth_data
228 token, auth_data = auth_func(**auth_params)
229 return token, auth_data
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000230
231 def get_token(self):
232 return self.auth_data[0]
233
234
235class KeystoneV2AuthProvider(KeystoneAuthProvider):
236
237 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
238
ghanshyam5ff763f2015-02-18 16:15:58 +0900239 def _auth_client(self, auth_url):
240 return json_id.TokenClientJSON(auth_url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000241
242 def _auth_params(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100243 return dict(
244 user=self.credentials.username,
245 password=self.credentials.password,
246 tenant=self.credentials.tenant_name,
247 auth_data=True)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000248
Andrea Frittoli2095d242014-03-20 08:36:23 +0000249 def _fill_credentials(self, auth_data_body):
250 tenant = auth_data_body['token']['tenant']
251 user = auth_data_body['user']
252 if self.credentials.tenant_name is None:
253 self.credentials.tenant_name = tenant['name']
254 if self.credentials.tenant_id is None:
255 self.credentials.tenant_id = tenant['id']
256 if self.credentials.username is None:
257 self.credentials.username = user['name']
258 if self.credentials.user_id is None:
259 self.credentials.user_id = user['id']
260
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000261 def base_url(self, filters, auth_data=None):
262 """
263 Filters can be:
264 - service: compute, image, etc
265 - region: the service region
266 - endpoint_type: adminURL, publicURL, internalURL
267 - api_version: replace catalog version with this
268 - skip_path: take just the base URL
269 """
270 if auth_data is None:
271 auth_data = self.auth_data
272 token, _auth_data = auth_data
273 service = filters.get('service')
274 region = filters.get('region')
275 endpoint_type = filters.get('endpoint_type', 'publicURL')
276
277 if service is None:
278 raise exceptions.EndpointNotFound("No service provided")
279
280 _base_url = None
281 for ep in _auth_data['serviceCatalog']:
282 if ep["type"] == service:
283 for _ep in ep['endpoints']:
284 if region is not None and _ep['region'] == region:
285 _base_url = _ep.get(endpoint_type)
286 if not _base_url:
287 # No region matching, use the first
288 _base_url = ep['endpoints'][0].get(endpoint_type)
289 break
290 if _base_url is None:
291 raise exceptions.EndpointNotFound(service)
292
293 parts = urlparse.urlparse(_base_url)
294 if filters.get('api_version', None) is not None:
295 path = "/" + filters['api_version']
296 noversion_path = "/".join(parts.path.split("/")[2:])
297 if noversion_path != "":
Zhi Kun Liube8bdbc2014-02-08 11:40:57 +0800298 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000299 _base_url = _base_url.replace(parts.path, path)
David Kranz5a2cb452014-07-29 13:51:26 -0400300 if filters.get('skip_path', None) is not None and parts.path != '':
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000301 _base_url = _base_url.replace(parts.path, "/")
302
303 return _base_url
304
305 def is_expired(self, auth_data):
306 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900307 expiry = datetime.datetime.strptime(access['token']['expires'],
308 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000309 return expiry - self.token_expiry_threshold <= \
310 datetime.datetime.utcnow()
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000311
312
313class KeystoneV3AuthProvider(KeystoneAuthProvider):
314
315 EXPIRY_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
316
ghanshyam5ff763f2015-02-18 16:15:58 +0900317 def _auth_client(self, auth_url):
318 return json_v3id.V3TokenClientJSON(auth_url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000319
320 def _auth_params(self):
Andrea Frittoli455e8442014-09-25 12:00:19 +0100321 return dict(
322 user=self.credentials.username,
323 password=self.credentials.password,
Jamie Lennoxe5a95d42015-02-11 07:19:57 +0000324 project=self.credentials.tenant_name,
325 user_domain=self.credentials.user_domain_name,
326 project_domain=self.credentials.project_domain_name,
Andrea Frittoli455e8442014-09-25 12:00:19 +0100327 auth_data=True)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000328
Andrea Frittoli2095d242014-03-20 08:36:23 +0000329 def _fill_credentials(self, auth_data_body):
330 # project or domain, depending on the scope
331 project = auth_data_body.get('project', None)
332 domain = auth_data_body.get('domain', None)
333 # user is always there
334 user = auth_data_body['user']
335 # Set project fields
336 if project is not None:
337 if self.credentials.project_name is None:
338 self.credentials.project_name = project['name']
339 if self.credentials.project_id is None:
340 self.credentials.project_id = project['id']
341 if self.credentials.project_domain_id is None:
342 self.credentials.project_domain_id = project['domain']['id']
343 if self.credentials.project_domain_name is None:
344 self.credentials.project_domain_name = \
345 project['domain']['name']
346 # Set domain fields
347 if domain is not None:
348 if self.credentials.domain_id is None:
349 self.credentials.domain_id = domain['id']
350 if self.credentials.domain_name is None:
351 self.credentials.domain_name = domain['name']
352 # Set user fields
353 if self.credentials.username is None:
354 self.credentials.username = user['name']
355 if self.credentials.user_id is None:
356 self.credentials.user_id = user['id']
357 if self.credentials.user_domain_id is None:
358 self.credentials.user_domain_id = user['domain']['id']
359 if self.credentials.user_domain_name is None:
360 self.credentials.user_domain_name = user['domain']['name']
361
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000362 def base_url(self, filters, auth_data=None):
363 """
364 Filters can be:
365 - service: compute, image, etc
366 - region: the service region
367 - endpoint_type: adminURL, publicURL, internalURL
368 - api_version: replace catalog version with this
369 - skip_path: take just the base URL
370 """
371 if auth_data is None:
372 auth_data = self.auth_data
373 token, _auth_data = auth_data
374 service = filters.get('service')
375 region = filters.get('region')
376 endpoint_type = filters.get('endpoint_type', 'public')
377
378 if service is None:
379 raise exceptions.EndpointNotFound("No service provided")
380
381 if 'URL' in endpoint_type:
382 endpoint_type = endpoint_type.replace('URL', '')
383 _base_url = None
384 catalog = _auth_data['catalog']
385 # Select entries with matching service type
386 service_catalog = [ep for ep in catalog if ep['type'] == service]
387 if len(service_catalog) > 0:
388 service_catalog = service_catalog[0]['endpoints']
389 else:
390 # No matching service
391 raise exceptions.EndpointNotFound(service)
392 # Filter by endpoint type (interface)
393 filtered_catalog = [ep for ep in service_catalog if
394 ep['interface'] == endpoint_type]
395 if len(filtered_catalog) == 0:
396 # No matching type, keep all and try matching by region at least
397 filtered_catalog = service_catalog
398 # Filter by region
399 filtered_catalog = [ep for ep in filtered_catalog if
400 ep['region'] == region]
401 if len(filtered_catalog) == 0:
402 # No matching region, take the first endpoint
Mauro S. M. Rodriguesd3d0d582014-02-19 07:51:56 -0500403 filtered_catalog = [service_catalog[0]]
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000404 # There should be only one match. If not take the first.
405 _base_url = filtered_catalog[0].get('url', None)
406 if _base_url is None:
407 raise exceptions.EndpointNotFound(service)
408
409 parts = urlparse.urlparse(_base_url)
410 if filters.get('api_version', None) is not None:
411 path = "/" + filters['api_version']
412 noversion_path = "/".join(parts.path.split("/")[2:])
413 if noversion_path != "":
Mauro S. M. Rodriguesb67129e2014-02-24 15:14:40 -0500414 path += "/" + noversion_path
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000415 _base_url = _base_url.replace(parts.path, path)
416 if filters.get('skip_path', None) is not None:
417 _base_url = _base_url.replace(parts.path, "/")
418
419 return _base_url
420
421 def is_expired(self, auth_data):
422 _, access = auth_data
Masayuki Igawa1edf94f2014-03-04 18:34:16 +0900423 expiry = datetime.datetime.strptime(access['expires_at'],
424 self.EXPIRY_DATE_FORMAT)
Andrea Frittolidbd02512014-03-21 10:06:19 +0000425 return expiry - self.token_expiry_threshold <= \
426 datetime.datetime.utcnow()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100427
428
Andrea Frittoli878d5ab2015-01-30 13:22:50 +0000429def is_identity_version_supported(identity_version):
430 return identity_version in IDENTITY_VERSION
431
432
ghanshyam5ff763f2015-02-18 16:15:58 +0900433def get_credentials(auth_url, fill_in=True, identity_version='v2', **kwargs):
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100434 """
435 Builds a credentials object based on the configured auth_version
436
ghanshyam5ff763f2015-02-18 16:15:58 +0900437 :param auth_url (string): Full URI of the OpenStack Identity API(Keystone)
438 which is used to fetch the token from Identity service.
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000439 :param fill_in (boolean): obtain a token and fill in all credential
440 details provided by the identity service. When fill_in is not
441 specified, credentials are not validated. Validation can be invoked
442 by invoking ``is_valid()``
Andrea Frittoli878d5ab2015-01-30 13:22:50 +0000443 :param identity_version (string): identity API version is used to
444 select the matching auth provider and credentials class
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000445 :param kwargs (dict): Dict of credential key/value pairs
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100446
447 Examples:
448
449 Returns credentials from the provided parameters:
450 >>> get_credentials(username='foo', password='bar')
451
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000452 Returns credentials including IDs:
453 >>> get_credentials(username='foo', password='bar', fill_in=True)
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100454 """
Andrea Frittoli878d5ab2015-01-30 13:22:50 +0000455 if not is_identity_version_supported(identity_version):
456 raise exceptions.InvalidIdentityVersion(
457 identity_version=identity_version)
458
459 credential_class, auth_provider_class = IDENTITY_VERSION.get(
460 identity_version)
461
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000462 creds = credential_class(**kwargs)
Andrea Frittoli2095d242014-03-20 08:36:23 +0000463 # Fill in the credentials fields that were not specified
464 if fill_in:
ghanshyam5ff763f2015-02-18 16:15:58 +0900465 auth_provider = auth_provider_class(creds, auth_url)
Andrea Frittoli2095d242014-03-20 08:36:23 +0000466 creds = auth_provider.fill_credentials()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100467 return creds
468
469
470class Credentials(object):
471 """
472 Set of credentials for accessing OpenStack services
473
474 ATTRIBUTES: list of valid class attributes representing credentials.
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100475 """
476
477 ATTRIBUTES = []
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100478
479 def __init__(self, **kwargs):
480 """
481 Enforce the available attributes at init time (only).
482 Additional attributes can still be set afterwards if tests need
483 to do so.
484 """
Andrea Frittoli2095d242014-03-20 08:36:23 +0000485 self._initial = kwargs
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100486 self._apply_credentials(kwargs)
487
488 def _apply_credentials(self, attr):
489 for key in attr.keys():
490 if key in self.ATTRIBUTES:
491 setattr(self, key, attr[key])
492 else:
493 raise exceptions.InvalidCredentials
494
495 def __str__(self):
496 """
497 Represent only attributes included in self.ATTRIBUTES
498 """
499 _repr = dict((k, getattr(self, k)) for k in self.ATTRIBUTES)
500 return str(_repr)
501
502 def __eq__(self, other):
503 """
504 Credentials are equal if attributes in self.ATTRIBUTES are equal
505 """
506 return str(self) == str(other)
507
508 def __getattr__(self, key):
509 # If an attribute is set, __getattr__ is not invoked
510 # If an attribute is not set, and it is a known one, return None
511 if key in self.ATTRIBUTES:
512 return None
513 else:
514 raise AttributeError
515
516 def __delitem__(self, key):
517 # For backwards compatibility, support dict behaviour
518 if key in self.ATTRIBUTES:
519 delattr(self, key)
520 else:
521 raise AttributeError
522
523 def get(self, item, default):
524 # In this patch act as dict for backward compatibility
525 try:
526 return getattr(self, item)
527 except AttributeError:
528 return default
529
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000530 def get_init_attributes(self):
531 return self._initial.keys()
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100532
533 def is_valid(self):
534 raise NotImplementedError
535
Andrea Frittoli2095d242014-03-20 08:36:23 +0000536 def reset(self):
537 # First delete all known attributes
538 for key in self.ATTRIBUTES:
539 if getattr(self, key) is not None:
540 delattr(self, key)
541 # Then re-apply initial setup
542 self._apply_credentials(self._initial)
543
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100544
545class KeystoneV2Credentials(Credentials):
546
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000547 ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id',
548 'tenant_id']
Andrea Frittoli7d707a52014-04-06 11:46:32 +0100549
550 def is_valid(self):
551 """
552 Minimum set of valid credentials, are username and password.
553 Tenant is optional.
554 """
555 return None not in (self.username, self.password)
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100556
557
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000558class KeystoneV3Credentials(Credentials):
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100559 """
560 Credentials suitable for the Keystone Identity V3 API
561 """
562
Andrea Frittoli9efbe952015-01-29 12:43:09 +0000563 ATTRIBUTES = ['domain_name', 'password', 'tenant_name', 'username',
564 'project_domain_id', 'project_domain_name', 'project_id',
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100565 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
566 'user_domain_name', 'user_id']
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100567
Andrea Frittolib1b04bb2014-04-06 11:57:07 +0100568 def __setattr__(self, key, value):
569 parent = super(KeystoneV3Credentials, self)
570 # for tenant_* set both project and tenant
571 if key == 'tenant_id':
572 parent.__setattr__('project_id', value)
573 elif key == 'tenant_name':
574 parent.__setattr__('project_name', value)
575 # for project_* set both project and tenant
576 if key == 'project_id':
577 parent.__setattr__('tenant_id', value)
578 elif key == 'project_name':
579 parent.__setattr__('tenant_name', value)
580 # for *_domain_* set both user and project if not set yet
581 if key == 'user_domain_id':
582 if self.project_domain_id is None:
583 parent.__setattr__('project_domain_id', value)
584 if key == 'project_domain_id':
585 if self.user_domain_id is None:
586 parent.__setattr__('user_domain_id', value)
587 if key == 'user_domain_name':
588 if self.project_domain_name is None:
589 parent.__setattr__('project_domain_name', value)
590 if key == 'project_domain_name':
591 if self.user_domain_name is None:
592 parent.__setattr__('user_domain_name', value)
593 # support domain_name coming from config
594 if key == 'domain_name':
595 parent.__setattr__('user_domain_name', value)
596 parent.__setattr__('project_domain_name', value)
597 # finally trigger default behaviour for all attributes
598 parent.__setattr__(key, value)
599
600 def is_valid(self):
601 """
602 Valid combinations of v3 credentials (excluding token, scope)
603 - User id, password (optional domain)
604 - User name, password and its domain id/name
605 For the scope, valid combinations are:
606 - None
607 - Project id (optional domain)
608 - Project name and its domain id/name
609 """
610 valid_user_domain = any(
611 [self.user_domain_id is not None,
612 self.user_domain_name is not None])
613 valid_project_domain = any(
614 [self.project_domain_id is not None,
615 self.project_domain_name is not None])
616 valid_user = any(
617 [self.user_id is not None,
618 self.username is not None and valid_user_domain])
619 valid_project = any(
620 [self.project_name is None and self.project_id is None,
621 self.project_id is not None,
622 self.project_name is not None and valid_project_domain])
623 return all([self.password is not None, valid_user, valid_project])
Andrea Frittoli878d5ab2015-01-30 13:22:50 +0000624
625
626IDENTITY_VERSION = {'v2': (KeystoneV2Credentials, KeystoneV2AuthProvider),
627 'v3': (KeystoneV3Credentials, KeystoneV3AuthProvider)}