Update dashboard, memory, root +2 more (+3 ~5)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Transport - HTTP client library support.
|
||||
|
||||
:mod:`google.auth` is designed to work with various HTTP client libraries such
|
||||
as urllib3 and requests. In order to work across these libraries with different
|
||||
interfaces some abstraction is needed.
|
||||
|
||||
This module provides two interfaces that are implemented by transport adapters
|
||||
to support HTTP libraries. :class:`Request` defines the interface expected by
|
||||
:mod:`google.auth` to make requests. :class:`Response` defines the interface
|
||||
for the return value of :class:`Request`.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import http.client as http_client
|
||||
|
||||
DEFAULT_RETRYABLE_STATUS_CODES = (
|
||||
http_client.INTERNAL_SERVER_ERROR,
|
||||
http_client.SERVICE_UNAVAILABLE,
|
||||
http_client.GATEWAY_TIMEOUT,
|
||||
http_client.REQUEST_TIMEOUT,
|
||||
http_client.TOO_MANY_REQUESTS,
|
||||
)
|
||||
"""Sequence[int]: HTTP status codes indicating a request can be retried.
|
||||
"""
|
||||
|
||||
|
||||
DEFAULT_REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
|
||||
"""Sequence[int]: Which HTTP status code indicate that credentials should be
|
||||
refreshed.
|
||||
"""
|
||||
|
||||
DEFAULT_MAX_REFRESH_ATTEMPTS = 2
|
||||
"""int: How many times to refresh the credentials and retry a request."""
|
||||
|
||||
|
||||
class Response(metaclass=abc.ABCMeta):
|
||||
"""HTTP Response data."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def status(self):
|
||||
"""int: The HTTP status code."""
|
||||
raise NotImplementedError("status must be implemented.")
|
||||
|
||||
@abc.abstractproperty
|
||||
def headers(self):
|
||||
"""Mapping[str, str]: The HTTP response headers."""
|
||||
raise NotImplementedError("headers must be implemented.")
|
||||
|
||||
@abc.abstractproperty
|
||||
def data(self):
|
||||
"""bytes: The response body."""
|
||||
raise NotImplementedError("data must be implemented.")
|
||||
|
||||
|
||||
class Request(metaclass=abc.ABCMeta):
|
||||
"""Interface for a callable that makes HTTP requests.
|
||||
|
||||
Specific transport implementations should provide an implementation of
|
||||
this that adapts their specific request / response API.
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __call__(
|
||||
self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
|
||||
):
|
||||
"""Make an HTTP request.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping[str, str]): Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
transport-specific default timeout will be used.
|
||||
kwargs: Additionally arguments passed on to the transport's
|
||||
request method.
|
||||
|
||||
Returns:
|
||||
Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
# pylint: disable=redundant-returns-doc, missing-raises-doc
|
||||
# (pylint doesn't play well with abstract docstrings.)
|
||||
raise NotImplementedError("__call__ must be implemented.")
|
||||
@@ -0,0 +1,396 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Transport adapter for Async HTTP (aiohttp).
|
||||
|
||||
NOTE: This async support is experimental and marked internal. This surface may
|
||||
change in minor releases.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
|
||||
import aiohttp # type: ignore
|
||||
import urllib3 # type: ignore
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
from google.auth.aio import _helpers as _helpers_async
|
||||
from google.auth.transport import requests
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Timeout can be re-defined depending on async requirement. Currently made 60s more than
|
||||
# sync timeout.
|
||||
_DEFAULT_TIMEOUT = 180 # in seconds
|
||||
|
||||
|
||||
class _CombinedResponse(transport.Response):
|
||||
"""
|
||||
In order to more closely resemble the `requests` interface, where a raw
|
||||
and deflated content could be accessed at once, this class lazily reads the
|
||||
stream in `transport.Response` so both return forms can be used.
|
||||
|
||||
The gzip and deflate transfer-encodings are automatically decoded for you
|
||||
because the default parameter for autodecompress into the ClientSession is set
|
||||
to False, and therefore we add this class to act as a wrapper for a user to be
|
||||
able to access both the raw and decoded response bodies - mirroring the sync
|
||||
implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
self._raw_content = None
|
||||
|
||||
def _is_compressed(self):
|
||||
headers = self._response.headers
|
||||
return "Content-Encoding" in headers and (
|
||||
headers["Content-Encoding"] == "gzip"
|
||||
or headers["Content-Encoding"] == "deflate"
|
||||
)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._response.status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._response.headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._response.content
|
||||
|
||||
async def raw_content(self):
|
||||
if self._raw_content is None:
|
||||
self._raw_content = await self._response.content.read()
|
||||
return self._raw_content
|
||||
|
||||
async def content(self):
|
||||
# Load raw_content if necessary
|
||||
await self.raw_content()
|
||||
if self._is_compressed():
|
||||
decoder = urllib3.response.MultiDecoder(
|
||||
self._response.headers["Content-Encoding"]
|
||||
)
|
||||
decompressed = decoder.decompress(self._raw_content)
|
||||
return decompressed
|
||||
|
||||
return self._raw_content
|
||||
|
||||
|
||||
class _Response(transport.Response):
|
||||
"""
|
||||
Requests transport response adapter.
|
||||
|
||||
Args:
|
||||
response (requests.Response): The raw Requests response.
|
||||
"""
|
||||
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._response.status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._response.headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._response.content
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""Requests request adapter.
|
||||
|
||||
This class is used internally for making requests using asyncio transports
|
||||
in a consistent way. If you use :class:`AuthorizedSession` you do not need
|
||||
to construct or use this class directly.
|
||||
|
||||
This class can be useful if you want to manually refresh a
|
||||
:class:`~google.auth.credentials.Credentials` instance::
|
||||
|
||||
import google.auth.transport.aiohttp_requests
|
||||
|
||||
request = google.auth.transport.aiohttp_requests.Request()
|
||||
|
||||
credentials.refresh(request)
|
||||
|
||||
Args:
|
||||
session (aiohttp.ClientSession): An instance :class:`aiohttp.ClientSession` used
|
||||
to make HTTP requests. If not specified, a session will be created.
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
|
||||
def __init__(self, session=None):
|
||||
# TODO: Use auto_decompress property for aiohttp 3.7+
|
||||
if session is not None and session._auto_decompress:
|
||||
raise exceptions.InvalidOperation(
|
||||
"Client sessions with auto_decompress=True are not supported."
|
||||
)
|
||||
self.session = session
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
url,
|
||||
method="GET",
|
||||
body=None,
|
||||
headers=None,
|
||||
timeout=_DEFAULT_TIMEOUT,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Make an HTTP request using aiohttp.
|
||||
|
||||
Args:
|
||||
url (str): The URL to be requested.
|
||||
method (Optional[str]):
|
||||
The HTTP method to use for the request. Defaults to 'GET'.
|
||||
body (Optional[bytes]):
|
||||
The payload or body in HTTP request.
|
||||
headers (Optional[Mapping[str, str]]):
|
||||
Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
requests default timeout will be used.
|
||||
kwargs: Additional arguments passed through to the underlying
|
||||
requests :meth:`requests.Session.request` method.
|
||||
|
||||
Returns:
|
||||
google.auth.transport.Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
|
||||
try:
|
||||
if self.session is None: # pragma: NO COVER
|
||||
self.session = aiohttp.ClientSession(
|
||||
auto_decompress=False
|
||||
) # pragma: NO COVER
|
||||
_helpers.request_log(_LOGGER, method, url, body, headers)
|
||||
response = await self.session.request(
|
||||
method, url, data=body, headers=headers, timeout=timeout, **kwargs
|
||||
)
|
||||
await _helpers_async.response_log_async(_LOGGER, response)
|
||||
return _CombinedResponse(response)
|
||||
|
||||
except aiohttp.ClientError as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
except asyncio.TimeoutError as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
|
||||
class AuthorizedSession(aiohttp.ClientSession):
|
||||
"""This is an async implementation of the Authorized Session class. We utilize an
|
||||
aiohttp transport instance, and the interface mirrors the google.auth.transport.requests
|
||||
Authorized Session class, except for the change in the transport used in the async use case.
|
||||
|
||||
A Requests Session class with credentials.
|
||||
|
||||
This class is used to perform requests to API endpoints that require
|
||||
authorization::
|
||||
|
||||
from google.auth.transport import aiohttp_requests
|
||||
|
||||
async with aiohttp_requests.AuthorizedSession(credentials) as authed_session:
|
||||
response = await authed_session.request(
|
||||
'GET', 'https://www.googleapis.com/storage/v1/b')
|
||||
|
||||
The underlying :meth:`request` implementation handles adding the
|
||||
credentials' headers to the request and refreshing credentials as needed.
|
||||
|
||||
Args:
|
||||
credentials (google.auth._credentials_async.Credentials):
|
||||
The credentials to add to the request.
|
||||
refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
|
||||
that credentials should be refreshed and the request should be
|
||||
retried.
|
||||
max_refresh_attempts (int): The maximum number of times to attempt to
|
||||
refresh the credentials and retry the request.
|
||||
refresh_timeout (Optional[int]): The timeout value in seconds for
|
||||
credential refresh HTTP requests.
|
||||
auth_request (google.auth.transport.aiohttp_requests.Request):
|
||||
(Optional) An instance of
|
||||
:class:`~google.auth.transport.aiohttp_requests.Request` used when
|
||||
refreshing credentials. If not passed,
|
||||
an instance of :class:`~google.auth.transport.aiohttp_requests.Request`
|
||||
is created.
|
||||
kwargs: Additional arguments passed through to the underlying
|
||||
ClientSession :meth:`aiohttp.ClientSession` object.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
credentials,
|
||||
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
|
||||
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
|
||||
refresh_timeout=None,
|
||||
auth_request=None,
|
||||
auto_decompress=False,
|
||||
**kwargs,
|
||||
):
|
||||
super(AuthorizedSession, self).__init__(**kwargs)
|
||||
self.credentials = credentials
|
||||
self._refresh_status_codes = refresh_status_codes
|
||||
self._max_refresh_attempts = max_refresh_attempts
|
||||
self._refresh_timeout = refresh_timeout
|
||||
self._is_mtls = False
|
||||
self._auth_request = auth_request
|
||||
self._auth_request_session = None
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self._refresh_lock = asyncio.Lock()
|
||||
self._auto_decompress = auto_decompress
|
||||
|
||||
async def request(
|
||||
self,
|
||||
method,
|
||||
url,
|
||||
data=None,
|
||||
headers=None,
|
||||
max_allowed_time=None,
|
||||
timeout=_DEFAULT_TIMEOUT,
|
||||
auto_decompress=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Implementation of Authorized Session aiohttp request.
|
||||
|
||||
Args:
|
||||
method (str):
|
||||
The http request method used (e.g. GET, PUT, DELETE)
|
||||
url (str):
|
||||
The url at which the http request is sent.
|
||||
data (Optional[dict]): Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the Request.
|
||||
headers (Optional[dict]): Dictionary of HTTP Headers to send with the
|
||||
Request.
|
||||
timeout (Optional[Union[float, aiohttp.ClientTimeout]]):
|
||||
The amount of time in seconds to wait for the server response
|
||||
with each individual request. Can also be passed as an
|
||||
``aiohttp.ClientTimeout`` object.
|
||||
max_allowed_time (Optional[float]):
|
||||
If the method runs longer than this, a ``Timeout`` exception is
|
||||
automatically raised. Unlike the ``timeout`` parameter, this
|
||||
value applies to the total method execution time, even if
|
||||
multiple requests are made under the hood.
|
||||
|
||||
Mind that it is not guaranteed that the timeout error is raised
|
||||
at ``max_allowed_time``. It might take longer, for example, if
|
||||
an underlying request takes a lot of time, but the request
|
||||
itself does not timeout, e.g. if a large file is being
|
||||
transmitted. The timeout error will be raised after such
|
||||
request completes.
|
||||
"""
|
||||
# Headers come in as bytes which isn't expected behavior, the resumable
|
||||
# media libraries in some cases expect a str type for the header values,
|
||||
# but sometimes the operations return these in bytes types.
|
||||
if headers:
|
||||
for key in headers.keys():
|
||||
if type(headers[key]) is bytes:
|
||||
headers[key] = headers[key].decode("utf-8")
|
||||
|
||||
async with aiohttp.ClientSession(
|
||||
auto_decompress=self._auto_decompress,
|
||||
trust_env=kwargs.get("trust_env", False),
|
||||
) as self._auth_request_session:
|
||||
auth_request = Request(self._auth_request_session)
|
||||
self._auth_request = auth_request
|
||||
|
||||
# Use a kwarg for this instead of an attribute to maintain
|
||||
# thread-safety.
|
||||
_credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
|
||||
# Make a copy of the headers. They will be modified by the credentials
|
||||
# and we want to pass the original headers if we recurse.
|
||||
request_headers = headers.copy() if headers is not None else {}
|
||||
|
||||
# Do not apply the timeout unconditionally in order to not override the
|
||||
# _auth_request's default timeout.
|
||||
auth_request = (
|
||||
self._auth_request
|
||||
if timeout is None
|
||||
else functools.partial(self._auth_request, timeout=timeout)
|
||||
)
|
||||
|
||||
remaining_time = max_allowed_time
|
||||
|
||||
with requests.TimeoutGuard(remaining_time, asyncio.TimeoutError) as guard:
|
||||
await self.credentials.before_request(
|
||||
auth_request, method, url, request_headers
|
||||
)
|
||||
|
||||
with requests.TimeoutGuard(remaining_time, asyncio.TimeoutError) as guard:
|
||||
response = await super(AuthorizedSession, self).request(
|
||||
method,
|
||||
url,
|
||||
data=data,
|
||||
headers=request_headers,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
remaining_time = guard.remaining_timeout
|
||||
|
||||
if (
|
||||
response.status in self._refresh_status_codes
|
||||
and _credential_refresh_attempt < self._max_refresh_attempts
|
||||
):
|
||||
requests._LOGGER.info(
|
||||
"Refreshing credentials due to a %s response. Attempt %s/%s.",
|
||||
response.status,
|
||||
_credential_refresh_attempt + 1,
|
||||
self._max_refresh_attempts,
|
||||
)
|
||||
|
||||
# Do not apply the timeout unconditionally in order to not override the
|
||||
# _auth_request's default timeout.
|
||||
auth_request = (
|
||||
self._auth_request
|
||||
if timeout is None
|
||||
else functools.partial(self._auth_request, timeout=timeout)
|
||||
)
|
||||
|
||||
with requests.TimeoutGuard(
|
||||
remaining_time, asyncio.TimeoutError
|
||||
) as guard:
|
||||
async with self._refresh_lock:
|
||||
await self._loop.run_in_executor(
|
||||
None, self.credentials.refresh, auth_request
|
||||
)
|
||||
|
||||
remaining_time = guard.remaining_timeout
|
||||
|
||||
return await self.request(
|
||||
method,
|
||||
url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
max_allowed_time=remaining_time,
|
||||
timeout=timeout,
|
||||
_credential_refresh_attempt=_credential_refresh_attempt + 1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,283 @@
|
||||
# Copyright 2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Code for configuring client side TLS to offload the signing operation to
|
||||
signing libraries.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import cffi # type: ignore
|
||||
|
||||
from google.auth import exceptions
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# C++ offload lib requires google-auth lib to provide the following callback:
|
||||
# using SignFunc = int (*)(unsigned char *sig, size_t *sig_len,
|
||||
# const unsigned char *tbs, size_t tbs_len)
|
||||
# The bytes to be signed and the length are provided via `tbs` and `tbs_len`,
|
||||
# the callback computes the signature, and write the signature and its length
|
||||
# into `sig` and `sig_len`.
|
||||
# If the signing is successful, the callback returns 1, otherwise it returns 0.
|
||||
SIGN_CALLBACK_CTYPE = ctypes.CFUNCTYPE(
|
||||
ctypes.c_int, # return type
|
||||
ctypes.POINTER(ctypes.c_ubyte), # sig
|
||||
ctypes.POINTER(ctypes.c_size_t), # sig_len
|
||||
ctypes.POINTER(ctypes.c_ubyte), # tbs
|
||||
ctypes.c_size_t, # tbs_len
|
||||
)
|
||||
|
||||
|
||||
# Cast SSL_CTX* to void*
|
||||
def _cast_ssl_ctx_to_void_p_pyopenssl(ssl_ctx):
|
||||
return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p)
|
||||
|
||||
|
||||
# Cast SSL_CTX* to void*
|
||||
def _cast_ssl_ctx_to_void_p_stdlib(context):
|
||||
return ctypes.c_void_p.from_address(
|
||||
id(context) + ctypes.sizeof(ctypes.c_void_p) * 2
|
||||
)
|
||||
|
||||
|
||||
# Load offload library and set up the function types.
|
||||
def load_offload_lib(offload_lib_path):
|
||||
_LOGGER.debug("loading offload library from %s", offload_lib_path)
|
||||
|
||||
# winmode parameter is only available for python 3.8+.
|
||||
lib = (
|
||||
ctypes.CDLL(offload_lib_path, winmode=0)
|
||||
if sys.version_info >= (3, 8) and os.name == "nt"
|
||||
else ctypes.CDLL(offload_lib_path)
|
||||
)
|
||||
|
||||
# Set up types for:
|
||||
# int ConfigureSslContext(SignFunc sign_func, const char *cert, SSL_CTX *ctx)
|
||||
lib.ConfigureSslContext.argtypes = [
|
||||
SIGN_CALLBACK_CTYPE,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_void_p,
|
||||
]
|
||||
lib.ConfigureSslContext.restype = ctypes.c_int
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
# Load signer library and set up the function types.
|
||||
# See: https://github.com/googleapis/enterprise-certificate-proxy/blob/main/cshared/main.go
|
||||
def load_signer_lib(signer_lib_path):
|
||||
_LOGGER.debug("loading signer library from %s", signer_lib_path)
|
||||
|
||||
# winmode parameter is only available for python 3.8+.
|
||||
lib = (
|
||||
ctypes.CDLL(signer_lib_path, winmode=0)
|
||||
if sys.version_info >= (3, 8) and os.name == "nt"
|
||||
else ctypes.CDLL(signer_lib_path)
|
||||
)
|
||||
|
||||
# Set up types for:
|
||||
# func GetCertPemForPython(configFilePath *C.char, certHolder *byte, certHolderLen int)
|
||||
lib.GetCertPemForPython.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
|
||||
# Returns: certLen
|
||||
lib.GetCertPemForPython.restype = ctypes.c_int
|
||||
|
||||
# Set up types for:
|
||||
# func SignForPython(configFilePath *C.char, digest *byte, digestLen int,
|
||||
# sigHolder *byte, sigHolderLen int)
|
||||
lib.SignForPython.argtypes = [
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
]
|
||||
# Returns: the signature length
|
||||
lib.SignForPython.restype = ctypes.c_int
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
def load_provider_lib(provider_lib_path):
|
||||
_LOGGER.debug("loading provider library from %s", provider_lib_path)
|
||||
|
||||
# winmode parameter is only available for python 3.8+.
|
||||
lib = (
|
||||
ctypes.CDLL(provider_lib_path, winmode=0)
|
||||
if sys.version_info >= (3, 8) and os.name == "nt"
|
||||
else ctypes.CDLL(provider_lib_path)
|
||||
)
|
||||
|
||||
lib.ECP_attach_to_ctx.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
||||
lib.ECP_attach_to_ctx.restype = ctypes.c_int
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
# Computes SHA256 hash.
|
||||
def _compute_sha256_digest(to_be_signed, to_be_signed_len):
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
|
||||
data = ctypes.string_at(to_be_signed, to_be_signed_len)
|
||||
hash = hashes.Hash(hashes.SHA256())
|
||||
hash.update(data)
|
||||
return hash.finalize()
|
||||
|
||||
|
||||
# Create the signing callback. The actual signing work is done by the
|
||||
# `SignForPython` method from the signer lib.
|
||||
def get_sign_callback(signer_lib, config_file_path):
|
||||
def sign_callback(sig, sig_len, tbs, tbs_len):
|
||||
_LOGGER.debug("calling sign callback...")
|
||||
|
||||
digest = _compute_sha256_digest(tbs, tbs_len)
|
||||
digestArray = ctypes.c_char * len(digest)
|
||||
|
||||
# reserve 2000 bytes for the signature, shoud be more then enough.
|
||||
# RSA signature is 256 bytes, EC signature is 70~72.
|
||||
sig_holder_len = 2000
|
||||
sig_holder = ctypes.create_string_buffer(sig_holder_len)
|
||||
|
||||
signature_len = signer_lib.SignForPython(
|
||||
config_file_path.encode(), # configFilePath
|
||||
digestArray.from_buffer(bytearray(digest)), # digest
|
||||
len(digest), # digestLen
|
||||
sig_holder, # sigHolder
|
||||
sig_holder_len, # sigHolderLen
|
||||
)
|
||||
|
||||
if signature_len == 0:
|
||||
# signing failed, return 0
|
||||
return 0
|
||||
|
||||
sig_len[0] = signature_len
|
||||
bs = bytearray(sig_holder)
|
||||
for i in range(signature_len):
|
||||
sig[i] = bs[i]
|
||||
|
||||
return 1
|
||||
|
||||
return SIGN_CALLBACK_CTYPE(sign_callback)
|
||||
|
||||
|
||||
# Obtain the certificate bytes by calling the `GetCertPemForPython` method from
|
||||
# the signer lib. The method is called twice, the first time is to compute the
|
||||
# cert length, then we create a buffer to hold the cert, and call it again to
|
||||
# fill the buffer.
|
||||
def get_cert(signer_lib, config_file_path):
|
||||
# First call to calculate the cert length
|
||||
cert_len = signer_lib.GetCertPemForPython(
|
||||
config_file_path.encode(), # configFilePath
|
||||
None, # certHolder
|
||||
0, # certHolderLen
|
||||
)
|
||||
if cert_len == 0:
|
||||
raise exceptions.MutualTLSChannelError("failed to get certificate")
|
||||
|
||||
# Then we create an array to hold the cert, and call again to fill the cert
|
||||
cert_holder = ctypes.create_string_buffer(cert_len)
|
||||
signer_lib.GetCertPemForPython(
|
||||
config_file_path.encode(), # configFilePath
|
||||
cert_holder, # certHolder
|
||||
cert_len, # certHolderLen
|
||||
)
|
||||
return bytes(cert_holder)
|
||||
|
||||
|
||||
class CustomTlsSigner(object):
|
||||
def __init__(self, enterprise_cert_file_path):
|
||||
"""
|
||||
This class loads the offload and signer library, and calls APIs from
|
||||
these libraries to obtain the cert and a signing callback, and attach
|
||||
them to SSL context. The cert and the signing callback will be used
|
||||
for client authentication in TLS handshake.
|
||||
|
||||
Args:
|
||||
enterprise_cert_file_path (str): the path to a enterprise cert JSON
|
||||
file. The file should contain the following field:
|
||||
|
||||
{
|
||||
"libs": {
|
||||
"ecp_client": "...",
|
||||
"tls_offload": "..."
|
||||
}
|
||||
}
|
||||
"""
|
||||
self._enterprise_cert_file_path = enterprise_cert_file_path
|
||||
self._cert = None
|
||||
self._sign_callback = None
|
||||
self._provider_lib = None
|
||||
|
||||
def load_libraries(self):
|
||||
with open(self._enterprise_cert_file_path, "r") as f:
|
||||
enterprise_cert_json = json.load(f)
|
||||
libs = enterprise_cert_json.get("libs", {})
|
||||
|
||||
signer_library = libs.get("ecp_client", None)
|
||||
offload_library = libs.get("tls_offload", None)
|
||||
provider_library = libs.get("ecp_provider", None)
|
||||
|
||||
# Using newer provider implementation. This is mutually exclusive to the
|
||||
# offload implementation.
|
||||
if provider_library:
|
||||
self._provider_lib = load_provider_lib(provider_library)
|
||||
return
|
||||
|
||||
# Using old offload implementation
|
||||
if offload_library and signer_library:
|
||||
self._offload_lib = load_offload_lib(offload_library)
|
||||
self._signer_lib = load_signer_lib(signer_library)
|
||||
self.set_up_custom_key()
|
||||
return
|
||||
|
||||
raise exceptions.MutualTLSChannelError("enterprise cert file is invalid")
|
||||
|
||||
def set_up_custom_key(self):
|
||||
# We need to keep a reference of the cert and sign callback so it won't
|
||||
# be garbage collected, otherwise it will crash when used by signer lib.
|
||||
self._cert = get_cert(self._signer_lib, self._enterprise_cert_file_path)
|
||||
self._sign_callback = get_sign_callback(
|
||||
self._signer_lib, self._enterprise_cert_file_path
|
||||
)
|
||||
|
||||
def should_use_provider(self):
|
||||
if self._provider_lib:
|
||||
return True
|
||||
return False
|
||||
|
||||
def attach_to_ssl_context(self, ctx):
|
||||
if self.should_use_provider():
|
||||
if not self._provider_lib.ECP_attach_to_ctx(
|
||||
_cast_ssl_ctx_to_void_p_stdlib(ctx),
|
||||
self._enterprise_cert_file_path.encode("ascii"),
|
||||
):
|
||||
raise exceptions.MutualTLSChannelError(
|
||||
"failed to configure ECP Provider SSL context"
|
||||
)
|
||||
elif self._offload_lib and self._signer_lib:
|
||||
if not self._offload_lib.ConfigureSslContext(
|
||||
self._sign_callback,
|
||||
ctypes.c_char_p(self._cert),
|
||||
_cast_ssl_ctx_to_void_p_pyopenssl(ctx._ctx._context),
|
||||
):
|
||||
raise exceptions.MutualTLSChannelError(
|
||||
"failed to configure ECP Offload SSL context"
|
||||
)
|
||||
else:
|
||||
raise exceptions.MutualTLSChannelError("Invalid ECP configuration.")
|
||||
@@ -0,0 +1,114 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Transport adapter for http.client, for internal use only."""
|
||||
|
||||
import http.client as http_client
|
||||
import logging
|
||||
import socket
|
||||
import urllib
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Response(transport.Response):
|
||||
"""http.client transport response adapter.
|
||||
|
||||
Args:
|
||||
response (http.client.HTTPResponse): The raw http client response.
|
||||
"""
|
||||
|
||||
def __init__(self, response):
|
||||
self._status = response.status
|
||||
self._headers = {key.lower(): value for key, value in response.getheaders()}
|
||||
self._data = response.read()
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""http.client transport request adapter."""
|
||||
|
||||
def __call__(
|
||||
self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
|
||||
):
|
||||
"""Make an HTTP request using http.client.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping): Request headers.
|
||||
timeout (Optional(int)): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
socket global default timeout will be used.
|
||||
kwargs: Additional arguments passed throught to the underlying
|
||||
:meth:`~http.client.HTTPConnection.request` method.
|
||||
|
||||
Returns:
|
||||
Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
# socket._GLOBAL_DEFAULT_TIMEOUT is the default in http.client.
|
||||
if timeout is None:
|
||||
timeout = socket._GLOBAL_DEFAULT_TIMEOUT
|
||||
|
||||
# http.client doesn't allow None as the headers argument.
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
# http.client needs the host and path parts specified separately.
|
||||
parts = urllib.parse.urlsplit(url)
|
||||
path = urllib.parse.urlunsplit(
|
||||
("", "", parts.path, parts.query, parts.fragment)
|
||||
)
|
||||
|
||||
if parts.scheme != "http":
|
||||
raise exceptions.TransportError(
|
||||
"http.client transport only supports the http scheme, {}"
|
||||
"was specified".format(parts.scheme)
|
||||
)
|
||||
|
||||
connection = http_client.HTTPConnection(parts.netloc, timeout=timeout)
|
||||
|
||||
try:
|
||||
_helpers.request_log(_LOGGER, method, url, body, headers)
|
||||
connection.request(method, path, body=body, headers=headers, **kwargs)
|
||||
response = connection.getresponse()
|
||||
_helpers.response_log(_LOGGER, response)
|
||||
return Response(response)
|
||||
|
||||
except (http_client.HTTPException, socket.error) as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
finally:
|
||||
connection.close()
|
||||
@@ -0,0 +1,511 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helper functions for getting mTLS cert and key."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from os import environ, getenv, path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from google.auth import _agent_identity_utils
|
||||
from google.auth import environment_vars
|
||||
from google.auth import exceptions
|
||||
|
||||
CONTEXT_AWARE_METADATA_PATH = "~/.secureConnect/context_aware_metadata.json"
|
||||
CERTIFICATE_CONFIGURATION_DEFAULT_PATH = "~/.config/gcloud/certificate_config.json"
|
||||
_CERT_PROVIDER_COMMAND = "cert_provider_command"
|
||||
_CERT_REGEX = re.compile(
|
||||
b"-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----\r?\n?", re.DOTALL
|
||||
)
|
||||
|
||||
# support various format of key files, e.g.
|
||||
# "-----BEGIN PRIVATE KEY-----...",
|
||||
# "-----BEGIN EC PRIVATE KEY-----...",
|
||||
# "-----BEGIN RSA PRIVATE KEY-----..."
|
||||
# "-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
||||
_KEY_REGEX = re.compile(
|
||||
b"-----BEGIN [A-Z ]*PRIVATE KEY-----.+-----END [A-Z ]*PRIVATE KEY-----\r?\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_PASSPHRASE_REGEX = re.compile(
|
||||
b"-----BEGIN PASSPHRASE-----(.+)-----END PASSPHRASE-----", re.DOTALL
|
||||
)
|
||||
|
||||
# Temporary patch to accomodate incorrect cert config in Cloud Run prod environment.
|
||||
_WELL_KNOWN_CLOUD_RUN_CERT_PATH = (
|
||||
"/var/run/secrets/workload-spiffe-credentials/certificates.pem"
|
||||
)
|
||||
_WELL_KNOWN_CLOUD_RUN_KEY_PATH = (
|
||||
"/var/run/secrets/workload-spiffe-credentials/private_key.pem"
|
||||
)
|
||||
_INCORRECT_CLOUD_RUN_CERT_PATH = (
|
||||
"/var/lib/volumes/certificate/workload-certificates/certificates.pem"
|
||||
)
|
||||
_INCORRECT_CLOUD_RUN_KEY_PATH = (
|
||||
"/var/lib/volumes/certificate/workload-certificates/private_key.pem"
|
||||
)
|
||||
|
||||
|
||||
def _check_config_path(config_path):
|
||||
"""Checks for config file path. If it exists, returns the absolute path with user expansion;
|
||||
otherwise returns None.
|
||||
|
||||
Args:
|
||||
config_path (str): The config file path for either context_aware_metadata.json or certificate_config.json for example
|
||||
|
||||
Returns:
|
||||
str: absolute path if exists and None otherwise.
|
||||
"""
|
||||
config_path = path.expanduser(config_path)
|
||||
if not path.exists(config_path):
|
||||
_LOGGER.debug("%s is not found.", config_path)
|
||||
return None
|
||||
return config_path
|
||||
|
||||
|
||||
def _load_json_file(path):
|
||||
"""Reads and loads JSON from the given path. Used to read both X509 workload certificate and
|
||||
secure connect configurations.
|
||||
|
||||
Args:
|
||||
path (str): the path to read from.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: The JSON stored at the file.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ClientCertError: If failed to parse the file as JSON.
|
||||
"""
|
||||
try:
|
||||
with open(path) as f:
|
||||
json_data = json.load(f)
|
||||
except ValueError as caught_exc:
|
||||
new_exc = exceptions.ClientCertError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
def _get_workload_cert_and_key(certificate_config_path=None):
|
||||
"""Read the workload identity cert and key files specified in the certificate config provided.
|
||||
If no config path is provided, check the environment variable: "GOOGLE_API_CERTIFICATE_CONFIG"
|
||||
first, then the well known gcloud location: "~/.config/gcloud/certificate_config.json".
|
||||
|
||||
Args:
|
||||
certificate_config_path (string): The certificate config path. If no path is provided,
|
||||
the environment variable will be checked first, then the well known gcloud location.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[bytes], Optional[bytes]]: client certificate bytes in PEM format and key
|
||||
bytes in PEM format.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ClientCertError: if problems occurs when retrieving
|
||||
the certificate or key information.
|
||||
"""
|
||||
|
||||
cert_path, key_path = _get_workload_cert_and_key_paths(certificate_config_path)
|
||||
|
||||
if cert_path is None and key_path is None:
|
||||
return None, None
|
||||
|
||||
return _read_cert_and_key_files(cert_path, key_path)
|
||||
|
||||
|
||||
def _get_cert_config_path(certificate_config_path=None):
|
||||
"""Get the certificate configuration path based on the following order:
|
||||
|
||||
1: Explicit override, if set
|
||||
2: Environment variable, if set
|
||||
3: Well-known location
|
||||
|
||||
Returns "None" if the selected config file does not exist.
|
||||
|
||||
Args:
|
||||
certificate_config_path (string): The certificate config path. If provided, the well known
|
||||
location and environment variable will be ignored.
|
||||
|
||||
Returns:
|
||||
The absolute path of the certificate config file, and None if the file does not exist.
|
||||
"""
|
||||
|
||||
if certificate_config_path is None:
|
||||
env_path = environ.get(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG, None)
|
||||
if env_path is not None and env_path != "":
|
||||
certificate_config_path = env_path
|
||||
else:
|
||||
certificate_config_path = CERTIFICATE_CONFIGURATION_DEFAULT_PATH
|
||||
|
||||
certificate_config_path = path.expanduser(certificate_config_path)
|
||||
if not path.exists(certificate_config_path):
|
||||
return None
|
||||
return certificate_config_path
|
||||
|
||||
|
||||
def _get_workload_cert_and_key_paths(config_path):
|
||||
absolute_path = _get_cert_config_path(config_path)
|
||||
if absolute_path is None:
|
||||
return None, None
|
||||
|
||||
data = _load_json_file(absolute_path)
|
||||
|
||||
if "cert_configs" not in data:
|
||||
raise exceptions.ClientCertError(
|
||||
'Certificate config file {} is in an invalid format, a "cert configs" object is expected'.format(
|
||||
absolute_path
|
||||
)
|
||||
)
|
||||
cert_configs = data["cert_configs"]
|
||||
|
||||
if "workload" not in cert_configs:
|
||||
raise exceptions.ClientCertError(
|
||||
'Certificate config file {} is in an invalid format, a "workload" cert config is expected'.format(
|
||||
absolute_path
|
||||
)
|
||||
)
|
||||
workload = cert_configs["workload"]
|
||||
|
||||
if "cert_path" not in workload:
|
||||
raise exceptions.ClientCertError(
|
||||
'Certificate config file {} is in an invalid format, a "cert_path" is expected in the workload cert config'.format(
|
||||
absolute_path
|
||||
)
|
||||
)
|
||||
cert_path = workload["cert_path"]
|
||||
|
||||
if "key_path" not in workload:
|
||||
raise exceptions.ClientCertError(
|
||||
'Certificate config file {} is in an invalid format, a "key_path" is expected in the workload cert config'.format(
|
||||
absolute_path
|
||||
)
|
||||
)
|
||||
key_path = workload["key_path"]
|
||||
|
||||
# == BEGIN Temporary Cloud Run PATCH ==
|
||||
# See https://github.com/googleapis/google-auth-library-python/issues/1881
|
||||
if (cert_path == _INCORRECT_CLOUD_RUN_CERT_PATH) and (
|
||||
key_path == _INCORRECT_CLOUD_RUN_KEY_PATH
|
||||
):
|
||||
if not path.exists(cert_path) and not path.exists(key_path):
|
||||
_LOGGER.debug(
|
||||
"Applying Cloud Run certificate path patch. "
|
||||
"Configured paths not found: %s, %s. "
|
||||
"Using well-known paths: %s, %s",
|
||||
cert_path,
|
||||
key_path,
|
||||
_WELL_KNOWN_CLOUD_RUN_CERT_PATH,
|
||||
_WELL_KNOWN_CLOUD_RUN_KEY_PATH,
|
||||
)
|
||||
cert_path = _WELL_KNOWN_CLOUD_RUN_CERT_PATH
|
||||
key_path = _WELL_KNOWN_CLOUD_RUN_KEY_PATH
|
||||
# == END Temporary Cloud Run PATCH ==
|
||||
|
||||
return cert_path, key_path
|
||||
|
||||
|
||||
def _read_cert_and_key_files(cert_path, key_path):
|
||||
cert_data = _read_cert_file(cert_path)
|
||||
key_data = _read_key_file(key_path)
|
||||
|
||||
return cert_data, key_data
|
||||
|
||||
|
||||
def _read_cert_file(cert_path):
|
||||
with open(cert_path, "rb") as cert_file:
|
||||
cert_data = cert_file.read()
|
||||
|
||||
cert_match = re.findall(_CERT_REGEX, cert_data)
|
||||
if len(cert_match) != 1:
|
||||
raise exceptions.ClientCertError(
|
||||
"Certificate file {} is in an invalid format, a single PEM formatted certificate is expected".format(
|
||||
cert_path
|
||||
)
|
||||
)
|
||||
return cert_match[0]
|
||||
|
||||
|
||||
def _read_key_file(key_path):
|
||||
with open(key_path, "rb") as key_file:
|
||||
key_data = key_file.read()
|
||||
|
||||
key_match = re.findall(_KEY_REGEX, key_data)
|
||||
if len(key_match) != 1:
|
||||
raise exceptions.ClientCertError(
|
||||
"Private key file {} is in an invalid format, a single PEM formatted private key is expected".format(
|
||||
key_path
|
||||
)
|
||||
)
|
||||
|
||||
return key_match[0]
|
||||
|
||||
|
||||
def _run_cert_provider_command(command, expect_encrypted_key=False):
|
||||
"""Run the provided command, and return client side mTLS cert, key and
|
||||
passphrase.
|
||||
|
||||
Args:
|
||||
command (List[str]): cert provider command.
|
||||
expect_encrypted_key (bool): If encrypted private key is expected.
|
||||
|
||||
Returns:
|
||||
Tuple[bytes, bytes, bytes]: client certificate bytes in PEM format, key
|
||||
bytes in PEM format and passphrase bytes.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ClientCertError: if problems occurs when running
|
||||
the cert provider command or generating cert, key and passphrase.
|
||||
"""
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = process.communicate()
|
||||
except OSError as caught_exc:
|
||||
new_exc = exceptions.ClientCertError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
# Check cert provider command execution error.
|
||||
if process.returncode != 0:
|
||||
raise exceptions.ClientCertError(
|
||||
"Cert provider command returns non-zero status code %s" % process.returncode
|
||||
)
|
||||
|
||||
# Extract certificate (chain), key and passphrase.
|
||||
cert_match = re.findall(_CERT_REGEX, stdout)
|
||||
if len(cert_match) != 1:
|
||||
raise exceptions.ClientCertError("Client SSL certificate is missing or invalid")
|
||||
key_match = re.findall(_KEY_REGEX, stdout)
|
||||
if len(key_match) != 1:
|
||||
raise exceptions.ClientCertError("Client SSL key is missing or invalid")
|
||||
passphrase_match = re.findall(_PASSPHRASE_REGEX, stdout)
|
||||
|
||||
if expect_encrypted_key:
|
||||
if len(passphrase_match) != 1:
|
||||
raise exceptions.ClientCertError("Passphrase is missing or invalid")
|
||||
if b"ENCRYPTED" not in key_match[0]:
|
||||
raise exceptions.ClientCertError("Encrypted private key is expected")
|
||||
return cert_match[0], key_match[0], passphrase_match[0].strip()
|
||||
|
||||
if b"ENCRYPTED" in key_match[0]:
|
||||
raise exceptions.ClientCertError("Encrypted private key is not expected")
|
||||
if len(passphrase_match) > 0:
|
||||
raise exceptions.ClientCertError("Passphrase is not expected")
|
||||
return cert_match[0], key_match[0], None
|
||||
|
||||
|
||||
def get_client_ssl_credentials(
|
||||
generate_encrypted_key=False,
|
||||
context_aware_metadata_path=CONTEXT_AWARE_METADATA_PATH,
|
||||
certificate_config_path=None,
|
||||
):
|
||||
"""Returns the client side certificate, private key and passphrase.
|
||||
|
||||
We look for certificates and keys with the following order of priority:
|
||||
1. Certificate and key specified by certificate_config.json.
|
||||
Currently, only X.509 workload certificates are supported.
|
||||
2. Certificate and key specified by context aware metadata (i.e. SecureConnect).
|
||||
|
||||
Args:
|
||||
generate_encrypted_key (bool): If set to True, encrypted private key
|
||||
and passphrase will be generated; otherwise, unencrypted private key
|
||||
will be generated and passphrase will be None. This option only
|
||||
affects keys obtained via context_aware_metadata.json.
|
||||
context_aware_metadata_path (str): The context_aware_metadata.json file path.
|
||||
certificate_config_path (str): The certificate_config.json file path.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, bytes, bytes, bytes]:
|
||||
A boolean indicating if cert, key and passphrase are obtained, the
|
||||
cert bytes and key bytes both in PEM format, and passphrase bytes.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ClientCertError: if problems occurs when getting
|
||||
the cert, key and passphrase.
|
||||
"""
|
||||
|
||||
# 1. Attempt to retrieve X.509 Workload cert and key.
|
||||
cert, key = _get_workload_cert_and_key(certificate_config_path)
|
||||
if cert and key:
|
||||
return True, cert, key, None
|
||||
|
||||
# 2. Check for context aware metadata json
|
||||
metadata_path = _check_config_path(context_aware_metadata_path)
|
||||
|
||||
if metadata_path:
|
||||
metadata_json = _load_json_file(metadata_path)
|
||||
|
||||
if _CERT_PROVIDER_COMMAND not in metadata_json:
|
||||
raise exceptions.ClientCertError("Cert provider command is not found")
|
||||
|
||||
command = metadata_json[_CERT_PROVIDER_COMMAND]
|
||||
|
||||
if generate_encrypted_key and "--with_passphrase" not in command:
|
||||
command.append("--with_passphrase")
|
||||
|
||||
# Execute the command.
|
||||
cert, key, passphrase = _run_cert_provider_command(
|
||||
command, expect_encrypted_key=generate_encrypted_key
|
||||
)
|
||||
return True, cert, key, passphrase
|
||||
|
||||
return False, None, None, None
|
||||
|
||||
|
||||
def get_client_cert_and_key(client_cert_callback=None):
|
||||
"""Returns the client side certificate and private key. The function first
|
||||
tries to get certificate and key from client_cert_callback; if the callback
|
||||
is None or doesn't provide certificate and key, the function tries application
|
||||
default SSL credentials.
|
||||
|
||||
Args:
|
||||
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An
|
||||
optional callback which returns client certificate bytes and private
|
||||
key bytes both in PEM format.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, bytes, bytes]:
|
||||
A boolean indicating if cert and key are obtained, the cert bytes
|
||||
and key bytes both in PEM format.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ClientCertError: if problems occurs when getting
|
||||
the cert and key.
|
||||
"""
|
||||
if client_cert_callback:
|
||||
cert, key = client_cert_callback()
|
||||
return True, cert, key
|
||||
|
||||
has_cert, cert, key, _ = get_client_ssl_credentials(generate_encrypted_key=False)
|
||||
return has_cert, cert, key
|
||||
|
||||
|
||||
def decrypt_private_key(key, passphrase):
|
||||
"""A helper function to decrypt the private key with the given passphrase.
|
||||
google-auth library doesn't support passphrase protected private key for
|
||||
mutual TLS channel. This helper function can be used to decrypt the
|
||||
passphrase protected private key in order to estalish mutual TLS channel.
|
||||
|
||||
For example, if you have a function which produces client cert, passphrase
|
||||
protected private key and passphrase, you can convert it to a client cert
|
||||
callback function accepted by google-auth::
|
||||
|
||||
from google.auth.transport import _mtls_helper
|
||||
|
||||
def your_client_cert_function():
|
||||
return cert, encrypted_key, passphrase
|
||||
|
||||
# callback accepted by google-auth for mutual TLS channel.
|
||||
def client_cert_callback():
|
||||
cert, encrypted_key, passphrase = your_client_cert_function()
|
||||
decrypted_key = _mtls_helper.decrypt_private_key(encrypted_key,
|
||||
passphrase)
|
||||
return cert, decrypted_key
|
||||
|
||||
Args:
|
||||
key (bytes): The private key bytes in PEM format.
|
||||
passphrase (bytes): The passphrase bytes.
|
||||
|
||||
Returns:
|
||||
bytes: The decrypted private key in PEM format.
|
||||
|
||||
Raises:
|
||||
ImportError: If pyOpenSSL is not installed.
|
||||
OpenSSL.crypto.Error: If there is any problem decrypting the private key.
|
||||
"""
|
||||
from OpenSSL import crypto
|
||||
|
||||
# First convert encrypted_key_bytes to PKey object
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key, passphrase=passphrase)
|
||||
|
||||
# Then dump the decrypted key bytes
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||
|
||||
|
||||
def check_use_client_cert():
|
||||
"""Returns boolean for whether the client certificate should be used for mTLS.
|
||||
|
||||
If GOOGLE_API_USE_CLIENT_CERTIFICATE is set to true or false, a corresponding
|
||||
bool value will be returned. If the value is set to an unexpected string, it
|
||||
will default to False.
|
||||
If GOOGLE_API_USE_CLIENT_CERTIFICATE is unset, the value will be inferred
|
||||
by reading a file pointed at by GOOGLE_API_CERTIFICATE_CONFIG, and verifying
|
||||
it contains a "workload" section. If so, the function will return True,
|
||||
otherwise False.
|
||||
|
||||
Returns:
|
||||
bool: Whether the client certificate should be used for mTLS connection.
|
||||
"""
|
||||
use_client_cert = getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE")
|
||||
# Check if the value of GOOGLE_API_USE_CLIENT_CERTIFICATE is set.
|
||||
if use_client_cert:
|
||||
return use_client_cert.lower() == "true"
|
||||
else:
|
||||
# Check if the value of GOOGLE_API_CERTIFICATE_CONFIG is set.
|
||||
cert_path = getenv("GOOGLE_API_CERTIFICATE_CONFIG")
|
||||
if cert_path:
|
||||
try:
|
||||
with open(cert_path, "r") as f:
|
||||
content = json.load(f)
|
||||
# verify json has workload key
|
||||
content["cert_configs"]["workload"]
|
||||
return True
|
||||
except (
|
||||
FileNotFoundError,
|
||||
OSError,
|
||||
KeyError,
|
||||
TypeError,
|
||||
json.JSONDecodeError,
|
||||
) as e:
|
||||
_LOGGER.debug("error decoding certificate: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def check_parameters_for_unauthorized_response(cached_cert):
|
||||
"""Returns the cached and current cert fingerprint for reconfiguring mTLS.
|
||||
|
||||
Args:
|
||||
cached_cert(bytes): The cached client certificate.
|
||||
|
||||
Returns:
|
||||
bytes: The client callback cert bytes.
|
||||
bytes: The client callback key bytes.
|
||||
str: The base64-encoded SHA256 cached fingerprint.
|
||||
str: The base64-encoded SHA256 current cert fingerprint.
|
||||
"""
|
||||
call_cert_bytes, call_key_bytes = call_client_cert_callback()
|
||||
cert_obj = _agent_identity_utils.parse_certificate(call_cert_bytes)
|
||||
current_cert_fingerprint = _agent_identity_utils.calculate_certificate_fingerprint(
|
||||
cert_obj
|
||||
)
|
||||
if cached_cert:
|
||||
cached_fingerprint = _agent_identity_utils.get_cached_cert_fingerprint(
|
||||
cached_cert
|
||||
)
|
||||
else:
|
||||
cached_fingerprint = current_cert_fingerprint
|
||||
return call_cert_bytes, call_key_bytes, cached_fingerprint, current_cert_fingerprint
|
||||
|
||||
|
||||
def call_client_cert_callback():
|
||||
"""Calls the client cert callback and returns the certificate and key."""
|
||||
_, cert_bytes, key_bytes, passphrase = get_client_ssl_credentials(
|
||||
generate_encrypted_key=True
|
||||
)
|
||||
return cert_bytes, key_bytes
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Transport adapter for Base Requests."""
|
||||
# NOTE: The coverage for this file is temporarily disabled in `.coveragerc`
|
||||
# since it is currently unused.
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
_DEFAULT_TIMEOUT = 120 # in second
|
||||
|
||||
|
||||
class _BaseAuthorizedSession(metaclass=abc.ABCMeta):
|
||||
"""Base class for a Request Session with credentials. This class is intended to capture
|
||||
the common logic between synchronous and asynchronous request sessions and is not intended to
|
||||
be instantiated directly.
|
||||
|
||||
Args:
|
||||
credentials (google.auth._credentials_base.BaseCredentials): The credentials to
|
||||
add to the request.
|
||||
"""
|
||||
|
||||
def __init__(self, credentials):
|
||||
self.credentials = credentials
|
||||
|
||||
@abc.abstractmethod
|
||||
def request(
|
||||
self,
|
||||
method,
|
||||
url,
|
||||
data=None,
|
||||
headers=None,
|
||||
max_allowed_time=None,
|
||||
timeout=_DEFAULT_TIMEOUT,
|
||||
**kwargs
|
||||
):
|
||||
raise NotImplementedError("Request must be implemented")
|
||||
|
||||
@abc.abstractmethod
|
||||
def close(self):
|
||||
raise NotImplementedError("Close must be implemented")
|
||||
337
venv/lib/python3.12/site-packages/google/auth/transport/grpc.py
Normal file
337
venv/lib/python3.12/site-packages/google/auth/transport/grpc.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Authorization support for gRPC."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth.transport import _mtls_helper
|
||||
from google.oauth2 import service_account
|
||||
|
||||
try:
|
||||
import grpc # type: ignore
|
||||
except ImportError as caught_exc: # pragma: NO COVER
|
||||
raise ImportError(
|
||||
"gRPC is not installed from please install the grpcio package to use the gRPC transport."
|
||||
) from caught_exc
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
|
||||
"""A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
|
||||
request.
|
||||
|
||||
.. _gRPC AuthMetadataPlugin:
|
||||
http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to requests.
|
||||
request (google.auth.transport.Request): A HTTP transport request
|
||||
object used to refresh credentials as needed.
|
||||
default_host (Optional[str]): A host like "pubsub.googleapis.com".
|
||||
This is used when a self-signed JWT is created from service
|
||||
account credentials.
|
||||
"""
|
||||
|
||||
def __init__(self, credentials, request, default_host=None):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
# pylint doesn't realize that the super method takes no arguments
|
||||
# because this class is the same name as the superclass.
|
||||
super(AuthMetadataPlugin, self).__init__()
|
||||
self._credentials = credentials
|
||||
self._request = request
|
||||
self._default_host = default_host
|
||||
|
||||
def _get_authorization_headers(self, context):
|
||||
"""Gets the authorization headers for a request.
|
||||
|
||||
Returns:
|
||||
Sequence[Tuple[str, str]]: A list of request headers (key, value)
|
||||
to add to the request.
|
||||
"""
|
||||
headers = {}
|
||||
|
||||
# https://google.aip.dev/auth/4111
|
||||
# Attempt to use self-signed JWTs when a service account is used.
|
||||
# A default host must be explicitly provided since it cannot always
|
||||
# be determined from the context.service_url.
|
||||
if isinstance(self._credentials, service_account.Credentials):
|
||||
self._credentials._create_self_signed_jwt(
|
||||
"https://{}/".format(self._default_host) if self._default_host else None
|
||||
)
|
||||
|
||||
self._credentials.before_request(
|
||||
self._request, context.method_name, context.service_url, headers
|
||||
)
|
||||
|
||||
return list(headers.items())
|
||||
|
||||
def __call__(self, context, callback):
|
||||
"""Passes authorization metadata into the given callback.
|
||||
|
||||
Args:
|
||||
context (grpc.AuthMetadataContext): The RPC context.
|
||||
callback (grpc.AuthMetadataPluginCallback): The callback that will
|
||||
be invoked to pass in the authorization metadata.
|
||||
"""
|
||||
callback(self._get_authorization_headers(context), None)
|
||||
|
||||
|
||||
def secure_authorized_channel(
|
||||
credentials,
|
||||
request,
|
||||
target,
|
||||
ssl_credentials=None,
|
||||
client_cert_callback=None,
|
||||
**kwargs
|
||||
):
|
||||
"""Creates a secure authorized gRPC channel.
|
||||
|
||||
This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
|
||||
channel can be used to create a stub that can make authorized requests.
|
||||
Users can configure client certificate or rely on device certificates to
|
||||
establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
||||
variable is explicitly set to `true`.
|
||||
|
||||
Example::
|
||||
|
||||
import google.auth
|
||||
import google.auth.transport.grpc
|
||||
import google.auth.transport.requests
|
||||
from google.cloud.speech.v1 import cloud_speech_pb2
|
||||
|
||||
# Get credentials.
|
||||
credentials, _ = google.auth.default()
|
||||
|
||||
# Get an HTTP request function to refresh credentials.
|
||||
request = google.auth.transport.requests.Request()
|
||||
|
||||
# Create a channel.
|
||||
channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, regular_endpoint, request,
|
||||
ssl_credentials=grpc.ssl_channel_credentials())
|
||||
|
||||
# Use the channel to create a stub.
|
||||
cloud_speech.create_Speech_stub(channel)
|
||||
|
||||
Usage:
|
||||
|
||||
There are actually a couple of options to create a channel, depending on if
|
||||
you want to create a regular or mutual TLS channel.
|
||||
|
||||
First let's list the endpoints (regular vs mutual TLS) to choose from::
|
||||
|
||||
regular_endpoint = 'speech.googleapis.com:443'
|
||||
mtls_endpoint = 'speech.mtls.googleapis.com:443'
|
||||
|
||||
Option 1: create a regular (non-mutual) TLS channel by explicitly setting
|
||||
the ssl_credentials::
|
||||
|
||||
regular_ssl_credentials = grpc.ssl_channel_credentials()
|
||||
|
||||
channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, request, regular_endpoint,
|
||||
ssl_credentials=regular_ssl_credentials)
|
||||
|
||||
Option 2: create a mutual TLS channel by calling a callback which returns
|
||||
the client side certificate and the key (Note that
|
||||
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
|
||||
set to `true`)::
|
||||
|
||||
def my_client_cert_callback():
|
||||
code_to_load_client_cert_and_key()
|
||||
if loaded:
|
||||
return (pem_cert_bytes, pem_key_bytes)
|
||||
raise MyClientCertFailureException()
|
||||
|
||||
try:
|
||||
channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, request, mtls_endpoint,
|
||||
client_cert_callback=my_client_cert_callback)
|
||||
except MyClientCertFailureException:
|
||||
# handle the exception
|
||||
|
||||
Option 3: use application default SSL credentials. It searches and uses
|
||||
the command in a context aware metadata file, which is available on devices
|
||||
with endpoint verification support (Note that
|
||||
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
|
||||
set to `true`).
|
||||
See https://cloud.google.com/endpoint-verification/docs/overview::
|
||||
|
||||
try:
|
||||
default_ssl_credentials = SslCredentials()
|
||||
except:
|
||||
# Exception can be raised if the context aware metadata is malformed.
|
||||
# See :class:`SslCredentials` for the possible exceptions.
|
||||
|
||||
# Choose the endpoint based on the SSL credentials type.
|
||||
if default_ssl_credentials.is_mtls:
|
||||
endpoint_to_use = mtls_endpoint
|
||||
else:
|
||||
endpoint_to_use = regular_endpoint
|
||||
channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, request, endpoint_to_use,
|
||||
ssl_credentials=default_ssl_credentials)
|
||||
|
||||
Option 4: not setting ssl_credentials and client_cert_callback. For devices
|
||||
without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
||||
environment variable is not `true`, a regular TLS channel is created;
|
||||
otherwise, a mutual TLS channel is created, however, the call should be
|
||||
wrapped in a try/except block in case of malformed context aware metadata.
|
||||
|
||||
The following code uses regular_endpoint, it works the same no matter the
|
||||
created channle is regular or mutual TLS. Regular endpoint ignores client
|
||||
certificate and key::
|
||||
|
||||
channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, request, regular_endpoint)
|
||||
|
||||
The following code uses mtls_endpoint, if the created channle is regular,
|
||||
and API mtls_endpoint is confgured to require client SSL credentials, API
|
||||
calls using this channel will be rejected::
|
||||
|
||||
channel = google.auth.transport.grpc.secure_authorized_channel(
|
||||
credentials, request, mtls_endpoint)
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to requests.
|
||||
request (google.auth.transport.Request): A HTTP transport request
|
||||
object used to refresh credentials as needed. Even though gRPC
|
||||
is a separate transport, there's no way to refresh the credentials
|
||||
without using a standard http transport.
|
||||
target (str): The host and port of the service.
|
||||
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
|
||||
credentials. This can be used to specify different certificates.
|
||||
This argument is mutually exclusive with client_cert_callback;
|
||||
providing both will raise an exception.
|
||||
If ssl_credentials and client_cert_callback are None, application
|
||||
default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
||||
environment variable is explicitly set to `true`, otherwise one way TLS
|
||||
SSL credentials are used.
|
||||
client_cert_callback (Callable[[], (bytes, bytes)]): Optional
|
||||
callback function to obtain client certicate and key for mutual TLS
|
||||
connection. This argument is mutually exclusive with
|
||||
ssl_credentials; providing both will raise an exception.
|
||||
This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
||||
environment variable is explicitly set to `true`.
|
||||
kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
|
||||
|
||||
Returns:
|
||||
grpc.Channel: The created gRPC channel.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
|
||||
creation failed for any reason.
|
||||
"""
|
||||
# Create the metadata plugin for inserting the authorization header.
|
||||
metadata_plugin = AuthMetadataPlugin(credentials, request)
|
||||
|
||||
# Create a set of grpc.CallCredentials using the metadata plugin.
|
||||
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
|
||||
|
||||
if ssl_credentials and client_cert_callback:
|
||||
raise exceptions.MalformedError(
|
||||
"Received both ssl_credentials and client_cert_callback; "
|
||||
"these are mutually exclusive."
|
||||
)
|
||||
|
||||
# If SSL credentials are not explicitly set, try client_cert_callback and ADC.
|
||||
if not ssl_credentials:
|
||||
use_client_cert = _mtls_helper.check_use_client_cert()
|
||||
if use_client_cert and client_cert_callback:
|
||||
# Use the callback if provided.
|
||||
cert, key = client_cert_callback()
|
||||
ssl_credentials = grpc.ssl_channel_credentials(
|
||||
certificate_chain=cert, private_key=key
|
||||
)
|
||||
elif use_client_cert:
|
||||
# Use application default SSL credentials.
|
||||
adc_ssl_credentils = SslCredentials()
|
||||
ssl_credentials = adc_ssl_credentils.ssl_credentials
|
||||
else:
|
||||
ssl_credentials = grpc.ssl_channel_credentials()
|
||||
|
||||
# Combine the ssl credentials and the authorization credentials.
|
||||
composite_credentials = grpc.composite_channel_credentials(
|
||||
ssl_credentials, google_auth_credentials
|
||||
)
|
||||
|
||||
return grpc.secure_channel(target, composite_credentials, **kwargs)
|
||||
|
||||
|
||||
class SslCredentials:
|
||||
"""Class for application default SSL credentials.
|
||||
|
||||
The behavior is controlled by `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment
|
||||
variable whose default value is `false`. Client certificate will not be used
|
||||
unless the environment variable is explicitly set to `true`. See
|
||||
https://google.aip.dev/auth/4114
|
||||
|
||||
If the environment variable is `true`, then for devices with endpoint verification
|
||||
support, a device certificate will be automatically loaded and mutual TLS will
|
||||
be established.
|
||||
See https://cloud.google.com/endpoint-verification/docs/overview.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
use_client_cert = _mtls_helper.check_use_client_cert()
|
||||
if not use_client_cert:
|
||||
self._is_mtls = False
|
||||
else:
|
||||
# Load client SSL credentials.
|
||||
metadata_path = _mtls_helper._check_config_path(
|
||||
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
|
||||
)
|
||||
self._is_mtls = metadata_path is not None
|
||||
|
||||
@property
|
||||
def ssl_credentials(self):
|
||||
"""Get the created SSL channel credentials.
|
||||
|
||||
For devices with endpoint verification support, if the device certificate
|
||||
loading has any problems, corresponding exceptions will be raised. For
|
||||
a device without endpoint verification support, no exceptions will be
|
||||
raised.
|
||||
|
||||
Returns:
|
||||
grpc.ChannelCredentials: The created grpc channel credentials.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
|
||||
creation failed for any reason.
|
||||
"""
|
||||
if self._is_mtls:
|
||||
try:
|
||||
_, cert, key, _ = _mtls_helper.get_client_ssl_credentials()
|
||||
self._ssl_credentials = grpc.ssl_channel_credentials(
|
||||
certificate_chain=cert, private_key=key
|
||||
)
|
||||
except exceptions.ClientCertError as caught_exc:
|
||||
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
else:
|
||||
self._ssl_credentials = grpc.ssl_channel_credentials()
|
||||
|
||||
return self._ssl_credentials
|
||||
|
||||
@property
|
||||
def is_mtls(self):
|
||||
"""Indicates if the created SSL channel credentials is mutual TLS."""
|
||||
return self._is_mtls
|
||||
137
venv/lib/python3.12/site-packages/google/auth/transport/mtls.py
Normal file
137
venv/lib/python3.12/site-packages/google/auth/transport/mtls.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilites for mutual TLS."""
|
||||
|
||||
from os import getenv
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth.transport import _mtls_helper
|
||||
|
||||
|
||||
def has_default_client_cert_source():
|
||||
"""Check if default client SSL credentials exists on the device.
|
||||
|
||||
Returns:
|
||||
bool: indicating if the default client cert source exists.
|
||||
"""
|
||||
if (
|
||||
_mtls_helper._check_config_path(_mtls_helper.CONTEXT_AWARE_METADATA_PATH)
|
||||
is not None
|
||||
):
|
||||
return True
|
||||
if (
|
||||
_mtls_helper._check_config_path(
|
||||
_mtls_helper.CERTIFICATE_CONFIGURATION_DEFAULT_PATH
|
||||
)
|
||||
is not None
|
||||
):
|
||||
return True
|
||||
cert_config_path = getenv("GOOGLE_API_CERTIFICATE_CONFIG")
|
||||
if (
|
||||
cert_config_path
|
||||
and _mtls_helper._check_config_path(cert_config_path) is not None
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def default_client_cert_source():
|
||||
"""Get a callback which returns the default client SSL credentials.
|
||||
|
||||
Returns:
|
||||
Callable[[], [bytes, bytes]]: A callback which returns the default
|
||||
client certificate bytes and private key bytes, both in PEM format.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.DefaultClientCertSourceError: If the default
|
||||
client SSL credentials don't exist or are malformed.
|
||||
"""
|
||||
if not has_default_client_cert_source():
|
||||
raise exceptions.MutualTLSChannelError(
|
||||
"Default client cert source doesn't exist"
|
||||
)
|
||||
|
||||
def callback():
|
||||
try:
|
||||
_, cert_bytes, key_bytes = _mtls_helper.get_client_cert_and_key()
|
||||
except (OSError, RuntimeError, ValueError) as caught_exc:
|
||||
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
return cert_bytes, key_bytes
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def default_client_encrypted_cert_source(cert_path, key_path):
|
||||
"""Get a callback which returns the default encrpyted client SSL credentials.
|
||||
|
||||
Args:
|
||||
cert_path (str): The cert file path. The default client certificate will
|
||||
be written to this file when the returned callback is called.
|
||||
key_path (str): The key file path. The default encrypted client key will
|
||||
be written to this file when the returned callback is called.
|
||||
|
||||
Returns:
|
||||
Callable[[], [str, str, bytes]]: A callback which generates the default
|
||||
client certificate, encrpyted private key and passphrase. It writes
|
||||
the certificate and private key into the cert_path and key_path, and
|
||||
returns the cert_path, key_path and passphrase bytes.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.DefaultClientCertSourceError: If any problem
|
||||
occurs when loading or saving the client certificate and key.
|
||||
"""
|
||||
if not has_default_client_cert_source():
|
||||
raise exceptions.MutualTLSChannelError(
|
||||
"Default client encrypted cert source doesn't exist"
|
||||
)
|
||||
|
||||
def callback():
|
||||
try:
|
||||
(
|
||||
_,
|
||||
cert_bytes,
|
||||
key_bytes,
|
||||
passphrase_bytes,
|
||||
) = _mtls_helper.get_client_ssl_credentials(generate_encrypted_key=True)
|
||||
with open(cert_path, "wb") as cert_file:
|
||||
cert_file.write(cert_bytes)
|
||||
with open(key_path, "wb") as key_file:
|
||||
key_file.write(key_bytes)
|
||||
except (exceptions.ClientCertError, OSError) as caught_exc:
|
||||
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
return cert_path, key_path, passphrase_bytes
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def should_use_client_cert():
|
||||
"""Returns boolean for whether the client certificate should be used for mTLS.
|
||||
|
||||
This is a wrapper around _mtls_helper.check_use_client_cert().
|
||||
If GOOGLE_API_USE_CLIENT_CERTIFICATE is set to true or false, a corresponding
|
||||
bool value will be returned
|
||||
If GOOGLE_API_USE_CLIENT_CERTIFICATE is unset, the value will be inferred by
|
||||
reading a file pointed at by GOOGLE_API_CERTIFICATE_CONFIG, and verifying it
|
||||
contains a "workload" section. If so, the function will return True,
|
||||
otherwise False.
|
||||
|
||||
Returns:
|
||||
bool: indicating whether the client certificate should be used for mTLS.
|
||||
"""
|
||||
return _mtls_helper.check_use_client_cert()
|
||||
@@ -0,0 +1,634 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Transport adapter for Requests."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
import http.client as http_client
|
||||
import logging
|
||||
import numbers
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError as caught_exc: # pragma: NO COVER
|
||||
raise ImportError(
|
||||
"The requests library is not installed from please install the requests package to use the requests transport."
|
||||
) from caught_exc
|
||||
import requests.adapters # pylint: disable=ungrouped-imports
|
||||
import requests.exceptions # pylint: disable=ungrouped-imports
|
||||
from requests.packages.urllib3.util.ssl_ import ( # type: ignore
|
||||
create_urllib3_context,
|
||||
) # pylint: disable=ungrouped-imports
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
from google.auth.transport import _mtls_helper
|
||||
import google.auth.transport._mtls_helper
|
||||
from google.oauth2 import service_account
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_TIMEOUT = 120 # in seconds
|
||||
|
||||
|
||||
class _Response(transport.Response):
|
||||
"""Requests transport response adapter.
|
||||
|
||||
Args:
|
||||
response (requests.Response): The raw Requests response.
|
||||
"""
|
||||
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._response.status_code
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._response.headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._response.content
|
||||
|
||||
|
||||
class TimeoutGuard(object):
|
||||
"""A context manager raising an error if the suite execution took too long.
|
||||
|
||||
Args:
|
||||
timeout (Union[None, Union[float, Tuple[float, float]]]):
|
||||
The maximum number of seconds a suite can run without the context
|
||||
manager raising a timeout exception on exit. If passed as a tuple,
|
||||
the smaller of the values is taken as a timeout. If ``None``, a
|
||||
timeout error is never raised.
|
||||
timeout_error_type (Optional[Exception]):
|
||||
The type of the error to raise on timeout. Defaults to
|
||||
:class:`requests.exceptions.Timeout`.
|
||||
"""
|
||||
|
||||
def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
|
||||
self._timeout = timeout
|
||||
self.remaining_timeout = timeout
|
||||
self._timeout_error_type = timeout_error_type
|
||||
|
||||
def __enter__(self):
|
||||
self._start = time.time()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if exc_value:
|
||||
return # let the error bubble up automatically
|
||||
|
||||
if self._timeout is None:
|
||||
return # nothing to do, the timeout was not specified
|
||||
|
||||
elapsed = time.time() - self._start
|
||||
deadline_hit = False
|
||||
|
||||
if isinstance(self._timeout, numbers.Number):
|
||||
self.remaining_timeout = self._timeout - elapsed
|
||||
deadline_hit = self.remaining_timeout <= 0
|
||||
else:
|
||||
self.remaining_timeout = tuple(x - elapsed for x in self._timeout)
|
||||
deadline_hit = min(self.remaining_timeout) <= 0
|
||||
|
||||
if deadline_hit:
|
||||
raise self._timeout_error_type()
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""Requests request adapter.
|
||||
|
||||
This class is used internally for making requests using various transports
|
||||
in a consistent way. If you use :class:`AuthorizedSession` you do not need
|
||||
to construct or use this class directly.
|
||||
|
||||
This class can be useful if you want to manually refresh a
|
||||
:class:`~google.auth.credentials.Credentials` instance::
|
||||
|
||||
import google.auth.transport.requests
|
||||
import requests
|
||||
|
||||
request = google.auth.transport.requests.Request()
|
||||
|
||||
credentials.refresh(request)
|
||||
|
||||
Args:
|
||||
session (requests.Session): An instance :class:`requests.Session` used
|
||||
to make HTTP requests. If not specified, a session will be created.
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
|
||||
def __init__(self, session: Optional[requests.Session] = None) -> None:
|
||||
if not session:
|
||||
session = requests.Session()
|
||||
|
||||
self.session = session
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if hasattr(self, "session") and self.session is not None:
|
||||
self.session.close()
|
||||
except TypeError:
|
||||
# NOTE: For certain Python binary built, the queue.Empty exception
|
||||
# might not be considered a normal Python exception causing
|
||||
# TypeError.
|
||||
pass
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
url,
|
||||
method="GET",
|
||||
body=None,
|
||||
headers=None,
|
||||
timeout=_DEFAULT_TIMEOUT,
|
||||
**kwargs
|
||||
):
|
||||
"""Make an HTTP request using requests.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload or body in HTTP request.
|
||||
headers (Mapping[str, str]): Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
requests default timeout will be used.
|
||||
kwargs: Additional arguments passed through to the underlying
|
||||
requests :meth:`~requests.Session.request` method.
|
||||
|
||||
Returns:
|
||||
google.auth.transport.Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
try:
|
||||
_helpers.request_log(_LOGGER, method, url, body, headers)
|
||||
response = self.session.request(
|
||||
method, url, data=body, headers=headers, timeout=timeout, **kwargs
|
||||
)
|
||||
_helpers.response_log(_LOGGER, response)
|
||||
return _Response(response)
|
||||
except requests.exceptions.RequestException as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
|
||||
class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
|
||||
"""
|
||||
A TransportAdapter that enables mutual TLS.
|
||||
|
||||
Args:
|
||||
cert (bytes): client certificate in PEM format
|
||||
key (bytes): client private key in PEM format
|
||||
|
||||
Raises:
|
||||
ImportError: if certifi or pyOpenSSL is not installed
|
||||
OpenSSL.crypto.Error: if client cert or key is invalid
|
||||
"""
|
||||
|
||||
def __init__(self, cert, key):
|
||||
import certifi
|
||||
from OpenSSL import crypto
|
||||
import urllib3.contrib.pyopenssl # type: ignore
|
||||
|
||||
urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
|
||||
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
|
||||
|
||||
ctx_poolmanager = create_urllib3_context()
|
||||
ctx_poolmanager.load_verify_locations(cafile=certifi.where())
|
||||
ctx_poolmanager._ctx.use_certificate(x509)
|
||||
ctx_poolmanager._ctx.use_privatekey(pkey)
|
||||
self._ctx_poolmanager = ctx_poolmanager
|
||||
|
||||
ctx_proxymanager = create_urllib3_context()
|
||||
ctx_proxymanager.load_verify_locations(cafile=certifi.where())
|
||||
ctx_proxymanager._ctx.use_certificate(x509)
|
||||
ctx_proxymanager._ctx.use_privatekey(pkey)
|
||||
self._ctx_proxymanager = ctx_proxymanager
|
||||
|
||||
super(_MutualTlsAdapter, self).__init__()
|
||||
|
||||
def init_poolmanager(self, *args, **kwargs):
|
||||
kwargs["ssl_context"] = self._ctx_poolmanager
|
||||
super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
|
||||
|
||||
def proxy_manager_for(self, *args, **kwargs):
|
||||
kwargs["ssl_context"] = self._ctx_proxymanager
|
||||
return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
|
||||
|
||||
|
||||
class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
|
||||
"""
|
||||
A TransportAdapter that enables mutual TLS and offloads the client side
|
||||
signing operation to the signing library.
|
||||
|
||||
Args:
|
||||
enterprise_cert_file_path (str): the path to a enterprise cert JSON
|
||||
file. The file should contain the following field:
|
||||
|
||||
{
|
||||
"libs": {
|
||||
"signer_library": "...",
|
||||
"offload_library": "..."
|
||||
}
|
||||
}
|
||||
|
||||
Raises:
|
||||
ImportError: if certifi or pyOpenSSL is not installed
|
||||
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
|
||||
creation failed for any reason.
|
||||
"""
|
||||
|
||||
def __init__(self, enterprise_cert_file_path):
|
||||
import certifi
|
||||
from google.auth.transport import _custom_tls_signer
|
||||
|
||||
self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
|
||||
self.signer.load_libraries()
|
||||
|
||||
import urllib3.contrib.pyopenssl
|
||||
|
||||
urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
poolmanager = create_urllib3_context()
|
||||
poolmanager.load_verify_locations(cafile=certifi.where())
|
||||
self.signer.attach_to_ssl_context(poolmanager)
|
||||
self._ctx_poolmanager = poolmanager
|
||||
|
||||
proxymanager = create_urllib3_context()
|
||||
proxymanager.load_verify_locations(cafile=certifi.where())
|
||||
self.signer.attach_to_ssl_context(proxymanager)
|
||||
self._ctx_proxymanager = proxymanager
|
||||
|
||||
super(_MutualTlsOffloadAdapter, self).__init__()
|
||||
|
||||
def init_poolmanager(self, *args, **kwargs):
|
||||
kwargs["ssl_context"] = self._ctx_poolmanager
|
||||
super(_MutualTlsOffloadAdapter, self).init_poolmanager(*args, **kwargs)
|
||||
|
||||
def proxy_manager_for(self, *args, **kwargs):
|
||||
kwargs["ssl_context"] = self._ctx_proxymanager
|
||||
return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)
|
||||
|
||||
|
||||
class AuthorizedSession(requests.Session):
|
||||
"""A Requests Session class with credentials.
|
||||
|
||||
This class is used to perform requests to API endpoints that require
|
||||
authorization::
|
||||
|
||||
from google.auth.transport.requests import AuthorizedSession
|
||||
|
||||
authed_session = AuthorizedSession(credentials)
|
||||
|
||||
response = authed_session.request(
|
||||
'GET', 'https://www.googleapis.com/storage/v1/b')
|
||||
|
||||
|
||||
The underlying :meth:`request` implementation handles adding the
|
||||
credentials' headers to the request and refreshing credentials as needed.
|
||||
|
||||
This class also supports mutual TLS via :meth:`configure_mtls_channel`
|
||||
method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
||||
environment variable must be explicitly set to ``true``, otherwise it does
|
||||
nothing. Assume the environment is set to ``true``, the method behaves in the
|
||||
following manner:
|
||||
|
||||
If client_cert_callback is provided, client certificate and private
|
||||
key are loaded using the callback; if client_cert_callback is None,
|
||||
application default SSL credentials will be used. Exceptions are raised if
|
||||
there are problems with the certificate, private key, or the loading process,
|
||||
so it should be called within a try/except block.
|
||||
|
||||
First we set the environment variable to ``true``, then create an :class:`AuthorizedSession`
|
||||
instance and specify the endpoints::
|
||||
|
||||
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
|
||||
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
|
||||
|
||||
authed_session = AuthorizedSession(credentials)
|
||||
|
||||
Now we can pass a callback to :meth:`configure_mtls_channel`::
|
||||
|
||||
def my_cert_callback():
|
||||
# some code to load client cert bytes and private key bytes, both in
|
||||
# PEM format.
|
||||
some_code_to_load_client_cert_and_key()
|
||||
if loaded:
|
||||
return cert, key
|
||||
raise MyClientCertFailureException()
|
||||
|
||||
# Always call configure_mtls_channel within a try/except block.
|
||||
try:
|
||||
authed_session.configure_mtls_channel(my_cert_callback)
|
||||
except:
|
||||
# handle exceptions.
|
||||
|
||||
if authed_session.is_mtls:
|
||||
response = authed_session.request('GET', mtls_endpoint)
|
||||
else:
|
||||
response = authed_session.request('GET', regular_endpoint)
|
||||
|
||||
|
||||
You can alternatively use application default SSL credentials like this::
|
||||
|
||||
try:
|
||||
authed_session.configure_mtls_channel()
|
||||
except:
|
||||
# handle exceptions.
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to the request.
|
||||
refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
|
||||
that credentials should be refreshed and the request should be
|
||||
retried.
|
||||
max_refresh_attempts (int): The maximum number of times to attempt to
|
||||
refresh the credentials and retry the request.
|
||||
refresh_timeout (Optional[int]): The timeout value in seconds for
|
||||
credential refresh HTTP requests.
|
||||
auth_request (google.auth.transport.requests.Request):
|
||||
(Optional) An instance of
|
||||
:class:`~google.auth.transport.requests.Request` used when
|
||||
refreshing credentials. If not passed,
|
||||
an instance of :class:`~google.auth.transport.requests.Request`
|
||||
is created.
|
||||
default_host (Optional[str]): A host like "pubsub.googleapis.com".
|
||||
This is used when a self-signed JWT is created from service
|
||||
account credentials.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
credentials,
|
||||
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
|
||||
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
|
||||
refresh_timeout=None,
|
||||
auth_request=None,
|
||||
default_host=None,
|
||||
):
|
||||
super(AuthorizedSession, self).__init__()
|
||||
self.credentials = credentials
|
||||
self._refresh_status_codes = refresh_status_codes
|
||||
self._max_refresh_attempts = max_refresh_attempts
|
||||
self._refresh_timeout = refresh_timeout
|
||||
self._is_mtls = False
|
||||
self._default_host = default_host
|
||||
|
||||
if auth_request is None:
|
||||
self._auth_request_session = requests.Session()
|
||||
|
||||
# Using an adapter to make HTTP requests robust to network errors.
|
||||
# This adapter retrys HTTP requests when network errors occur
|
||||
# and the requests seems safely retryable.
|
||||
retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
|
||||
self._auth_request_session.mount("https://", retry_adapter)
|
||||
|
||||
# Do not pass `self` as the session here, as it can lead to
|
||||
# infinite recursion.
|
||||
auth_request = Request(self._auth_request_session)
|
||||
else:
|
||||
self._auth_request_session = None
|
||||
|
||||
# Request instance used by internal methods (for example,
|
||||
# credentials.refresh).
|
||||
self._auth_request = auth_request
|
||||
|
||||
# https://google.aip.dev/auth/4111
|
||||
# Attempt to use self-signed JWTs when a service account is used.
|
||||
if isinstance(self.credentials, service_account.Credentials):
|
||||
self.credentials._create_self_signed_jwt(
|
||||
"https://{}/".format(self._default_host) if self._default_host else None
|
||||
)
|
||||
|
||||
def configure_mtls_channel(self, client_cert_callback=None):
|
||||
"""Configure the client certificate and key for SSL connection.
|
||||
|
||||
The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is
|
||||
explicitly set to `true`. In this case if client certificate and key are
|
||||
successfully obtained (from the given client_cert_callback or from application
|
||||
default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted
|
||||
to "https://" prefix.
|
||||
|
||||
Args:
|
||||
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
|
||||
The optional callback returns the client certificate and private
|
||||
key bytes both in PEM format.
|
||||
If the callback is None, application default SSL credentials
|
||||
will be used.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
|
||||
creation failed for any reason.
|
||||
"""
|
||||
use_client_cert = google.auth.transport._mtls_helper.check_use_client_cert()
|
||||
if not use_client_cert:
|
||||
self._is_mtls = False
|
||||
return
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError as caught_exc:
|
||||
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
try:
|
||||
(
|
||||
self._is_mtls,
|
||||
cert,
|
||||
key,
|
||||
) = google.auth.transport._mtls_helper.get_client_cert_and_key(
|
||||
client_cert_callback
|
||||
)
|
||||
|
||||
if self._is_mtls:
|
||||
mtls_adapter = _MutualTlsAdapter(cert, key)
|
||||
self._cached_cert = cert
|
||||
self.mount("https://", mtls_adapter)
|
||||
except (
|
||||
exceptions.ClientCertError,
|
||||
ImportError,
|
||||
OpenSSL.crypto.Error,
|
||||
) as caught_exc:
|
||||
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
def request(
|
||||
self,
|
||||
method,
|
||||
url,
|
||||
data=None,
|
||||
headers=None,
|
||||
max_allowed_time=None,
|
||||
timeout=_DEFAULT_TIMEOUT,
|
||||
**kwargs
|
||||
):
|
||||
"""Implementation of Requests' request.
|
||||
|
||||
Args:
|
||||
timeout (Optional[Union[float, Tuple[float, float]]]):
|
||||
The amount of time in seconds to wait for the server response
|
||||
with each individual request. Can also be passed as a tuple
|
||||
``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request`
|
||||
documentation for details.
|
||||
max_allowed_time (Optional[float]):
|
||||
If the method runs longer than this, a ``Timeout`` exception is
|
||||
automatically raised. Unlike the ``timeout`` parameter, this
|
||||
value applies to the total method execution time, even if
|
||||
multiple requests are made under the hood.
|
||||
|
||||
Mind that it is not guaranteed that the timeout error is raised
|
||||
at ``max_allowed_time``. It might take longer, for example, if
|
||||
an underlying request takes a lot of time, but the request
|
||||
itself does not timeout, e.g. if a large file is being
|
||||
transmitted. The timeout error will be raised after such
|
||||
request completes.
|
||||
Raises:
|
||||
google.auth.exceptions.MutualTLSChannelError: If mutual TLS
|
||||
channel creation fails for any reason.
|
||||
ValueError: If the client certificate is invalid.
|
||||
"""
|
||||
# pylint: disable=arguments-differ
|
||||
# Requests has a ton of arguments to request, but only two
|
||||
# (method, url) are required. We pass through all of the other
|
||||
# arguments to super, so no need to exhaustively list them here.
|
||||
|
||||
# Use a kwarg for this instead of an attribute to maintain
|
||||
# thread-safety.
|
||||
_credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
|
||||
|
||||
# Make a copy of the headers. They will be modified by the credentials
|
||||
# and we want to pass the original headers if we recurse.
|
||||
request_headers = headers.copy() if headers is not None else {}
|
||||
|
||||
# Do not apply the timeout unconditionally in order to not override the
|
||||
# _auth_request's default timeout.
|
||||
auth_request = (
|
||||
self._auth_request
|
||||
if timeout is None
|
||||
else functools.partial(self._auth_request, timeout=timeout)
|
||||
)
|
||||
|
||||
remaining_time = max_allowed_time
|
||||
|
||||
with TimeoutGuard(remaining_time) as guard:
|
||||
self.credentials.before_request(auth_request, method, url, request_headers)
|
||||
remaining_time = guard.remaining_timeout
|
||||
|
||||
with TimeoutGuard(remaining_time) as guard:
|
||||
_helpers.request_log(_LOGGER, method, url, data, headers)
|
||||
response = super(AuthorizedSession, self).request(
|
||||
method,
|
||||
url,
|
||||
data=data,
|
||||
headers=request_headers,
|
||||
timeout=timeout,
|
||||
**kwargs
|
||||
)
|
||||
remaining_time = guard.remaining_timeout
|
||||
|
||||
# If the response indicated that the credentials needed to be
|
||||
# refreshed, then refresh the credentials and re-attempt the
|
||||
# request.
|
||||
# A stored token may expire between the time it is retrieved and
|
||||
# the time the request is made, so we may need to try twice.
|
||||
if (
|
||||
response.status_code in self._refresh_status_codes
|
||||
and _credential_refresh_attempt < self._max_refresh_attempts
|
||||
):
|
||||
# Handle unauthorized permission error(401 status code)
|
||||
if response.status_code == http_client.UNAUTHORIZED:
|
||||
if self.is_mtls:
|
||||
(
|
||||
call_cert_bytes,
|
||||
call_key_bytes,
|
||||
cached_fingerprint,
|
||||
current_cert_fingerprint,
|
||||
) = _mtls_helper.check_parameters_for_unauthorized_response(
|
||||
self._cached_cert
|
||||
)
|
||||
if cached_fingerprint != current_cert_fingerprint:
|
||||
try:
|
||||
_LOGGER.info(
|
||||
"Client certificate has changed, reconfiguring mTLS "
|
||||
"channel."
|
||||
)
|
||||
self.configure_mtls_channel(
|
||||
lambda: (call_cert_bytes, call_key_bytes)
|
||||
)
|
||||
except Exception as e:
|
||||
_LOGGER.error("Failed to reconfigure mTLS channel: %s", e)
|
||||
raise exceptions.MutualTLSChannelError(
|
||||
"Failed to reconfigure mTLS channel"
|
||||
) from e
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Skipping reconfiguration of mTLS channel because the client"
|
||||
" certificate has not changed."
|
||||
)
|
||||
_LOGGER.info(
|
||||
"Refreshing credentials due to a %s response. Attempt %s/%s.",
|
||||
response.status_code,
|
||||
_credential_refresh_attempt + 1,
|
||||
self._max_refresh_attempts,
|
||||
)
|
||||
|
||||
# Do not apply the timeout unconditionally in order to not override the
|
||||
# _auth_request's default timeout.
|
||||
auth_request = (
|
||||
self._auth_request
|
||||
if timeout is None
|
||||
else functools.partial(self._auth_request, timeout=timeout)
|
||||
)
|
||||
|
||||
with TimeoutGuard(remaining_time) as guard:
|
||||
self.credentials.refresh(auth_request)
|
||||
remaining_time = guard.remaining_timeout
|
||||
|
||||
# Recurse. Pass in the original headers, not our modified set, but
|
||||
# do pass the adjusted max allowed time (i.e. the remaining total time).
|
||||
return self.request(
|
||||
method,
|
||||
url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
max_allowed_time=remaining_time,
|
||||
timeout=timeout,
|
||||
_credential_refresh_attempt=_credential_refresh_attempt + 1,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@property
|
||||
def is_mtls(self):
|
||||
"""Indicates if the created SSL channel is mutual TLS."""
|
||||
return self._is_mtls
|
||||
|
||||
def close(self):
|
||||
if self._auth_request_session is not None:
|
||||
self._auth_request_session.close()
|
||||
super(AuthorizedSession, self).close()
|
||||
@@ -0,0 +1,493 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Transport adapter for urllib3."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import http.client as http_client
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
|
||||
# to verify HTTPS requests, and certifi is the recommended and most reliable
|
||||
# way to get a root certificate bundle. See
|
||||
# http://urllib3.readthedocs.io/en/latest/user-guide.html\
|
||||
# #certificate-verification
|
||||
# For more details.
|
||||
try:
|
||||
import certifi
|
||||
except ImportError: # pragma: NO COVER
|
||||
certifi = None # type: ignore
|
||||
|
||||
try:
|
||||
import urllib3 # type: ignore
|
||||
import urllib3.exceptions # type: ignore
|
||||
from packaging import version # type: ignore
|
||||
except ImportError as caught_exc: # pragma: NO COVER
|
||||
raise ImportError(
|
||||
""
|
||||
f"Error: {caught_exc}."
|
||||
" The 'google-auth' library requires the extras installed "
|
||||
"for urllib3 network transport."
|
||||
"\n"
|
||||
"Please install the necessary dependencies using pip:\n"
|
||||
" pip install google-auth[urllib3]\n"
|
||||
"\n"
|
||||
"(Note: Using '[urllib3]' ensures the specific dependencies needed for this feature are installed. "
|
||||
"We recommend running this command in your virtual environment.)"
|
||||
) from caught_exc
|
||||
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import exceptions
|
||||
from google.auth import transport
|
||||
from google.auth.transport import _mtls_helper
|
||||
from google.oauth2 import service_account
|
||||
|
||||
if version.parse(urllib3.__version__) >= version.parse("2.0.0"): # pragma: NO COVER
|
||||
RequestMethods = urllib3._request_methods.RequestMethods # type: ignore
|
||||
else: # pragma: NO COVER
|
||||
RequestMethods = urllib3.request.RequestMethods # type: ignore
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _Response(transport.Response):
|
||||
"""urllib3 transport response adapter.
|
||||
|
||||
Args:
|
||||
response (urllib3.response.HTTPResponse): The raw urllib3 response.
|
||||
"""
|
||||
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._response.status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._response.headers
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._response.data
|
||||
|
||||
|
||||
class Request(transport.Request):
|
||||
"""urllib3 request adapter.
|
||||
|
||||
This class is used internally for making requests using various transports
|
||||
in a consistent way. If you use :class:`AuthorizedHttp` you do not need
|
||||
to construct or use this class directly.
|
||||
|
||||
This class can be useful if you want to manually refresh a
|
||||
:class:`~google.auth.credentials.Credentials` instance::
|
||||
|
||||
import google.auth.transport.urllib3
|
||||
import urllib3
|
||||
|
||||
http = urllib3.PoolManager()
|
||||
request = google.auth.transport.urllib3.Request(http)
|
||||
|
||||
credentials.refresh(request)
|
||||
|
||||
Args:
|
||||
http (urllib3.PoolManager): An instance of a urllib3 class that implements
|
||||
the request interface (e.g. :class:`urllib3.PoolManager`).
|
||||
|
||||
.. automethod:: __call__
|
||||
"""
|
||||
|
||||
def __init__(self, http):
|
||||
self.http = http
|
||||
|
||||
def __call__(
|
||||
self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
|
||||
):
|
||||
"""Make an HTTP request using urllib3.
|
||||
|
||||
Args:
|
||||
url (str): The URI to be requested.
|
||||
method (str): The HTTP method to use for the request. Defaults
|
||||
to 'GET'.
|
||||
body (bytes): The payload / body in HTTP request.
|
||||
headers (Mapping[str, str]): Request headers.
|
||||
timeout (Optional[int]): The number of seconds to wait for a
|
||||
response from the server. If not specified or if None, the
|
||||
urllib3 default timeout will be used.
|
||||
kwargs: Additional arguments passed throught to the underlying
|
||||
urllib3 :meth:`urlopen` method.
|
||||
|
||||
Returns:
|
||||
google.auth.transport.Response: The HTTP response.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.TransportError: If any exception occurred.
|
||||
"""
|
||||
# urllib3 uses a sentinel default value for timeout, so only set it if
|
||||
# specified.
|
||||
if timeout is not None:
|
||||
kwargs["timeout"] = timeout
|
||||
|
||||
try:
|
||||
_helpers.request_log(_LOGGER, method, url, body, headers)
|
||||
response = self.http.request(
|
||||
method, url, body=body, headers=headers, **kwargs
|
||||
)
|
||||
_helpers.response_log(_LOGGER, response)
|
||||
return _Response(response)
|
||||
except urllib3.exceptions.HTTPError as caught_exc:
|
||||
new_exc = exceptions.TransportError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
|
||||
def _make_default_http():
|
||||
if certifi is not None:
|
||||
return urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
|
||||
else:
|
||||
return urllib3.PoolManager()
|
||||
|
||||
|
||||
def _make_mutual_tls_http(cert, key):
|
||||
"""Create a mutual TLS HTTP connection with the given client cert and key.
|
||||
See https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
|
||||
|
||||
Args:
|
||||
cert (bytes): client certificate in PEM format
|
||||
key (bytes): client private key in PEM format
|
||||
|
||||
Returns:
|
||||
urllib3.PoolManager: Mutual TLS HTTP connection.
|
||||
|
||||
Raises:
|
||||
ImportError: If certifi or pyOpenSSL is not installed.
|
||||
OpenSSL.crypto.Error: If the cert or key is invalid.
|
||||
"""
|
||||
import certifi
|
||||
from OpenSSL import crypto
|
||||
import urllib3.contrib.pyopenssl # type: ignore
|
||||
|
||||
urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
ctx = urllib3.util.ssl_.create_urllib3_context()
|
||||
ctx.load_verify_locations(cafile=certifi.where())
|
||||
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
|
||||
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
|
||||
|
||||
ctx._ctx.use_certificate(x509)
|
||||
ctx._ctx.use_privatekey(pkey)
|
||||
|
||||
http = urllib3.PoolManager(ssl_context=ctx)
|
||||
return http
|
||||
|
||||
|
||||
class AuthorizedHttp(RequestMethods): # type: ignore
|
||||
"""A urllib3 HTTP class with credentials.
|
||||
|
||||
This class is used to perform requests to API endpoints that require
|
||||
authorization::
|
||||
|
||||
from google.auth.transport.urllib3 import AuthorizedHttp
|
||||
|
||||
authed_http = AuthorizedHttp(credentials)
|
||||
|
||||
response = authed_http.request(
|
||||
'GET', 'https://www.googleapis.com/storage/v1/b')
|
||||
|
||||
This class implements the urllib3 request interface and can be
|
||||
used just like any other :class:`urllib3.PoolManager`.
|
||||
|
||||
The underlying :meth:`urlopen` implementation handles adding the
|
||||
credentials' headers to the request and refreshing credentials as needed.
|
||||
|
||||
This class also supports mutual TLS via :meth:`configure_mtls_channel`
|
||||
method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
||||
environment variable must be explicitly set to `true`, otherwise it does
|
||||
nothing. Assume the environment is set to `true`, the method behaves in the
|
||||
following manner:
|
||||
If client_cert_callback is provided, client certificate and private
|
||||
key are loaded using the callback; if client_cert_callback is None,
|
||||
application default SSL credentials will be used. Exceptions are raised if
|
||||
there are problems with the certificate, private key, or the loading process,
|
||||
so it should be called within a try/except block.
|
||||
|
||||
First we set the environment variable to `true`, then create an :class:`AuthorizedHttp`
|
||||
instance and specify the endpoints::
|
||||
|
||||
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
|
||||
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
|
||||
|
||||
authed_http = AuthorizedHttp(credentials)
|
||||
|
||||
Now we can pass a callback to :meth:`configure_mtls_channel`::
|
||||
|
||||
def my_cert_callback():
|
||||
# some code to load client cert bytes and private key bytes, both in
|
||||
# PEM format.
|
||||
some_code_to_load_client_cert_and_key()
|
||||
if loaded:
|
||||
return cert, key
|
||||
raise MyClientCertFailureException()
|
||||
|
||||
# Always call configure_mtls_channel within a try/except block.
|
||||
try:
|
||||
is_mtls = authed_http.configure_mtls_channel(my_cert_callback)
|
||||
except:
|
||||
# handle exceptions.
|
||||
|
||||
if is_mtls:
|
||||
response = authed_http.request('GET', mtls_endpoint)
|
||||
else:
|
||||
response = authed_http.request('GET', regular_endpoint)
|
||||
|
||||
You can alternatively use application default SSL credentials like this::
|
||||
|
||||
try:
|
||||
is_mtls = authed_http.configure_mtls_channel()
|
||||
except:
|
||||
# handle exceptions.
|
||||
|
||||
Args:
|
||||
credentials (google.auth.credentials.Credentials): The credentials to
|
||||
add to the request.
|
||||
http (urllib3.PoolManager): The underlying HTTP object to
|
||||
use to make requests. If not specified, a
|
||||
:class:`urllib3.PoolManager` instance will be constructed with
|
||||
sane defaults.
|
||||
refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
|
||||
that credentials should be refreshed and the request should be
|
||||
retried.
|
||||
max_refresh_attempts (int): The maximum number of times to attempt to
|
||||
refresh the credentials and retry the request.
|
||||
default_host (Optional[str]): A host like "pubsub.googleapis.com".
|
||||
This is used when a self-signed JWT is created from service
|
||||
account credentials.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
credentials,
|
||||
http=None,
|
||||
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
|
||||
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
|
||||
default_host=None,
|
||||
):
|
||||
if http is None:
|
||||
self.http = _make_default_http()
|
||||
self._has_user_provided_http = False
|
||||
else:
|
||||
self.http = http
|
||||
self._has_user_provided_http = True
|
||||
|
||||
self.credentials = credentials
|
||||
self._refresh_status_codes = refresh_status_codes
|
||||
self._max_refresh_attempts = max_refresh_attempts
|
||||
self._default_host = default_host
|
||||
# Request instance used by internal methods (for example,
|
||||
# credentials.refresh).
|
||||
self._request = Request(self.http)
|
||||
self._is_mtls = False
|
||||
|
||||
# https://google.aip.dev/auth/4111
|
||||
# Attempt to use self-signed JWTs when a service account is used.
|
||||
if isinstance(self.credentials, service_account.Credentials):
|
||||
self.credentials._create_self_signed_jwt(
|
||||
"https://{}/".format(self._default_host) if self._default_host else None
|
||||
)
|
||||
|
||||
super(AuthorizedHttp, self).__init__()
|
||||
|
||||
def configure_mtls_channel(self, client_cert_callback=None):
|
||||
"""Configures mutual TLS channel using the given client_cert_callback or
|
||||
application default SSL credentials. The behavior is controlled by
|
||||
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable.
|
||||
(1) If the environment variable value is `true`, the function returns True
|
||||
if the channel is mutual TLS and False otherwise. The `http` provided
|
||||
in the constructor will be overwritten.
|
||||
(2) If the environment variable is not set or `false`, the function does
|
||||
nothing and it always return False.
|
||||
|
||||
Args:
|
||||
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
|
||||
The optional callback returns the client certificate and private
|
||||
key bytes both in PEM format.
|
||||
If the callback is None, application default SSL credentials
|
||||
will be used.
|
||||
|
||||
Returns:
|
||||
True if the channel is mutual TLS and False otherwise.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
|
||||
creation failed for any reason.
|
||||
"""
|
||||
use_client_cert = transport._mtls_helper.check_use_client_cert()
|
||||
if not use_client_cert:
|
||||
self._is_mtls = False
|
||||
return False
|
||||
else:
|
||||
self._is_mtls = True
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError as caught_exc:
|
||||
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
try:
|
||||
found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
|
||||
client_cert_callback
|
||||
)
|
||||
|
||||
if found_cert_key:
|
||||
self.http = _make_mutual_tls_http(cert, key)
|
||||
self._cached_cert = cert
|
||||
else:
|
||||
self.http = _make_default_http()
|
||||
except (
|
||||
exceptions.ClientCertError,
|
||||
ImportError,
|
||||
OpenSSL.crypto.Error,
|
||||
) as caught_exc:
|
||||
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
if self._has_user_provided_http:
|
||||
self._has_user_provided_http = False
|
||||
warnings.warn(
|
||||
"`http` provided in the constructor is overwritten", UserWarning
|
||||
)
|
||||
|
||||
return found_cert_key
|
||||
|
||||
def urlopen(self, method, url, body=None, headers=None, **kwargs):
|
||||
"""Implementation of urllib3's urlopen."""
|
||||
# pylint: disable=arguments-differ
|
||||
# We use kwargs to collect additional args that we don't need to
|
||||
# introspect here. However, we do explicitly collect the two
|
||||
# positional arguments.
|
||||
|
||||
# Use a kwarg for this instead of an attribute to maintain
|
||||
# thread-safety.
|
||||
_credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
|
||||
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
use_mtls = False
|
||||
if self._is_mtls:
|
||||
MTLS_URL_PREFIXES = ["mtls.googleapis.com", "mtls.sandbox.googleapis.com"]
|
||||
use_mtls = any([prefix in url for prefix in MTLS_URL_PREFIXES])
|
||||
|
||||
# Make a copy of the headers. They will be modified by the credentials
|
||||
# and we want to pass the original headers if we recurse.
|
||||
request_headers = headers.copy()
|
||||
|
||||
self.credentials.before_request(self._request, method, url, request_headers)
|
||||
|
||||
response = self.http.urlopen(
|
||||
method, url, body=body, headers=request_headers, **kwargs
|
||||
)
|
||||
|
||||
# If the response indicated that the credentials needed to be
|
||||
# refreshed, then refresh the credentials and re-attempt the
|
||||
# request.
|
||||
# A stored token may expire between the time it is retrieved and
|
||||
# the time the request is made, so we may need to try twice.
|
||||
# The reason urllib3's retries aren't used is because they
|
||||
# don't allow you to modify the request headers. :/
|
||||
if (
|
||||
response.status in self._refresh_status_codes
|
||||
and _credential_refresh_attempt < self._max_refresh_attempts
|
||||
):
|
||||
if response.status == http_client.UNAUTHORIZED:
|
||||
if use_mtls:
|
||||
(
|
||||
call_cert_bytes,
|
||||
call_key_bytes,
|
||||
cached_fingerprint,
|
||||
current_cert_fingerprint,
|
||||
) = _mtls_helper.check_parameters_for_unauthorized_response(
|
||||
self._cached_cert
|
||||
)
|
||||
if cached_fingerprint != current_cert_fingerprint:
|
||||
try:
|
||||
_LOGGER.info(
|
||||
"Client certificate has changed, reconfiguring mTLS "
|
||||
"channel."
|
||||
)
|
||||
self.configure_mtls_channel(
|
||||
client_cert_callback=lambda: (
|
||||
call_cert_bytes,
|
||||
call_key_bytes,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
_LOGGER.error("Failed to reconfigure mTLS channel: %s", e)
|
||||
raise exceptions.MutualTLSChannelError(
|
||||
"Failed to reconfigure mTLS channel"
|
||||
) from e
|
||||
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Skipping reconfiguration of mTLS channel because the "
|
||||
"client certificate has not changed."
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"Refreshing credentials due to a %s response. Attempt %s/%s.",
|
||||
response.status,
|
||||
_credential_refresh_attempt + 1,
|
||||
self._max_refresh_attempts,
|
||||
)
|
||||
|
||||
self.credentials.refresh(self._request)
|
||||
|
||||
# Recurse. Pass in the original headers, not our modified set.
|
||||
return self.urlopen(
|
||||
method,
|
||||
url,
|
||||
body=body,
|
||||
headers=headers,
|
||||
_credential_refresh_attempt=_credential_refresh_attempt + 1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
# Proxy methods for compliance with the urllib3.PoolManager interface
|
||||
|
||||
def __enter__(self):
|
||||
"""Proxy to ``self.http``."""
|
||||
return self.http.__enter__()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Proxy to ``self.http``."""
|
||||
return self.http.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, "http") and self.http is not None:
|
||||
self.http.clear()
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
"""Proxy to ``self.http``."""
|
||||
return self.http.headers
|
||||
|
||||
@headers.setter
|
||||
def headers(self, value):
|
||||
"""Proxy to ``self.http``."""
|
||||
self.http.headers = value
|
||||
Reference in New Issue
Block a user