blob: 71c4f4fd057328ca814e9df8b9df6183f090fd16 [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
34@six.add_metaclass(abc.ABCMeta)
35class AuthProvider(object):
36 """Provide authentication"""
37
38 def __init__(self, credentials):
39 """Auth provider __init__
40
41 :param credentials: credentials for authentication
42 """
43 if self.check_credentials(credentials):
44 self.credentials = credentials
45 else:
46 if isinstance(credentials, Credentials):
47 password = credentials.get('password')
48 message = "Credentials are: " + str(credentials)
49 if password is None:
50 message += " Password is not defined."
51 else:
52 message += " Password is defined."
53 raise exceptions.InvalidCredentials(message)
54 else:
55 raise TypeError("credentials object is of type %s, which is"
56 " not a valid Credentials object type." %
57 credentials.__class__.__name__)
58 self.cache = None
59 self.alt_auth_data = None
60 self.alt_part = None
61
62 def __str__(self):
63 return "Creds :{creds}, cached auth data: {cache}".format(
64 creds=self.credentials, cache=self.cache)
65
66 @abc.abstractmethod
67 def _decorate_request(self, filters, method, url, headers=None, body=None,
68 auth_data=None):
69 """Decorate request with authentication data"""
70 return
71
72 @abc.abstractmethod
73 def _get_auth(self):
74 return
75
76 @abc.abstractmethod
77 def _fill_credentials(self, auth_data_body):
78 return
79
80 def fill_credentials(self):
81 """Fill credentials object with data from auth"""
82 auth_data = self.get_auth()
83 self._fill_credentials(auth_data[1])
84 return self.credentials
85
86 @classmethod
87 def check_credentials(cls, credentials):
88 """Verify credentials are valid."""
89 return isinstance(credentials, Credentials) and credentials.is_valid()
90
91 @property
92 def auth_data(self):
93 return self.get_auth()
94
95 @auth_data.deleter
96 def auth_data(self):
97 self.clear_auth()
98
99 def get_auth(self):
100 """Returns auth from cache if available, else auth first"""
101 if self.cache is None or self.is_expired(self.cache):
102 self.set_auth()
103 return self.cache
104
105 def set_auth(self):
106 """Forces setting auth.
107
108 Forces setting auth, ignores cache if it exists.
109 Refills credentials
110 """
111 self.cache = self._get_auth()
112 self._fill_credentials(self.cache[1])
113
114 def clear_auth(self):
115 """Clear access cache
116
117 Can be called to clear the access cache so that next request
118 will fetch a new token and base_url.
119 """
120 self.cache = None
121 self.credentials.reset()
122
123 @abc.abstractmethod
124 def is_expired(self, auth_data):
125 return
126
127 def auth_request(self, method, url, headers=None, body=None, filters=None):
128 """Obtains auth data and decorates a request with that.
129
130 :param method: HTTP method of the request
131 :param url: relative URL of the request (path)
132 :param headers: HTTP headers of the request
133 :param body: HTTP body in case of POST / PUT
134 :param filters: select a base URL out of the catalog
135 :returns a Tuple (url, headers, body)
136 """
137 orig_req = dict(url=url, headers=headers, body=body)
138
139 auth_url, auth_headers, auth_body = self._decorate_request(
140 filters, method, url, headers, body)
141 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
142
143 # Overwrite part if the request if it has been requested
144 if self.alt_part is not None:
145 if self.alt_auth_data is not None:
146 alt_url, alt_headers, alt_body = self._decorate_request(
147 filters, method, url, headers, body,
148 auth_data=self.alt_auth_data)
149 alt_auth_req = dict(url=alt_url, headers=alt_headers,
150 body=alt_body)
151 if auth_req[self.alt_part] == alt_auth_req[self.alt_part]:
152 raise exceptions.BadAltAuth(part=self.alt_part)
153 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
154
155 else:
156 # If the requested part is not affected by auth, we are
157 # not altering auth as expected, raise an exception
158 if auth_req[self.alt_part] == orig_req[self.alt_part]:
159 raise exceptions.BadAltAuth(part=self.alt_part)
160 # If alt auth data is None, skip auth in the requested part
161 auth_req[self.alt_part] = orig_req[self.alt_part]
162
163 # Next auth request will be normal, unless otherwise requested
164 self.reset_alt_auth_data()
165
166 return auth_req['url'], auth_req['headers'], auth_req['body']
167
168 def reset_alt_auth_data(self):
169 """Configure auth provider to provide valid authentication data"""
170 self.alt_part = None
171 self.alt_auth_data = None
172
173 def set_alt_auth_data(self, request_part, auth_data):
174 """Alternate auth data on next request
175
176 Configure auth provider to provide alt authentication data
177 on a part of the *next* auth_request. If credentials are None,
178 set invalid data.
179 :param request_part: request part to contain invalid auth: url,
180 headers, body
181 :param auth_data: alternative auth_data from which to get the
182 invalid data to be injected
183 """
184 self.alt_part = request_part
185 self.alt_auth_data = auth_data
186
187 @abc.abstractmethod
188 def base_url(self, filters, auth_data=None):
189 """Extracts the base_url based on provided filters"""
190 return
191
192
193class KeystoneAuthProvider(AuthProvider):
194
195 EXPIRY_DATE_FORMATS = (ISO8601_FLOAT_SECONDS, ISO8601_INT_SECONDS)
196
197 token_expiry_threshold = datetime.timedelta(seconds=60)
198
199 def __init__(self, credentials, auth_url,
200 disable_ssl_certificate_validation=None,
201 ca_certs=None, trace_requests=None):
202 super(KeystoneAuthProvider, self).__init__(credentials)
203 self.dsvm = disable_ssl_certificate_validation
204 self.ca_certs = ca_certs
205 self.trace_requests = trace_requests
206 self.auth_client = self._auth_client(auth_url)
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
216 _headers = copy.deepcopy(headers) if headers is not None else {}
217 _headers['X-Auth-Token'] = str(token)
218 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
227 return str(_url), _headers, body
228
229 @abc.abstractmethod
230 def _auth_client(self):
231 return
232
233 @abc.abstractmethod
234 def _auth_params(self):
235 return
236
237 def _get_auth(self):
238 # Bypasses the cache
239 auth_func = getattr(self.auth_client, 'get_token')
240 auth_params = self._auth_params()
241
242 # returns token, auth_data
243 token, auth_data = auth_func(**auth_params)
244 return token, auth_data
245
246 def _parse_expiry_time(self, expiry_string):
247 expiry = None
248 for date_format in self.EXPIRY_DATE_FORMATS:
249 try:
250 expiry = datetime.datetime.strptime(
251 expiry_string, date_format)
252 except ValueError:
253 pass
254 if expiry is None:
255 raise ValueError(
256 "time data '{data}' does not match any of the"
257 "expected formats: {formats}".format(
258 data=expiry_string, formats=self.EXPIRY_DATE_FORMATS))
259 return expiry
260
261 def get_token(self):
262 return self.auth_data[0]
263
264
265class KeystoneV2AuthProvider(KeystoneAuthProvider):
266
267 def _auth_client(self, auth_url):
268 return json_v2id.TokenClient(
269 auth_url, disable_ssl_certificate_validation=self.dsvm,
270 ca_certs=self.ca_certs, trace_requests=self.trace_requests)
271
272 def _auth_params(self):
273 return dict(
274 user=self.credentials.username,
275 password=self.credentials.password,
276 tenant=self.credentials.tenant_name,
277 auth_data=True)
278
279 def _fill_credentials(self, auth_data_body):
280 tenant = auth_data_body['token']['tenant']
281 user = auth_data_body['user']
282 if self.credentials.tenant_name is None:
283 self.credentials.tenant_name = tenant['name']
284 if self.credentials.tenant_id is None:
285 self.credentials.tenant_id = tenant['id']
286 if self.credentials.username is None:
287 self.credentials.username = user['name']
288 if self.credentials.user_id is None:
289 self.credentials.user_id = user['id']
290
291 def base_url(self, filters, auth_data=None):
292 """Base URL from catalog
293
294 Filters can be:
295 - service: compute, image, etc
296 - region: the service region
297 - endpoint_type: adminURL, publicURL, internalURL
298 - api_version: replace catalog version with this
299 - skip_path: take just the base URL
300 """
301 if auth_data is None:
302 auth_data = self.auth_data
303 token, _auth_data = auth_data
304 service = filters.get('service')
305 region = filters.get('region')
306 endpoint_type = filters.get('endpoint_type', 'publicURL')
307
308 if service is None:
309 raise exceptions.EndpointNotFound("No service provided")
310
311 _base_url = None
312 for ep in _auth_data['serviceCatalog']:
313 if ep["type"] == service:
314 for _ep in ep['endpoints']:
315 if region is not None and _ep['region'] == region:
316 _base_url = _ep.get(endpoint_type)
317 if not _base_url:
318 # No region matching, use the first
319 _base_url = ep['endpoints'][0].get(endpoint_type)
320 break
321 if _base_url is None:
Ken'ichi Ohmichib6cf83a2016-03-02 17:56:45 -0800322 raise exceptions.EndpointNotFound(
323 "service: %s, region: %s, endpoint_type: %s" %
324 (service, region, endpoint_type))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500325
326 parts = urlparse.urlparse(_base_url)
327 if filters.get('api_version', None) is not None:
Jamie Lennox823a0042016-03-02 18:33:24 -0600328 version_path = '/%s' % filters['api_version']
Sergey Nikitinba678672016-02-25 12:55:51 +0300329 path = re.sub(r'(^|/)+v\d+(?:\.\d+)?',
Jamie Lennox823a0042016-03-02 18:33:24 -0600330 version_path,
Sergey Nikitinba678672016-02-25 12:55:51 +0300331 parts.path,
332 count=1)
Jamie Lennox823a0042016-03-02 18:33:24 -0600333 _base_url = urlparse.urlunparse((parts.scheme,
334 parts.netloc,
335 path or version_path,
336 parts.params,
337 parts.query,
338 parts.fragment))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500339 if filters.get('skip_path', None) is not None and parts.path != '':
Jamie Lennox823a0042016-03-02 18:33:24 -0600340 _base_url = urlparse.urlunparse((parts.scheme,
341 parts.netloc,
342 '/',
343 parts.params,
344 parts.query,
345 parts.fragment))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500346
347 return _base_url
348
349 def is_expired(self, auth_data):
350 _, access = auth_data
351 expiry = self._parse_expiry_time(access['token']['expires'])
352 return (expiry - self.token_expiry_threshold <=
353 datetime.datetime.utcnow())
354
355
356class KeystoneV3AuthProvider(KeystoneAuthProvider):
357
358 def _auth_client(self, auth_url):
359 return json_v3id.V3TokenClient(
360 auth_url, disable_ssl_certificate_validation=self.dsvm,
361 ca_certs=self.ca_certs, trace_requests=self.trace_requests)
362
363 def _auth_params(self):
364 return dict(
365 user_id=self.credentials.user_id,
366 username=self.credentials.username,
367 password=self.credentials.password,
368 project_id=self.credentials.project_id,
369 project_name=self.credentials.project_name,
370 user_domain_id=self.credentials.user_domain_id,
371 user_domain_name=self.credentials.user_domain_name,
372 project_domain_id=self.credentials.project_domain_id,
373 project_domain_name=self.credentials.project_domain_name,
374 domain_id=self.credentials.domain_id,
375 domain_name=self.credentials.domain_name,
376 auth_data=True)
377
378 def _fill_credentials(self, auth_data_body):
379 # project or domain, depending on the scope
380 project = auth_data_body.get('project', None)
381 domain = auth_data_body.get('domain', None)
382 # user is always there
383 user = auth_data_body['user']
384 # Set project fields
385 if project is not None:
386 if self.credentials.project_name is None:
387 self.credentials.project_name = project['name']
388 if self.credentials.project_id is None:
389 self.credentials.project_id = project['id']
390 if self.credentials.project_domain_id is None:
391 self.credentials.project_domain_id = project['domain']['id']
392 if self.credentials.project_domain_name is None:
393 self.credentials.project_domain_name = (
394 project['domain']['name'])
395 # Set domain fields
396 if domain is not None:
397 if self.credentials.domain_id is None:
398 self.credentials.domain_id = domain['id']
399 if self.credentials.domain_name is None:
400 self.credentials.domain_name = domain['name']
401 # Set user fields
402 if self.credentials.username is None:
403 self.credentials.username = user['name']
404 if self.credentials.user_id is None:
405 self.credentials.user_id = user['id']
406 if self.credentials.user_domain_id is None:
407 self.credentials.user_domain_id = user['domain']['id']
408 if self.credentials.user_domain_name is None:
409 self.credentials.user_domain_name = user['domain']['name']
410
411 def base_url(self, filters, auth_data=None):
412 """Base URL from catalog
413
414 Filters can be:
415 - service: compute, image, etc
416 - region: the service region
417 - endpoint_type: adminURL, publicURL, internalURL
418 - api_version: replace catalog version with this
419 - skip_path: take just the base URL
420 """
421 if auth_data is None:
422 auth_data = self.auth_data
423 token, _auth_data = auth_data
424 service = filters.get('service')
425 region = filters.get('region')
426 endpoint_type = filters.get('endpoint_type', 'public')
427
428 if service is None:
429 raise exceptions.EndpointNotFound("No service provided")
430
431 if 'URL' in endpoint_type:
432 endpoint_type = endpoint_type.replace('URL', '')
433 _base_url = None
434 catalog = _auth_data['catalog']
435 # Select entries with matching service type
436 service_catalog = [ep for ep in catalog if ep['type'] == service]
437 if len(service_catalog) > 0:
438 service_catalog = service_catalog[0]['endpoints']
439 else:
440 # No matching service
441 raise exceptions.EndpointNotFound(service)
442 # Filter by endpoint type (interface)
443 filtered_catalog = [ep for ep in service_catalog if
444 ep['interface'] == endpoint_type]
445 if len(filtered_catalog) == 0:
446 # No matching type, keep all and try matching by region at least
447 filtered_catalog = service_catalog
448 # Filter by region
449 filtered_catalog = [ep for ep in filtered_catalog if
450 ep['region'] == region]
451 if len(filtered_catalog) == 0:
452 # No matching region, take the first endpoint
453 filtered_catalog = [service_catalog[0]]
454 # There should be only one match. If not take the first.
455 _base_url = filtered_catalog[0].get('url', None)
456 if _base_url is None:
457 raise exceptions.EndpointNotFound(service)
458
459 parts = urlparse.urlparse(_base_url)
460 if filters.get('api_version', None) is not None:
Jamie Lennox823a0042016-03-02 18:33:24 -0600461 version_path = '/%s' % filters['api_version']
Sergey Nikitinba678672016-02-25 12:55:51 +0300462 path = re.sub(r'(^|/)+v\d+(?:\.\d+)?',
Jamie Lennox823a0042016-03-02 18:33:24 -0600463 version_path,
Sergey Nikitinba678672016-02-25 12:55:51 +0300464 parts.path,
465 count=1)
Jamie Lennox823a0042016-03-02 18:33:24 -0600466 _base_url = urlparse.urlunparse((parts.scheme,
467 parts.netloc,
468 path or version_path,
469 parts.params,
470 parts.query,
471 parts.fragment))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500472 if filters.get('skip_path', None) is not None:
Jamie Lennox823a0042016-03-02 18:33:24 -0600473 _base_url = urlparse.urlunparse((parts.scheme,
474 parts.netloc,
475 '/',
476 parts.params,
477 parts.query,
478 parts.fragment))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500479
480 return _base_url
481
482 def is_expired(self, auth_data):
483 _, access = auth_data
484 expiry = self._parse_expiry_time(access['expires_at'])
485 return (expiry - self.token_expiry_threshold <=
486 datetime.datetime.utcnow())
487
488
489def is_identity_version_supported(identity_version):
490 return identity_version in IDENTITY_VERSION
491
492
493def get_credentials(auth_url, fill_in=True, identity_version='v2',
494 disable_ssl_certificate_validation=None, ca_certs=None,
495 trace_requests=None, **kwargs):
496 """Builds a credentials object based on the configured auth_version
497
498 :param auth_url (string): Full URI of the OpenStack Identity API(Keystone)
499 which is used to fetch the token from Identity service.
500 :param fill_in (boolean): obtain a token and fill in all credential
501 details provided by the identity service. When fill_in is not
502 specified, credentials are not validated. Validation can be invoked
503 by invoking ``is_valid()``
504 :param identity_version (string): identity API version is used to
505 select the matching auth provider and credentials class
506 :param disable_ssl_certificate_validation: whether to enforce SSL
507 certificate validation in SSL API requests to the auth system
508 :param ca_certs: CA certificate bundle for validation of certificates
509 in SSL API requests to the auth system
510 :param trace_requests: trace in log API requests to the auth system
511 :param kwargs (dict): Dict of credential key/value pairs
512
513 Examples:
514
515 Returns credentials from the provided parameters:
516 >>> get_credentials(username='foo', password='bar')
517
518 Returns credentials including IDs:
519 >>> get_credentials(username='foo', password='bar', fill_in=True)
520 """
521 if not is_identity_version_supported(identity_version):
522 raise exceptions.InvalidIdentityVersion(
523 identity_version=identity_version)
524
525 credential_class, auth_provider_class = IDENTITY_VERSION.get(
526 identity_version)
527
528 creds = credential_class(**kwargs)
529 # Fill in the credentials fields that were not specified
530 if fill_in:
531 dsvm = disable_ssl_certificate_validation
532 auth_provider = auth_provider_class(
533 creds, auth_url, disable_ssl_certificate_validation=dsvm,
534 ca_certs=ca_certs, trace_requests=trace_requests)
535 creds = auth_provider.fill_credentials()
536 return creds
537
538
539class Credentials(object):
540 """Set of credentials for accessing OpenStack services
541
542 ATTRIBUTES: list of valid class attributes representing credentials.
543 """
544
545 ATTRIBUTES = []
546
547 def __init__(self, **kwargs):
548 """Enforce the available attributes at init time (only).
549
550 Additional attributes can still be set afterwards if tests need
551 to do so.
552 """
553 self._initial = kwargs
554 self._apply_credentials(kwargs)
555
556 def _apply_credentials(self, attr):
557 for key in attr.keys():
558 if key in self.ATTRIBUTES:
559 setattr(self, key, attr[key])
560 else:
561 msg = '%s is not a valid attr for %s' % (key, self.__class__)
562 raise exceptions.InvalidCredentials(msg)
563
564 def __str__(self):
565 """Represent only attributes included in self.ATTRIBUTES"""
566 attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
567 _repr = dict((k, getattr(self, k)) for k in attrs)
568 return str(_repr)
569
570 def __eq__(self, other):
571 """Credentials are equal if attributes in self.ATTRIBUTES are equal"""
572 return str(self) == str(other)
573
574 def __getattr__(self, key):
575 # If an attribute is set, __getattr__ is not invoked
576 # If an attribute is not set, and it is a known one, return None
577 if key in self.ATTRIBUTES:
578 return None
579 else:
580 raise AttributeError
581
582 def __delitem__(self, key):
583 # For backwards compatibility, support dict behaviour
584 if key in self.ATTRIBUTES:
585 delattr(self, key)
586 else:
587 raise AttributeError
588
589 def get(self, item, default=None):
590 # In this patch act as dict for backward compatibility
591 try:
592 return getattr(self, item)
593 except AttributeError:
594 return default
595
596 def get_init_attributes(self):
597 return self._initial.keys()
598
599 def is_valid(self):
600 raise NotImplementedError
601
602 def reset(self):
603 # First delete all known attributes
604 for key in self.ATTRIBUTES:
605 if getattr(self, key) is not None:
606 delattr(self, key)
607 # Then re-apply initial setup
608 self._apply_credentials(self._initial)
609
610
611class KeystoneV2Credentials(Credentials):
612
613 ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id',
614 'tenant_id']
615
616 def is_valid(self):
617 """Check of credentials (no API call)
618
619 Minimum set of valid credentials, are username and password.
620 Tenant is optional.
621 """
622 return None not in (self.username, self.password)
623
624
John Warrenb10c6ca2016-02-26 15:32:37 -0500625COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
626
627
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500628class KeystoneV3Credentials(Credentials):
629 """Credentials suitable for the Keystone Identity V3 API"""
630
631 ATTRIBUTES = ['domain_id', 'domain_name', 'password', 'username',
632 'project_domain_id', 'project_domain_name', 'project_id',
633 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
634 'user_domain_name', 'user_id']
635
John Warrenb10c6ca2016-02-26 15:32:37 -0500636 def _apply_credentials(self, attr):
637 for (key1, key2) in COLLISIONS:
638 val1 = attr.get(key1)
639 val2 = attr.get(key2)
640 if val1 and val2 and val1 != val2:
641 msg = ('Cannot have conflicting values for %s and %s' %
642 (key1, key2))
643 raise exceptions.InvalidCredentials(msg)
644 super(KeystoneV3Credentials, self)._apply_credentials(attr)
645
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500646 def __setattr__(self, key, value):
647 parent = super(KeystoneV3Credentials, self)
648 # for tenant_* set both project and tenant
649 if key == 'tenant_id':
650 parent.__setattr__('project_id', value)
651 elif key == 'tenant_name':
652 parent.__setattr__('project_name', value)
653 # for project_* set both project and tenant
654 if key == 'project_id':
655 parent.__setattr__('tenant_id', value)
656 elif key == 'project_name':
657 parent.__setattr__('tenant_name', value)
658 # for *_domain_* set both user and project if not set yet
659 if key == 'user_domain_id':
660 if self.project_domain_id is None:
661 parent.__setattr__('project_domain_id', value)
662 if key == 'project_domain_id':
663 if self.user_domain_id is None:
664 parent.__setattr__('user_domain_id', value)
665 if key == 'user_domain_name':
666 if self.project_domain_name is None:
667 parent.__setattr__('project_domain_name', value)
668 if key == 'project_domain_name':
669 if self.user_domain_name is None:
670 parent.__setattr__('user_domain_name', value)
671 # support domain_name coming from config
672 if key == 'domain_name':
John Warrenb10c6ca2016-02-26 15:32:37 -0500673 if self.user_domain_name is None:
674 parent.__setattr__('user_domain_name', value)
675 if self.project_domain_name is None:
676 parent.__setattr__('project_domain_name', value)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500677 # finally trigger default behaviour for all attributes
678 parent.__setattr__(key, value)
679
680 def is_valid(self):
681 """Check of credentials (no API call)
682
683 Valid combinations of v3 credentials (excluding token, scope)
684 - User id, password (optional domain)
685 - User name, password and its domain id/name
686 For the scope, valid combinations are:
687 - None
688 - Project id (optional domain)
689 - Project name and its domain id/name
690 - Domain id
691 - Domain name
692 """
693 valid_user_domain = any(
694 [self.user_domain_id is not None,
695 self.user_domain_name is not None])
696 valid_project_domain = any(
697 [self.project_domain_id is not None,
698 self.project_domain_name is not None])
699 valid_user = any(
700 [self.user_id is not None,
701 self.username is not None and valid_user_domain])
702 valid_project_scope = any(
703 [self.project_name is None and self.project_id is None,
704 self.project_id is not None,
705 self.project_name is not None and valid_project_domain])
706 valid_domain_scope = any(
707 [self.domain_id is None and self.domain_name is None,
708 self.domain_id or self.domain_name])
709 return all([self.password is not None,
710 valid_user,
711 valid_project_scope and valid_domain_scope])
712
713
714IDENTITY_VERSION = {'v2': (KeystoneV2Credentials, KeystoneV2AuthProvider),
715 'v3': (KeystoneV3Credentials, KeystoneV3AuthProvider)}