Tests for googlestorage_container.AuthenticationClient.

Author: Itamar Turner-Trauring <itamar@futurefoundries.com>
This commit is contained in:
Itamar Turner-Trauring 2013-03-04 10:53:45 -05:00 committed by Daira Hopwood
parent d42b232e6a
commit 818648cbf5
2 changed files with 156 additions and 9 deletions

View File

@ -6,7 +6,7 @@ http://code.google.com/p/google-api-python-client/downloads/list
import httplib2 import httplib2
from twisted.internet.defer import DeferredLock from twisted.internet.defer import DeferredLock, maybeDeferred
from twisted.internet.threads import deferToThread from twisted.internet.threads import deferToThread
from oauth2client.client import SignedJwtAssertionCredentials from oauth2client.client import SignedJwtAssertionCredentials
@ -34,29 +34,48 @@ class AuthenticationClient(object):
more details. more details.
""" """
def __init__(self, account_name, private_key, private_key_password='notasecret'): def __init__(self, account_name, private_key, private_key_password='notasecret',
self.credentials = SignedJwtAssertionCredentials( _credentialsClass=SignedJwtAssertionCredentials,
_deferToThread=deferToThread):
# Google ships pkcs12 private keys encrypted with "notasecret" as the
# password. In order for automated running to work we'd need to
# include the password in the config file, so it adds no extra
# security even if someone chooses a different password. So it's seems
# simplest to hardcode it for now and it'll work with unmodified
# private keys issued by Google.
self._credentials = _credentialsClass(
account_name, private_key, account_name, private_key,
"https://www.googleapis.com/auth/devstorage.read_write", "https://www.googleapis.com/auth/devstorage.read_write",
private_key_password = private_key_password, private_key_password = private_key_password,
) )
self._deferToThread = _deferToThread
self._need_first_auth = True self._need_first_auth = True
self._lock = DeferredLock() self._lock = DeferredLock()
# Get initial token:
self._refresh_if_necessary(force=True)
def _refresh_if_necessary(self, force=False):
"""
Get a new authorization token, if necessary.
"""
def run():
if force or self._credentials.access_token_expired:
# Generally using a task-specific thread pool is better than using
# the reactor one. However, this particular call will only run
# once an hour, so it's not likely to tie up all the threads.
return self._deferToThread(self._credentials.refresh, httplib2.Http())
return self._lock.run(run)
def get_authorization_header(self): def get_authorization_header(self):
""" """
Return a Deferred that fires with the value to use for the Return a Deferred that fires with the value to use for the
Authorization header in HTTP requests. Authorization header in HTTP requests.
""" """
def refreshIfNecessary(): d = self._refresh_if_necessary()
if self._need_first_auth or self.credentials.access_token_expired:
self._need_first_auth = False
return deferToThread(self.credentials.refresh, httplib2.Http())
d = self._lock.run(refreshIfNecessary)
def refreshed(ignore): def refreshed(ignore):
headers = {} headers = {}
self.credentials.apply(headers) self._credentials.apply(headers)
return headers['Authorization'] return headers['Authorization']
d.addCallback(refreshed) d.addCallback(refreshed)
return d return d

View File

