From 35b986e2be347e5c8f02382b846b80f914a93a86 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Fri, 9 Feb 2018 12:35:13 -0500 Subject: [PATCH] Update google libraries --- src/google/auth/_cloud_sdk.py | 20 +---- src/google/auth/_default.py | 53 +++++++------- src/google/auth/_oauth2client.py | 12 ++- src/google/auth/compute_engine/_metadata.py | 6 +- src/google/auth/compute_engine/credentials.py | 7 +- src/google/auth/credentials.py | 45 +++++++++++- src/google/auth/crypt/__init__.py | 2 +- src/google/auth/jwt.py | 6 +- src/google/auth/transport/_http_client.py | 6 +- src/google/auth/transport/grpc.py | 14 ++-- src/google/auth/transport/requests.py | 42 ++++++++--- src/google/auth/transport/urllib3.py | 21 ++++-- src/google/oauth2/_client.py | 11 ++- src/google/oauth2/credentials.py | 73 ++++++++++++++++--- src/google/oauth2/service_account.py | 2 +- 15 files changed, 223 insertions(+), 97 deletions(-) diff --git a/src/google/auth/_cloud_sdk.py b/src/google/auth/_cloud_sdk.py index 898c6ec8..31be5e7c 100644 --- a/src/google/auth/_cloud_sdk.py +++ b/src/google/auth/_cloud_sdk.py @@ -18,13 +18,9 @@ import json import os import subprocess -import six - from google.auth import environment_vars import google.oauth2.credentials -# The Google OAuth 2.0 token endpoint. Used for authorized user credentials. -_GOOGLE_OAUTH2_TOKEN_ENDPOINT = 'https://accounts.google.com/o/oauth2/token' # The ~/.config subdirectory containing gcloud credentials. _CONFIG_DIRECTORY = 'gcloud' @@ -94,20 +90,8 @@ def load_authorized_user_credentials(info): Raises: ValueError: if the info is in the wrong format or missing data. """ - keys_needed = set(('refresh_token', 'client_id', 'client_secret')) - missing = keys_needed.difference(six.iterkeys(info)) - - if missing: - raise ValueError( - 'Authorized user info was not in the expected format, missing ' - 'fields {}.'.format(', '.join(missing))) - - return google.oauth2.credentials.Credentials( - None, # No access token, must be refreshed. - refresh_token=info['refresh_token'], - token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, - client_id=info['client_id'], - client_secret=info['client_secret']) + return google.oauth2.credentials.Credentials.from_authorized_user_info( + info) def get_project_id(): diff --git a/src/google/auth/_default.py b/src/google/auth/_default.py index 7dac642e..d63dcee3 100644 --- a/src/google/auth/_default.py +++ b/src/google/auth/_default.py @@ -22,6 +22,8 @@ import json import logging import os +import six + from google.auth import environment_vars from google.auth import exceptions import google.auth.transport._http_client @@ -67,9 +69,11 @@ def _load_credentials_from_file(filename): with io.open(filename, 'r') as file_obj: try: info = json.load(file_obj) - except ValueError as exc: - raise exceptions.DefaultCredentialsError( - 'File {} is not a valid json file.'.format(filename), exc) + except ValueError as caught_exc: + new_exc = exceptions.DefaultCredentialsError( + 'File {} is not a valid json file.'.format(filename), + caught_exc) + six.raise_from(new_exc, caught_exc) # The type key should indicate that the file is either a service account # credentials file or an authorized user credentials file. @@ -80,10 +84,11 @@ def _load_credentials_from_file(filename): try: credentials = _cloud_sdk.load_authorized_user_credentials(info) - except ValueError as exc: - raise exceptions.DefaultCredentialsError( - 'Failed to load authorized user credentials from {}'.format( - filename), exc) + except ValueError as caught_exc: + msg = 'Failed to load authorized user credentials from {}'.format( + filename) + new_exc = exceptions.DefaultCredentialsError(msg, caught_exc) + six.raise_from(new_exc, caught_exc) # Authorized user credentials do not contain the project ID. return credentials, None @@ -93,10 +98,11 @@ def _load_credentials_from_file(filename): try: credentials = ( service_account.Credentials.from_service_account_info(info)) - except ValueError as exc: - raise exceptions.DefaultCredentialsError( - 'Failed to load service account credentials from {}'.format( - filename), exc) + except ValueError as caught_exc: + msg = 'Failed to load service account credentials from {}'.format( + filename) + new_exc = exceptions.DefaultCredentialsError(msg, caught_exc) + six.raise_from(new_exc, caught_exc) return credentials, info.get('project_id') else: @@ -123,12 +129,6 @@ def _get_gcloud_sdk_credentials(): if not project_id: project_id = _cloud_sdk.get_project_id() - if not project_id: - _LOGGER.warning( - 'No project ID could be determined from the Cloud SDK ' - 'configuration. Consider running `gcloud config set project` or ' - 'setting the %s environment variable', environment_vars.PROJECT) - return credentials, project_id @@ -141,12 +141,6 @@ def _get_explicit_environ_credentials(): credentials, project_id = _load_credentials_from_file( os.environ[environment_vars.CREDENTIALS]) - if not project_id: - _LOGGER.warning( - 'No project ID could be determined from the credentials at %s ' - 'Consider setting the %s environment variable', - environment_vars.CREDENTIALS, environment_vars.PROJECT) - return credentials, project_id else: @@ -182,10 +176,6 @@ def _get_gce_credentials(request=None): try: project_id = _metadata.get_project_id(request=request) except exceptions.TransportError: - _LOGGER.warning( - 'No project ID could be determined from the Compute Engine ' - 'metadata service. Consider setting the %s environment ' - 'variable.', environment_vars.PROJECT) project_id = None return compute_engine.Credentials(), project_id @@ -281,6 +271,13 @@ def default(scopes=None, request=None): credentials, project_id = checker() if credentials is not None: credentials = with_scopes_if_required(credentials, scopes) - return credentials, explicit_project_id or project_id + effective_project_id = explicit_project_id or project_id + if not effective_project_id: + _LOGGER.warning( + 'No project ID could be determined. Consider running ' + '`gcloud config set project` or setting the %s ' + 'environment variable', + environment_vars.PROJECT) + return credentials, effective_project_id raise exceptions.DefaultCredentialsError(_HELP_MESSAGE) diff --git a/src/google/auth/_oauth2client.py b/src/google/auth/_oauth2client.py index 312326e1..71fd7bf4 100644 --- a/src/google/auth/_oauth2client.py +++ b/src/google/auth/_oauth2client.py @@ -21,6 +21,8 @@ from __future__ import absolute_import +import six + from google.auth import _helpers import google.auth.app_engine import google.oauth2.credentials @@ -30,8 +32,9 @@ try: import oauth2client.client import oauth2client.contrib.gce import oauth2client.service_account -except ImportError: - raise ImportError('oauth2client is not installed.') +except ImportError as caught_exc: + six.raise_from( + ImportError('oauth2client is not installed.'), caught_exc) try: import oauth2client.contrib.appengine @@ -162,5 +165,6 @@ def convert(credentials): try: return _CLASS_CONVERSION_MAP[credentials_class](credentials) - except KeyError: - raise ValueError(_CONVERT_ERROR_TMPL.format(credentials_class)) + except KeyError as caught_exc: + new_exc = ValueError(_CONVERT_ERROR_TMPL.format(credentials_class)) + six.raise_from(new_exc, caught_exc) diff --git a/src/google/auth/compute_engine/_metadata.py b/src/google/auth/compute_engine/_metadata.py index 9a22d7a8..c47be3fa 100644 --- a/src/google/auth/compute_engine/_metadata.py +++ b/src/google/auth/compute_engine/_metadata.py @@ -22,6 +22,7 @@ import json import logging import os +import six from six.moves import http_client from six.moves.urllib import parse as urlparse @@ -118,10 +119,11 @@ def get(request, path, root=_METADATA_ROOT, recursive=False): if response.headers['content-type'] == 'application/json': try: return json.loads(content) - except ValueError: - raise exceptions.TransportError( + except ValueError as caught_exc: + new_exc = exceptions.TransportError( 'Received invalid JSON from the Google Compute Engine' 'metadata service: {:.20}'.format(content)) + six.raise_from(new_exc, caught_exc) else: return content else: diff --git a/src/google/auth/compute_engine/credentials.py b/src/google/auth/compute_engine/credentials.py index b8fe6f5f..3841df2a 100644 --- a/src/google/auth/compute_engine/credentials.py +++ b/src/google/auth/compute_engine/credentials.py @@ -19,6 +19,8 @@ Engine using the Compute Engine metadata server. """ +import six + from google.auth import credentials from google.auth import exceptions from google.auth.compute_engine import _metadata @@ -89,8 +91,9 @@ class Credentials(credentials.ReadOnlyScoped, credentials.Credentials): self.token, self.expiry = _metadata.get_service_account_token( request, service_account=self._service_account_email) - except exceptions.TransportError as exc: - raise exceptions.RefreshError(exc) + except exceptions.TransportError as caught_exc: + new_exc = exceptions.RefreshError(caught_exc) + six.raise_from(new_exc, caught_exc) @property def service_account_email(self): diff --git a/src/google/auth/credentials.py b/src/google/auth/credentials.py index 28f9c9f7..64b33382 100644 --- a/src/google/auth/credentials.py +++ b/src/google/auth/credentials.py @@ -122,6 +122,43 @@ class Credentials(object): self.apply(headers) +class AnonymousCredentials(Credentials): + """Credentials that do not provide any authentication information. + + These are useful in the case of services that support anonymous access or + local service emulators that do not use credentials. + """ + + @property + def expired(self): + """Returns `False`, anonymous credentials never expire.""" + return False + + @property + def valid(self): + """Returns `True`, anonymous credentials are always valid.""" + return True + + def refresh(self, request): + """Raises :class:`ValueError``, anonymous credentials cannot be + refreshed.""" + raise ValueError("Anonymous credentials cannot be refreshed.") + + def apply(self, headers, token=None): + """Anonymous credentials do nothing to the request. + + The optional ``token`` argument is not supported. + + Raises: + ValueError: If a token was specified. + """ + if token is not None: + raise ValueError("Anonymous credentials don't support tokens.") + + def before_request(self, request, method, url, headers): + """Anonymous credentials do nothing to the request.""" + + @six.add_metaclass(abc.ABCMeta) class ReadOnlyScoped(object): """Interface for credentials whose scopes can be queried. @@ -136,7 +173,7 @@ class ReadOnlyScoped(object): if credentials.requires_scopes: # Scoping is required. - credentials = credentials.create_scoped(['one', 'two']) + credentials = credentials.with_scopes(scopes=['one', 'two']) Credentials that require scopes must either be constructed with scopes:: @@ -172,6 +209,9 @@ class ReadOnlyScoped(object): .. warning: This method is not guaranteed to be accurate if the credentials are :attr:`~Credentials.invalid`. + Args: + scopes (Sequence[str]): The list of scopes to check. + Returns: bool: True if the credentials have the given scopes. """ @@ -211,7 +251,8 @@ class Scoped(ReadOnlyScoped): """Create a copy of these credentials with the specified scopes. Args: - scopes (Sequence[str]): The list of scopes to request. + scopes (Sequence[str]): The list of scopes to attach to the + current credentials. Raises: NotImplementedError: If the credentials' scopes can not be changed. diff --git a/src/google/auth/crypt/__init__.py b/src/google/auth/crypt/__init__.py index fa59662e..7baa206e 100644 --- a/src/google/auth/crypt/__init__.py +++ b/src/google/auth/crypt/__init__.py @@ -29,7 +29,7 @@ If you're going to verify many messages with the same certificate, you can use To sign messages use :class:`RSASigner` with a private key:: private_key = open('private_key.pem').read() - signer = crypt.RSASigner(private_key) + signer = crypt.RSASigner.from_string(private_key) signature = signer.sign(message) """ diff --git a/src/google/auth/jwt.py b/src/google/auth/jwt.py index b1eb5fb9..02533762 100644 --- a/src/google/auth/jwt.py +++ b/src/google/auth/jwt.py @@ -47,6 +47,7 @@ import datetime import json import cachetools +import six from six.moves import urllib from google.auth import _helpers @@ -101,8 +102,9 @@ def _decode_jwt_segment(encoded_section): section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section) try: return json.loads(section_bytes.decode('utf-8')) - except ValueError: - raise ValueError('Can\'t parse segment: {0}'.format(section_bytes)) + except ValueError as caught_exc: + new_exc = ValueError('Can\'t parse segment: {0}'.format(section_bytes)) + six.raise_from(new_exc, caught_exc) def _unverified_decode(token): diff --git a/src/google/auth/transport/_http_client.py b/src/google/auth/transport/_http_client.py index 4a100964..08b1ab6c 100644 --- a/src/google/auth/transport/_http_client.py +++ b/src/google/auth/transport/_http_client.py @@ -17,6 +17,7 @@ import logging import socket +import six from six.moves import http_client from six.moves import urllib @@ -104,8 +105,9 @@ class Request(transport.Request): response = connection.getresponse() return Response(response) - except (http_client.HTTPException, socket.error) as exc: - raise exceptions.TransportError(exc) + except (http_client.HTTPException, socket.error) as caught_exc: + new_exc = exceptions.TransportError(caught_exc) + six.raise_from(new_exc, caught_exc) finally: connection.close() diff --git a/src/google/auth/transport/grpc.py b/src/google/auth/transport/grpc.py index 8554ffa3..0d44f645 100644 --- a/src/google/auth/transport/grpc.py +++ b/src/google/auth/transport/grpc.py @@ -16,13 +16,17 @@ from __future__ import absolute_import +import six try: import grpc -except ImportError: # pragma: NO COVER - raise ImportError( - 'gRPC is not installed, please install the grpcio package to use the ' - 'gRPC transport.') -import six +except ImportError as caught_exc: # pragma: NO COVER + six.raise_from( + ImportError( + 'gRPC is not installed, please install the grpcio package ' + 'to use the gRPC transport.' + ), + caught_exc, + ) class AuthMetadataPlugin(grpc.AuthMetadataPlugin): diff --git a/src/google/auth/transport/requests.py b/src/google/auth/transport/requests.py index 6fc395e2..2268243a 100644 --- a/src/google/auth/transport/requests.py +++ b/src/google/auth/transport/requests.py @@ -16,15 +16,23 @@ from __future__ import absolute_import +import functools import logging try: import requests -except ImportError: # pragma: NO COVER - raise ImportError( - 'The requests library is not installed, please install the requests ' - 'package to use the requests transport.') -import requests.exceptions +except ImportError as caught_exc: # pragma: NO COVER + import six + six.raise_from( + ImportError( + 'The requests library is not installed, please install the ' + 'requests package to use the requests transport.' + ), + caught_exc, + ) +import requests.adapters # pylint: disable=ungrouped-imports +import requests.exceptions # pylint: disable=ungrouped-imports +import six # pylint: disable=ungrouped-imports from google.auth import exceptions from google.auth import transport @@ -111,8 +119,9 @@ class Request(transport.Request): method, url, data=body, headers=headers, timeout=timeout, **kwargs) return _Response(response) - except requests.exceptions.RequestException as exc: - raise exceptions.TransportError(exc) + except requests.exceptions.RequestException as caught_exc: + new_exc = exceptions.TransportError(caught_exc) + six.raise_from(new_exc, caught_exc) class AuthorizedSession(requests.Session): @@ -139,22 +148,35 @@ class AuthorizedSession(requests.Session): 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. kwargs: Additional arguments passed to the :class:`requests.Session` constructor. """ def __init__(self, credentials, refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES, max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS, + refresh_timeout=None, **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 + + 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) + auth_request_session.mount("https://", retry_adapter) + # Request instance used by internal methods (for example, # credentials.refresh). # Do not pass `self` as the session here, as it can lead to infinite # recursion. - self._auth_request = Request() + self._auth_request = Request(auth_request_session) def request(self, method, url, data=None, headers=None, **kwargs): """Implementation of Requests' request.""" @@ -191,7 +213,9 @@ class AuthorizedSession(requests.Session): response.status_code, _credential_refresh_attempt + 1, self._max_refresh_attempts) - self.credentials.refresh(self._auth_request) + auth_request_with_timeout = functools.partial( + self._auth_request, timeout=self._refresh_timeout) + self.credentials.refresh(auth_request_with_timeout) # Recurse. Pass in the original headers, not our modified set. return self.request( diff --git a/src/google/auth/transport/urllib3.py b/src/google/auth/transport/urllib3.py index 0dfe9130..37eb3175 100644 --- a/src/google/auth/transport/urllib3.py +++ b/src/google/auth/transport/urllib3.py @@ -32,11 +32,17 @@ except ImportError: # pragma: NO COVER try: import urllib3 -except ImportError: # pragma: NO COVER - raise ImportError( - 'The urllib3 library is not installed, please install the urllib3 ' - 'package to use the urllib3 transport.') -import urllib3.exceptions +except ImportError as caught_exc: # pragma: NO COVER + import six + six.raise_from( + ImportError( + 'The urllib3 library is not installed, please install the ' + 'urllib3 package to use the urllib3 transport.' + ), + caught_exc, + ) +import six +import urllib3.exceptions # pylint: disable=ungrouped-imports from google.auth import exceptions from google.auth import transport @@ -126,8 +132,9 @@ class Request(transport.Request): response = self.http.request( method, url, body=body, headers=headers, **kwargs) return _Response(response) - except urllib3.exceptions.HTTPError as exc: - raise exceptions.TransportError(exc) + except urllib3.exceptions.HTTPError as caught_exc: + new_exc = exceptions.TransportError(caught_exc) + six.raise_from(new_exc, caught_exc) def _make_default_http(): diff --git a/src/google/oauth2/_client.py b/src/google/oauth2/_client.py index 468cb7e8..66251df4 100644 --- a/src/google/oauth2/_client.py +++ b/src/google/oauth2/_client.py @@ -26,6 +26,7 @@ For more information about the token endpoint, see import datetime import json +import six from six.moves import http_client from six.moves import urllib @@ -144,9 +145,10 @@ def jwt_grant(request, token_uri, assertion): try: access_token = response_data['access_token'] - except KeyError: - raise exceptions.RefreshError( + except KeyError as caught_exc: + new_exc = exceptions.RefreshError( 'No access token in response.', response_data) + six.raise_from(new_exc, caught_exc) expiry = _parse_expiry(response_data) @@ -190,9 +192,10 @@ def refresh_grant(request, token_uri, refresh_token, client_id, client_secret): try: access_token = response_data['access_token'] - except KeyError: - raise exceptions.RefreshError( + except KeyError as caught_exc: + new_exc = exceptions.RefreshError( 'No access token in response.', response_data) + six.raise_from(new_exc, caught_exc) refresh_token = response_data.get('refresh_token', refresh_token) expiry = _parse_expiry(response_data) diff --git a/src/google/oauth2/credentials.py b/src/google/oauth2/credentials.py index 6a635dda..24b3a3ee 100644 --- a/src/google/oauth2/credentials.py +++ b/src/google/oauth2/credentials.py @@ -31,12 +31,21 @@ Authorization Code grant flow. .. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1 """ +import io +import json + +import six + from google.auth import _helpers from google.auth import credentials from google.oauth2 import _client -class Credentials(credentials.Scoped, credentials.Credentials): +# The Google OAuth 2.0 token endpoint. Used for authorized user credentials. +_GOOGLE_OAUTH2_TOKEN_ENDPOINT = 'https://accounts.google.com/o/oauth2/token' + + +class Credentials(credentials.ReadOnlyScoped, credentials.Credentials): """Credentials using OAuth 2.0 access and refresh tokens.""" def __init__(self, token, refresh_token=None, id_token=None, @@ -109,15 +118,6 @@ class Credentials(credentials.Scoped, credentials.Credentials): the initial token is requested and can not be changed.""" return False - def with_scopes(self, scopes): - """Unavailable, OAuth 2.0 credentials can not be re-scoped. - - OAuth 2.0 credentials have their scopes set when the initial token is - requested and can not be changed. - """ - raise NotImplementedError( - 'OAuth 2.0 Credentials can not modify their scopes.') - @_helpers.copy_docstring(credentials.Credentials) def refresh(self, request): access_token, refresh_token, expiry, grant_response = ( @@ -129,3 +129,56 @@ class Credentials(credentials.Scoped, credentials.Credentials): self.expiry = expiry self._refresh_token = refresh_token self._id_token = grant_response.get('id_token') + + @classmethod + def from_authorized_user_info(cls, info, scopes=None): + """Creates a Credentials instance from parsed authorized user info. + + Args: + info (Mapping[str, str]): The authorized user info in Google + format. + scopes (Sequence[str]): Optional list of scopes to include in the + credentials. + + Returns: + google.oauth2.credentials.Credentials: The constructed + credentials. + + Raises: + ValueError: If the info is not in the expected format. + """ + keys_needed = set(('refresh_token', 'client_id', 'client_secret')) + missing = keys_needed.difference(six.iterkeys(info)) + + if missing: + raise ValueError( + 'Authorized user info was not in the expected format, missing ' + 'fields {}.'.format(', '.join(missing))) + + return Credentials( + None, # No access token, must be refreshed. + refresh_token=info['refresh_token'], + token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, + scopes=scopes, + client_id=info['client_id'], + client_secret=info['client_secret']) + + @classmethod + def from_authorized_user_file(cls, filename, scopes=None): + """Creates a Credentials instance from an authorized user json file. + + Args: + filename (str): The path to the authorized user json file. + scopes (Sequence[str]): Optional list of scopes to include in the + credentials. + + Returns: + google.oauth2.credentials.Credentials: The constructed + credentials. + + Raises: + ValueError: If the file is not in the expected format. + """ + with io.open(filename, 'r', encoding='utf-8') as json_file: + data = json.load(json_file) + return cls.from_authorized_user_info(data, scopes) diff --git a/src/google/oauth2/service_account.py b/src/google/oauth2/service_account.py index 81b5dabe..54bd8d67 100644 --- a/src/google/oauth2/service_account.py +++ b/src/google/oauth2/service_account.py @@ -79,7 +79,7 @@ from google.auth import credentials from google.auth import jwt from google.oauth2 import _client -_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in sections +_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds class Credentials(credentials.Signing,