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

Adding common sign_blob() service account types. #421

Merged
merged 1 commit into from
Feb 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions oauth2client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,18 @@ 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('This method is abstract.')


def _RequireCryptoOrDie():
"""Ensure we have a crypto library, or throw CryptoUnavailableError.
Expand Down
29 changes: 29 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,34 @@ 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 comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


Implements abstract method
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.

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.

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
73 changes: 71 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 All @@ -45,6 +48,30 @@
"""


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:
tuple, A pair where the first entry is an optional response (from a
failed request) and the second is service account email found (as
a string).
"""
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 None, content
else:
return response, content

This comment was marked as spam.



class AppAssertionCredentials(AssertionCredentials):
"""Credentials object for Compute Engine Assertion Grants

Expand Down Expand Up @@ -78,6 +105,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 +151,44 @@ 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

This comment was marked as spam.

This comment was marked as spam.

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.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

"""
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

This comment was marked as spam.

This comment was marked as spam.

of the default service account.

Returns:
string, The email associated with the Google Compute Engine
service account.

Raises:
AttributeError, if the email can not be retrieved from the Google
Compute Engine metadata service.
"""
if self._service_account_email is None:
failure, email = _get_service_account_email()
if failure is None:
self._service_account_email = email
else:
raise AttributeError('Failed to retrieve the email from the '
'Google Compute Engine metadata service',
failure, email)
return self._service_account_email
17 changes: 17 additions & 0 deletions oauth2client/service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,27 @@ def _generate_assertion(self):
key_id=self._private_key_id)

def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).

This comment was marked as spam.

This comment was marked as spam.


Implements abstract method
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.

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 = 'new-value@appspot.gserviceaccount.com'
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 = 'existing@appspot.gserviceaccount.com'
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
90 changes: 87 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__ = 'jcgregorio@google.com (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,49 @@ 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=(None, 'retrieved@email.com'))
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[1])
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 = 'existing@email.com'
credentials._service_account_email = acct_name
self.assertEqual(credentials.service_account_email, acct_name)
get_email.assert_not_called()

@mock.patch('oauth2client.contrib.gce._get_service_account_email')
def test_service_account_email_failure(self, get_email):
# Set-up the mock.
bad_response = httplib2.Response({'status': http_client.NOT_FOUND})
content = b'bad-bytes-nothing-here'
get_email.return_value = (bad_response, content)
# Test the failure.
credentials = AppAssertionCredentials([])
self.assertIsNone(credentials._service_account_email)
with self.assertRaises(AttributeError) as exc_manager:
getattr(credentials, 'service_account_email')

error_msg = ('Failed to retrieve the email from the '
'Google Compute Engine metadata service')
self.assertEqual(
exc_manager.exception.args,
(error_msg, bad_response, content))
self.assertIsNone(credentials._service_account_email)
get_email.assert_called_once_with()

def test_get_access_token(self):
http = mock.MagicMock()
http.request = mock.MagicMock(
Expand Down Expand Up @@ -178,5 +224,43 @@ 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'1234567890@developer.gserviceaccount.com'
http_request.return_value = (
httplib2.Response({'status': http_client.OK}), acct_name)
result = _get_service_account_email(http_request)
self.assertEqual(result, (None, 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'1234567890@developer.gserviceaccount.com'
http_request.return_value = (
httplib2.Response({'status': http_client.OK}), acct_name)
result = _get_service_account_email()
self.assertEqual(result, (None, 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)
result = _get_service_account_email(http_request)

self.assertEqual(result, (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()
Loading