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