From 8df4379bd802e22ff31506bcfe90e177df4192b0 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Tue, 19 Jan 2016 12:46:48 -0800 Subject: [PATCH] Add to/from json methods to Credentials classes This adds to_json and from_json methods to GoogleCredentials and _ServiceAccountCredentials classes. This allows them to be serialized by multistore_file. Resolves: #384 --- oauth2client/client.py | 49 +++++++++++++++++++++++++++------ oauth2client/service_account.py | 28 +++++++++++++++++++ tests/test_client.py | 35 +++++++++++++++++++++++ 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/oauth2client/client.py b/oauth2client/client.py index 15c4731..de386ae 100644 --- a/oauth2client/client.py +++ b/oauth2client/client.py @@ -198,6 +198,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. @@ -208,7 +215,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. @@ -265,9 +272,7 @@ class Credentials(object): 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) + d['token_expiry'] = _parse_expiry(d.get('token_expiry')) # Add in information we will need later to reconsistitue this instance. d['_class'] = t.__name__ d['_module'] = t.__module__ @@ -285,7 +290,7 @@ 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): @@ -699,9 +704,6 @@ 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): """Instantiate a Credentials object from a JSON description of it. @@ -1180,6 +1182,11 @@ class GoogleCredentials(OAuth2Credentials): print(response) """ + NON_SERIALIZED_MEMBERS = ( + frozenset(['_private_key']) | + OAuth2Credentials.NON_SERIALIZED_MEMBERS) + + def __init__(self, access_token, client_id, client_secret, refresh_token, token_expiry, token_uri, user_agent, revoke_uri=GOOGLE_REVOKE_URI): @@ -1222,6 +1229,32 @@ class GoogleCredentials(OAuth2Credentials): """ return self + @classmethod + def from_json(cls, s): + # 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(s)) + + # 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(s) + + 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.""" diff --git a/oauth2client/service_account.py b/oauth2client/service_account.py index 8d3dc65..46ed04a 100644 --- a/oauth2client/service_account.py +++ b/oauth2client/service_account.py @@ -18,6 +18,8 @@ This credentials class is implemented on top of rsa library. """ import base64 +import datetime +import json import time from pyasn1.codec.ber import decoder @@ -27,10 +29,12 @@ import rsa from oauth2client import GOOGLE_REVOKE_URI from oauth2client import GOOGLE_TOKEN_URI from oauth2client._helpers import _json_encode +from oauth2client._helpers import _from_bytes from oauth2client._helpers import _to_bytes from oauth2client._helpers import _urlsafe_b64encode from oauth2client import util from oauth2client.client import AssertionCredentials +from oauth2client.client import EXPIRY_FORMAT class _ServiceAccountCredentials(AssertionCredentials): @@ -38,6 +42,11 @@ class _ServiceAccountCredentials(AssertionCredentials): MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds + NON_SERIALIZED_MEMBERS = ( + frozenset(['_private_key']) | + AssertionCredentials.NON_SERIALIZED_MEMBERS) + + def __init__(self, service_account_id, service_account_email, private_key_id, private_key_pkcs8_text, scopes, user_agent=None, token_uri=GOOGLE_TOKEN_URI, @@ -108,6 +117,25 @@ class _ServiceAccountCredentials(AssertionCredentials): 'private_key': self._private_key_pkcs8_text } + @classmethod + def from_json(cls, s): + data = json.loads(_from_bytes(s)) + + credentials = cls( + service_account_id=data['_service_account_id'], + service_account_email=data['_service_account_email'], + private_key_id=data['_private_key_id'], + private_key_pkcs8_text=data['_private_key_pkcs8_text'], + scopes=[], + user_agent=data['_user_agent']) + credentials.invalid = data['invalid'] + credentials.access_token = data['access_token'] + token_expiry = data.get('token_expiry', None) + if token_expiry is not None: + credentials.token_expiry = datetime.datetime.strptime( + token_expiry, EXPIRY_FORMAT) + return credentials + def create_scoped_required(self): return not self._scopes diff --git a/tests/test_client.py b/tests/test_client.py index ef9d633..131e72e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -618,6 +618,35 @@ class GoogleCredentialsTests(unittest2.TestCase): self.get_a_google_credentials_object().from_stream, credentials_file) + def test_to_from_json_authorized_user(self): + credentials_file = datafile( + os.path.join('gcloud', 'application_default_credentials_authorized_user.json')) + creds = GoogleCredentials.from_stream(credentials_file) + json = creds.to_json() + creds2 = GoogleCredentials.from_json(json) + + self.assertEqual(creds.__dict__, creds2.__dict__) + + def test_to_from_json_service_account(self): + self.maxDiff=None + credentials_file = datafile( + os.path.join('gcloud', _WELL_KNOWN_CREDENTIALS_FILE)) + creds = GoogleCredentials.from_stream(credentials_file) + + json = creds.to_json() + creds2 = GoogleCredentials.from_json(json) + + self.assertEqual(creds.__dict__, creds2.__dict__) + + def test_parse_expiry(self): + dt = datetime.datetime(2016, 1, 1) + parsed_expiry = client._parse_expiry(dt) + self.assertEqual('2016-01-01T00:00:00Z', parsed_expiry) + + def test_bad_expiry(self): + dt = object() + parsed_expiry = client._parse_expiry(dt) + self.assertEqual(None, parsed_expiry) class DummyDeleteStorage(Storage): delete_called = False @@ -774,6 +803,12 @@ class BasicCredentialsTests(unittest2.TestCase): instance = OAuth2Credentials.from_json(json.dumps(data)) self.assertTrue(isinstance(instance, OAuth2Credentials)) + def test_from_json_bad_token_expiry(self): + data = json.loads(self.credentials.to_json()) + data['token_expiry'] = 'foobar' + instance = OAuth2Credentials.from_json(json.dumps(data)) + self.assertTrue(isinstance(instance, OAuth2Credentials)) + def test_unicode_header_checks(self): access_token = u'foo' client_id = u'some_client_id'