From 036dac43018b8cc26b5608e1bb21d6e3ee62a282 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 16 Jul 2024 11:22:14 -0400 Subject: [PATCH] feat: implement base classes for credentials and request sessions (#1551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implement base classes for credentials and request sessions * remove public methods apply and before_request from base credentials * move MTLS logic back to requests * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * minor fix * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * address PR comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * address PR comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: Refresh system test creds. * revert copyright date * fix import statements order --------- Co-authored-by: Owl Bot Co-authored-by: Carl Lundin <108372512+clundin25@users.noreply.github.com> Co-authored-by: Carl Lundin --- google/auth/_credentials_base.py | 73 ++++++++++++++++++++++++ google/auth/credentials.py | 12 ++-- google/auth/transport/_requests_base.py | 52 +++++++++++++++++ google/auth/transport/requests.py | 5 +- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 5 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 google/auth/_credentials_base.py create mode 100644 google/auth/transport/_requests_base.py diff --git a/google/auth/_credentials_base.py b/google/auth/_credentials_base.py new file mode 100644 index 000000000..29462dc0c --- /dev/null +++ b/google/auth/_credentials_base.py @@ -0,0 +1,73 @@ +# 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. + + +"""Interface for base credentials.""" + +import abc + +from google.auth import _helpers + + +class _BaseCredentials(metaclass=abc.ABCMeta): + """Base class for all credentials. + + All credentials have a :attr:`token` that is used for authentication and + may also optionally set an :attr:`expiry` to indicate when the token will + no longer be valid. + + Most credentials will be :attr:`invalid` until :meth:`refresh` is called. + Credentials can do this automatically before the first HTTP request in + :meth:`before_request`. + + Although the token and expiration will change as the credentials are + :meth:`refreshed ` and used, credentials should be considered + immutable. Various credentials will accept configuration such as private + keys, scopes, and other options. These options are not changeable after + construction. Some classes will provide mechanisms to copy the credentials + with modifications such as :meth:`ScopedCredentials.with_scopes`. + """ + + def __init__(self): + self.token = None + """str: The bearer token that can be used in HTTP headers to make + authenticated requests.""" + + @abc.abstractmethod + def refresh(self, request): + """Refreshes the access token. + + Args: + request (google.auth.transport.Request): The object used to make + HTTP requests. + + Raises: + google.auth.exceptions.RefreshError: If the credentials could + not be refreshed. + """ + # pylint: disable=missing-raises-doc + # (pylint doesn't recognize that this is abstract) + raise NotImplementedError("Refresh must be implemented") + + def _apply(self, headers, token=None): + """Apply the token to the authentication header. + + Args: + headers (Mapping): The HTTP request headers. + token (Optional[str]): If specified, overrides the current access + token. + """ + headers["authorization"] = "Bearer {}".format( + _helpers.from_bytes(token or self.token) + ) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 27abd443d..e31930311 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -22,12 +22,13 @@ from google.auth import _helpers, environment_vars from google.auth import exceptions from google.auth import metrics +from google.auth._credentials_base import _BaseCredentials from google.auth._refresh_worker import RefreshThreadManager DEFAULT_UNIVERSE_DOMAIN = "googleapis.com" -class Credentials(metaclass=abc.ABCMeta): +class Credentials(_BaseCredentials): """Base class for all credentials. All credentials have a :attr:`token` that is used for authentication and @@ -47,9 +48,8 @@ class Credentials(metaclass=abc.ABCMeta): """ def __init__(self): - self.token = None - """str: The bearer token that can be used in HTTP headers to make - authenticated requests.""" + super(Credentials, self).__init__() + self.expiry = None """Optional[datetime]: When the token expires and is no longer valid. If this is None, the token is assumed to never expire.""" @@ -167,9 +167,7 @@ def apply(self, headers, token=None): token (Optional[str]): If specified, overrides the current access token. """ - headers["authorization"] = "Bearer {}".format( - _helpers.from_bytes(token or self.token) - ) + self._apply(headers, token=token) """Trust boundary value will be a cached value from global lookup. The response of trust boundary will be a list of regions and a hex diff --git a/google/auth/transport/_requests_base.py b/google/auth/transport/_requests_base.py new file mode 100644 index 000000000..ec718d909 --- /dev/null +++ b/google/auth/transport/_requests_base.py @@ -0,0 +1,52 @@ +# 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.""" + + +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") diff --git a/google/auth/transport/requests.py b/google/auth/transport/requests.py index 23a69783d..68f67c59b 100644 --- a/google/auth/transport/requests.py +++ b/google/auth/transport/requests.py @@ -38,6 +38,7 @@ from google.auth import exceptions from google.auth import transport import google.auth.transport._mtls_helper +from google.auth.transport._requests_base import _BaseAuthorizedSession from google.oauth2 import service_account _LOGGER = logging.getLogger(__name__) @@ -292,7 +293,7 @@ def proxy_manager_for(self, *args, **kwargs): return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs) -class AuthorizedSession(requests.Session): +class AuthorizedSession(requests.Session, _BaseAuthorizedSession): """A Requests Session class with credentials. This class is used to perform requests to API endpoints that require @@ -389,7 +390,7 @@ def __init__( default_host=None, ): super(AuthorizedSession, self).__init__() - self.credentials = credentials + _BaseAuthorizedSession.__init__(self, credentials) self._refresh_status_codes = refresh_status_codes self._max_refresh_attempts = max_refresh_attempts self._refresh_timeout = refresh_timeout diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 3d9bd3327546ea90e45e2a84311b752d1744c692..77ab87e0f1bf6d7d084d21c1c2fa8691e6c59709 100644 GIT binary patch literal 10324 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 literal 10324 zcmV-aD67{BB>?tKRTDvxhv+ac|FOqQ=Vu-py_^L#6k-ae0V0+lz#c%7=JC)8m>u?U*O*1pWWyLag9oF%bI-˓eC8e(xEgXU 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