blob: 9f8c7c5eca207de1c3368e768a71f67853abcef4 [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2014 Hewlett-Packard Development Company, L.P.
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -06002# Copyright 2016 Rackspace Inc.
Matthew Treinish9e26ca82016-02-23 11:43:20 -05003# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import abc
18import copy
19import datetime
20import re
21
22from oslo_log import log as logging
23import six
24from six.moves.urllib import parse as urlparse
25
26from tempest.lib import exceptions
27from tempest.lib.services.identity.v2 import token_client as json_v2id
28from tempest.lib.services.identity.v3 import token_client as json_v3id
29
30ISO8601_FLOAT_SECONDS = '%Y-%m-%dT%H:%M:%S.%fZ'
31ISO8601_INT_SECONDS = '%Y-%m-%dT%H:%M:%SZ'
32LOG = logging.getLogger(__name__)
33
34
Brant Knudsonf2d1f572016-04-11 15:02:01 -050035def replace_version(url, new_version):
36 parts = urlparse.urlparse(url)
37 version_path = '/%s' % new_version
Brant Knudson77293802016-04-11 15:14:54 -050038 path, subs = re.subn(r'(^|/)+v\d+(?:\.\d+)?',
39 version_path,
40 parts.path,
41 count=1)
42 if not subs:
43 path = '%s%s' % (parts.path.rstrip('/'), version_path)
Brant Knudsonf2d1f572016-04-11 15:02:01 -050044 url = urlparse.urlunparse((parts.scheme,
45 parts.netloc,
Brant Knudson77293802016-04-11 15:14:54 -050046 path,
Brant Knudsonf2d1f572016-04-11 15:02:01 -050047 parts.params,
48 parts.query,
49 parts.fragment))
50 return url
51
52
53def apply_url_filters(url, filters):
54 if filters.get('api_version', None) is not None:
55 url = replace_version(url, filters['api_version'])
56 parts = urlparse.urlparse(url)
57 if filters.get('skip_path', None) is not None and parts.path != '':
58 url = urlparse.urlunparse((parts.scheme,
59 parts.netloc,
60 '/',
61 parts.params,
62 parts.query,
63 parts.fragment))
64
65 return url
66
67
Matthew Treinish9e26ca82016-02-23 11:43:20 -050068@six.add_metaclass(abc.ABCMeta)
69class AuthProvider(object):
70 """Provide authentication"""
71
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +010072 SCOPES = set(['project'])
73
74 def __init__(self, credentials, scope='project'):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050075 """Auth provider __init__
76
77 :param credentials: credentials for authentication
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +010078 :param scope: the default scope to be used by the credential providers
79 when requesting a token. Valid values depend on the
80 AuthProvider class implementation, and are defined in
81 the set SCOPES. Default value is 'project'.
Matthew Treinish9e26ca82016-02-23 11:43:20 -050082 """
83 if self.check_credentials(credentials):
84 self.credentials = credentials
85 else:
86 if isinstance(credentials, Credentials):
87 password = credentials.get('password')
88 message = "Credentials are: " + str(credentials)
89 if password is None:
90 message += " Password is not defined."
91 else:
92 message += " Password is defined."
93 raise exceptions.InvalidCredentials(message)
94 else:
95 raise TypeError("credentials object is of type %s, which is"
96 " not a valid Credentials object type." %
97 credentials.__class__.__name__)
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +010098 self._scope = None
99 self.scope = scope
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500100 self.cache = None
101 self.alt_auth_data = None
102 self.alt_part = None
103
104 def __str__(self):
105 return "Creds :{creds}, cached auth data: {cache}".format(
106 creds=self.credentials, cache=self.cache)
107
108 @abc.abstractmethod
109 def _decorate_request(self, filters, method, url, headers=None, body=None,
110 auth_data=None):
111 """Decorate request with authentication data"""
112 return
113
114 @abc.abstractmethod
115 def _get_auth(self):
116 return
117
118 @abc.abstractmethod
119 def _fill_credentials(self, auth_data_body):
120 return
121
122 def fill_credentials(self):
123 """Fill credentials object with data from auth"""
124 auth_data = self.get_auth()
125 self._fill_credentials(auth_data[1])
126 return self.credentials
127
128 @classmethod
129 def check_credentials(cls, credentials):
130 """Verify credentials are valid."""
131 return isinstance(credentials, Credentials) and credentials.is_valid()
132
133 @property
134 def auth_data(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100135 """Auth data for set scope"""
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500136 return self.get_auth()
137
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100138 @property
139 def scope(self):
140 """Scope used in auth requests"""
141 return self._scope
142
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500143 @auth_data.deleter
144 def auth_data(self):
145 self.clear_auth()
146
147 def get_auth(self):
148 """Returns auth from cache if available, else auth first"""
149 if self.cache is None or self.is_expired(self.cache):
150 self.set_auth()
151 return self.cache
152
153 def set_auth(self):
154 """Forces setting auth.
155
156 Forces setting auth, ignores cache if it exists.
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100157 Refills credentials.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500158 """
159 self.cache = self._get_auth()
160 self._fill_credentials(self.cache[1])
161
162 def clear_auth(self):
163 """Clear access cache
164
165 Can be called to clear the access cache so that next request
166 will fetch a new token and base_url.
167 """
168 self.cache = None
169 self.credentials.reset()
170
171 @abc.abstractmethod
172 def is_expired(self, auth_data):
173 return
174
175 def auth_request(self, method, url, headers=None, body=None, filters=None):
176 """Obtains auth data and decorates a request with that.
177
178 :param method: HTTP method of the request
179 :param url: relative URL of the request (path)
180 :param headers: HTTP headers of the request
181 :param body: HTTP body in case of POST / PUT
182 :param filters: select a base URL out of the catalog
Masayuki Igawaeb11b252016-06-10 10:40:02 +0900183 :return: a Tuple (url, headers, body)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500184 """
185 orig_req = dict(url=url, headers=headers, body=body)
186
187 auth_url, auth_headers, auth_body = self._decorate_request(
188 filters, method, url, headers, body)
189 auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body)
190
191 # Overwrite part if the request if it has been requested
192 if self.alt_part is not None:
193 if self.alt_auth_data is not None:
194 alt_url, alt_headers, alt_body = self._decorate_request(
195 filters, method, url, headers, body,
196 auth_data=self.alt_auth_data)
197 alt_auth_req = dict(url=alt_url, headers=alt_headers,
198 body=alt_body)
199 if auth_req[self.alt_part] == alt_auth_req[self.alt_part]:
200 raise exceptions.BadAltAuth(part=self.alt_part)
201 auth_req[self.alt_part] = alt_auth_req[self.alt_part]
202
203 else:
204 # If the requested part is not affected by auth, we are
205 # not altering auth as expected, raise an exception
206 if auth_req[self.alt_part] == orig_req[self.alt_part]:
207 raise exceptions.BadAltAuth(part=self.alt_part)
208 # If alt auth data is None, skip auth in the requested part
209 auth_req[self.alt_part] = orig_req[self.alt_part]
210
211 # Next auth request will be normal, unless otherwise requested
212 self.reset_alt_auth_data()
213
214 return auth_req['url'], auth_req['headers'], auth_req['body']
215
216 def reset_alt_auth_data(self):
217 """Configure auth provider to provide valid authentication data"""
218 self.alt_part = None
219 self.alt_auth_data = None
220
221 def set_alt_auth_data(self, request_part, auth_data):
222 """Alternate auth data on next request
223
224 Configure auth provider to provide alt authentication data
225 on a part of the *next* auth_request. If credentials are None,
226 set invalid data.
Masayuki Igawaeb11b252016-06-10 10:40:02 +0900227
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500228 :param request_part: request part to contain invalid auth: url,
229 headers, body
230 :param auth_data: alternative auth_data from which to get the
231 invalid data to be injected
232 """
233 self.alt_part = request_part
234 self.alt_auth_data = auth_data
235
236 @abc.abstractmethod
237 def base_url(self, filters, auth_data=None):
238 """Extracts the base_url based on provided filters"""
239 return
240
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100241 @scope.setter
242 def scope(self, value):
243 """Set the scope to be used in token requests
244
245 :param scope: scope to be used. If the scope is different, clear caches
246 """
247 if value not in self.SCOPES:
248 raise exceptions.InvalidScope(
249 scope=value, auth_provider=self.__class__.__name__)
250 if value != self.scope:
251 self.clear_auth()
252 self._scope = value
253
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500254
255class KeystoneAuthProvider(AuthProvider):
256
257 EXPIRY_DATE_FORMATS = (ISO8601_FLOAT_SECONDS, ISO8601_INT_SECONDS)
258
259 token_expiry_threshold = datetime.timedelta(seconds=60)
260
261 def __init__(self, credentials, auth_url,
262 disable_ssl_certificate_validation=None,
zhufl071e94c2016-07-12 10:26:34 +0800263 ca_certs=None, trace_requests=None, scope='project',
Matthew Treinish74514402016-09-01 11:44:57 -0400264 http_timeout=None, proxy_url=None):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100265 super(KeystoneAuthProvider, self).__init__(credentials, scope)
Andrea Frittoli (andreaf)97aa6042016-06-10 13:22:56 +0100266 self.dscv = disable_ssl_certificate_validation
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500267 self.ca_certs = ca_certs
268 self.trace_requests = trace_requests
zhufl071e94c2016-07-12 10:26:34 +0800269 self.http_timeout = http_timeout
Matthew Treinish74514402016-09-01 11:44:57 -0400270 self.proxy_url = proxy_url
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100271 self.auth_url = auth_url
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500272 self.auth_client = self._auth_client(auth_url)
273
274 def _decorate_request(self, filters, method, url, headers=None, body=None,
275 auth_data=None):
276 if auth_data is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100277 auth_data = self.get_auth()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500278 token, _ = auth_data
279 base_url = self.base_url(filters=filters, auth_data=auth_data)
280 # build authenticated request
281 # returns new request, it does not touch the original values
282 _headers = copy.deepcopy(headers) if headers is not None else {}
283 _headers['X-Auth-Token'] = str(token)
284 if url is None or url == "":
285 _url = base_url
286 else:
287 # Join base URL and url, and remove multiple contiguous slashes
288 _url = "/".join([base_url, url])
289 parts = [x for x in urlparse.urlparse(_url)]
290 parts[2] = re.sub("/{2,}", "/", parts[2])
291 _url = urlparse.urlunparse(parts)
292 # no change to method or body
293 return str(_url), _headers, body
294
295 @abc.abstractmethod
296 def _auth_client(self):
297 return
298
299 @abc.abstractmethod
300 def _auth_params(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100301 """Auth parameters to be passed to the token request
302
303 By default all fields available in Credentials are passed to the
304 token request. Scope may affect this.
305 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500306 return
307
308 def _get_auth(self):
309 # Bypasses the cache
310 auth_func = getattr(self.auth_client, 'get_token')
311 auth_params = self._auth_params()
312
313 # returns token, auth_data
314 token, auth_data = auth_func(**auth_params)
315 return token, auth_data
316
317 def _parse_expiry_time(self, expiry_string):
318 expiry = None
319 for date_format in self.EXPIRY_DATE_FORMATS:
320 try:
321 expiry = datetime.datetime.strptime(
322 expiry_string, date_format)
323 except ValueError:
324 pass
325 if expiry is None:
326 raise ValueError(
zhuflde676372018-11-16 15:34:56 +0800327 "time data '{data}' does not match any of the "
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500328 "expected formats: {formats}".format(
329 data=expiry_string, formats=self.EXPIRY_DATE_FORMATS))
330 return expiry
331
332 def get_token(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100333 return self.get_auth()[0]
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500334
335
336class KeystoneV2AuthProvider(KeystoneAuthProvider):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100337 """Provides authentication based on the Identity V2 API
338
339 The Keystone Identity V2 API defines both unscoped and project scoped
340 tokens. This auth provider only implements 'project'.
341 """
342
343 SCOPES = set(['project'])
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500344
345 def _auth_client(self, auth_url):
346 return json_v2id.TokenClient(
Andrea Frittoli (andreaf)97aa6042016-06-10 13:22:56 +0100347 auth_url, disable_ssl_certificate_validation=self.dscv,
zhufl071e94c2016-07-12 10:26:34 +0800348 ca_certs=self.ca_certs, trace_requests=self.trace_requests,
Matthew Treinish74514402016-09-01 11:44:57 -0400349 http_timeout=self.http_timeout, proxy_url=self.proxy_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500350
351 def _auth_params(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100352 """Auth parameters to be passed to the token request
353
354 All fields available in Credentials are passed to the token request.
355 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500356 return dict(
357 user=self.credentials.username,
358 password=self.credentials.password,
359 tenant=self.credentials.tenant_name,
360 auth_data=True)
361
362 def _fill_credentials(self, auth_data_body):
363 tenant = auth_data_body['token']['tenant']
364 user = auth_data_body['user']
365 if self.credentials.tenant_name is None:
366 self.credentials.tenant_name = tenant['name']
367 if self.credentials.tenant_id is None:
368 self.credentials.tenant_id = tenant['id']
369 if self.credentials.username is None:
370 self.credentials.username = user['name']
371 if self.credentials.user_id is None:
372 self.credentials.user_id = user['id']
373
374 def base_url(self, filters, auth_data=None):
375 """Base URL from catalog
376
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600377 :param filters: Used to filter results
Masayuki Igawaeb11b252016-06-10 10:40:02 +0900378
379 Filters can be:
380
381 - service: service type name such as compute, image, etc.
382 - region: service region name
383 - name: service name, only if service exists
384 - endpoint_type: type of endpoint such as
385 adminURL, publicURL, internalURL
386 - api_version: the version of api used to replace catalog version
387 - skip_path: skips the suffix path of the url and uses base URL
388
389 :rtype: string
390 :return: url with filters applied
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500391 """
392 if auth_data is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100393 auth_data = self.get_auth()
zhufl3ead9982020-11-19 14:39:04 +0800394 _, _auth_data = auth_data
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500395 service = filters.get('service')
396 region = filters.get('region')
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600397 name = filters.get('name')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500398 endpoint_type = filters.get('endpoint_type', 'publicURL')
399
400 if service is None:
401 raise exceptions.EndpointNotFound("No service provided")
402
403 _base_url = None
404 for ep in _auth_data['serviceCatalog']:
405 if ep["type"] == service:
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600406 if name is not None and ep["name"] != name:
407 continue
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500408 for _ep in ep['endpoints']:
409 if region is not None and _ep['region'] == region:
410 _base_url = _ep.get(endpoint_type)
411 if not _base_url:
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600412 # No region or name matching, use the first
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500413 _base_url = ep['endpoints'][0].get(endpoint_type)
414 break
415 if _base_url is None:
Ken'ichi Ohmichib6cf83a2016-03-02 17:56:45 -0800416 raise exceptions.EndpointNotFound(
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600417 "service: %s, region: %s, endpoint_type: %s, name: %s" %
418 (service, region, endpoint_type, name))
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500419 return apply_url_filters(_base_url, filters)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500420
421 def is_expired(self, auth_data):
422 _, access = auth_data
423 expiry = self._parse_expiry_time(access['token']['expires'])
424 return (expiry - self.token_expiry_threshold <=
425 datetime.datetime.utcnow())
426
427
428class KeystoneV3AuthProvider(KeystoneAuthProvider):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100429 """Provides authentication based on the Identity V3 API"""
430
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700431 SCOPES = set(['system', 'project', 'domain', 'unscoped', None])
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500432
433 def _auth_client(self, auth_url):
434 return json_v3id.V3TokenClient(
Andrea Frittoli (andreaf)97aa6042016-06-10 13:22:56 +0100435 auth_url, disable_ssl_certificate_validation=self.dscv,
zhufl071e94c2016-07-12 10:26:34 +0800436 ca_certs=self.ca_certs, trace_requests=self.trace_requests,
Matthew Treinish74514402016-09-01 11:44:57 -0400437 http_timeout=self.http_timeout, proxy_url=self.proxy_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500438
439 def _auth_params(self):
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100440 """Auth parameters to be passed to the token request
441
442 Fields available in Credentials are passed to the token request,
443 depending on the value of scope. Valid values for scope are: "project",
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700444 "domain", or "system". Any other string (e.g. "unscoped") or None will
445 lead to an unscoped token request.
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100446 """
447
448 auth_params = dict(
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500449 user_id=self.credentials.user_id,
450 username=self.credentials.username,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500451 user_domain_id=self.credentials.user_domain_id,
452 user_domain_name=self.credentials.user_domain_name,
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100453 password=self.credentials.password,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500454 auth_data=True)
455
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100456 if self.scope == 'project':
457 auth_params.update(
458 project_domain_id=self.credentials.project_domain_id,
459 project_domain_name=self.credentials.project_domain_name,
460 project_id=self.credentials.project_id,
461 project_name=self.credentials.project_name)
462
463 if self.scope == 'domain':
464 auth_params.update(
465 domain_id=self.credentials.domain_id,
466 domain_name=self.credentials.domain_name)
467
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700468 if self.scope == 'system':
469 auth_params.update(system='all')
470
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100471 return auth_params
472
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500473 def _fill_credentials(self, auth_data_body):
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700474 # project, domain, or system depending on the scope
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500475 project = auth_data_body.get('project', None)
476 domain = auth_data_body.get('domain', None)
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700477 system = auth_data_body.get('system', None)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500478 # user is always there
479 user = auth_data_body['user']
480 # Set project fields
481 if project is not None:
482 if self.credentials.project_name is None:
483 self.credentials.project_name = project['name']
484 if self.credentials.project_id is None:
485 self.credentials.project_id = project['id']
486 if self.credentials.project_domain_id is None:
487 self.credentials.project_domain_id = project['domain']['id']
488 if self.credentials.project_domain_name is None:
489 self.credentials.project_domain_name = (
490 project['domain']['name'])
491 # Set domain fields
492 if domain is not None:
493 if self.credentials.domain_id is None:
494 self.credentials.domain_id = domain['id']
495 if self.credentials.domain_name is None:
496 self.credentials.domain_name = domain['name']
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700497 # Set system scope
498 if system is not None:
499 self.credentials.system = 'all'
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500500 # Set user fields
501 if self.credentials.username is None:
502 self.credentials.username = user['name']
503 if self.credentials.user_id is None:
504 self.credentials.user_id = user['id']
505 if self.credentials.user_domain_id is None:
506 self.credentials.user_domain_id = user['domain']['id']
507 if self.credentials.user_domain_name is None:
508 self.credentials.user_domain_name = user['domain']['name']
509
510 def base_url(self, filters, auth_data=None):
511 """Base URL from catalog
512
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100513 If scope is not 'project', it may be that there is not catalog in
514 the auth_data. In such case, as long as the requested service is
515 'identity', we can use the original auth URL to build the base_url.
516
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600517 :param filters: Used to filter results
Masayuki Igawaeb11b252016-06-10 10:40:02 +0900518
519 Filters can be:
520
521 - service: service type name such as compute, image, etc.
522 - region: service region name
523 - name: service name, only if service exists
524 - endpoint_type: type of endpoint such as
525 adminURL, publicURL, internalURL
526 - api_version: the version of api used to replace catalog version
527 - skip_path: skips the suffix path of the url and uses base URL
528
529 :rtype: string
530 :return: url with filters applied
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500531 """
532 if auth_data is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100533 auth_data = self.get_auth()
zhufl3ead9982020-11-19 14:39:04 +0800534 _, _auth_data = auth_data
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500535 service = filters.get('service')
536 region = filters.get('region')
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600537 name = filters.get('name')
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500538 endpoint_type = filters.get('endpoint_type', 'public')
539
540 if service is None:
541 raise exceptions.EndpointNotFound("No service provided")
542
543 if 'URL' in endpoint_type:
544 endpoint_type = endpoint_type.replace('URL', '')
545 _base_url = None
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100546 catalog = _auth_data.get('catalog', [])
Andrea Frittoli (andreaf)100d18d2016-05-05 23:34:52 +0100547
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500548 # Select entries with matching service type
549 service_catalog = [ep for ep in catalog if ep['type'] == service]
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900550 if service_catalog:
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600551 if name is not None:
552 service_catalog = (
553 [ep for ep in service_catalog if ep['name'] == name])
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900554 if service_catalog:
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600555 service_catalog = service_catalog[0]['endpoints']
556 else:
557 raise exceptions.EndpointNotFound(name)
558 else:
559 service_catalog = service_catalog[0]['endpoints']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500560 else:
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900561 if not catalog and service == 'identity':
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100562 # NOTE(andreaf) If there's no catalog at all and the service
563 # is identity, it's a valid use case. Having a non-empty
564 # catalog with no identity in it is not valid instead.
Andrea Frittoli (andreaf)100d18d2016-05-05 23:34:52 +0100565 msg = ('Got an empty catalog. Scope: %s. '
566 'Falling back to configured URL for %s: %s')
567 LOG.debug(msg, self.scope, service, self.auth_url)
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100568 return apply_url_filters(self.auth_url, filters)
569 else:
570 # No matching service
Andrea Frittoli (andreaf)100d18d2016-05-05 23:34:52 +0100571 msg = ('No matching service found in the catalog.\n'
572 'Scope: %s, Credentials: %s\n'
573 'Auth data: %s\n'
574 'Service: %s, Region: %s, endpoint_type: %s\n'
575 'Catalog: %s')
576 raise exceptions.EndpointNotFound(msg % (
577 self.scope, self.credentials, _auth_data, service, region,
578 endpoint_type, catalog))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500579 # Filter by endpoint type (interface)
580 filtered_catalog = [ep for ep in service_catalog if
581 ep['interface'] == endpoint_type]
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900582 if not filtered_catalog:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500583 # No matching type, keep all and try matching by region at least
584 filtered_catalog = service_catalog
585 # Filter by region
586 filtered_catalog = [ep for ep in filtered_catalog if
587 ep['region'] == region]
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900588 if not filtered_catalog:
Eric Wehrmeister54c7bd42016-02-24 11:11:07 -0600589 # No matching region (or name), take the first endpoint
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500590 filtered_catalog = [service_catalog[0]]
591 # There should be only one match. If not take the first.
592 _base_url = filtered_catalog[0].get('url', None)
593 if _base_url is None:
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100594 raise exceptions.EndpointNotFound(service)
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500595 return apply_url_filters(_base_url, filters)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500596
597 def is_expired(self, auth_data):
598 _, access = auth_data
599 expiry = self._parse_expiry_time(access['expires_at'])
600 return (expiry - self.token_expiry_threshold <=
601 datetime.datetime.utcnow())
602
603
604def is_identity_version_supported(identity_version):
605 return identity_version in IDENTITY_VERSION
606
607
608def get_credentials(auth_url, fill_in=True, identity_version='v2',
609 disable_ssl_certificate_validation=None, ca_certs=None,
Andrea Frittolicb94b5e2017-10-23 16:53:34 +0200610 trace_requests=None, http_timeout=None, proxy_url=None,
611 **kwargs):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500612 """Builds a credentials object based on the configured auth_version
613
614 :param auth_url (string): Full URI of the OpenStack Identity API(Keystone)
615 which is used to fetch the token from Identity service.
616 :param fill_in (boolean): obtain a token and fill in all credential
617 details provided by the identity service. When fill_in is not
618 specified, credentials are not validated. Validation can be invoked
619 by invoking ``is_valid()``
620 :param identity_version (string): identity API version is used to
621 select the matching auth provider and credentials class
622 :param disable_ssl_certificate_validation: whether to enforce SSL
623 certificate validation in SSL API requests to the auth system
624 :param ca_certs: CA certificate bundle for validation of certificates
625 in SSL API requests to the auth system
626 :param trace_requests: trace in log API requests to the auth system
zhufl071e94c2016-07-12 10:26:34 +0800627 :param http_timeout: timeout in seconds to wait for the http request to
628 return
Andrea Frittolicb94b5e2017-10-23 16:53:34 +0200629 :param proxy_url: URL of HTTP(s) proxy used when fill_in is True
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500630 :param kwargs (dict): Dict of credential key/value pairs
631
632 Examples:
633
634 Returns credentials from the provided parameters:
635 >>> get_credentials(username='foo', password='bar')
636
637 Returns credentials including IDs:
638 >>> get_credentials(username='foo', password='bar', fill_in=True)
639 """
640 if not is_identity_version_supported(identity_version):
641 raise exceptions.InvalidIdentityVersion(
642 identity_version=identity_version)
643
644 credential_class, auth_provider_class = IDENTITY_VERSION.get(
645 identity_version)
646
647 creds = credential_class(**kwargs)
648 # Fill in the credentials fields that were not specified
649 if fill_in:
Andrea Frittoli (andreaf)97aa6042016-06-10 13:22:56 +0100650 dscv = disable_ssl_certificate_validation
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500651 auth_provider = auth_provider_class(
Andrea Frittoli (andreaf)97aa6042016-06-10 13:22:56 +0100652 creds, auth_url, disable_ssl_certificate_validation=dscv,
zhufl071e94c2016-07-12 10:26:34 +0800653 ca_certs=ca_certs, trace_requests=trace_requests,
Andrea Frittolicb94b5e2017-10-23 16:53:34 +0200654 http_timeout=http_timeout, proxy_url=proxy_url)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500655 creds = auth_provider.fill_credentials()
656 return creds
657
658
659class Credentials(object):
660 """Set of credentials for accessing OpenStack services
661
662 ATTRIBUTES: list of valid class attributes representing credentials.
663 """
664
665 ATTRIBUTES = []
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100666 COLLISIONS = []
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500667
668 def __init__(self, **kwargs):
669 """Enforce the available attributes at init time (only).
670
671 Additional attributes can still be set afterwards if tests need
672 to do so.
673 """
674 self._initial = kwargs
675 self._apply_credentials(kwargs)
676
677 def _apply_credentials(self, attr):
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100678 for (key1, key2) in self.COLLISIONS:
679 val1 = attr.get(key1)
680 val2 = attr.get(key2)
681 if val1 and val2 and val1 != val2:
682 msg = ('Cannot have conflicting values for %s and %s' %
683 (key1, key2))
684 raise exceptions.InvalidCredentials(msg)
Joe H. Rahmea72f2c62016-07-11 16:28:19 +0200685 for key in attr:
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500686 if key in self.ATTRIBUTES:
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700687 if attr[key] is not None:
688 setattr(self, key, attr[key])
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500689 else:
690 msg = '%s is not a valid attr for %s' % (key, self.__class__)
691 raise exceptions.InvalidCredentials(msg)
692
693 def __str__(self):
694 """Represent only attributes included in self.ATTRIBUTES"""
Andreas Jaegerf27a3342020-03-29 10:21:39 +0200695 attrs = [attr for attr in self.ATTRIBUTES if attr != 'password']
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500696 _repr = dict((k, getattr(self, k)) for k in attrs)
697 return str(_repr)
698
699 def __eq__(self, other):
700 """Credentials are equal if attributes in self.ATTRIBUTES are equal"""
701 return str(self) == str(other)
702
Ji-Wei79f5efd2016-07-14 16:58:43 +0800703 def __ne__(self, other):
704 """Contrary to the __eq__"""
705 return not self.__eq__(other)
706
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500707 def __getattr__(self, key):
708 # If an attribute is set, __getattr__ is not invoked
709 # If an attribute is not set, and it is a known one, return None
710 if key in self.ATTRIBUTES:
711 return None
712 else:
713 raise AttributeError
714
715 def __delitem__(self, key):
716 # For backwards compatibility, support dict behaviour
717 if key in self.ATTRIBUTES:
718 delattr(self, key)
719 else:
720 raise AttributeError
721
722 def get(self, item, default=None):
723 # In this patch act as dict for backward compatibility
724 try:
725 return getattr(self, item)
726 except AttributeError:
727 return default
728
729 def get_init_attributes(self):
730 return self._initial.keys()
731
732 def is_valid(self):
733 raise NotImplementedError
734
735 def reset(self):
736 # First delete all known attributes
737 for key in self.ATTRIBUTES:
738 if getattr(self, key) is not None:
739 delattr(self, key)
740 # Then re-apply initial setup
741 self._apply_credentials(self._initial)
742
743
744class KeystoneV2Credentials(Credentials):
745
746 ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id',
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100747 'tenant_id', 'project_id', 'project_name']
748 COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
749
750 def __str__(self):
751 """Represent only attributes included in self.ATTRIBUTES"""
Andreas Jaegerf27a3342020-03-29 10:21:39 +0200752 attrs = [attr for attr in self.ATTRIBUTES if attr != 'password']
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100753 _repr = dict((k, getattr(self, k)) for k in attrs)
754 return str(_repr)
755
756 def __setattr__(self, key, value):
757 # NOTE(andreaf) In order to ease the migration towards 'project' we
758 # support v2 credentials configured with 'project' and translate it
759 # to tenant on the fly. The original kwargs are stored for clients
760 # that may rely on them. We also set project when tenant is defined
761 # so clients can rely on project being part of credentials.
762 parent = super(KeystoneV2Credentials, self)
763 # for project_* set tenant only
764 if key == 'project_id':
765 parent.__setattr__('tenant_id', value)
766 elif key == 'project_name':
767 parent.__setattr__('tenant_name', value)
768 if key == 'tenant_id':
769 parent.__setattr__('project_id', value)
770 elif key == 'tenant_name':
771 parent.__setattr__('project_name', value)
772 # trigger default behaviour for all attributes
773 parent.__setattr__(key, value)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500774
775 def is_valid(self):
776 """Check of credentials (no API call)
777
778 Minimum set of valid credentials, are username and password.
779 Tenant is optional.
780 """
781 return None not in (self.username, self.password)
782
783
784class KeystoneV3Credentials(Credentials):
785 """Credentials suitable for the Keystone Identity V3 API"""
786
787 ATTRIBUTES = ['domain_id', 'domain_name', 'password', 'username',
788 'project_domain_id', 'project_domain_name', 'project_id',
789 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
Colleen Murphycd0bbbd2019-10-01 16:18:36 -0700790 'user_domain_name', 'user_id', 'system']
Andrea Frittoli (andreaf)52deb8b2016-05-18 19:14:22 +0100791 COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
John Warrenb10c6ca2016-02-26 15:32:37 -0500792
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500793 def __setattr__(self, key, value):
794 parent = super(KeystoneV3Credentials, self)
795 # for tenant_* set both project and tenant
796 if key == 'tenant_id':
797 parent.__setattr__('project_id', value)
798 elif key == 'tenant_name':
799 parent.__setattr__('project_name', value)
800 # for project_* set both project and tenant
801 if key == 'project_id':
802 parent.__setattr__('tenant_id', value)
803 elif key == 'project_name':
804 parent.__setattr__('tenant_name', value)
805 # for *_domain_* set both user and project if not set yet
806 if key == 'user_domain_id':
807 if self.project_domain_id is None:
808 parent.__setattr__('project_domain_id', value)
809 if key == 'project_domain_id':
810 if self.user_domain_id is None:
811 parent.__setattr__('user_domain_id', value)
812 if key == 'user_domain_name':
813 if self.project_domain_name is None:
814 parent.__setattr__('project_domain_name', value)
815 if key == 'project_domain_name':
816 if self.user_domain_name is None:
817 parent.__setattr__('user_domain_name', value)
818 # support domain_name coming from config
819 if key == 'domain_name':
John Warrenb10c6ca2016-02-26 15:32:37 -0500820 if self.user_domain_name is None:
821 parent.__setattr__('user_domain_name', value)
822 if self.project_domain_name is None:
823 parent.__setattr__('project_domain_name', value)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500824 # finally trigger default behaviour for all attributes
825 parent.__setattr__(key, value)
826
827 def is_valid(self):
828 """Check of credentials (no API call)
829
Andrea Frittoli (andreaf)3e82af72016-05-05 22:53:38 +0100830 Valid combinations of v3 credentials (excluding token)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500831 - User id, password (optional domain)
832 - User name, password and its domain id/name
833 For the scope, valid combinations are:
834 - None
835 - Project id (optional domain)
836 - Project name and its domain id/name
837 - Domain id
838 - Domain name
839 """
840 valid_user_domain = any(
841 [self.user_domain_id is not None,
842 self.user_domain_name is not None])
843 valid_project_domain = any(
844 [self.project_domain_id is not None,
845 self.project_domain_name is not None])
846 valid_user = any(
847 [self.user_id is not None,
848 self.username is not None and valid_user_domain])
849 valid_project_scope = any(
850 [self.project_name is None and self.project_id is None,
851 self.project_id is not None,
852 self.project_name is not None and valid_project_domain])
853 valid_domain_scope = any(
854 [self.domain_id is None and self.domain_name is None,
855 self.domain_id or self.domain_name])
856 return all([self.password is not None,
857 valid_user,
858 valid_project_scope and valid_domain_scope])
859
860
861IDENTITY_VERSION = {'v2': (KeystoneV2Credentials, KeystoneV2AuthProvider),
862 'v3': (KeystoneV3Credentials, KeystoneV3AuthProvider)}