Skip to content
This repository has been archived by the owner on Nov 5, 2019. It is now read-only.

Commit

Permalink
Adding common sign_blob() service account types.
Browse files Browse the repository at this point in the history
Also adding service_account_email() property.
  • Loading branch information
dhermes committed Feb 20, 2016
1 parent 1f18216 commit eae8ba6
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 6 deletions.
17 changes: 17 additions & 0 deletions oauth2client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,23 @@ def _revoke(self, http_request):
"""
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

@property
def service_account_email(self):
"""Get the email for the current service account."""
raise NotImplementedError


def _RequireCryptoOrDie():
"""Ensure we have a crypto library, or throw CryptoUnavailableError.
Expand Down
30 changes: 30 additions & 0 deletions oauth2client/contrib/appengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def __init__(self, scope, **kwargs):
self.scope = util.scopes_to_string(scope)
self._kwargs = kwargs
self.service_account_id = kwargs.get('service_account_id', None)
self._service_account_email = None

# Assertion type is no longer used, but still in the
# parent class signature.
Expand Down Expand Up @@ -210,6 +211,35 @@ def create_scoped_required(self):
def create_scoped(self, scopes):
return AppAssertionCredentials(scopes, **self._kwargs)

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.
"""
return app_identity.sign_blob(blob)

@property
def service_account_email(self):
"""Get the email for the current service account.
Uses the ``app_identity`` service to retrieve the service
account "name" (which is the email) and then caches it on
the current object for future use.
Returns:
string, The email associated with the Google App Engine
service account.
"""
if self._service_account_email is None:
self._service_account_email = (
app_identity.get_service_account_name())
return self._service_account_email


class FlowProperty(db.Property):
"""App Engine datastore Property for Flow.
Expand Down
65 changes: 63 additions & 2 deletions oauth2client/contrib/gce.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import warnings

import httplib2
from six.moves import http_client
from six.moves import urllib

Expand All @@ -35,8 +36,10 @@
logger = logging.getLogger(__name__)

# URI Template for the endpoint that returns access_tokens.
META = ('http://metadata.google.internal/computeMetadata/v1/instance/'
'service-accounts/default/token')
_METADATA_ROOT = ('http://metadata.google.internal/computeMetadata/v1/'
'instance/service-accounts/default/')
META = _METADATA_ROOT + 'token'
_DEFAULT_EMAIL_METADATA = _METADATA_ROOT + 'email'
_SCOPES_WARNING = """\
You have requested explicit scopes to be used with a GCE service account.
Using this argument will have no effect on the actual scopes for tokens
Expand Down Expand Up @@ -78,6 +81,7 @@ def __init__(self, scope='', **kwargs):
# Assertion type is no longer used, but still in the
# parent class signature.
super(AppAssertionCredentials, self).__init__(None)
self._service_account_email = None

@classmethod
def from_json(cls, json_data):
Expand Down Expand Up @@ -123,3 +127,60 @@ def create_scoped_required(self):

def create_scoped(self, scopes):
return AppAssertionCredentials(scopes, **self.kwargs)

def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).
This method is provided to support a common interface, but
the actual key used for a Google Compute Engine service account
is not available, so it can't be used to sign content.
Args:
blob: bytes, Message to be signed.
Raises:
NotImplementedError, always.
"""
raise NotImplementedError(
'Compute Engine service accounts cannot sign blobs')

@property
def service_account_email(self):
"""Get the email for the current service account.
Uses the Google Compute Engine metadata service to retrieve the email
of the default service account and then caches it on
the current object for future use.
Returns:
string, The email associated with the Google Compute Engine
service account.
"""
if self._service_account_email is None:
self._service_account_email = _get_service_account_email()
return self._service_account_email


