Merge pull request #398 from dhermes/consolidate-service-accounts-v3

Implemented p12 support in ServiceAccountCredentials.
This commit is contained in:
Nathaniel Manista
2016-02-05 06:24:47 -08:00
4 changed files with 147 additions and 26 deletions

View File

@@ -256,32 +256,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]
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__
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.

View File

@@ -15,6 +15,7 @@
"""oauth2client Service account credentials class."""
import base64
import copy
import datetime
import json
import time
@@ -31,6 +32,18 @@ from oauth2client.client import SERVICE_ACCOUNT
from oauth2client import crypt
_PASSWORD_DEFAULT = 'notasecret'
_PKCS12_KEY = '_private_key_pkcs12'
_PKCS12_ERROR = r"""
This library only implements PKCS#12 support via the pyOpenSSL library.
Either install pyOpenSSL, or please convert the .p12 file
to .pem format:
$ cat key.p12 | \
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
> openssl rsa > key.pem
"""
class ServiceAccountCredentials(AssertionCredentials):
"""Service Account credential for OAuth 2.0 signed JWT grants.
@@ -38,6 +51,7 @@ class ServiceAccountCredentials(AssertionCredentials):
* JSON keyfile (typically contains a PKCS8 key stored as
PEM text)
* ``.p12`` key (stores PKCS12 key and certificate)
Makes an assertion to server using a signed JWT assertion in exchange
for an access token.
@@ -74,6 +88,8 @@ class ServiceAccountCredentials(AssertionCredentials):
# Can be over-ridden by factory constructors. Used for
# serialization/deserialization purposes.
_private_key_pkcs8_pem = None
_private_key_pkcs12 = None
_private_key_password = None
def __init__(self,
service_account_email,
@@ -95,6 +111,31 @@ class ServiceAccountCredentials(AssertionCredentials):
self._user_agent = user_agent
self._kwargs = kwargs
def _to_json(self, strip, to_serialize=None):
"""Utility function that creates JSON repr. of a credentials object.
Over-ride is needed since PKCS#12 keys will not in general be JSON
serializable.
Args:
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().
"""
if to_serialize is None:
to_serialize = copy.copy(self.__dict__)
pkcs12_val = to_serialize.get(_PKCS12_KEY)
if pkcs12_val is not None:
to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
return super(ServiceAccountCredentials, self)._to_json(
strip, to_serialize=to_serialize)
@classmethod
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes):
"""Helper for factory constructors from JSON keyfile.
@@ -174,6 +215,39 @@ class ServiceAccountCredentials(AssertionCredentials):
"""
return cls._from_parsed_json_keyfile(keyfile_dict, scopes)
@classmethod
def from_p12_keyfile(cls, service_account_email, filename,
private_key_password=None, scopes=''):
"""Factory constructor from JSON keyfile.
Args:
service_account_email: string, The email associated with the
service account.
filename: string, The location of the PKCS#12 keyfile.
private_key_password: string, (Optional) Password for PKCS#12
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile.
Raises:
NotImplementedError if pyOpenSSL is not installed / not the
active crypto library.
"""
with open(filename, 'rb') as file_obj:
private_key_pkcs12 = file_obj.read()
if private_key_password is None:
private_key_password = _PASSWORD_DEFAULT
signer = crypt.Signer.from_string(private_key_pkcs12,
private_key_password)
credentials = cls(service_account_email, signer, scopes=scopes)
credentials._private_key_pkcs12 = private_key_pkcs12
credentials._private_key_password = private_key_password
return credentials
def _generate_assertion(self):
"""Generate the assertion that will be used in the request."""
now = int(time.time())
@@ -197,6 +271,7 @@ class ServiceAccountCredentials(AssertionCredentials):
@property
def serialization_data(self):
# NOTE: This is only useful for JSON keyfile.
return {
'type': 'service_account',
'client_email': self._service_account_email,
@@ -221,8 +296,21 @@ class ServiceAccountCredentials(AssertionCredentials):
if not isinstance(json_data, dict):
json_data = json.loads(_from_bytes(json_data))
private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
private_key_pkcs8_pem = None
pkcs12_val = json_data.get(_PKCS12_KEY)
password = None
if pkcs12_val is None:
private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
else:
# NOTE: This assumes that private_key_pkcs8_pem is not also
# in the serialized data. This would be very incorrect
# state.
pkcs12_val = base64.b64decode(pkcs12_val)
password = json_data['_private_key_password']
signer = crypt.Signer.from_string(private_key_pkcs12,
private_key_password)
credentials = cls(
json_data['_service_account_email'],
signer,
@@ -232,7 +320,12 @@ class ServiceAccountCredentials(AssertionCredentials):
user_agent=json_data['_user_agent'],
**json_data['_kwargs']
)
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
if private_key_pkcs8_pem is not None:
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
if pkcs12_val is not None:
credentials._private_key_pkcs12 = pkcs12_val
if password is not None:
credentials._private_key_password = password
credentials.invalid = json_data['invalid']
credentials.access_token = json_data['access_token']
credentials.token_uri = json_data['token_uri']
@@ -256,4 +349,7 @@ class ServiceAccountCredentials(AssertionCredentials):
**self._kwargs)
result.token_uri = self.token_uri
result.revoke_uri = self.revoke_uri
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
result._private_key_pkcs12 = self._private_key_pkcs12
result._private_key_password = self._private_key_password
return result

View File

@@ -58,15 +58,8 @@ def run_json():
def run_p12():
with open(P12_KEY_PATH, 'rb') as file_object:
private_key_contents = file_object.read()
credentials = client.SignedJwtAssertionCredentials(
service_account_name=P12_KEY_EMAIL,
private_key=private_key_contents,
scope=SCOPE,
)
credentials = ServiceAccountCredentials.from_p12_keyfile(
P12_KEY_EMAIL, P12_KEY_PATH, scopes=SCOPE)
_check_user_info(credentials, P12_KEY_EMAIL)

View File

@@ -129,6 +129,33 @@ class ServiceAccountCredentialsTests(unittest2.TestCase):
with self.assertRaises(KeyError):
self._from_json_keyfile_name_helper(payload)
def _from_p12_keyfile_helper(self, private_key_password=None, scopes=''):
service_account_email = 'name@email.com'
filename = data_filename('privatekey.p12')
with open(filename, 'rb') as file_obj:
key_contents = file_obj.read()
creds = ServiceAccountCredentials.from_p12_keyfile(
service_account_email, filename,
private_key_password=private_key_password,
scopes=scopes)
self.assertIsInstance(creds, ServiceAccountCredentials)
self.assertEqual(creds.client_id, None)
self.assertEqual(creds._service_account_email, service_account_email)
self.assertEqual(creds._private_key_id, None)
self.assertEqual(creds._private_key_pkcs8_pem, None)
self.assertEqual(creds._private_key_pkcs12, key_contents)
if private_key_password is not None:
self.assertEqual(creds._private_key_password, private_key_password)
self.assertEqual(creds._scopes, ' '.join(scopes))
def test_from_p12_keyfile_defaults(self):
self._from_p12_keyfile_helper()
def test_from_p12_keyfile_explicit(self):
password = 'notasecret'
self._from_p12_keyfile_helper(private_key_password=password,
scopes=['foo', 'bar'])
def test_create_scoped_required_without_scopes(self):
self.assertTrue(self.credentials.create_scoped_required())