Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/auth/transport/requests.py: 28%
184 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 07:30 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 07:30 +0000
1# Copyright 2016 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""Transport adapter for Requests."""
17from __future__ import absolute_import
19import functools
20import logging
21import numbers
22import os
23import time
25try:
26 import requests
27except ImportError as caught_exc: # pragma: NO COVER
28 import six
30 six.raise_from(
31 ImportError(
32 "The requests library is not installed, please install the "
33 "requests package to use the requests transport."
34 ),
35 caught_exc,
36 )
37import requests.adapters # pylint: disable=ungrouped-imports
38import requests.exceptions # pylint: disable=ungrouped-imports
39from requests.packages.urllib3.util.ssl_ import ( # type: ignore
40 create_urllib3_context,
41) # pylint: disable=ungrouped-imports
42import six # pylint: disable=ungrouped-imports
44from google.auth import environment_vars
45from google.auth import exceptions
46from google.auth import transport
47import google.auth.transport._mtls_helper
48from google.oauth2 import service_account
50_LOGGER = logging.getLogger(__name__)
52_DEFAULT_TIMEOUT = 120 # in seconds
55class _Response(transport.Response):
56 """Requests transport response adapter.
58 Args:
59 response (requests.Response): The raw Requests response.
60 """
62 def __init__(self, response):
63 self._response = response
65 @property
66 def status(self):
67 return self._response.status_code
69 @property
70 def headers(self):
71 return self._response.headers
73 @property
74 def data(self):
75 return self._response.content
78class TimeoutGuard(object):
79 """A context manager raising an error if the suite execution took too long.
81 Args:
82 timeout (Union[None, Union[float, Tuple[float, float]]]):
83 The maximum number of seconds a suite can run without the context
84 manager raising a timeout exception on exit. If passed as a tuple,
85 the smaller of the values is taken as a timeout. If ``None``, a
86 timeout error is never raised.
87 timeout_error_type (Optional[Exception]):
88 The type of the error to raise on timeout. Defaults to
89 :class:`requests.exceptions.Timeout`.
90 """
92 def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
93 self._timeout = timeout
94 self.remaining_timeout = timeout
95 self._timeout_error_type = timeout_error_type
97 def __enter__(self):
98 self._start = time.time()
99 return self
101 def __exit__(self, exc_type, exc_value, traceback):
102 if exc_value:
103 return # let the error bubble up automatically
105 if self._timeout is None:
106 return # nothing to do, the timeout was not specified
108 elapsed = time.time() - self._start
109 deadline_hit = False
111 if isinstance(self._timeout, numbers.Number):
112 self.remaining_timeout = self._timeout - elapsed
113 deadline_hit = self.remaining_timeout <= 0
114 else:
115 self.remaining_timeout = tuple(x - elapsed for x in self._timeout)
116 deadline_hit = min(self.remaining_timeout) <= 0
118 if deadline_hit:
119 raise self._timeout_error_type()
122class Request(transport.Request):
123 """Requests request adapter.
125 This class is used internally for making requests using various transports
126 in a consistent way. If you use :class:`AuthorizedSession` you do not need
127 to construct or use this class directly.
129 This class can be useful if you want to manually refresh a
130 :class:`~google.auth.credentials.Credentials` instance::
132 import google.auth.transport.requests
133 import requests
135 request = google.auth.transport.requests.Request()
137 credentials.refresh(request)
139 Args:
140 session (requests.Session): An instance :class:`requests.Session` used
141 to make HTTP requests. If not specified, a session will be created.
143 .. automethod:: __call__
144 """
146 def __init__(self, session=None):
147 if not session:
148 session = requests.Session()
150 self.session = session
152 def __del__(self):
153 try:
154 if hasattr(self, "session") and self.session is not None:
155 self.session.close()
156 except TypeError:
157 # NOTE: For certain Python binary built, the queue.Empty exception
158 # might not be considered a normal Python exception causing
159 # TypeError.
160 pass
162 def __call__(
163 self,
164 url,
165 method="GET",
166 body=None,
167 headers=None,
168 timeout=_DEFAULT_TIMEOUT,
169 **kwargs
170 ):
171 """Make an HTTP request using requests.
173 Args:
174 url (str): The URI to be requested.
175 method (str): The HTTP method to use for the request. Defaults
176 to 'GET'.
177 body (bytes): The payload or body in HTTP request.
178 headers (Mapping[str, str]): Request headers.
179 timeout (Optional[int]): The number of seconds to wait for a
180 response from the server. If not specified or if None, the
181 requests default timeout will be used.
182 kwargs: Additional arguments passed through to the underlying
183 requests :meth:`~requests.Session.request` method.
185 Returns:
186 google.auth.transport.Response: The HTTP response.
188 Raises:
189 google.auth.exceptions.TransportError: If any exception occurred.
190 """
191 try:
192 _LOGGER.debug("Making request: %s %s", method, url)
193 response = self.session.request(
194 method, url, data=body, headers=headers, timeout=timeout, **kwargs
195 )
196 return _Response(response)
197 except requests.exceptions.RequestException as caught_exc:
198 new_exc = exceptions.TransportError(caught_exc)
199 six.raise_from(new_exc, caught_exc)
202class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
203 """
204 A TransportAdapter that enables mutual TLS.
206 Args:
207 cert (bytes): client certificate in PEM format
208 key (bytes): client private key in PEM format
210 Raises:
211 ImportError: if certifi or pyOpenSSL is not installed
212 OpenSSL.crypto.Error: if client cert or key is invalid
213 """
215 def __init__(self, cert, key):
216 import certifi
217 from OpenSSL import crypto
218 import urllib3.contrib.pyopenssl # type: ignore
220 urllib3.contrib.pyopenssl.inject_into_urllib3()
222 pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
223 x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
225 ctx_poolmanager = create_urllib3_context()
226 ctx_poolmanager.load_verify_locations(cafile=certifi.where())
227 ctx_poolmanager._ctx.use_certificate(x509)
228 ctx_poolmanager._ctx.use_privatekey(pkey)
229 self._ctx_poolmanager = ctx_poolmanager
231 ctx_proxymanager = create_urllib3_context()
232 ctx_proxymanager.load_verify_locations(cafile=certifi.where())
233 ctx_proxymanager._ctx.use_certificate(x509)
234 ctx_proxymanager._ctx.use_privatekey(pkey)
235 self._ctx_proxymanager = ctx_proxymanager
237 super(_MutualTlsAdapter, self).__init__()
239 def init_poolmanager(self, *args, **kwargs):
240 kwargs["ssl_context"] = self._ctx_poolmanager
241 super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
243 def proxy_manager_for(self, *args, **kwargs):
244 kwargs["ssl_context"] = self._ctx_proxymanager
245 return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
248class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
249 """
250 A TransportAdapter that enables mutual TLS and offloads the client side
251 signing operation to the signing library.
253 Args:
254 enterprise_cert_file_path (str): the path to a enterprise cert JSON
255 file. The file should contain the following field:
257 {
258 "libs": {
259 "signer_library": "...",
260 "offload_library": "..."
261 }
262 }
264 Raises:
265 ImportError: if certifi or pyOpenSSL is not installed
266 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
267 creation failed for any reason.
268 """
270 def __init__(self, enterprise_cert_file_path):
271 import certifi
272 import urllib3.contrib.pyopenssl
274 from google.auth.transport import _custom_tls_signer
276 # Call inject_into_urllib3 to activate certificate checking. See the
277 # following links for more info:
278 # (1) doc: https://github.com/urllib3/urllib3/blob/cb9ebf8aac5d75f64c8551820d760b72b619beff/src/urllib3/contrib/pyopenssl.py#L31-L32
279 # (2) mTLS example: https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
280 urllib3.contrib.pyopenssl.inject_into_urllib3()
282 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
283 self.signer.load_libraries()
284 self.signer.set_up_custom_key()
286 poolmanager = create_urllib3_context()
287 poolmanager.load_verify_locations(cafile=certifi.where())
288 self.signer.attach_to_ssl_context(poolmanager)
289 self._ctx_poolmanager = poolmanager
291 proxymanager = create_urllib3_context()
292 proxymanager.load_verify_locations(cafile=certifi.where())
293 self.signer.attach_to_ssl_context(proxymanager)
294 self._ctx_proxymanager = proxymanager
296 super(_MutualTlsOffloadAdapter, self).__init__()
298 def init_poolmanager(self, *args, **kwargs):
299 kwargs["ssl_context"] = self._ctx_poolmanager
300 super(_MutualTlsOffloadAdapter, self).init_poolmanager(*args, **kwargs)
302 def proxy_manager_for(self, *args, **kwargs):
303 kwargs["ssl_context"] = self._ctx_proxymanager
304 return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)
307class AuthorizedSession(requests.Session):
308 """A Requests Session class with credentials.
310 This class is used to perform requests to API endpoints that require
311 authorization::
313 from google.auth.transport.requests import AuthorizedSession
315 authed_session = AuthorizedSession(credentials)
317 response = authed_session.request(
318 'GET', 'https://www.googleapis.com/storage/v1/b')
321 The underlying :meth:`request` implementation handles adding the
322 credentials' headers to the request and refreshing credentials as needed.
324 This class also supports mutual TLS via :meth:`configure_mtls_channel`
325 method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
326 environment variable must be explicitly set to ``true``, otherwise it does
327 nothing. Assume the environment is set to ``true``, the method behaves in the
328 following manner:
330 If client_cert_callback is provided, client certificate and private
331 key are loaded using the callback; if client_cert_callback is None,
332 application default SSL credentials will be used. Exceptions are raised if
333 there are problems with the certificate, private key, or the loading process,
334 so it should be called within a try/except block.
336 First we set the environment variable to ``true``, then create an :class:`AuthorizedSession`
337 instance and specify the endpoints::
339 regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
340 mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
342 authed_session = AuthorizedSession(credentials)
344 Now we can pass a callback to :meth:`configure_mtls_channel`::
346 def my_cert_callback():
347 # some code to load client cert bytes and private key bytes, both in
348 # PEM format.
349 some_code_to_load_client_cert_and_key()
350 if loaded:
351 return cert, key
352 raise MyClientCertFailureException()
354 # Always call configure_mtls_channel within a try/except block.
355 try:
356 authed_session.configure_mtls_channel(my_cert_callback)
357 except:
358 # handle exceptions.
360 if authed_session.is_mtls:
361 response = authed_session.request('GET', mtls_endpoint)
362 else:
363 response = authed_session.request('GET', regular_endpoint)
366 You can alternatively use application default SSL credentials like this::
368 try:
369 authed_session.configure_mtls_channel()
370 except:
371 # handle exceptions.
373 Args:
374 credentials (google.auth.credentials.Credentials): The credentials to
375 add to the request.
376 refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
377 that credentials should be refreshed and the request should be
378 retried.
379 max_refresh_attempts (int): The maximum number of times to attempt to
380 refresh the credentials and retry the request.
381 refresh_timeout (Optional[int]): The timeout value in seconds for
382 credential refresh HTTP requests.
383 auth_request (google.auth.transport.requests.Request):
384 (Optional) An instance of
385 :class:`~google.auth.transport.requests.Request` used when
386 refreshing credentials. If not passed,
387 an instance of :class:`~google.auth.transport.requests.Request`
388 is created.
389 default_host (Optional[str]): A host like "pubsub.googleapis.com".
390 This is used when a self-signed JWT is created from service
391 account credentials.
392 """
394 def __init__(
395 self,
396 credentials,
397 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
398 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
399 refresh_timeout=None,
400 auth_request=None,
401 default_host=None,
402 ):
403 super(AuthorizedSession, self).__init__()
404 self.credentials = credentials
405 self._refresh_status_codes = refresh_status_codes
406 self._max_refresh_attempts = max_refresh_attempts
407 self._refresh_timeout = refresh_timeout
408 self._is_mtls = False
409 self._default_host = default_host
411 if auth_request is None:
412 self._auth_request_session = requests.Session()
414 # Using an adapter to make HTTP requests robust to network errors.
415 # This adapter retrys HTTP requests when network errors occur
416 # and the requests seems safely retryable.
417 retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
418 self._auth_request_session.mount("https://", retry_adapter)
420 # Do not pass `self` as the session here, as it can lead to
421 # infinite recursion.
422 auth_request = Request(self._auth_request_session)
423 else:
424 self._auth_request_session = None
426 # Request instance used by internal methods (for example,
427 # credentials.refresh).
428 self._auth_request = auth_request
430 # https://google.aip.dev/auth/4111
431 # Attempt to use self-signed JWTs when a service account is used.
432 if isinstance(self.credentials, service_account.Credentials):
433 self.credentials._create_self_signed_jwt(
434 "https://{}/".format(self._default_host) if self._default_host else None
435 )
437 def configure_mtls_channel(self, client_cert_callback=None):
438 """Configure the client certificate and key for SSL connection.
440 The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is
441 explicitly set to `true`. In this case if client certificate and key are
442 successfully obtained (from the given client_cert_callback or from application
443 default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted
444 to "https://" prefix.
446 Args:
447 client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
448 The optional callback returns the client certificate and private
449 key bytes both in PEM format.
450 If the callback is None, application default SSL credentials
451 will be used.
453 Raises:
454 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
455 creation failed for any reason.
456 """
457 use_client_cert = os.getenv(
458 environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
459 )
460 if use_client_cert != "true":
461 self._is_mtls = False
462 return
464 try:
465 import OpenSSL
466 except ImportError as caught_exc:
467 new_exc = exceptions.MutualTLSChannelError(caught_exc)
468 six.raise_from(new_exc, caught_exc)
470 try:
471 (
472 self._is_mtls,
473 cert,
474 key,
475 ) = google.auth.transport._mtls_helper.get_client_cert_and_key(
476 client_cert_callback
477 )
479 if self._is_mtls:
480 mtls_adapter = _MutualTlsAdapter(cert, key)
481 self.mount("https://", mtls_adapter)
482 except (
483 exceptions.ClientCertError,
484 ImportError,
485 OpenSSL.crypto.Error,
486 ) as caught_exc:
487 new_exc = exceptions.MutualTLSChannelError(caught_exc)
488 six.raise_from(new_exc, caught_exc)
490 def request(
491 self,
492 method,
493 url,
494 data=None,
495 headers=None,
496 max_allowed_time=None,
497 timeout=_DEFAULT_TIMEOUT,
498 **kwargs
499 ):
500 """Implementation of Requests' request.
502 Args:
503 timeout (Optional[Union[float, Tuple[float, float]]]):
504 The amount of time in seconds to wait for the server response
505 with each individual request. Can also be passed as a tuple
506 ``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request`
507 documentation for details.
508 max_allowed_time (Optional[float]):
509 If the method runs longer than this, a ``Timeout`` exception is
510 automatically raised. Unlike the ``timeout`` parameter, this
511 value applies to the total method execution time, even if
512 multiple requests are made under the hood.
514 Mind that it is not guaranteed that the timeout error is raised
515 at ``max_allowed_time``. It might take longer, for example, if
516 an underlying request takes a lot of time, but the request
517 itself does not timeout, e.g. if a large file is being
518 transmitted. The timout error will be raised after such
519 request completes.
520 """
521 # pylint: disable=arguments-differ
522 # Requests has a ton of arguments to request, but only two
523 # (method, url) are required. We pass through all of the other
524 # arguments to super, so no need to exhaustively list them here.
526 # Use a kwarg for this instead of an attribute to maintain
527 # thread-safety.
528 _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
530 # Make a copy of the headers. They will be modified by the credentials
531 # and we want to pass the original headers if we recurse.
532 request_headers = headers.copy() if headers is not None else {}
534 # Do not apply the timeout unconditionally in order to not override the
535 # _auth_request's default timeout.
536 auth_request = (
537 self._auth_request
538 if timeout is None
539 else functools.partial(self._auth_request, timeout=timeout)
540 )
542 remaining_time = max_allowed_time
544 with TimeoutGuard(remaining_time) as guard:
545 self.credentials.before_request(auth_request, method, url, request_headers)
546 remaining_time = guard.remaining_timeout
548 with TimeoutGuard(remaining_time) as guard:
549 response = super(AuthorizedSession, self).request(
550 method,
551 url,
552 data=data,
553 headers=request_headers,
554 timeout=timeout,
555 **kwargs
556 )
557 remaining_time = guard.remaining_timeout
559 # If the response indicated that the credentials needed to be
560 # refreshed, then refresh the credentials and re-attempt the
561 # request.
562 # A stored token may expire between the time it is retrieved and
563 # the time the request is made, so we may need to try twice.
564 if (
565 response.status_code in self._refresh_status_codes
566 and _credential_refresh_attempt < self._max_refresh_attempts
567 ):
569 _LOGGER.info(
570 "Refreshing credentials due to a %s response. Attempt %s/%s.",
571 response.status_code,
572 _credential_refresh_attempt + 1,
573 self._max_refresh_attempts,
574 )
576 # Do not apply the timeout unconditionally in order to not override the
577 # _auth_request's default timeout.
578 auth_request = (
579 self._auth_request
580 if timeout is None
581 else functools.partial(self._auth_request, timeout=timeout)
582 )
584 with TimeoutGuard(remaining_time) as guard:
585 self.credentials.refresh(auth_request)
586 remaining_time = guard.remaining_timeout
588 # Recurse. Pass in the original headers, not our modified set, but
589 # do pass the adjusted max allowed time (i.e. the remaining total time).
590 return self.request(
591 method,
592 url,
593 data=data,
594 headers=headers,
595 max_allowed_time=remaining_time,
596 timeout=timeout,
597 _credential_refresh_attempt=_credential_refresh_attempt + 1,
598 **kwargs
599 )
601 return response
603 @property
604 def is_mtls(self):
605 """Indicates if the created SSL channel is mutual TLS."""
606 return self._is_mtls
608 def close(self):
609 if self._auth_request_session is not None:
610 self._auth_request_session.close()
611 super(AuthorizedSession, self).close()