| Index: client/third_party/oauth2client/client.py
|
| diff --git a/client/third_party/oauth2client/client.py b/client/third_party/oauth2client/client.py
|
| index cd5959f24a44ccedc9604636dfc332f8841e8ff4..a388fb88e8961259daf1e1e92e4687c37a144eed 100644
|
| --- a/client/third_party/oauth2client/client.py
|
| +++ b/client/third_party/oauth2client/client.py
|
| @@ -30,6 +30,7 @@ import tempfile
|
| import time
|
| import shutil
|
| import six
|
| +from six.moves import http_client
|
| from six.moves import urllib
|
|
|
| import httplib2
|
| @@ -73,7 +74,7 @@ ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
|
| OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
|
|
|
| # Google Data client libraries may need to set this to [401, 403].
|
| -REFRESH_STATUS_CODES = [401]
|
| +REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
|
|
|
| # The value representing user credentials.
|
| AUTHORIZED_USER = 'authorized_user'
|
| @@ -101,6 +102,8 @@ ADC_HELP_MSG = (
|
| 'https://developers.google.com/accounts/docs/'
|
| 'application-default-credentials for more information.')
|
|
|
| +_WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
|
| +
|
| # The access token along with the seconds in which it expires.
|
| AccessTokenInfo = collections.namedtuple(
|
| 'AccessTokenInfo', ['access_token', 'expires_in'])
|
| @@ -115,6 +118,10 @@ _GCE_METADATA_HOST = '169.254.169.254'
|
| _METADATA_FLAVOR_HEADER = 'Metadata-Flavor'
|
| _DESIRED_METADATA_FLAVOR = 'Google'
|
|
|
| +# Expose utcnow() at module level to allow for
|
| +# easier testing (by replacing with a stub).
|
| +_UTCNOW = datetime.datetime.utcnow
|
| +
|
|
|
| class SETTINGS(object):
|
| """Settings namespace for globally defined values."""
|
| @@ -192,6 +199,13 @@ class MemoryCache(object):
|
| self.cache.pop(key, None)
|
|
|
|
|
| +def _parse_expiry(expiry):
|
| + if expiry and isinstance(expiry, datetime.datetime):
|
| + return expiry.strftime(EXPIRY_FORMAT)
|
| + else:
|
| + return None
|
| +
|
| +
|
| class Credentials(object):
|
| """Base class for all Credentials objects.
|
|
|
| @@ -202,7 +216,7 @@ class Credentials(object):
|
| JSON string as input and returns an instantiated Credentials object.
|
| """
|
|
|
| - NON_SERIALIZED_MEMBERS = ['store']
|
| + NON_SERIALIZED_MEMBERS = frozenset(['store'])
|
|
|
| def authorize(self, http):
|
| """Take an httplib2.Http instance (or equivalent) and authorizes it.
|
| @@ -243,34 +257,37 @@ class Credentials(object):
|
| """
|
| _abstract()
|
|
|
| - def _to_json(self, strip):
|
| + def _to_json(self, strip, to_serialize=None):
|
| """Utility function that creates JSON repr. of a Credentials object.
|
|
|
| Args:
|
| - strip: array, An array of names of members to not include in the
|
| + strip: array, An array of names of members to exclude from the
|
| JSON.
|
| + to_serialize: dict, (Optional) The properties for this object
|
| + that will be serialized. This allows callers to modify
|
| + before serializing.
|
|
|
| Returns:
|
| string, a JSON representation of this instance, suitable to pass to
|
| from_json().
|
| """
|
| - t = type(self)
|
| - d = copy.copy(self.__dict__)
|
| + curr_type = self.__class__
|
| + if to_serialize is None:
|
| + to_serialize = copy.copy(self.__dict__)
|
| for member in strip:
|
| - if member in d:
|
| - del d[member]
|
| - if (d.get('token_expiry') and
|
| - isinstance(d['token_expiry'], datetime.datetime)):
|
| - d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
|
| - # Add in information we will need later to reconsistitue this instance.
|
| - d['_class'] = t.__name__
|
| - d['_module'] = t.__module__
|
| - for key, val in d.items():
|
| + if member in to_serialize:
|
| + del to_serialize[member]
|
| + to_serialize['token_expiry'] = _parse_expiry(
|
| + to_serialize.get('token_expiry'))
|
| + # Add in information we will need later to reconstitute this instance.
|
| + to_serialize['_class'] = curr_type.__name__
|
| + to_serialize['_module'] = curr_type.__module__
|
| + for key, val in to_serialize.items():
|
| if isinstance(val, bytes):
|
| - d[key] = val.decode('utf-8')
|
| + to_serialize[key] = val.decode('utf-8')
|
| if isinstance(val, set):
|
| - d[key] = list(val)
|
| - return json.dumps(d)
|
| + to_serialize[key] = list(val)
|
| + return json.dumps(to_serialize)
|
|
|
| def to_json(self):
|
| """Creating a JSON representation of an instance of Credentials.
|
| @@ -279,23 +296,23 @@ class Credentials(object):
|
| string, a JSON representation of this instance, suitable to pass to
|
| from_json().
|
| """
|
| - return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
|
| + return self._to_json(self.NON_SERIALIZED_MEMBERS)
|
|
|
| @classmethod
|
| - def new_from_json(cls, s):
|
| + def new_from_json(cls, json_data):
|
| """Utility class method to instantiate a Credentials subclass from JSON.
|
|
|
| Expects the JSON string to have been produced by to_json().
|
|
|
| Args:
|
| - s: string or bytes, JSON from to_json().
|
| + json_data: string or bytes, JSON from to_json().
|
|
|
| Returns:
|
| An instance of the subclass of Credentials that was serialized with
|
| to_json().
|
| """
|
| - json_string_as_unicode = _from_bytes(s)
|
| - data = json.loads(json_string_as_unicode)
|
| + json_data_as_unicode = _from_bytes(json_data)
|
| + data = json.loads(json_data_as_unicode)
|
| # Find and call the right classmethod from_json() to restore
|
| # the object.
|
| module_name = data['_module']
|
| @@ -310,8 +327,7 @@ class Credentials(object):
|
| module_obj = __import__(module_name,
|
| fromlist=module_name.split('.')[:-1])
|
| kls = getattr(module_obj, data['_class'])
|
| - from_json = getattr(kls, 'from_json')
|
| - return from_json(json_string_as_unicode)
|
| + return kls.from_json(json_data_as_unicode)
|
|
|
| @classmethod
|
| def from_json(cls, unused_data):
|
| @@ -340,21 +356,31 @@ class Storage(object):
|
| such that multiple processes and threads can operate on a single
|
| store.
|
| """
|
| + def __init__(self, lock=None):
|
| + """Create a Storage instance.
|
| +
|
| + Args:
|
| + lock: An optional threading.Lock-like object. Must implement at
|
| + least acquire() and release(). Does not need to be re-entrant.
|
| + """
|
| + self._lock = lock
|
|
|
| def acquire_lock(self):
|
| """Acquires any lock necessary to access this Storage.
|
|
|
| This lock is not reentrant.
|
| """
|
| - pass
|
| + if self._lock is not None:
|
| + self._lock.acquire()
|
|
|
| def release_lock(self):
|
| """Release the Storage lock.
|
|
|
| Trying to release a lock that isn't held will result in a
|
| - RuntimeError.
|
| + RuntimeError in the case of a threading.Lock or multiprocessing.Lock.
|
| """
|
| - pass
|
| + if self._lock is not None:
|
| + self._lock.release()
|
|
|
| def locked_get(self):
|
| """Retrieve credential.
|
| @@ -683,23 +709,19 @@ class OAuth2Credentials(Credentials):
|
| self._retrieve_scopes(http.request)
|
| return self.scopes
|
|
|
| - def to_json(self):
|
| - return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
|
| -
|
| @classmethod
|
| - def from_json(cls, s):
|
| + def from_json(cls, json_data):
|
| """Instantiate a Credentials object from a JSON description of it.
|
|
|
| The JSON should have been produced by calling .to_json() on the object.
|
|
|
| Args:
|
| - data: dict, A deserialized JSON object.
|
| + json_data: string or bytes, JSON to deserialize.
|
|
|
| Returns:
|
| An instance of a Credentials subclass.
|
| """
|
| - s = _from_bytes(s)
|
| - data = json.loads(s)
|
| + data = json.loads(_from_bytes(json_data))
|
| if (data.get('token_expiry') and
|
| not isinstance(data['token_expiry'], datetime.datetime)):
|
| try:
|
| @@ -735,7 +757,7 @@ class OAuth2Credentials(Credentials):
|
| if not self.token_expiry:
|
| return False
|
|
|
| - now = datetime.datetime.utcnow()
|
| + now = _UTCNOW()
|
| if now >= self.token_expiry:
|
| logger.info('access_token is expired. Now: %s, token_expiry: %s',
|
| now, self.token_expiry)
|
| @@ -778,7 +800,7 @@ class OAuth2Credentials(Credentials):
|
| valid; we just don't know anything about it.
|
| """
|
| if self.token_expiry:
|
| - now = datetime.datetime.utcnow()
|
| + now = _UTCNOW()
|
| if self.token_expiry > now:
|
| time_delta = self.token_expiry - now
|
| # TODO(orestica): return time_delta.total_seconds()
|
| @@ -873,16 +895,20 @@ class OAuth2Credentials(Credentials):
|
| resp, content = http_request(
|
| self.token_uri, method='POST', body=body, headers=headers)
|
| content = _from_bytes(content)
|
| - if resp.status == 200:
|
| + if resp.status == http_client.OK:
|
| d = json.loads(content)
|
| self.token_response = d
|
| self.access_token = d['access_token']
|
| self.refresh_token = d.get('refresh_token', self.refresh_token)
|
| if 'expires_in' in d:
|
| - self.token_expiry = datetime.timedelta(
|
| - seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
|
| + delta = datetime.timedelta(seconds=int(d['expires_in']))
|
| + self.token_expiry = delta + _UTCNOW()
|
| else:
|
| self.token_expiry = None
|
| + if 'id_token' in d:
|
| + self.id_token = _extract_id_token(d['id_token'])
|
| + else:
|
| + self.id_token = None
|
| # On temporary refresh errors, the user does not actually have to
|
| # re-authorize, so we unflag here.
|
| self.invalid = False
|
| @@ -934,7 +960,7 @@ class OAuth2Credentials(Credentials):
|
| query_params = {'token': token}
|
| token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
|
| resp, content = http_request(token_revoke_uri)
|
| - if resp.status == 200:
|
| + if resp.status == http_client.OK:
|
| self.invalid = True
|
| else:
|
| error_msg = 'Invalid response %s.' % resp.status
|
| @@ -979,7 +1005,7 @@ class OAuth2Credentials(Credentials):
|
| query_params)
|
| resp, content = http_request(token_info_uri)
|
| content = _from_bytes(content)
|
| - if resp.status == 200:
|
| + if resp.status == http_client.OK:
|
| d = json.loads(content)
|
| self.scopes = set(util.string_to_scopes(d.get('scope', '')))
|
| else:
|
| @@ -1043,8 +1069,8 @@ class AccessTokenCredentials(OAuth2Credentials):
|
| revoke_uri=revoke_uri)
|
|
|
| @classmethod
|
| - def from_json(cls, s):
|
| - data = json.loads(_from_bytes(s))
|
| + def from_json(cls, json_data):
|
| + data = json.loads(_from_bytes(json_data))
|
| retval = AccessTokenCredentials(
|
| data['access_token'],
|
| data['user_agent'])
|
| @@ -1085,7 +1111,7 @@ def _detect_gce_environment():
|
| headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
|
| connection.request('GET', '/', headers=headers)
|
| response = connection.getresponse()
|
| - if response.status == 200:
|
| + if response.status == http_client.OK:
|
| return (response.getheader(_METADATA_FLAVOR_HEADER) ==
|
| _DESIRED_METADATA_FLAVOR)
|
| except socket.error: # socket.timeout or socket.error(64, 'Host is down')
|
| @@ -1160,6 +1186,11 @@ class GoogleCredentials(OAuth2Credentials):
|
| print(response)
|
| """
|
|
|
| + NON_SERIALIZED_MEMBERS = (
|
| + frozenset(['_private_key']) |
|
| + OAuth2Credentials.NON_SERIALIZED_MEMBERS)
|
| + """Members that aren't serialized when object is converted to JSON."""
|
| +
|
| def __init__(self, access_token, client_id, client_secret, refresh_token,
|
| token_expiry, token_uri, user_agent,
|
| revoke_uri=GOOGLE_REVOKE_URI):
|
| @@ -1202,6 +1233,32 @@ class GoogleCredentials(OAuth2Credentials):
|
| """
|
| return self
|
|
|
| + @classmethod
|
| + def from_json(cls, json_data):
|
| + # TODO(issue 388): eliminate the circularity that is the reason for
|
| + # this non-top-level import.
|
| + from oauth2client.service_account import ServiceAccountCredentials
|
| + data = json.loads(_from_bytes(json_data))
|
| +
|
| + # We handle service_account.ServiceAccountCredentials since it is a
|
| + # possible return type of GoogleCredentials.get_application_default()
|
| + if (data['_module'] == 'oauth2client.service_account' and
|
| + data['_class'] == 'ServiceAccountCredentials'):
|
| + return ServiceAccountCredentials.from_json(data)
|
| +
|
| + token_expiry = _parse_expiry(data.get('token_expiry'))
|
| + google_credentials = cls(
|
| + data['access_token'],
|
| + data['client_id'],
|
| + data['client_secret'],
|
| + data['refresh_token'],
|
| + token_expiry,
|
| + data['token_uri'],
|
| + data['user_agent'],
|
| + revoke_uri=data.get('revoke_uri', None))
|
| + google_credentials.invalid = data['invalid']
|
| + return google_credentials
|
| +
|
| @property
|
| def serialization_data(self):
|
| """Get the fields and values identifying the current credentials."""
|
| @@ -1415,9 +1472,6 @@ def _get_well_known_file():
|
| """Get the well known file produced by command 'gcloud auth login'."""
|
| # TODO(orestica): Revisit this method once gcloud provides a better way
|
| # of pinpointing the exact location of the file.
|
| -
|
| - WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
|
| -
|
| default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR)
|
| if default_config_dir is None:
|
| if os.name == 'nt':
|
| @@ -1435,14 +1489,11 @@ def _get_well_known_file():
|
| '.config',
|
| _CLOUDSDK_CONFIG_DIRECTORY)
|
|
|
| - return os.path.join(default_config_dir, WELL_KNOWN_CREDENTIALS_FILE)
|
| + return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE)
|
|
|
|
|
| def _get_application_default_credential_from_file(filename):
|
| """Build the Application Default Credentials from file."""
|
| -
|
| - from oauth2client import service_account
|
| -
|
| # read the credentials from the file
|
| with open(filename) as file_obj:
|
| client_credentials = json.load(file_obj)
|
| @@ -1473,12 +1524,9 @@ def _get_application_default_credential_from_file(filename):
|
| token_uri=GOOGLE_TOKEN_URI,
|
| user_agent='Python client library')
|
| else: # client_credentials['type'] == SERVICE_ACCOUNT
|
| - return service_account._ServiceAccountCredentials(
|
| - service_account_id=client_credentials['client_id'],
|
| - service_account_email=client_credentials['client_email'],
|
| - private_key_id=client_credentials['private_key_id'],
|
| - private_key_pkcs8_text=client_credentials['private_key'],
|
| - scopes=[])
|
| + from oauth2client.service_account import ServiceAccountCredentials
|
| + return ServiceAccountCredentials.from_json_keyfile_dict(
|
| + client_credentials)
|
|
|
|
|
| def _raise_exception_for_missing_fields(missing_fields):
|
| @@ -1495,15 +1543,15 @@ def _raise_exception_for_reading_json(credential_file,
|
|
|
|
|
| def _get_application_default_credential_GAE():
|
| - from oauth2client.appengine import AppAssertionCredentials
|
| + from oauth2client.contrib.appengine import AppAssertionCredentials
|
|
|
| return AppAssertionCredentials([])
|
|
|
|
|
| def _get_application_default_credential_GCE():
|
| - from oauth2client.gce import AppAssertionCredentials
|
| + from oauth2client.contrib.gce import AppAssertionCredentials
|
|
|
| - return AppAssertionCredentials([])
|
| + return AppAssertionCredentials()
|
|
|
|
|
| class AssertionCredentials(GoogleCredentials):
|
| @@ -1569,6 +1617,18 @@ class AssertionCredentials(GoogleCredentials):
|
| """
|
| self._do_revoke(http_request, self.access_token)
|
|
|
| + def sign_blob(self, blob):
|
| + """Cryptographically sign a blob (of bytes).
|
| +
|
| + Args:
|
| + blob: bytes, Message to be signed.
|
| +
|
| + Returns:
|
| + tuple, A pair of the private key ID used to sign the blob and
|
| + the signed contents.
|
| + """
|
| + raise NotImplementedError('This method is abstract.')
|
| +
|
|
|
| def _RequireCryptoOrDie():
|
| """Ensure we have a crypto library, or throw CryptoUnavailableError.
|
| @@ -1581,101 +1641,6 @@ def _RequireCryptoOrDie():
|
| raise CryptoUnavailableError('No crypto library available')
|
|
|
|
|
| -class SignedJwtAssertionCredentials(AssertionCredentials):
|
| - """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
|
| -
|
| - This credential does not require a flow to instantiate because it
|
| - represents a two legged flow, and therefore has all of the required
|
| - information to generate and refresh its own access tokens.
|
| -
|
| - SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto
|
| - 2.6 or later. For App Engine you may also consider using
|
| - AppAssertionCredentials.
|
| - """
|
| -
|
| - MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
| -
|
| - @util.positional(4)
|
| - def __init__(self,
|
| - service_account_name,
|
| - private_key,
|
| - scope,
|
| - private_key_password='notasecret',
|
| - user_agent=None,
|
| - token_uri=GOOGLE_TOKEN_URI,
|
| - revoke_uri=GOOGLE_REVOKE_URI,
|
| - **kwargs):
|
| - """Constructor for SignedJwtAssertionCredentials.
|
| -
|
| - Args:
|
| - service_account_name: string, id for account, usually an email
|
| - address.
|
| - private_key: string or bytes, private key in PKCS12 or PEM format.
|
| - scope: string or iterable of strings, scope(s) of the credentials
|
| - being requested.
|
| - private_key_password: string, password for private_key, unused if
|
| - private_key is in PEM format.
|
| - user_agent: string, HTTP User-Agent to provide for this
|
| - application.
|
| - token_uri: string, URI for token endpoint. For convenience defaults
|
| - to Google's endpoints but any OAuth 2.0 provider can be
|
| - used.
|
| - revoke_uri: string, URI for revoke endpoint.
|
| - kwargs: kwargs, Additional parameters to add to the JWT token, for
|
| - example sub=joe@xample.org.
|
| -
|
| - Raises:
|
| - CryptoUnavailableError if no crypto library is available.
|
| - """
|
| - _RequireCryptoOrDie()
|
| - super(SignedJwtAssertionCredentials, self).__init__(
|
| - None,
|
| - user_agent=user_agent,
|
| - token_uri=token_uri,
|
| - revoke_uri=revoke_uri,
|
| - )
|
| -
|
| - self.scope = util.scopes_to_string(scope)
|
| -
|
| - # Keep base64 encoded so it can be stored in JSON.
|
| - self.private_key = base64.b64encode(_to_bytes(private_key))
|
| - self.private_key_password = private_key_password
|
| - self.service_account_name = service_account_name
|
| - self.kwargs = kwargs
|
| -
|
| - @classmethod
|
| - def from_json(cls, s):
|
| - data = json.loads(_from_bytes(s))
|
| - retval = SignedJwtAssertionCredentials(
|
| - data['service_account_name'],
|
| - base64.b64decode(data['private_key']),
|
| - data['scope'],
|
| - private_key_password=data['private_key_password'],
|
| - user_agent=data['user_agent'],
|
| - token_uri=data['token_uri'],
|
| - **data['kwargs']
|
| - )
|
| - retval.invalid = data['invalid']
|
| - retval.access_token = data['access_token']
|
| - return retval
|
| -
|
| - def _generate_assertion(self):
|
| - """Generate the assertion that will be used in the request."""
|
| - now = int(time.time())
|
| - payload = {
|
| - 'aud': self.token_uri,
|
| - 'scope': self.scope,
|
| - 'iat': now,
|
| - 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
|
| - 'iss': self.service_account_name
|
| - }
|
| - payload.update(self.kwargs)
|
| - logger.debug(str(payload))
|
| -
|
| - private_key = base64.b64decode(self.private_key)
|
| - return crypt.make_signed_jwt(crypt.Signer.from_string(
|
| - private_key, self.private_key_password), payload)
|
| -
|
| # Only used in verify_id_token(), which is always calling to the same URI
|
| # for the certs.
|
| _cached_http = httplib2.Http(MemoryCache())
|
| @@ -1709,7 +1674,7 @@ def verify_id_token(id_token, audience, http=None,
|
| http = _cached_http
|
|
|
| resp, content = http.request(cert_uri)
|
| - if resp.status == 200:
|
| + if resp.status == http_client.OK:
|
| certs = json.loads(_from_bytes(content))
|
| return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
|
| else:
|
| @@ -2054,7 +2019,7 @@ class OAuth2WebServerFlow(Flow):
|
| resp, content = http.request(self.device_uri, method='POST', body=body,
|
| headers=headers)
|
| content = _from_bytes(content)
|
| - if resp.status == 200:
|
| + if resp.status == http_client.OK:
|
| try:
|
| flow_info = json.loads(content)
|
| except ValueError as e:
|
| @@ -2137,7 +2102,7 @@ class OAuth2WebServerFlow(Flow):
|
| resp, content = http.request(self.token_uri, method='POST', body=body,
|
| headers=headers)
|
| d = _parse_exchange_token_response(content)
|
| - if resp.status == 200 and 'access_token' in d:
|
| + if resp.status == http_client.OK and 'access_token' in d:
|
| access_token = d['access_token']
|
| refresh_token = d.get('refresh_token', None)
|
| if not refresh_token:
|
| @@ -2146,9 +2111,8 @@ class OAuth2WebServerFlow(Flow):
|
| "reauthenticating with approval_prompt='force'.")
|
| token_expiry = None
|
| if 'expires_in' in d:
|
| - token_expiry = (
|
| - datetime.datetime.utcnow() +
|
| - datetime.timedelta(seconds=int(d['expires_in'])))
|
| + delta = datetime.timedelta(seconds=int(d['expires_in']))
|
| + token_expiry = delta + _UTCNOW()
|
|
|
| extracted_id_token = None
|
| if 'id_token' in d:
|
|
|