Merge pull request #398 from dhermes/consolidate-service-accounts-v3
Implemented p12 support in ServiceAccountCredentials.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 = 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']
|
||||
)
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user