Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(36)

Unified Diff: client/third_party/oauth2client/client.py

Issue 1768993002: Update oauth2client to v2.0.1 and googleapiclient to v1.5.0. Base URL: git@github.com:luci/luci-py.git@master
Patch Set: . Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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:

Powered by Google App Engine
This is Rietveld 408576698