blob: 974ba82c844d1427d50780536bf6af37e80be0c8 [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# 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 abc
17import copy
18import datetime
19import re
20
21from oslo_log import log as logging
22import six
23from six.moves.urllib import parse as urlparse
24
25from tempest.lib import exceptions
26from tempest.lib.services.identity.v2 import token_client as json_v2id
27from tempest.lib.services.identity.v3 import token_client as json_v3id
28
29ISO8601_FLOAT_SECONDS = '%Y-%m-%dT%H:%M:%S.%fZ'
30ISO8601_INT_SECONDS = '%Y-%m-%dT%H:%M:%SZ'
31LOG = logging.getLogger(__name__)
32
33
Brant Knudsonf2d1f572016-04-11 15:02:01 -050034def replace_version(url, new_version):
35 parts = urlparse.urlparse(url)
36 version_path = '/%s' % new_version
Brant Knudson77293802016-04-11 15:14:54 -050037 path, subs = re.subn(r'(^|/)+v\d+(?:\.\d+)?',
38 version_path,
39 parts.path,
40 count=1)
41 if not subs:
42 path = '%s%s' % (parts.path.rstrip('/'), version_path)
Brant Knudsonf2d1f572016-04-11 15:02:01 -050043 url = urlparse.urlunparse((parts.scheme,
44 parts.netloc,
Brant Knudson77293802016-04-11 15:14:54 -050045 path,
Brant Knudsonf2d1f572016-04-11 15:02:01 -050046 parts.params,
47 parts.query,
48 parts.fragment))
49 return url
50
51
52def apply_url_filters(url, filters):
53 if filters.get('api_version', None) is not None:
54 url = replace_version(url, filters['api_version'])
55 parts = urlparse.urlparse(url)
56 if filters.get('skip_path', None) is not None and parts.path != '':
57 url = urlparse.urlunparse((parts.scheme,
58 parts.netloc,
59 '/',
60 parts.params,
61 parts.query,
62 parts.fragment))
63
64 return url
65
66
Matthew Treinish9e26ca82016-02-23 11:43:20 -050067@six.add_metaclass(abc.ABCMeta)
68class AuthProvider(object):
69 """Provide authentication"""
70
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +010071 SCOPES = set(['project'])
72
73 def __init__(self, credentials, scope='project'):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050074 """Auth provider __init__
75
76 :param credentials: credentials for authentication
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +010077 :param scope: the default scope to be used by the credential providers
78 when requesting a token. Valid values depend on the
79 AuthProvider class implementation, and are defined in
80 the set SCOPES. Default value is 'project'.
Matthew Treinish9e26ca82016-02-23 11:43:20 -050081 """
82 if self.check_credentials(credentials):
83 self.credentials = credentials
84 else:
85 if isinstance(credentials, Credentials):
86 password = credentials.get('password')
87 message = "Credentials are: " + str(credentials)
88 if password is None:
89 message += " Password is not defined."
90 else:
91 message += " Password is defined."
92 raise exceptions.InvalidCredentials(message)
93 else:
94 raise TypeError("credentials object is of type %s, which is"
95 " not a valid Credentials object type." %
96 credentials.__class__.__name__)
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +010097 self._scope = None
98 self.scope = scope
Matthew Treinish9e26ca82016-02-23 11:43:20 -050099 self.cache = None
100 self.alt_auth_data = None
101 self.alt_part = None
102
103 def __str__(self):
104 return "Creds :{creds}, cached auth data: {cache}".format(
105 creds=self.credentials, cache=self.cache)
106
107 @abc.abstractmethod
108 def _decorate_request(self, filters, method, url, headers=None, body=None,
109 auth_data=None):
110 """Decorate request with authentication data"""
111 return
112
113 @abc.abstractmethod
114 def _get_auth(self):
115 return
116
117 @abc.abstractmethod
118 def _fill_credentials(self, auth_data_body):
119 return
120
121 def fill_credentials(self):
122 """Fill credentials object with data from auth"""
123 auth_data = self.get_auth()
124 self._fill_credentials(auth_data[1])
125 return self.credentials
126
127 @classmethod
128 def check_credentials(cls, credentials):
129 """Verify credentials are valid."""
130 return isinstance(credentials, Credentials) and credentials.is_valid()
131
132 @property
133 def auth_data(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100134 """Auth data for set scope"""
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500135 return self.get_auth()
136
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100137 @property
138 def scope(self):
139 """Scope used in auth requests"""
140 return self._scope
141
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500142 @auth_data.deleter
143 def auth_data(self):
144 self.clear_auth()
145
146 def get_auth(self):
147 """Returns auth from cache if available, else auth first"""
148 if self.cache is None or self.is_expired(self.cache):
149 self.set_auth()
150 return self.cache
151
152 def set_auth(self):
153 """Forces setting auth.
154
155 Forces setting auth, ignores cache if it exists.
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100156 Refills credentials.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500157 """
158 self.cache = self._get_auth()
159 self._fill_credentials(self.cache[1])
160
161 def clear_auth(self):
162 """Clear access cache
163
164 Can be called to clear the access cache so that next request
165 will fetch a new token and base_url.
166 """
167 self.cache = None
168 self.credentials.reset()
169
170 @abc.abstractmethod
171 def is_expired(self, auth_data):
172 return
173
174 def auth_request(self, method, url, headers=None, body=None, filters=None):
175 """Obtains auth data and decorates a request with that.
176
177 :param method: HTTP method of the request
178 :param url: relative URL of the request (path)
179 :param headers: HTTP headers of the request
180 :param body: HTTP body in case of POST / PUT
181 :param filters: select a base URL out of the catalog
182 :returns a Tuple (url, headers, body)
183 """
184 orig_req = dict(url=url, headers=headers, body=body)
185
186 auth_url, auth_headers, auth_body = self._decorate_request(
187 filters, method, url, headers, body)
188 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
189
190 # Overwrite part if the request if it has been requested
191 if self.alt_part is not None:
192 if self.alt_auth_data is not None:
193 alt_url, alt_headers, alt_body = self._decorate_request(
194 filters, method, url, headers, body,
195 auth_data=self.alt_auth_data)
196 alt_auth_req = dict(url=alt_url, headers=alt_headers,
197 body=alt_body)
198 if auth_req[self.alt_part] == alt_auth_req[self.alt_part]:
199 raise exceptions.BadAltAuth(part=self.alt_part)
200 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
201
202 else:
203 # If the requested part is not affected by auth, we are
204 # not altering auth as expected, raise an exception
205 if auth_req[self.alt_part] == orig_req[self.alt_part]:
206 raise exceptions.BadAltAuth(part=self.alt_part)
207 # If alt auth data is None, skip auth in the requested part
208 auth_req[self.alt_part] = orig_req[self.alt_part]
209
210 # Next auth request will be normal, unless otherwise requested
211 self.reset_alt_auth_data()
212
213 return auth_req['url'], auth_req['headers'], auth_req['body']
214
215 def reset_alt_auth_data(self):
216 """Configure auth provider to provide valid authentication data"""
217 self.alt_part = None
218 self.alt_auth_data = None
219
220 def set_alt_auth_data(self, request_part, auth_data):
221 """Alternate auth data on next request
222
223 Configure auth provider to provide alt authentication data
224 on a part of the *next* auth_request. If credentials are None,
225 set invalid data.
226 :param request_part: request part to contain invalid auth: url,
227 headers, body
228 :param auth_data: alternative auth_data from which to get the
229 invalid data to be injected
230 """
231 self.alt_part = request_part
232 self.alt_auth_data = auth_data
233
234 @abc.abstractmethod
235 def base_url(self, filters, auth_data=None):
236 """Extracts the base_url based on provided filters"""
237 return
238
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100239 @scope.setter
240 def scope(self, value):
241 """Set the scope to be used in token requests
242
243 :param scope: scope to be used. If the scope is different, clear caches
244 """
245 if value not in self.SCOPES:
246 raise exceptions.InvalidScope(
247 scope=value, auth_provider=self.__class__.__name__)
248 if value != self.scope:
249 self.clear_auth()
250 self._scope = value
251
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500252
253class KeystoneAuthProvider(AuthProvider):
254
255 EXPIRY_DATE_FORMATS = (ISO8601_FLOAT_SECONDS, ISO8601_INT_SECONDS)
256
257 token_expiry_threshold = datetime.timedelta(seconds=60)
258
259 def __init__(self, credentials, auth_url,
260 disable_ssl_certificate_validation=None,
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100261 ca_certs=None, trace_requests=None, scope='project'):
262 super(KeystoneAuthProvider, self).__init__(credentials, scope)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500263 self.dsvm = disable_ssl_certificate_validation
264 self.ca_certs = ca_certs
265 self.trace_requests = trace_requests
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100266 self.auth_url = auth_url
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500267 self.auth_client = self._auth_client(auth_url)
268
269 def _decorate_request(self, filters, method, url, headers=None, body=None,
270 auth_data=None):
271 if auth_data is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100272 auth_data = self.get_auth()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500273 token, _ = auth_data
274 base_url = self.base_url(filters=filters, auth_data=auth_data)
275 # build authenticated request
276 # returns new request, it does not touch the original values
277 _headers = copy.deepcopy(headers) if headers is not None else {}
278 _headers['X-Auth-Token'] = str(token)
279 if url is None or url == "":
280 _url = base_url
281 else:
282 # Join base URL and url, and remove multiple contiguous slashes
283 _url = "/".join([base_url, url])
284 parts = [x for x in urlparse.urlparse(_url)]
285 parts[2] = re.sub("/{2,}", "/", parts[2])
286 _url = urlparse.urlunparse(parts)
287 # no change to method or body
288 return str(_url), _headers, body
289
290 @abc.abstractmethod
291 def _auth_client(self):
292 return
293
294 @abc.abstractmethod
295 def _auth_params(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100296 """Auth parameters to be passed to the token request
297
298 By default all fields available in Credentials are passed to the
299 token request. Scope may affect this.
300 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500301 return
302
303 def _get_auth(self):
304 # Bypasses the cache
305 auth_func = getattr(self.auth_client, 'get_token')
306 auth_params = self._auth_params()
307
308 # returns token, auth_data
309 token, auth_data = auth_func(**auth_params)
310 return token, auth_data
311
312 def _parse_expiry_time(self, expiry_string):
313 expiry = None
314 for date_format in self.EXPIRY_DATE_FORMATS:
315 try:
316 expiry = datetime.datetime.strptime(
317 expiry_string, date_format)
318 except ValueError:
319 pass
320 if expiry is None:
321 raise ValueError(
322 "time data '{data}' does not match any of the"
323 "expected formats: {formats}".format(
324 data=expiry_string, formats=self.EXPIRY_DATE_FORMATS))
325 return expiry
326
327 def get_token(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100328 return self.get_auth()[0]
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500329
330
331class KeystoneV2AuthProvider(KeystoneAuthProvider):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100332 """Provides authentication based on the Identity V2 API
333
334 The Keystone Identity V2 API defines both unscoped and project scoped
335 tokens. This auth provider only implements 'project'.
336 """
337
338 SCOPES = set(['project'])
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500339
340 def _auth_client(self, auth_url):
341 return json_v2id.TokenClient(
342 auth_url, disable_ssl_certificate_validation=self.dsvm,
343 ca_certs=self.ca_certs, trace_requests=self.trace_requests)
344
345 def _auth_params(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100346 """Auth parameters to be passed to the token request
347
348 All fields available in Credentials are passed to the token request.
349 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500350 return dict(
351 user=self.credentials.username,
352 password=self.credentials.password,
353 tenant=self.credentials.tenant_name,
354 auth_data=True)
355
356 def _fill_credentials(self, auth_data_body):
357 tenant = auth_data_body['token']['tenant']
358 user = auth_data_body['user']
359 if self.credentials.tenant_name is None:
360 self.credentials.tenant_name = tenant['name']
361 if self.credentials.tenant_id is None:
362 self.credentials.tenant_id = tenant['id']
363 if self.credentials.username is None:
364 self.credentials.username = user['name']
365 if self.credentials.user_id is None:
366 self.credentials.user_id = user['id']
367
368 def base_url(self, filters, auth_data=None):
369 """Base URL from catalog
370
371 Filters can be:
372 - service: compute, image, etc
373 - region: the service region
374 - endpoint_type: adminURL, publicURL, internalURL
375 - api_version: replace catalog version with this
376 - skip_path: take just the base URL
377 """
378 if auth_data is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100379 auth_data = self.get_auth()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500380 token, _auth_data = auth_data
381 service = filters.get('service')
382 region = filters.get('region')
383 endpoint_type = filters.get('endpoint_type', 'publicURL')
384
385 if service is None:
386 raise exceptions.EndpointNotFound("No service provided")
387
388 _base_url = None
389 for ep in _auth_data['serviceCatalog']:
390 if ep["type"] == service:
391 for _ep in ep['endpoints']:
392 if region is not None and _ep['region'] == region:
393 _base_url = _ep.get(endpoint_type)
394 if not _base_url:
395 # No region matching, use the first
396 _base_url = ep['endpoints'][0].get(endpoint_type)
397 break
398 if _base_url is None:
Ken'ichi Ohmichib6cf83a2016-03-02 17:56:45 -0800399 raise exceptions.EndpointNotFound(
400 "service: %s, region: %s, endpoint_type: %s" %
401 (service, region, endpoint_type))
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500402 return apply_url_filters(_base_url, filters)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500403
404 def is_expired(self, auth_data):
405 _, access = auth_data
406 expiry = self._parse_expiry_time(access['token']['expires'])
407 return (expiry - self.token_expiry_threshold <=
408 datetime.datetime.utcnow())
409
410
411class KeystoneV3AuthProvider(KeystoneAuthProvider):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100412 """Provides authentication based on the Identity V3 API"""
413
414 SCOPES = set(['project', 'domain', 'unscoped', None])
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500415
416 def _auth_client(self, auth_url):
417 return json_v3id.V3TokenClient(
418 auth_url, disable_ssl_certificate_validation=self.dsvm,
419 ca_certs=self.ca_certs, trace_requests=self.trace_requests)
420
421 def _auth_params(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100422 """Auth parameters to be passed to the token request
423
424 Fields available in Credentials are passed to the token request,
425 depending on the value of scope. Valid values for scope are: "project",
426 "domain". Any other string (e.g. "unscoped") or None will lead to an
427 unscoped token request.
428 """
429
430 auth_params = dict(
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500431 user_id=self.credentials.user_id,
432 username=self.credentials.username,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500433 user_domain_id=self.credentials.user_domain_id,
434 user_domain_name=self.credentials.user_domain_name,
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100435 password=self.credentials.password,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500436 auth_data=True)
437
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100438 if self.scope == 'project':
439 auth_params.update(
440 project_domain_id=self.credentials.project_domain_id,
441 project_domain_name=self.credentials.project_domain_name,
442 project_id=self.credentials.project_id,
443 project_name=self.credentials.project_name)
444
445 if self.scope == 'domain':
446 auth_params.update(
447 domain_id=self.credentials.domain_id,
448 domain_name=self.credentials.domain_name)
449
450 return auth_params
451
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500452 def _fill_credentials(self, auth_data_body):
453 # project or domain, depending on the scope
454 project = auth_data_body.get('project', None)
455 domain = auth_data_body.get('domain', None)
456 # user is always there
457 user = auth_data_body['user']
458 # Set project fields
459 if project is not None:
460 if self.credentials.project_name is None:
461 self.credentials.project_name = project['name']
462 if self.credentials.project_id is None:
463 self.credentials.project_id = project['id']
464 if self.credentials.project_domain_id is None:
465 self.credentials.project_domain_id = project['domain']['id']
466 if self.credentials.project_domain_name is None:
467 self.credentials.project_domain_name = (
468 project['domain']['name'])
469 # Set domain fields
470 if domain is not None:
471 if self.credentials.domain_id is None:
472 self.credentials.domain_id = domain['id']
473 if self.credentials.domain_name is None:
474 self.credentials.domain_name = domain['name']
475 # Set user fields
476 if self.credentials.username is None:
477 self.credentials.username = user['name']
478 if self.credentials.user_id is None:
479 self.credentials.user_id = user['id']
480 if self.credentials.user_domain_id is None:
481 self.credentials.user_domain_id = user['domain']['id']
482 if self.credentials.user_domain_name is None:
483 self.credentials.user_domain_name = user['domain']['name']
484
485 def base_url(self, filters, auth_data=None):
486 """Base URL from catalog
487
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100488 If scope is not 'project', it may be that there is not catalog in
489 the auth_data. In such case, as long as the requested service is
490 'identity', we can use the original auth URL to build the base_url.
491
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500492 Filters can be:
493 - service: compute, image, etc
494 - region: the service region
495 - endpoint_type: adminURL, publicURL, internalURL
496 - api_version: replace catalog version with this
497 - skip_path: take just the base URL
498 """
499 if auth_data is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100500 auth_data = self.get_auth()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500501 token, _auth_data = auth_data
502 service = filters.get('service')
503 region = filters.get('region')
504 endpoint_type = filters.get('endpoint_type', 'public')
505
506 if service is None:
507 raise exceptions.EndpointNotFound("No service provided")
508
509 if 'URL' in endpoint_type:
510 endpoint_type = endpoint_type.replace('URL', '')
511 _base_url = None
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100512 catalog = _auth_data.get('catalog', [])
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500513 # Select entries with matching service type
514 service_catalog = [ep for ep in catalog if ep['type'] == service]
515 if len(service_catalog) > 0:
516 service_catalog = service_catalog[0]['endpoints']
517 else:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100518 if len(catalog) == 0 and service == 'identity':
519 # NOTE(andreaf) If there's no catalog at all and the service
520 # is identity, it's a valid use case. Having a non-empty
521 # catalog with no identity in it is not valid instead.
522 return apply_url_filters(self.auth_url, filters)
523 else:
524 # No matching service
525 raise exceptions.EndpointNotFound(service)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500526 # Filter by endpoint type (interface)
527 filtered_catalog = [ep for ep in service_catalog if
528 ep['interface'] == endpoint_type]
529 if len(filtered_catalog) == 0:
530 # No matching type, keep all and try matching by region at least
531 filtered_catalog = service_catalog
532 # Filter by region
533 filtered_catalog = [ep for ep in filtered_catalog if
534 ep['region'] == region]
535 if len(filtered_catalog) == 0:
536 # No matching region, take the first endpoint
537 filtered_catalog = [service_catalog[0]]
538 # There should be only one match. If not take the first.
539 _base_url = filtered_catalog[0].get('url', None)
540 if _base_url is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100541 raise exceptions.EndpointNotFound(service)
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500542 return apply_url_filters(_base_url, filters)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500543
544 def is_expired(self, auth_data):
545 _, access = auth_data
546 expiry = self._parse_expiry_time(access['expires_at'])
547 return (expiry - self.token_expiry_threshold <=
548 datetime.datetime.utcnow())
549
550
551def is_identity_version_supported(identity_version):
552 return identity_version in IDENTITY_VERSION
553
554
555def get_credentials(auth_url, fill_in=True, identity_version='v2',
556 disable_ssl_certificate_validation=None, ca_certs=None,
557 trace_requests=None, **kwargs):
558 """Builds a credentials object based on the configured auth_version
559
560 :param auth_url (string): Full URI of the OpenStack Identity API(Keystone)
561 which is used to fetch the token from Identity service.
562 :param fill_in (boolean): obtain a token and fill in all credential
563 details provided by the identity service. When fill_in is not
564 specified, credentials are not validated. Validation can be invoked
565 by invoking ``is_valid()``
566 :param identity_version (string): identity API version is used to
567 select the matching auth provider and credentials class
568 :param disable_ssl_certificate_validation: whether to enforce SSL
569 certificate validation in SSL API requests to the auth system
570 :param ca_certs: CA certificate bundle for validation of certificates
571 in SSL API requests to the auth system
572 :param trace_requests: trace in log API requests to the auth system
573 :param kwargs (dict): Dict of credential key/value pairs
574
575 Examples:
576
577 Returns credentials from the provided parameters:
578 >>> get_credentials(username='foo', password='bar')
579
580 Returns credentials including IDs:
581 >>> get_credentials(username='foo', password='bar', fill_in=True)
582 """
583 if not is_identity_version_supported(identity_version):
584 raise exceptions.InvalidIdentityVersion(
585 identity_version=identity_version)
586
587 credential_class, auth_provider_class = IDENTITY_VERSION.get(
588 identity_version)
589
590 creds = credential_class(**kwargs)
591 # Fill in the credentials fields that were not specified
592 if fill_in:
593 dsvm = disable_ssl_certificate_validation
594 auth_provider = auth_provider_class(
595 creds, auth_url, disable_ssl_certificate_validation=dsvm,
596 ca_certs=ca_certs, trace_requests=trace_requests)
597 creds = auth_provider.fill_credentials()
598 return creds
599
600
601class Credentials(object):
602 """Set of credentials for accessing OpenStack services
603
604 ATTRIBUTES: list of valid class attributes representing credentials.
605 """
606
607 ATTRIBUTES = []
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100608 COLLISIONS = []
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500609
610 def __init__(self, **kwargs):
611 """Enforce the available attributes at init time (only).
612
613 Additional attributes can still be set afterwards if tests need
614 to do so.
615 """
616 self._initial = kwargs
617 self._apply_credentials(kwargs)
618
619 def _apply_credentials(self, attr):
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100620 for (key1, key2) in self.COLLISIONS:
621 val1 = attr.get(key1)
622 val2 = attr.get(key2)
623 if val1 and val2 and val1 != val2:
624 msg = ('Cannot have conflicting values for %s and %s' %
625 (key1, key2))
626 raise exceptions.InvalidCredentials(msg)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500627 for key in attr.keys():
628 if key in self.ATTRIBUTES:
629 setattr(self, key, attr[key])
630 else:
631 msg = '%s is not a valid attr for %s' % (key, self.__class__)
632 raise exceptions.InvalidCredentials(msg)
633
634 def __str__(self):
635 """Represent only attributes included in self.ATTRIBUTES"""
636 attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
637 _repr = dict((k, getattr(self, k)) for k in attrs)
638 return str(_repr)
639
640 def __eq__(self, other):
641 """Credentials are equal if attributes in self.ATTRIBUTES are equal"""
642 return str(self) == str(other)
643
644 def __getattr__(self, key):
645 # If an attribute is set, __getattr__ is not invoked
646 # If an attribute is not set, and it is a known one, return None
647 if key in self.ATTRIBUTES:
648 return None
649 else:
650 raise AttributeError
651
652 def __delitem__(self, key):
653 # For backwards compatibility, support dict behaviour
654 if key in self.ATTRIBUTES:
655 delattr(self, key)
656 else:
657 raise AttributeError
658
659 def get(self, item, default=None):
660 # In this patch act as dict for backward compatibility
661 try:
662 return getattr(self, item)
663 except AttributeError:
664 return default
665
666 def get_init_attributes(self):
667 return self._initial.keys()
668
669 def is_valid(self):
670 raise NotImplementedError
671
672 def reset(self):
673 # First delete all known attributes
674 for key in self.ATTRIBUTES:
675 if getattr(self, key) is not None:
676 delattr(self, key)
677 # Then re-apply initial setup
678 self._apply_credentials(self._initial)
679
680
681class KeystoneV2Credentials(Credentials):
682
683 ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id',
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100684 'tenant_id', 'project_id', 'project_name']
685 COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
686
687 def __str__(self):
688 """Represent only attributes included in self.ATTRIBUTES"""
689 attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
690 _repr = dict((k, getattr(self, k)) for k in attrs)
691 return str(_repr)
692
693 def __setattr__(self, key, value):
694 # NOTE(andreaf) In order to ease the migration towards 'project' we
695 # support v2 credentials configured with 'project' and translate it
696 # to tenant on the fly. The original kwargs are stored for clients
697 # that may rely on them. We also set project when tenant is defined
698 # so clients can rely on project being part of credentials.
699 parent = super(KeystoneV2Credentials, self)
700 # for project_* set tenant only
701 if key == 'project_id':
702 parent.__setattr__('tenant_id', value)
703 elif key == 'project_name':
704 parent.__setattr__('tenant_name', value)
705 if key == 'tenant_id':
706 parent.__setattr__('project_id', value)
707 elif key == 'tenant_name':
708 parent.__setattr__('project_name', value)
709 # trigger default behaviour for all attributes
710 parent.__setattr__(key, value)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500711
712 def is_valid(self):
713 """Check of credentials (no API call)
714
715 Minimum set of valid credentials, are username and password.
716 Tenant is optional.
717 """
718 return None not in (self.username, self.password)
719
720
721class KeystoneV3Credentials(Credentials):
722 """Credentials suitable for the Keystone Identity V3 API"""
723
724 ATTRIBUTES = ['domain_id', 'domain_name', 'password', 'username',
725 'project_domain_id', 'project_domain_name', 'project_id',
726 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
727 'user_domain_name', 'user_id']
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100728 COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
John Warrenb10c6ca2016-02-26 15:32:37 -0500729
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500730 def __setattr__(self, key, value):
731 parent = super(KeystoneV3Credentials, self)
732 # for tenant_* set both project and tenant
733 if key == 'tenant_id':
734 parent.__setattr__('project_id', value)
735 elif key == 'tenant_name':
736 parent.__setattr__('project_name', value)
737 # for project_* set both project and tenant
738 if key == 'project_id':
739 parent.__setattr__('tenant_id', value)
740 elif key == 'project_name':
741 parent.__setattr__('tenant_name', value)
742 # for *_domain_* set both user and project if not set yet
743 if key == 'user_domain_id':
744 if self.project_domain_id is None:
745 parent.__setattr__('project_domain_id', value)
746 if key == 'project_domain_id':
747 if self.user_domain_id is None:
748 parent.__setattr__('user_domain_id', value)
749 if key == 'user_domain_name':
750 if self.project_domain_name is None:
751 parent.__setattr__('project_domain_name', value)
752 if key == 'project_domain_name':
753 if self.user_domain_name is None:
754 parent.__setattr__('user_domain_name', value)
755 # support domain_name coming from config
756 if key == 'domain_name':
John Warrenb10c6ca2016-02-26 15:32:37 -0500757 if self.user_domain_name is None:
758 parent.__setattr__('user_domain_name', value)
759 if self.project_domain_name is None:
760 parent.__setattr__('project_domain_name', value)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500761 # finally trigger default behaviour for all attributes
762 parent.__setattr__(key, value)
763
764 def is_valid(self):
765 """Check of credentials (no API call)
766
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100767 Valid combinations of v3 credentials (excluding token)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500768 - User id, password (optional domain)
769 - User name, password and its domain id/name
770 For the scope, valid combinations are:
771 - None
772 - Project id (optional domain)
773 - Project name and its domain id/name
774 - Domain id
775 - Domain name
776 """
777 valid_user_domain = any(
778 [self.user_domain_id is not None,
779 self.user_domain_name is not None])
780 valid_project_domain = any(
781 [self.project_domain_id is not None,
782 self.project_domain_name is not None])
783 valid_user = any(
784 [self.user_id is not None,
785 self.username is not None and valid_user_domain])
786 valid_project_scope = any(
787 [self.project_name is None and self.project_id is None,
788 self.project_id is not None,
789 self.project_name is not None and valid_project_domain])
790 valid_domain_scope = any(
791 [self.domain_id is None and self.domain_name is None,
792 self.domain_id or self.domain_name])
793 return all([self.password is not None,
794 valid_user,
795 valid_project_scope and valid_domain_scope])
796
797
798IDENTITY_VERSION = {'v2': (KeystoneV2Credentials, KeystoneV2AuthProvider),
799 'v3': (KeystoneV3Credentials, KeystoneV3AuthProvider)}