@ -2,6 +2,7 @@
import time, os.path, platform, re, simplejson, struct, itertools, urllib import time, os.path, platform, re, simplejson, struct, itertools, urllib
from collections import deque from collections import deque
from cStringIO import StringIO from cStringIO import StringIO
import thread
import mock import mock
from twisted.trial import unittest from twisted.trial import unittest
@ -33,6 +34,7 @@ from allmydata.storage.backends.cloud.cloud_common import CloudError, CloudServi
from allmydata.storage.backends.cloud import mock_cloud, cloud_common from allmydata.storage.backends.cloud import mock_cloud, cloud_common
from allmydata.storage.backends.cloud.mock_cloud import MockContainer from allmydata.storage.backends.cloud.mock_cloud import MockContainer
from allmydata.storage.backends.cloud.openstack import openstack_container from allmydata.storage.backends.cloud.openstack import openstack_container
from allmydata.storage.backends.cloud.googlestorage import googlestorage_container
from allmydata.storage.bucket import BucketWriter, BucketReader from allmydata.storage.bucket import BucketWriter, BucketReader
from allmydata.storage.common import DataTooLargeError, storage_index_to_dir from allmydata.storage.common import DataTooLargeError, storage_index_to_dir
from allmydata.storage.leasedb import SHARETYPE_IMMUTABLE, SHARETYPE_MUTABLE from allmydata.storage.leasedb import SHARETYPE_IMMUTABLE, SHARETYPE_MUTABLE
@ -629,6 +631,132 @@ class OpenStackCloudBackend(ServiceParentMixin, WorkdirMixin, ShouldFailMixin, u
return d return d
class GoogleStorageBackend(ShouldFailMixin, unittest.TestCase):
"""
Tests for the Google Storage API backend.
All code references in docstrings/comments are to classes/functions in
allmydata.storage.backends.cloud.googlestorage.googlestorage_container
unless noted otherwise.
"""
def test_authentication_credentials(self):
"""
AuthenticationClient.get_authorization_header() initializes a
SignedJwtAssertionCredentials with the correct parameters.
"""
# Somewhat fragile tests, but better than nothing.
auth = googlestorage_container.AuthenticationClient("u@example.com", "xxx123")
self.assertEqual(auth._credentials.service_account_name, "u@example.com")
self.assertEqual(auth._credentials.private_key, "xxx123".encode("base64").strip())
def test_authentication_initial(self):
"""
When AuthenticationClient() is created, it refreshes its access token.
"""
from oauth2client.client import SignedJwtAssertionCredentials
auth = googlestorage_container.AuthenticationClient(
"u@example.com", "xxx123",
_credentialsClass=mock.create_autospec(SignedJwtAssertionCredentials),
_deferToThread=defer.maybeDeferred)
self.assertEqual(auth._credentials.refresh.call_count, 1)
def test_authentication_expired(self):
"""
AuthenticationClient.get_authorization_header() refreshes its
credentials if the access token has expired.
"""
from oauth2client.client import SignedJwtAssertionCredentials
auth = googlestorage_container.AuthenticationClient(
"u@example.com", "xxx123",
_credentialsClass=mock.create_autospec(SignedJwtAssertionCredentials),
_deferToThread=defer.maybeDeferred)
auth._credentials.apply = lambda d: d.__setitem__('Authorization', 'xxx')
auth._credentials.access_token_expired = True
auth.get_authorization_header()
self.assertEqual(auth._credentials.refresh.call_count, 2)
def test_authentication_no_refresh(self):
"""
AuthenticationClient.get_authorization_header() does not refresh its
credentials if the access token has not expired.
"""
from oauth2client.client import SignedJwtAssertionCredentials
auth = googlestorage_container.AuthenticationClient(
"u@example.com", "xxx123",
_credentialsClass=mock.create_autospec(SignedJwtAssertionCredentials),
_deferToThread=defer.maybeDeferred)
auth._credentials.apply = lambda d: d.__setitem__('Authorization', 'xxx')
auth._credentials.access_token_expired = False
auth.get_authorization_header()
self.assertEqual(auth._credentials.refresh.call_count, 1)
def test_authentication_header(self):
"""
AuthenticationClient.get_authorization_header() returns a value to be
used for the Authorization header.
"""
from oauth2client.client import SignedJwtAssertionCredentials
class NoNetworkCreds(SignedJwtAssertionCredentials):
def refresh(self, http):
self.access_token = "xxx"
auth = googlestorage_container.AuthenticationClient(
"u@example.com", "xxx123",
_credentialsClass=NoNetworkCreds,
_deferToThread=defer.maybeDeferred)
result = []
auth.get_authorization_header().addCallback(result.append)
self.assertEqual(result, ["Bearer xxx"])
def test_authentication_one_refresh(self):
"""
AuthenticationClient._refresh_if_necessary() only runs one refresh
request at a time.
"""
# The second call shouldn't happen until the first Deferred fires!
results = [defer.Deferred(), defer.succeed(None)]
first = results[0]
def fakeDeferToThread(f, *args):
return results.pop(0)
from oauth2client.client import SignedJwtAssertionCredentials
auth = googlestorage_container.AuthenticationClient(
"u@example.com", "xxx123",
_credentialsClass=mock.create_autospec(SignedJwtAssertionCredentials),
_deferToThread=fakeDeferToThread)
# Initial authorization call happens...
self.assertEqual(len(results), 1)
# ... and still isn't finished, so next one doesn't run yet:
auth._refresh_if_necessary(force=True)
self.assertEqual(len(results), 1)
# When first one finishes, second one can run:
first.callback(None)
self.assertEqual(len(results), 0)
def test_authentication_refresh_call(self):
"""
AuthenticationClient._refresh_if_necessary() runs the
authentication refresh in a thread, since it blocks, with a
httplib2.Http instance.
"""
from httplib2 import Http
from oauth2client.client import SignedJwtAssertionCredentials
class NoNetworkCreds(SignedJwtAssertionCredentials):
def refresh(cred_self, http):
cred_self.access_token = "xxx"
self.assertIsInstance(http, Http)
self.thread = thread.get_ident()
auth = googlestorage_container.AuthenticationClient(
"u@example.com", "xxx123",
_credentialsClass=NoNetworkCreds)
def gotResult(ignore):
self.assertNotEqual(thread.get_ident(), self.thread)
return auth.get_authorization_header().addCallback(gotResult)
class ServerMixin: class ServerMixin:
def allocate(self, account, storage_index, sharenums, size, canary=None): def allocate(self, account, storage_index, sharenums, size, canary=None):
# These secrets are not used, but clients still provide them. # These secrets are not used, but clients still provide them.