def _get_service_account_email(http_request=None):
"""Get the GCE service account email from the current environment.
Args:
http_request: callable, (Optional) a callable that matches the method
signature of httplib2.Http.request, used to make
the request to the metadata service.
Returns:
string, The service account email found.
Raises:
RuntimeError, If the request to the metadata service fails.
"""
if http_request is None:
http_request = httplib2.Http().request
response, content = http_request(
_DEFAULT_EMAIL_METADATA, headers={'Metadata-Flavor': 'Google'})
if response.status == http_client.OK:
content = _from_bytes(content)
return content
else:
raise RuntimeError(response, content)
14 changes: 14 additions & 0 deletions oauth2client/service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,24 @@ def _generate_assertion(self):
key_id=self._private_key_id)

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.
"""
return self._private_key_id, self._signer.sign(blob)

@property
def service_account_email(self):
"""Get the email for the current service account.
Returns:
string, The email associated with the service account.
"""
return self._service_account_email

@property
Expand Down
60 changes: 59 additions & 1 deletion tests/contrib/test_appengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,29 @@ class TestAppAssertionCredentials(unittest.TestCase):

class AppIdentityStubImpl(apiproxy_stub.APIProxyStub):

def __init__(self):
def __init__(self, key_name=None, sig_bytes=None,
svc_acct=None):
super(TestAppAssertionCredentials.AppIdentityStubImpl,
self).__init__('app_identity_service')
self._key_name = key_name
self._sig_bytes = sig_bytes
self._sign_calls = []
self._svc_acct = svc_acct
self._get_acct_name_calls = 0

def _Dynamic_GetAccessToken(self, request, response):
response.set_access_token('a_token_123')
response.set_expiration_time(time.time() + 1800)

def _Dynamic_SignForApp(self, request, response):
response.set_key_name(self._key_name)
response.set_signature_bytes(self._sig_bytes)
self._sign_calls.append(request.bytes_to_sign())

def _Dynamic_GetServiceAccountName(self, request, response):
response.set_service_account_name(self._svc_acct)
self._get_acct_name_calls += 1

class ErroringAppIdentityStubImpl(apiproxy_stub.APIProxyStub):

def __init__(self):
Expand Down Expand Up @@ -210,6 +225,49 @@ def test_create_scoped(self):
self.assertTrue(isinstance(new_credentials, AppAssertionCredentials))
self.assertEqual('dummy_scope', new_credentials.scope)

def test_sign_blob(self):
key_name = b'1234567890'
sig_bytes = b'himom'
app_identity_stub = self.AppIdentityStubImpl(
key_name=key_name, sig_bytes=sig_bytes)
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
app_identity_stub)
credentials = AppAssertionCredentials([])
to_sign = b'blob'
self.assertEqual(app_identity_stub._sign_calls, [])
result = credentials.sign_blob(to_sign)
self.assertEqual(result, (key_name, sig_bytes))
self.assertEqual(app_identity_stub._sign_calls, [to_sign])

def test_service_account_email(self):
acct_name = '[email protected]'
app_identity_stub = self.AppIdentityStubImpl(svc_acct=acct_name)
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
app_identity_stub)

credentials = AppAssertionCredentials([])
self.assertIsNone(credentials._service_account_email)
self.assertEqual(app_identity_stub._get_acct_name_calls, 0)
self.assertEqual(credentials.service_account_email, acct_name)
self.assertIsNotNone(credentials._service_account_email)
self.assertEqual(app_identity_stub._get_acct_name_calls, 1)

def test_service_account_email_already_set(self):
acct_name = '[email protected]'
credentials = AppAssertionCredentials([])
credentials._service_account_email = acct_name

app_identity_stub = self.AppIdentityStubImpl(svc_acct=acct_name)
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
app_identity_stub)

self.assertEqual(app_identity_stub._get_acct_name_calls, 0)
self.assertEqual(credentials.service_account_email, acct_name)
self.assertEqual(app_identity_stub._get_acct_name_calls, 0)

def test_get_access_token(self):
app_identity_stub = self.AppIdentityStubImpl()
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
Expand Down
71 changes: 68 additions & 3 deletions tests/contrib/test_gce.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,25 @@
import json
from six.moves import http_client
from six.moves import urllib
import unittest
import unittest2

import mock

import httplib2
from oauth2client._helpers import _to_bytes
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import Credentials
from oauth2client.client import save_to_well_known_file
from oauth2client.contrib.gce import _DEFAULT_EMAIL_METADATA
from oauth2client.contrib.gce import _get_service_account_email
from oauth2client.contrib.gce import _SCOPES_WARNING
from oauth2client.contrib.gce import AppAssertionCredentials


__author__ = '[email protected] (Joe Gregorio)'


class AppAssertionCredentialsTests(unittest.TestCase):
class AppAssertionCredentialsTests(unittest2.TestCase):

def test_constructor(self):
credentials = AppAssertionCredentials(foo='bar')
Expand Down Expand Up @@ -150,6 +153,29 @@ def test_create_scoped(self, warn_mock):
self.assertEqual('dummy_scope', new_credentials.scope)
warn_mock.assert_called_once_with(_SCOPES_WARNING)

def test_sign_blob_not_implemented(self):
credentials = AppAssertionCredentials([])
with self.assertRaises(NotImplementedError):
credentials.sign_blob(b'blob')

@mock.patch('oauth2client.contrib.gce._get_service_account_email',
return_value='[email protected]')
def test_service_account_email(self, get_email):
credentials = AppAssertionCredentials([])
self.assertIsNone(credentials._service_account_email)
self.assertEqual(credentials.service_account_email,
get_email.return_value)
self.assertIsNotNone(credentials._service_account_email)
get_email.assert_called_once_with()

@mock.patch('oauth2client.contrib.gce._get_service_account_email')
def test_service_account_email_already_set(self, get_email):
credentials = AppAssertionCredentials([])
acct_name = '[email protected]'
credentials._service_account_email = acct_name
self.assertEqual(credentials.service_account_email, acct_name)
get_email.assert_not_called()

def test_get_access_token(self):
http = mock.MagicMock()
http.request = mock.MagicMock(
Expand Down Expand Up @@ -178,5 +204,44 @@ def test_save_to_well_known_file(self):
os.path.isdir = ORIGINAL_ISDIR


class Test__get_service_account_email(unittest2.TestCase):

def test_success(self):
http_request = mock.MagicMock()
acct_name = b'[email protected]'
http_request.return_value = (
httplib2.Response({'status': http_client.OK}), acct_name)
result = _get_service_account_email(http_request)
self.assertEqual(result, acct_name.decode('utf-8'))
http_request.assert_called_once_with(
_DEFAULT_EMAIL_METADATA,
headers={'Metadata-Flavor': 'Google'})

@mock.patch.object(httplib2.Http, 'request')
def test_success_default_http(self, http_request):
# Don't make _from_bytes() work too hard.
acct_name = u'[email protected]'
http_request.return_value = (
httplib2.Response({'status': http_client.OK}), acct_name)
result = _get_service_account_email()
self.assertEqual(result, acct_name)
http_request.assert_called_once_with(
_DEFAULT_EMAIL_METADATA,
headers={'Metadata-Flavor': 'Google'})

def test_failure(self):
http_request = mock.MagicMock()
response = httplib2.Response({'status': http_client.NOT_FOUND})
content = b'Not found'
http_request.return_value = (response, content)
with self.assertRaises(RuntimeError) as exc_manager:
_get_service_account_email(http_request)

self.assertEqual(exc_manager.exception.args, (response, content))
http_request.assert_called_once_with(
_DEFAULT_EMAIL_METADATA,
headers={'Metadata-Flavor': 'Google'})


if __name__ == '__main__': # pragma: NO COVER
unittest.main()
unittest2.main()
10 changes: 10 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,16 @@ def test_token_revoke_failure(self):
self, '400', revoke_raise=True,
valid_bool_value=False, token_attr='access_token')

def test_sign_blob_abstract(self):
credentials = AssertionCredentials(None)
with self.assertRaises(NotImplementedError):
credentials.sign_blob(b'blob')

def test_service_account_email_abstract(self):
credentials = AssertionCredentials(None)
with self.assertRaises(NotImplementedError):
getattr(credentials, 'service_account_email')


class UpdateQueryParamsTest(unittest2.TestCase):
def test_update_query_params_no_params(self):
Expand Down

0 comments on commit eae8ba6

Please sign in to comment.