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
This commit is contained in:
dlorenc
2016-01-19 12:46:48 -08:00
committed by dlorenc
parent 8f8df1f597
commit 8df4379bd8
3 changed files with 104 additions and 8 deletions

View File

@@ -198,6 +198,13 @@ class MemoryCache(object):
self.cache.pop(key, None) 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): class Credentials(object):
"""Base class for all Credentials objects. """Base class for all Credentials objects.
@@ -208,7 +215,7 @@ class Credentials(object):
JSON string as input and returns an instantiated 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): def authorize(self, http):
"""Take an httplib2.Http instance (or equivalent) and authorizes it. """Take an httplib2.Http instance (or equivalent) and authorizes it.
@@ -265,9 +272,7 @@ class Credentials(object):
for member in strip: for member in strip:
if member in d: if member in d:
del d[member] del d[member]
if (d.get('token_expiry') and d['token_expiry'] = _parse_expiry(d.get('token_expiry'))
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. # Add in information we will need later to reconsistitue this instance.
d['_class'] = t.__name__ d['_class'] = t.__name__
d['_module'] = t.__module__ d['_module'] = t.__module__
@@ -285,7 +290,7 @@ class Credentials(object):
string, a JSON representation of this instance, suitable to pass to string, a JSON representation of this instance, suitable to pass to
from_json(). from_json().
""" """
return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) return self._to_json(self.NON_SERIALIZED_MEMBERS)
@classmethod @classmethod
def new_from_json(cls, s): def new_from_json(cls, s):
@@ -699,9 +704,6 @@ class OAuth2Credentials(Credentials):
self._retrieve_scopes(http.request) self._retrieve_scopes(http.request)
return self.scopes return self.scopes
def to_json(self):
return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
@classmethod @classmethod
def from_json(cls, s): def from_json(cls, s):
"""Instantiate a Credentials object from a JSON description of it. """Instantiate a Credentials object from a JSON description of it.
@@ -1180,6 +1182,11 @@ class GoogleCredentials(OAuth2Credentials):
print(response) print(response)
""" """
NON_SERIALIZED_MEMBERS = (
frozenset(['_private_key']) |
OAuth2Credentials.NON_SERIALIZED_MEMBERS)
def __init__(self, access_token, client_id, client_secret, refresh_token, def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, token_expiry, token_uri, user_agent,
revoke_uri=GOOGLE_REVOKE_URI): revoke_uri=GOOGLE_REVOKE_URI):
@@ -1222,6 +1229,32 @@ class GoogleCredentials(OAuth2Credentials):
""" """
return self 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 @property
def serialization_data(self): def serialization_data(self):
"""Get the fields and values identifying the current credentials.""" """Get the fields and values identifying the current credentials."""

View File

@@ -18,6 +18,8 @@ This credentials class is implemented on top of rsa library.
""" """
import base64 import base64
import datetime
import json
import time import time
from pyasn1.codec.ber import decoder from pyasn1.codec.ber import decoder
@@ -27,10 +29,12 @@ import rsa
from oauth2client import GOOGLE_REVOKE_URI from oauth2client import GOOGLE_REVOKE_URI
from oauth2client import GOOGLE_TOKEN_URI from oauth2client import GOOGLE_TOKEN_URI
from oauth2client._helpers import _json_encode from oauth2client._helpers import _json_encode
from oauth2client._helpers import _from_bytes
from oauth2client._helpers import _to_bytes from oauth2client._helpers import _to_bytes
from oauth2client._helpers import _urlsafe_b64encode from oauth2client._helpers import _urlsafe_b64encode
from oauth2client import util from oauth2client import util
from oauth2client.client import AssertionCredentials from oauth2client.client import AssertionCredentials
from oauth2client.client import EXPIRY_FORMAT
class _ServiceAccountCredentials(AssertionCredentials): class _ServiceAccountCredentials(AssertionCredentials):
@@ -38,6 +42,11 @@ class _ServiceAccountCredentials(AssertionCredentials):
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 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, def __init__(self, service_account_id, service_account_email,
private_key_id, private_key_pkcs8_text, scopes, private_key_id, private_key_pkcs8_text, scopes,
user_agent=None, token_uri=GOOGLE_TOKEN_URI, user_agent=None, token_uri=GOOGLE_TOKEN_URI,
@@ -108,6 +117,25 @@ class _ServiceAccountCredentials(AssertionCredentials):
'private_key': self._private_key_pkcs8_text '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): def create_scoped_required(self):
return not self._scopes return not self._scopes

View File

@@ -618,6 +618,35 @@ class GoogleCredentialsTests(unittest2.TestCase):
self.get_a_google_credentials_object().from_stream, self.get_a_google_credentials_object().from_stream,
credentials_file) 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): class DummyDeleteStorage(Storage):
delete_called = False delete_called = False
@@ -774,6 +803,12 @@ class BasicCredentialsTests(unittest2.TestCase):
instance = OAuth2Credentials.from_json(json.dumps(data)) instance = OAuth2Credentials.from_json(json.dumps(data))
self.assertTrue(isinstance(instance, OAuth2Credentials)) 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): def test_unicode_header_checks(self):
access_token = u'foo' access_token = u'foo'
client_id = u'some_client_id' client_id = u'some_client_id'