From 53f4b9574db0ea18303c27f8021a334be2e59695 Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 13:09:11 -0700 Subject: [PATCH 01/15] Factor metadata interface into a separate module --- oauth2client/contrib/gce.py | 70 ++---------- oauth2client/contrib/metadata.py | 146 ++++++++++++++++++++++++ tests/contrib/test_gce.py | 190 ++++--------------------------- tests/contrib/test_metadata.py | 170 +++++++++++++++++++++++++++ 4 files changed, 349 insertions(+), 227 deletions(-) create mode 100644 oauth2client/contrib/metadata.py create mode 100644 tests/contrib/test_metadata.py diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index 6542008e0..6a73feedc 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -21,25 +21,19 @@ import logging import warnings -import httplib2 -from six.moves import http_client -from six.moves import urllib - from oauth2client._helpers import _from_bytes from oauth2client import util -from oauth2client.client import HttpAccessTokenRefreshError from oauth2client.client import AssertionCredentials +from oauth2client.contrib.metadata import MetadataServer __author__ = 'jcgregorio@google.com (Joe Gregorio)' logger = logging.getLogger(__name__) -# URI Template for the endpoint that returns access_tokens. -_METADATA_ROOT = ('http://metadata.google.internal/computeMetadata/v1/' - 'instance/service-accounts/default/') -META = _METADATA_ROOT + 'token' -_DEFAULT_EMAIL_METADATA = _METADATA_ROOT + 'email' +# Backwards Compat +META = ('http://metadata.google.internal/computeMetadata/v1/' + 'instance/service-accounts/default/token') _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 @@ -48,30 +42,6 @@ """ -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 - - class AppAssertionCredentials(AssertionCredentials): """Credentials object for Compute Engine Assertion Grants @@ -86,7 +56,7 @@ class AppAssertionCredentials(AssertionCredentials): """ @util.positional(2) - def __init__(self, scope='', **kwargs): + def __init__(self, scope='', metadata_server=None, **kwargs): """Constructor for AppAssertionCredentials Args: @@ -102,10 +72,11 @@ def __init__(self, scope='', **kwargs): self.scope = util.scopes_to_string(scope) self.kwargs = kwargs + self._metadata = metadata_server or MetadataServer() + # 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): @@ -125,21 +96,8 @@ def _refresh(self, http_request): Raises: HttpAccessTokenRefreshError: When the refresh fails. """ - response, content = http_request( - META, headers={'Metadata-Flavor': 'Google'}) - content = _from_bytes(content) - if response.status == http_client.OK: - try: - token_content = json.loads(content) - except Exception as e: - raise HttpAccessTokenRefreshError(str(e), - status=response.status) - self.access_token = token_content['access_token'] - else: - if response.status == http_client.NOT_FOUND: - content += (' This can occur if a VM was created' - ' with no service account or scopes.') - raise HttpAccessTokenRefreshError(content, status=response.status) + self.access_token, self.token_expiry = self._metadata.get_token( + http_request=http_request) @property def serialization_data(self): @@ -183,12 +141,4 @@ def service_account_email(self): 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 + return self._metadata.get_service_account_info()['email'] diff --git a/oauth2client/contrib/metadata.py b/oauth2client/contrib/metadata.py new file mode 100644 index 000000000..8af8ae80f --- /dev/null +++ b/oauth2client/contrib/metadata.py @@ -0,0 +1,146 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Thin wrapper class for talking to the GCE Metadata Server.""" +import datetime +import httplib2 +import json + +from six.moves import http_client + +from oauth2client._helpers import _from_bytes +from oauth2client.client import _UTCNOW +from oauth2client.client import HttpAccessTokenRefreshError + +class NestedDict(dict): + """Stores a dict and allows setting and retrieving + values by path (list of keys).""" + + def get_path(self, path): + leaf = self + for key in path: + leaf = leaf.get(key) + if leaf is None: + return None + return leaf + + def set_path(self, path, value): + leaf = self + for key in path[:-1]: + leaf = leaf.setdefault(key, {}) + leaf[path[-1]] = value + + +class MetadataServerHttpError(Exception): + """Error for Http failures originating from the Metadata Server""" + + +class MetadataServer: + """handles requests to and from the metadata server, + and caches requests by default""" + + def __init__(self, + client=None, + cache=None, + root='http://metadata.google.internal/computeMetadata/v1/'): + self._client = client or httplib2.Http() + self._root = root + self.cache = cache or NestedDict() + + def _make_request(self, path, recursive=True, http_request=None): + if path is None: + path = [] + + if not http_request: + http_request = self._client.request + + r_string = '/?recursive=true' if recursive else '' + full_path = self._root + '/'.join(path) + r_string + response, content = http_request( + full_path, + headers={'Metadata-Flavor': 'Google'} + ) + if response.status == http_client.OK: + decoded = _from_bytes(content) + if response['content-type'] == 'application/json': + return json.loads(decoded) + else: + return decoded + else: + msg = ( + 'Failed to retrieve {path} from the Google Compute Engine' + 'metadata service. Response:\n{error}' + ).format(path=full_path, error=response) + raise MetadataServerHttpError(msg) + + def get(self, path, use_cache=True, recursive=True, http_request=None): + """ Retrieve a value from the metadata server. + :param path: Path on the metadata server to fetch from + :param use_cache: Use a cached value (if available) and update the cache (if not) + :param recursive: True if this is not a leaf + :param http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make + the refresh request. + :return: The value from the metadata server (String if recursive=False, dict otherwise) + """ + + if use_cache: + cached_value = self.cache.get_path(path) + if cached_value is not None: + return cached_value + value = self._make_request(path, recursive=recursive, http_request=http_request) + if use_cache: + self.cache.set_path(path, value) + return value + + def get_service_account_info(self, service_account='default', http_request=None): + """ Get information about a service account from the metadata server. + :param service_account: a service account email. Left blank information for + the default service account of current compute engine instance will be looked up. + :param http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make + the refresh request. + :return: A dictionary with information about the specified service account. + """ + return self.get( + ['instance', 'service-accounts', service_account], + use_cache=True, + recursive=True, + http_request=http_request + ) + + def get_token(self, service_account='default', http_request=None): + """Fetch an OAuth access token from the metadata server + :param service_account: a service account email. Left blank information for + the default service account of current compute engine instance will be looked up. + :param http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make + the refresh request. + :return: + """ + try: + token_json = self.get( + ['instance', 'service-accounts', service_account, 'token'], + use_cache=False, + recursive=False, + http_request=http_request + ) + except MetadataServerHttpError as failed_fetch: + raise HttpAccessTokenRefreshError(str(failed_fetch)) + + token_expiry = _UTCNOW() + datetime.timedelta( + seconds=token_json['expires_in']) + return token_json['access_token'], token_expiry + + diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py index 48da97632..eebd0a552 100644 --- a/tests/contrib/test_gce.py +++ b/tests/contrib/test_gce.py @@ -14,20 +14,13 @@ """Unit tests for oauth2client.contrib.gce.""" -import json -from six.moves import http_client -from six.moves import urllib +import datetime 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 @@ -60,74 +53,19 @@ def test_to_json_and_from_json(self): self.assertEqual(credentials.access_token, credentials_from_json.access_token) - def _refresh_success_helper(self, bytes_response=False): - access_token = u'this-is-a-token' - return_val = json.dumps({u'access_token': access_token}) - if bytes_response: - return_val = _to_bytes(return_val) - http = mock.MagicMock() - http.request = mock.MagicMock( - return_value=(mock.Mock(status=http_client.OK), return_val)) - - credentials = AppAssertionCredentials() - self.assertEquals(None, credentials.access_token) - credentials.refresh(http) - self.assertEquals(access_token, credentials.access_token) - - base_metadata_uri = ( - 'http://metadata.google.internal/computeMetadata/v1/instance/' - 'service-accounts/default/token') - http.request.assert_called_once_with( - base_metadata_uri, headers={'Metadata-Flavor': 'Google'}) - - def test_refresh_success(self): - self._refresh_success_helper(bytes_response=False) - - def test_refresh_success_bytes(self): - self._refresh_success_helper(bytes_response=True) - - def test_refresh_failure_bad_json(self): - http = mock.MagicMock() - content = '{BADJSON' - http.request = mock.MagicMock( - return_value=(mock.Mock(status=http_client.OK), content)) - - credentials = AppAssertionCredentials() - self.assertRaises(AccessTokenRefreshError, credentials.refresh, http) - - def test_refresh_failure_400(self): - http = mock.MagicMock() - content = '{}' - http.request = mock.MagicMock( - return_value=(mock.Mock(status=http_client.BAD_REQUEST), content)) - - credentials = AppAssertionCredentials() - exception_caught = None - try: - credentials.refresh(http) - except AccessTokenRefreshError as exc: - exception_caught = exc - - self.assertNotEqual(exception_caught, None) - self.assertEqual(str(exception_caught), content) - - def test_refresh_failure_404(self): - http = mock.MagicMock() - content = '{}' - http.request = mock.MagicMock( - return_value=(mock.Mock(status=http_client.NOT_FOUND), content)) - - credentials = AppAssertionCredentials() - exception_caught = None - try: - credentials.refresh(http) - except AccessTokenRefreshError as exc: - exception_caught = exc - - self.assertNotEqual(exception_caught, None) - expanded_content = content + (' This can occur if a VM was created' - ' with no service account or scopes.') - self.assertEqual(str(exception_caught), expanded_content) + @mock.patch('oauth2client.contrib.metadata.Metadata', + return_value=mock.MagicMock( + get_access_token=mock.Mock( + side_effect=[('A', 0), ('B', datetime.datetime.max)]))) + def test_refresh_token(self, metadata): + credentials = AppAssertionCredentials(metadata_server=metadata) + self.assertIsNone(credentials.access_token) + credentials.get_access_token() + self.assertEqual(credentials.access_token, 'A') + self.assertTrue(credentials.access_token_expired) + credentials.get_access_token() + self.assertEqual(credentials.access_token, 'B') + self.assertFalse(credentials.access_token_expired) def test_serialization_data(self): credentials = AppAssertionCredentials() @@ -158,59 +96,15 @@ def test_sign_blob_not_implemented(self): 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( - return_value=(mock.Mock(status=http_client.OK), - '{"access_token": "this-is-a-token"}')) - - credentials = AppAssertionCredentials() - token = credentials.get_access_token(http=http) - self.assertEqual('this-is-a-token', token.access_token) - self.assertEqual(None, token.expires_in) - - http.request.assert_called_once_with( - 'http://metadata.google.internal/computeMetadata/v1/instance/' - 'service-accounts/default/token', - headers={'Metadata-Flavor': 'Google'}) + @mock.patch('oauth2client.contrib.metadata.Metadata', + return_value=mock.MagicMock( + get_service_account_info=mock.MagicMock( + return_value={'email': 'a@example.com'}))) + def test_service_account_email(self, metadata): + credentials = AppAssertionCredentials(metadata_server=metadata) + # Assert that service account isn't pre-fetched + metadata.assert_not_called() + self.assertEqual(credentials.service_account_email, 'a@example.com') def test_save_to_well_known_file(self): import os @@ -224,43 +118,5 @@ 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 unittest2.main() diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py new file mode 100644 index 000000000..e3f196d2c --- /dev/null +++ b/tests/contrib/test_metadata.py @@ -0,0 +1,170 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for oauth2client.contrib.metadata""" +import datetime +import json +import mock +import unittest2 + +from oauth2client.client import HttpAccessTokenRefreshError +from oauth2client.contrib.metadata import MetadataServer +from oauth2client.contrib.metadata import MetadataServerHttpError +from oauth2client.contrib.metadata import NestedDict + +PATH = ['a', 'b'] +DATA = {'foo': 'bar'} +EXPECTED_ARGS = ['http://metadata.google.internal/computeMetadata/v1/a/b/?recursive=true'] +EXPECTED_KWARGS = dict(headers={'Metadata-Flavor': 'Google'}) + + +def get_json_request_mock(): + return mock.MagicMock(return_value=( + {'status': http_client.OK, 'content-type': 'application/json'}, + json.dumps(DATA).encode('utf-8') + )) + +def get_string_request_mock(): + return mock.MagicMock(return_value=( + {'status': http_client.OK, 'content-type': 'text/html'}, + '

Hello World!

'.encode('utf-8') + )) + + +def get_error_request_mock(): + return mock.MagicMock(return_value=( + {'status': http_client.NOT_FOUND, 'content-type': 'text/html'}, + '

Error

'.encode('utf-8') + )) + + +class TestNestedDict(unittest2.TestCase): + + def test_get_path(self): + self.assertEqual(NestedDict(a={'b': {'c': 'd'}}).get_path(['a','b','c']), 'd') + + def test_set_path(self): + test_dict = NestedDict(a={'b': {'c': 'd'}}) + test_dict.set_path(['a','b', 'e'], 'f') + self.assertEqual(test_dict, {'a': {'b': {'c': 'd', 'e': 'f'}}}) + + +class TestMetadata(unittest2.TestCase): + + def test_constructor(self): + cache = NestedDict(a='b', c={'d': 'e'}) + self.assertEqual(MetadataServer(cache=cache).cache, cache) + + def test_make_request_success_json(self): + http_request = get_json_request_mock() + metadata = MetadataServer() + self.assertEqual( + metadata._make_request(PATH, http_request=http_request), + DATA + ) + http_request.assert_called_once_with( + ) + + def test_make_request_success_string(self): + http_request = get_string_request_mock() + metadata = MetadataServer() + self.assertEqual( + metadata._make_request(PATH, http_request=http_request), + '

Hello World!

' + ) + http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) + + def test_make_request_failure(self): + http_request = get_error_request_mock() + metadata = MetadataServer() + with self.assertRaises(MetadataServerHttpError): + metadata._make_request(PATH, http_request=http_request) + + http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) + + def test_get_cached_present(self): + cache = NestedDict(a={'b': DATA}) + http_request = get_json_request_mock() + metadata = MetadataServer(cache=cache) + + result = metadata.get(PATH, http_request=http_request) + self.assertEqual(result, DATA) + http_request.assert_not_called() + + def test_get_cached_absent(self): + http_request = get_json_request_mock() + metadata = MetadataServer() + self.assertEqual( + metadata.get(PATH, http_request=http_request), + DATA + ) + http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) + self.assertTrue(PATH[0] in metadata.cache) + self.assertTrue(PATH[1] in metadata.cache[PATH[0]]) + self.assertEqual(metadata.cache['a']['b'], DATA) + + def test_uncached(self): + http_request = get_json_request_mock() + cache = NestedDict() + metadata = MetadataServer(cache=cache) + self.assertEqual( + metadata.get(PATH, use_cache=False, http_request=http_request), + DATA + ) + http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) + self.assertTrue('a' not in metadata.cache) + + @mock.patch('oauth2client.client._UTCNOW', return_value=datetime.datetime.min) + def test_get_token_success(self): + http_request = mock.MagicMock( + return_value=( + {'status': http_client.OK, 'content-type': 'application/json'}, + json.dumps({'access_token': 'a', 'expires_in': 100}).encode('utf-8') + ) + ) + metadata = MetadataServer() + token, expiry = metadata.get_token(http_request=http_request) + self.assertEqual(token, 'a') + self.assertEqual(expiry, datetime.datetime.min + datetime.timedelta(seconds=100)) + http_request.assert_called_once_with( + 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', + **EXPECTED_KWARGS + ) + + def test_get_token_failed_fetch(self): + http_request = mock.MagicMock( + return_value=( + {'status': http_client.NOT_FOUND, 'content-type': 'application/json'}, + json.dumps({'access_token': 'a', 'expires_in': 100}).encode('utf-8') + ) + ) + metadata = MetadataServer() + with self.assertRaises(HttpAccessTokenRefreshError): + metadata.get_token(http_request=http_request) + + http_request.assert_called_once_with( + 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', + **EXPECTED_KWARGS + ) + + def test_service_account_info(self): + http_request = get_json_request_mock() + metadata = MetadataServer() + info = metadata.get_service_account_info(http_request=http_request) + self.assertEqual(info, DATA) + + http_request.assert_called_once_with( + 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/?recursive=true', + **EXPECTED_KWARGS + ) From 83049e4c300a32658d3c6ff3cdc824c13ad62aaa Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 13:40:54 -0700 Subject: [PATCH 02/15] Remove serialization fix typo --- oauth2client/contrib/gce.py | 4 ++++ oauth2client/contrib/metadata.py | 2 -- tests/contrib/test_gce.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index 6a73feedc..e74bbb4c5 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -21,6 +21,7 @@ import logging import warnings + from oauth2client._helpers import _from_bytes from oauth2client import util from oauth2client.client import AssertionCredentials @@ -55,6 +56,9 @@ class AppAssertionCredentials(AssertionCredentials): information to generate and refresh its own access tokens. """ + NON_SERIALIZED_MEMBERS = AssertionCredentials.NON_SERIALIZED_MEMBERS.extend( + ['_metadata', 'kwargs']) + @util.positional(2) def __init__(self, scope='', metadata_server=None, **kwargs): """Constructor for AppAssertionCredentials diff --git a/oauth2client/contrib/metadata.py b/oauth2client/contrib/metadata.py index 8af8ae80f..da8bd465b 100644 --- a/oauth2client/contrib/metadata.py +++ b/oauth2client/contrib/metadata.py @@ -142,5 +142,3 @@ def get_token(self, service_account='default', http_request=None): token_expiry = _UTCNOW() + datetime.timedelta( seconds=token_json['expires_in']) return token_json['access_token'], token_expiry - - diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py index eebd0a552..0d2acc9b2 100644 --- a/tests/contrib/test_gce.py +++ b/tests/contrib/test_gce.py @@ -53,7 +53,7 @@ def test_to_json_and_from_json(self): self.assertEqual(credentials.access_token, credentials_from_json.access_token) - @mock.patch('oauth2client.contrib.metadata.Metadata', + @mock.patch('oauth2client.contrib.metadata.MetadataServer', return_value=mock.MagicMock( get_access_token=mock.Mock( side_effect=[('A', 0), ('B', datetime.datetime.max)]))) @@ -96,7 +96,7 @@ def test_sign_blob_not_implemented(self): with self.assertRaises(NotImplementedError): credentials.sign_blob(b'blob') - @mock.patch('oauth2client.contrib.metadata.Metadata', + @mock.patch('oauth2client.contrib.metadata.MetadataServer', return_value=mock.MagicMock( get_service_account_info=mock.MagicMock( return_value={'email': 'a@example.com'}))) From fb05c41571db558e903b23a82ac4aee1ea58d8f2 Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 13:45:15 -0700 Subject: [PATCH 03/15] Fix bad frozenset usage added six to test deps --- oauth2client/contrib/gce.py | 4 ++-- tox.ini | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index e74bbb4c5..1555d5c41 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -56,8 +56,8 @@ class AppAssertionCredentials(AssertionCredentials): information to generate and refresh its own access tokens. """ - NON_SERIALIZED_MEMBERS = AssertionCredentials.NON_SERIALIZED_MEMBERS.extend( - ['_metadata', 'kwargs']) + NON_SERIALIZED_MEMBERS = frozenset( + AssertionCredentials.NON_SERIALIZED_MEMBERS | set(['_metadata', 'kwargs'])) @util.positional(2) def __init__(self, scope='', metadata_server=None, **kwargs): diff --git a/tox.ini b/tox.ini index f05051907..248705138 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ basedeps = mock>=1.3.0 nose flask unittest2 + six>=1.10 deps = {[testenv]basedeps} django keyring From 4b95966dfabfef888c3e6cd6c2a6c9d438d5b8a3 Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 15:03:19 -0700 Subject: [PATCH 04/15] Remove caching from metadata server --- oauth2client/contrib/gce.py | 18 ++-- oauth2client/contrib/metadata.py | 173 +++++++++++-------------------- tests/contrib/test_metadata.py | 77 +++----------- 3 files changed, 83 insertions(+), 185 deletions(-) diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index 1555d5c41..3085bf09b 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -25,7 +25,7 @@ from oauth2client._helpers import _from_bytes from oauth2client import util from oauth2client.client import AssertionCredentials -from oauth2client.contrib.metadata import MetadataServer +from oauth2client.contrib import metadata __author__ = 'jcgregorio@google.com (Joe Gregorio)' @@ -56,11 +56,8 @@ class AppAssertionCredentials(AssertionCredentials): information to generate and refresh its own access tokens. """ - NON_SERIALIZED_MEMBERS = frozenset( - AssertionCredentials.NON_SERIALIZED_MEMBERS | set(['_metadata', 'kwargs'])) - @util.positional(2) - def __init__(self, scope='', metadata_server=None, **kwargs): + def __init__(self, scope='', **kwargs): """Constructor for AppAssertionCredentials Args: @@ -76,12 +73,13 @@ def __init__(self, scope='', metadata_server=None, **kwargs): self.scope = util.scopes_to_string(scope) self.kwargs = kwargs - self._metadata = metadata_server or MetadataServer() - # Assertion type is no longer used, but still in the # parent class signature. super(AppAssertionCredentials, self).__init__(None) + # Cache until Metadata Server supports Cache-Control Header + self._service_account_email = None + @classmethod def from_json(cls, json_data): data = json.loads(_from_bytes(json_data)) @@ -100,7 +98,7 @@ def _refresh(self, http_request): Raises: HttpAccessTokenRefreshError: When the refresh fails. """ - self.access_token, self.token_expiry = self._metadata.get_token( + self.access_token, self.token_expiry = metadata.get_token( http_request=http_request) @property @@ -145,4 +143,6 @@ def service_account_email(self): AttributeError, if the email can not be retrieved from the Google Compute Engine metadata service. """ - return self._metadata.get_service_account_info()['email'] + if self._service_account_email is None: + self._service_account_email = metadata.get_service_account_info()['email'] + return self._service_account_email diff --git a/oauth2client/contrib/metadata.py b/oauth2client/contrib/metadata.py index da8bd465b..54cd62dbb 100644 --- a/oauth2client/contrib/metadata.py +++ b/oauth2client/contrib/metadata.py @@ -1,4 +1,4 @@ -# Copyright 2014 Google Inc. All rights reserved. +# Copyright 2016 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,122 +23,71 @@ from oauth2client.client import _UTCNOW from oauth2client.client import HttpAccessTokenRefreshError -class NestedDict(dict): - """Stores a dict and allows setting and retrieving - values by path (list of keys).""" +METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/' +METADATA_HEADERS = {'Metadata-Flavor': 'Google'} - def get_path(self, path): - leaf = self - for key in path: - leaf = leaf.get(key) - if leaf is None: - return None - return leaf - def set_path(self, path, value): - leaf = self - for key in path[:-1]: - leaf = leaf.setdefault(key, {}) - leaf[path[-1]] = value +def get(path, recursive=True, http_request=None, root=METADATA_ROOT): + if path is None: + path = [] + if not http_request: + http_request = httplib2.Http().request -class MetadataServerHttpError(Exception): - """Error for Http failures originating from the Metadata Server""" - - -class MetadataServer: - """handles requests to and from the metadata server, - and caches requests by default""" - - def __init__(self, - client=None, - cache=None, - root='http://metadata.google.internal/computeMetadata/v1/'): - self._client = client or httplib2.Http() - self._root = root - self.cache = cache or NestedDict() - - def _make_request(self, path, recursive=True, http_request=None): - if path is None: - path = [] - - if not http_request: - http_request = self._client.request - - r_string = '/?recursive=true' if recursive else '' - full_path = self._root + '/'.join(path) + r_string - response, content = http_request( - full_path, - headers={'Metadata-Flavor': 'Google'} - ) - if response.status == http_client.OK: - decoded = _from_bytes(content) - if response['content-type'] == 'application/json': - return json.loads(decoded) - else: - return decoded + r_string = '/?recursive=true' if recursive else '' + full_path = root + '/'.join(path) + r_string + response, content = http_request( + full_path, + headers=METADATA_HEADERS + ) + if response.status == http_client.OK: + decoded = _from_bytes(content) + if response['content-type'] == 'application/json': + return json.loads(decoded) else: - msg = ( - 'Failed to retrieve {path} from the Google Compute Engine' - 'metadata service. Response:\n{error}' - ).format(path=full_path, error=response) - raise MetadataServerHttpError(msg) - - def get(self, path, use_cache=True, recursive=True, http_request=None): - """ Retrieve a value from the metadata server. - :param path: Path on the metadata server to fetch from - :param use_cache: Use a cached value (if available) and update the cache (if not) - :param recursive: True if this is not a leaf - :param http_request: callable, a callable that matches the method - signature of httplib2.Http.request, used to make - the refresh request. - :return: The value from the metadata server (String if recursive=False, dict otherwise) - """ - - if use_cache: - cached_value = self.cache.get_path(path) - if cached_value is not None: - return cached_value - value = self._make_request(path, recursive=recursive, http_request=http_request) - if use_cache: - self.cache.set_path(path, value) - return value - - def get_service_account_info(self, service_account='default', http_request=None): - """ Get information about a service account from the metadata server. - :param service_account: a service account email. Left blank information for - the default service account of current compute engine instance will be looked up. - :param http_request: callable, a callable that matches the method - signature of httplib2.Http.request, used to make - the refresh request. - :return: A dictionary with information about the specified service account. - """ - return self.get( - ['instance', 'service-accounts', service_account], - use_cache=True, - recursive=True, + return decoded + else: + msg = ( + 'Failed to retrieve {path} from the Google Compute Engine' + 'metadata service. Response:\n{error}' + ).format(path=full_path, error=response) + raise httplib2.HttpLib2Error(msg) + + +def get_service_account_info(service_account='default', http_request=None): + """ Get information about a service account from the metadata server. + :param service_account: a service account email. Left blank information for + the default service account of current compute engine instance will be looked up. + :param http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make + the refresh request. + :return: A dictionary with information about the specified service account. + """ + return get( + ['instance', 'service-accounts', service_account], + recursive=True, + http_request=http_request + ) + + +def get_token(service_account='default', http_request=None): + """Fetch an OAuth access token from the metadata server + :param service_account: a service account email. Left blank information for + the default service account of current compute engine instance will be looked up. + :param http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make + the refresh request. + :return: + """ + try: + token_json = get( + ['instance', 'service-accounts', service_account, 'token'], + recursive=False, http_request=http_request ) + except httplib2.HttpLib2Error as failed_fetch: + raise HttpAccessTokenRefreshError(str(failed_fetch)) - def get_token(self, service_account='default', http_request=None): - """Fetch an OAuth access token from the metadata server - :param service_account: a service account email. Left blank information for - the default service account of current compute engine instance will be looked up. - :param http_request: callable, a callable that matches the method - signature of httplib2.Http.request, used to make - the refresh request. - :return: - """ - try: - token_json = self.get( - ['instance', 'service-accounts', service_account, 'token'], - use_cache=False, - recursive=False, - http_request=http_request - ) - except MetadataServerHttpError as failed_fetch: - raise HttpAccessTokenRefreshError(str(failed_fetch)) - - token_expiry = _UTCNOW() + datetime.timedelta( - seconds=token_json['expires_in']) - return token_json['access_token'], token_expiry + token_expiry = _UTCNOW() + datetime.timedelta( + seconds=token_json['expires_in']) + return token_json['access_token'], token_expiry diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py index e3f196d2c..0dbca4265 100644 --- a/tests/contrib/test_metadata.py +++ b/tests/contrib/test_metadata.py @@ -14,19 +14,20 @@ """Unit tests for oauth2client.contrib.metadata""" import datetime +import httplib2 import json import mock import unittest2 +from six.moves import http_client + from oauth2client.client import HttpAccessTokenRefreshError -from oauth2client.contrib.metadata import MetadataServer -from oauth2client.contrib.metadata import MetadataServerHttpError -from oauth2client.contrib.metadata import NestedDict +from oauth2client.contrib import metadata PATH = ['a', 'b'] DATA = {'foo': 'bar'} EXPECTED_ARGS = ['http://metadata.google.internal/computeMetadata/v1/a/b/?recursive=true'] -EXPECTED_KWARGS = dict(headers={'Metadata-Flavor': 'Google'}) +EXPECTED_KWARGS = metadata.METADATA_HEADERS def get_json_request_mock(): @@ -35,6 +36,7 @@ def get_json_request_mock(): json.dumps(DATA).encode('utf-8') )) + def get_string_request_mock(): return mock.MagicMock(return_value=( {'status': http_client.OK, 'content-type': 'text/html'}, @@ -49,81 +51,31 @@ def get_error_request_mock(): )) -class TestNestedDict(unittest2.TestCase): - - def test_get_path(self): - self.assertEqual(NestedDict(a={'b': {'c': 'd'}}).get_path(['a','b','c']), 'd') - - def test_set_path(self): - test_dict = NestedDict(a={'b': {'c': 'd'}}) - test_dict.set_path(['a','b', 'e'], 'f') - self.assertEqual(test_dict, {'a': {'b': {'c': 'd', 'e': 'f'}}}) - - class TestMetadata(unittest2.TestCase): - def test_constructor(self): - cache = NestedDict(a='b', c={'d': 'e'}) - self.assertEqual(MetadataServer(cache=cache).cache, cache) - - def test_make_request_success_json(self): + def test_get_success_json(self): http_request = get_json_request_mock() - metadata = MetadataServer() self.assertEqual( - metadata._make_request(PATH, http_request=http_request), + metadata.get(PATH, http_request=http_request), DATA ) http_request.assert_called_once_with( ) - def test_make_request_success_string(self): + def test_get_success_string(self): http_request = get_string_request_mock() - metadata = MetadataServer() self.assertEqual( - metadata._make_request(PATH, http_request=http_request), + metadata.get(PATH, http_request=http_request), '

Hello World!

' ) http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) - def test_make_request_failure(self): + def test_get_failure(self): http_request = get_error_request_mock() - metadata = MetadataServer() - with self.assertRaises(MetadataServerHttpError): - metadata._make_request(PATH, http_request=http_request) - - http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) - - def test_get_cached_present(self): - cache = NestedDict(a={'b': DATA}) - http_request = get_json_request_mock() - metadata = MetadataServer(cache=cache) + with self.assertRaises(httplib2.HttpLib2Error): + metadata.get(PATH, http_request=http_request) - result = metadata.get(PATH, http_request=http_request) - self.assertEqual(result, DATA) - http_request.assert_not_called() - - def test_get_cached_absent(self): - http_request = get_json_request_mock() - metadata = MetadataServer() - self.assertEqual( - metadata.get(PATH, http_request=http_request), - DATA - ) - http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) - self.assertTrue(PATH[0] in metadata.cache) - self.assertTrue(PATH[1] in metadata.cache[PATH[0]]) - self.assertEqual(metadata.cache['a']['b'], DATA) - - def test_uncached(self): - http_request = get_json_request_mock() - cache = NestedDict() - metadata = MetadataServer(cache=cache) - self.assertEqual( - metadata.get(PATH, use_cache=False, http_request=http_request), - DATA - ) http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) - self.assertTrue('a' not in metadata.cache) @mock.patch('oauth2client.client._UTCNOW', return_value=datetime.datetime.min) def test_get_token_success(self): @@ -133,7 +85,6 @@ def test_get_token_success(self): json.dumps({'access_token': 'a', 'expires_in': 100}).encode('utf-8') ) ) - metadata = MetadataServer() token, expiry = metadata.get_token(http_request=http_request) self.assertEqual(token, 'a') self.assertEqual(expiry, datetime.datetime.min + datetime.timedelta(seconds=100)) @@ -149,7 +100,6 @@ def test_get_token_failed_fetch(self): json.dumps({'access_token': 'a', 'expires_in': 100}).encode('utf-8') ) ) - metadata = MetadataServer() with self.assertRaises(HttpAccessTokenRefreshError): metadata.get_token(http_request=http_request) @@ -160,7 +110,6 @@ def test_get_token_failed_fetch(self): def test_service_account_info(self): http_request = get_json_request_mock() - metadata = MetadataServer() info = metadata.get_service_account_info(http_request=http_request) self.assertEqual(info, DATA) From 0f7607d4b87ac622642ddbd87086c53df63e1e63 Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 15:04:22 -0700 Subject: [PATCH 05/15] Clean up old META var --- oauth2client/contrib/gce.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index 3085bf09b..e55f875fd 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -32,9 +32,6 @@ logger = logging.getLogger(__name__) -# Backwards Compat -META = ('http://metadata.google.internal/computeMetadata/v1/' - 'instance/service-accounts/default/token') _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 From 653f7585657fb6f6757bf77e2e1f922e0a3212b4 Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 15:18:16 -0700 Subject: [PATCH 06/15] Test fixes --- tests/contrib/test_gce.py | 16 +++++++--------- tests/contrib/test_metadata.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py index 0d2acc9b2..8e7c63004 100644 --- a/tests/contrib/test_gce.py +++ b/tests/contrib/test_gce.py @@ -53,12 +53,11 @@ def test_to_json_and_from_json(self): self.assertEqual(credentials.access_token, credentials_from_json.access_token) - @mock.patch('oauth2client.contrib.metadata.MetadataServer', - return_value=mock.MagicMock( - get_access_token=mock.Mock( - side_effect=[('A', 0), ('B', datetime.datetime.max)]))) + @mock.patch('oauth2client.contrib.metadata.get_access_token', + return_value=mock.Mock( + side_effect=[('A', 0), ('B', datetime.datetime.max)])) def test_refresh_token(self, metadata): - credentials = AppAssertionCredentials(metadata_server=metadata) + credentials = AppAssertionCredentials() self.assertIsNone(credentials.access_token) credentials.get_access_token() self.assertEqual(credentials.access_token, 'A') @@ -96,12 +95,11 @@ def test_sign_blob_not_implemented(self): with self.assertRaises(NotImplementedError): credentials.sign_blob(b'blob') - @mock.patch('oauth2client.contrib.metadata.MetadataServer', + @mock.patch('oauth2client.contrib.metadata.get_service_account_info', return_value=mock.MagicMock( - get_service_account_info=mock.MagicMock( - return_value={'email': 'a@example.com'}))) + return_value={'email': 'a@example.com'})) def test_service_account_email(self, metadata): - credentials = AppAssertionCredentials(metadata_server=metadata) + credentials = AppAssertionCredentials() # Assert that service account isn't pre-fetched metadata.assert_not_called() self.assertEqual(credentials.service_account_email, 'a@example.com') diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py index 0dbca4265..9a2913979 100644 --- a/tests/contrib/test_metadata.py +++ b/tests/contrib/test_metadata.py @@ -32,21 +32,27 @@ def get_json_request_mock(): return mock.MagicMock(return_value=( - {'status': http_client.OK, 'content-type': 'application/json'}, + httplib2.Response( + {'status': http_client.OK, 'content-type': 'application/json'} + ), json.dumps(DATA).encode('utf-8') )) def get_string_request_mock(): return mock.MagicMock(return_value=( - {'status': http_client.OK, 'content-type': 'text/html'}, + httplib2.Response( + {'status': http_client.OK, 'content-type': 'text/html'} + ), '

Hello World!

'.encode('utf-8') )) def get_error_request_mock(): return mock.MagicMock(return_value=( - {'status': http_client.NOT_FOUND, 'content-type': 'text/html'}, + httplib2.Response( + {'status': http_client.NOT_FOUND, 'content-type': 'text/html'} + ), '

Error

'.encode('utf-8') )) @@ -78,7 +84,7 @@ def test_get_failure(self): http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) @mock.patch('oauth2client.client._UTCNOW', return_value=datetime.datetime.min) - def test_get_token_success(self): + def test_get_token_success(self, now): http_request = mock.MagicMock( return_value=( {'status': http_client.OK, 'content-type': 'application/json'}, @@ -92,6 +98,7 @@ def test_get_token_success(self): 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', **EXPECTED_KWARGS ) + now.assert_called_once_with() def test_get_token_failed_fetch(self): http_request = mock.MagicMock( From 055fce6d4df08cb07f4469c4d266eac3094a2ef0 Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 15:36:50 -0700 Subject: [PATCH 07/15] More test fixes --- tests/contrib/test_gce.py | 2 +- tests/contrib/test_metadata.py | 55 ++++++++++++---------------------- 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py index 8e7c63004..f9f374c00 100644 --- a/tests/contrib/test_gce.py +++ b/tests/contrib/test_gce.py @@ -53,7 +53,7 @@ def test_to_json_and_from_json(self): self.assertEqual(credentials.access_token, credentials_from_json.access_token) - @mock.patch('oauth2client.contrib.metadata.get_access_token', + @mock.patch('oauth2client.contrib.metadata.get_token', return_value=mock.Mock( side_effect=[('A', 0), ('B', datetime.datetime.max)])) def test_refresh_token(self, metadata): diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py index 9a2913979..852b96a6b 100644 --- a/tests/contrib/test_metadata.py +++ b/tests/contrib/test_metadata.py @@ -30,37 +30,20 @@ EXPECTED_KWARGS = metadata.METADATA_HEADERS -def get_json_request_mock(): +def request_mock(status, content_type, content): return mock.MagicMock(return_value=( httplib2.Response( - {'status': http_client.OK, 'content-type': 'application/json'} + {'status': status, 'content-type': content_type} ), - json.dumps(DATA).encode('utf-8') - )) - - -def get_string_request_mock(): - return mock.MagicMock(return_value=( - httplib2.Response( - {'status': http_client.OK, 'content-type': 'text/html'} - ), - '

Hello World!

'.encode('utf-8') - )) - - -def get_error_request_mock(): - return mock.MagicMock(return_value=( - httplib2.Response( - {'status': http_client.NOT_FOUND, 'content-type': 'text/html'} - ), - '

Error

'.encode('utf-8') + content.encode('utf-8') )) class TestMetadata(unittest2.TestCase): def test_get_success_json(self): - http_request = get_json_request_mock() + http_request = request_mock( + http_client.OK, 'application/json', json.dumps(DATA)) self.assertEqual( metadata.get(PATH, http_request=http_request), DATA @@ -69,7 +52,8 @@ def test_get_success_json(self): ) def test_get_success_string(self): - http_request = get_string_request_mock() + http_request = request_mock( + http_client.OK, 'text/html', '

Hello World!

') self.assertEqual( metadata.get(PATH, http_request=http_request), '

Hello World!

' @@ -77,7 +61,8 @@ def test_get_success_string(self): http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) def test_get_failure(self): - http_request = get_error_request_mock() + http_request = request_mock( + http_client.NOT_FOUND, 'text/html', '

Error

') with self.assertRaises(httplib2.HttpLib2Error): metadata.get(PATH, http_request=http_request) @@ -85,11 +70,10 @@ def test_get_failure(self): @mock.patch('oauth2client.client._UTCNOW', return_value=datetime.datetime.min) def test_get_token_success(self, now): - http_request = mock.MagicMock( - return_value=( - {'status': http_client.OK, 'content-type': 'application/json'}, - json.dumps({'access_token': 'a', 'expires_in': 100}).encode('utf-8') - ) + http_request = request_mock( + http_client.NOT_FOUND, + 'application/json', + json.dumps({'access_token': 'a', 'expires_in': 100}) ) token, expiry = metadata.get_token(http_request=http_request) self.assertEqual(token, 'a') @@ -101,11 +85,10 @@ def test_get_token_success(self, now): now.assert_called_once_with() def test_get_token_failed_fetch(self): - http_request = mock.MagicMock( - return_value=( - {'status': http_client.NOT_FOUND, 'content-type': 'application/json'}, - json.dumps({'access_token': 'a', 'expires_in': 100}).encode('utf-8') - ) + http_request = request_mock( + http_client.NOT_FOUND, + 'application/json', + json.dumps({'access_token': 'a', 'expires_in': 100}) ) with self.assertRaises(HttpAccessTokenRefreshError): metadata.get_token(http_request=http_request) @@ -116,10 +99,10 @@ def test_get_token_failed_fetch(self): ) def test_service_account_info(self): - http_request = get_json_request_mock() + http_request = request_mock( + http_client.OK, 'application/json', json.dumps(DATA)) info = metadata.get_service_account_info(http_request=http_request) self.assertEqual(info, DATA) - http_request.assert_called_once_with( 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/?recursive=true', **EXPECTED_KWARGS From 396d227b429eb31617f4fa24f338e9b0a73ba4bc Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 15:52:10 -0700 Subject: [PATCH 08/15] More silly test fixes --- tests/contrib/test_gce.py | 7 +++---- tests/contrib/test_metadata.py | 9 ++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py index f9f374c00..b8bb289b2 100644 --- a/tests/contrib/test_gce.py +++ b/tests/contrib/test_gce.py @@ -54,8 +54,8 @@ def test_to_json_and_from_json(self): credentials_from_json.access_token) @mock.patch('oauth2client.contrib.metadata.get_token', - return_value=mock.Mock( - side_effect=[('A', 0), ('B', datetime.datetime.max)])) + side_effect=[('A', datetime.datetime.min), + ('B', datetime.datetime.max)]) def test_refresh_token(self, metadata): credentials = AppAssertionCredentials() self.assertIsNone(credentials.access_token) @@ -96,8 +96,7 @@ def test_sign_blob_not_implemented(self): credentials.sign_blob(b'blob') @mock.patch('oauth2client.contrib.metadata.get_service_account_info', - return_value=mock.MagicMock( - return_value={'email': 'a@example.com'})) + return_value={'email': 'a@example.com'}) def test_service_account_email(self, metadata): credentials = AppAssertionCredentials() # Assert that service account isn't pre-fetched diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py index 852b96a6b..fd0ac7902 100644 --- a/tests/contrib/test_metadata.py +++ b/tests/contrib/test_metadata.py @@ -27,7 +27,7 @@ PATH = ['a', 'b'] DATA = {'foo': 'bar'} EXPECTED_ARGS = ['http://metadata.google.internal/computeMetadata/v1/a/b/?recursive=true'] -EXPECTED_KWARGS = metadata.METADATA_HEADERS +EXPECTED_KWARGS = dict(headers=metadata.METADATA_HEADERS) def request_mock(status, content_type, content): @@ -48,8 +48,7 @@ def test_get_success_json(self): metadata.get(PATH, http_request=http_request), DATA ) - http_request.assert_called_once_with( - ) + http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) def test_get_success_string(self): http_request = request_mock( @@ -68,10 +67,10 @@ def test_get_failure(self): http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) - @mock.patch('oauth2client.client._UTCNOW', return_value=datetime.datetime.min) + @mock.patch('oauth2client.contrib.metadata._UTCNOW', return_value=datetime.datetime.min) def test_get_token_success(self, now): http_request = request_mock( - http_client.NOT_FOUND, + http_client.OK, 'application/json', json.dumps({'access_token': 'a', 'expires_in': 100}) ) From 893c0e47c8a47f340993432949f81997157129b9 Mon Sep 17 00:00:00 2001 From: elibixby Date: Wed, 8 Jun 2016 16:21:16 -0700 Subject: [PATCH 09/15] Docs updates --- docs/source/oauth2client.contrib.metadata.rst | 7 +++++++ docs/source/oauth2client.contrib.rst | 1 + oauth2client/contrib/metadata.py | 12 ++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 docs/source/oauth2client.contrib.metadata.rst diff --git a/docs/source/oauth2client.contrib.metadata.rst b/docs/source/oauth2client.contrib.metadata.rst new file mode 100644 index 000000000..974e04d00 --- /dev/null +++ b/docs/source/oauth2client.contrib.metadata.rst @@ -0,0 +1,7 @@ +oauth2client.contrib.metadata module +==================================== + +.. automodule:: oauth2client.contrib.metadata + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/oauth2client.contrib.rst b/docs/source/oauth2client.contrib.rst index 0caaa2977..c6eb406df 100644 --- a/docs/source/oauth2client.contrib.rst +++ b/docs/source/oauth2client.contrib.rst @@ -21,6 +21,7 @@ Submodules oauth2client.contrib.gce oauth2client.contrib.keyring_storage oauth2client.contrib.locked_file + oauth2client.contrib.metadata oauth2client.contrib.multistore_file oauth2client.contrib.xsrfutil diff --git a/oauth2client/contrib/metadata.py b/oauth2client/contrib/metadata.py index 54cd62dbb..6496dc02e 100644 --- a/oauth2client/contrib/metadata.py +++ b/oauth2client/contrib/metadata.py @@ -57,10 +57,10 @@ def get(path, recursive=True, http_request=None, root=METADATA_ROOT): def get_service_account_info(service_account='default', http_request=None): """ Get information about a service account from the metadata server. :param service_account: a service account email. Left blank information for - the default service account of current compute engine instance will be looked up. + the default service account of current compute engine instance will be looked up. :param http_request: callable, a callable that matches the method - signature of httplib2.Http.request, used to make - the refresh request. + signature of httplib2.Http.request, used to make + the refresh request. :return: A dictionary with information about the specified service account. """ return get( @@ -73,10 +73,10 @@ def get_service_account_info(service_account='default', http_request=None): def get_token(service_account='default', http_request=None): """Fetch an OAuth access token from the metadata server :param service_account: a service account email. Left blank information for - the default service account of current compute engine instance will be looked up. + the default service account of current compute engine instance will be looked up. :param http_request: callable, a callable that matches the method - signature of httplib2.Http.request, used to make - the refresh request. + signature of httplib2.Http.request, used to make + the refresh request. :return: """ try: From 2209c71e517efd354dde13816f3a23b287ff2b4d Mon Sep 17 00:00:00 2001 From: elibixby Date: Thu, 9 Jun 2016 14:44:09 -0700 Subject: [PATCH 10/15] Style updates --- docs/source/oauth2client.contrib.metadata.rst | 7 -- docs/source/oauth2client.contrib.rst | 1 - .../contrib/{metadata.py => _metadata.py} | 75 +++++++++++-------- oauth2client/contrib/gce.py | 13 +++- tests/contrib/test_gce.py | 22 +++++- tests/contrib/test_metadata.py | 38 +++------- 6 files changed, 82 insertions(+), 74 deletions(-) delete mode 100644 docs/source/oauth2client.contrib.metadata.rst rename oauth2client/contrib/{metadata.py => _metadata.py} (51%) diff --git a/docs/source/oauth2client.contrib.metadata.rst b/docs/source/oauth2client.contrib.metadata.rst deleted file mode 100644 index 974e04d00..000000000 --- a/docs/source/oauth2client.contrib.metadata.rst +++ /dev/null @@ -1,7 +0,0 @@ -oauth2client.contrib.metadata module -==================================== - -.. automodule:: oauth2client.contrib.metadata - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/oauth2client.contrib.rst b/docs/source/oauth2client.contrib.rst index c6eb406df..0caaa2977 100644 --- a/docs/source/oauth2client.contrib.rst +++ b/docs/source/oauth2client.contrib.rst @@ -21,7 +21,6 @@ Submodules oauth2client.contrib.gce oauth2client.contrib.keyring_storage oauth2client.contrib.locked_file - oauth2client.contrib.metadata oauth2client.contrib.multistore_file oauth2client.contrib.xsrfutil diff --git a/oauth2client/contrib/metadata.py b/oauth2client/contrib/_metadata.py similarity index 51% rename from oauth2client/contrib/metadata.py rename to oauth2client/contrib/_metadata.py index 6496dc02e..3146129b5 100644 --- a/oauth2client/contrib/metadata.py +++ b/oauth2client/contrib/_metadata.py @@ -12,30 +12,36 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Thin wrapper class for talking to the GCE Metadata Server.""" +"""Provides helper methods for talking to the Compute Engine metadata server. + +See https://cloud.google.com/compute/docs/metadata +""" + import datetime import httplib2 import json +import urllib from six.moves import http_client from oauth2client._helpers import _from_bytes from oauth2client.client import _UTCNOW -from oauth2client.client import HttpAccessTokenRefreshError METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/' METADATA_HEADERS = {'Metadata-Flavor': 'Google'} -def get(path, recursive=True, http_request=None, root=METADATA_ROOT): +def get(path, http_request=None, root=METADATA_ROOT, **kwargs): if path is None: path = [] if not http_request: http_request = httplib2.Http().request - r_string = '/?recursive=true' if recursive else '' - full_path = root + '/'.join(path) + r_string + if kwargs: + path.append('?' + urllib.urlencode(kwargs)) + + full_path = root + '/'.join(path) response, content = http_request( full_path, headers=METADATA_HEADERS @@ -47,21 +53,26 @@ def get(path, recursive=True, http_request=None, root=METADATA_ROOT): else: return decoded else: - msg = ( - 'Failed to retrieve {path} from the Google Compute Engine' - 'metadata service. Response:\n{error}' - ).format(path=full_path, error=response) - raise httplib2.HttpLib2Error(msg) + raise httplib2.HttpLib2Error( + ( + 'Failed to retrieve {path} from the Google Compute Engine' + 'metadata service. Response:\n{error}' + ).format(path=full_path, error=response) + ) def get_service_account_info(service_account='default', http_request=None): """ Get information about a service account from the metadata server. - :param service_account: a service account email. Left blank information for - the default service account of current compute engine instance will be looked up. - :param http_request: callable, a callable that matches the method - signature of httplib2.Http.request, used to make - the refresh request. - :return: A dictionary with information about the specified service account. + + Args: + service_account: An email specifying the service account for which to + look up information. Default will be information for the "default" + service account of the current compute engine instance. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request. Used to make the request to the metadata + server. + Returns: + A dictionary with information about the specified service account. """ return get( ['instance', 'service-accounts', service_account], @@ -71,23 +82,23 @@ def get_service_account_info(service_account='default', http_request=None): def get_token(service_account='default', http_request=None): - """Fetch an OAuth access token from the metadata server - :param service_account: a service account email. Left blank information for - the default service account of current compute engine instance will be looked up. - :param http_request: callable, a callable that matches the method - signature of httplib2.Http.request, used to make - the refresh request. - :return: - """ - try: - token_json = get( - ['instance', 'service-accounts', service_account, 'token'], - recursive=False, - http_request=http_request - ) - except httplib2.HttpLib2Error as failed_fetch: - raise HttpAccessTokenRefreshError(str(failed_fetch)) + """ Fetch an oauth token for the + + Args: + service_account: An email specifying the service account this token should + represent. Default will be a token for the "default" service account + of the current compute engine instance. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request. Used to make the request to the metadata + server. + Returns: + A dictionary with information about the specified service account. + """ + token_json = get( + ['instance', 'service-accounts', service_account, 'token'], + http_request=http_request + ) token_expiry = _UTCNOW() + datetime.timedelta( seconds=token_json['expires_in']) return token_json['access_token'], token_expiry diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index e55f875fd..b19ec61f6 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -17,6 +17,7 @@ Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. """ +import httplib2 import json import logging import warnings @@ -25,7 +26,8 @@ from oauth2client._helpers import _from_bytes from oauth2client import util from oauth2client.client import AssertionCredentials -from oauth2client.contrib import metadata +from oauth2client.client import HttpAccessTokenRefreshError +from oauth2client.contrib import _metadata __author__ = 'jcgregorio@google.com (Joe Gregorio)' @@ -95,8 +97,11 @@ def _refresh(self, http_request): Raises: HttpAccessTokenRefreshError: When the refresh fails. """ - self.access_token, self.token_expiry = metadata.get_token( - http_request=http_request) + try: + self.access_token, self.token_expiry = _metadata.get_token( + http_request=http_request) + except httplib2.HttpLib2Error as e: + raise HttpAccessTokenRefreshError(str(e)) @property def serialization_data(self): @@ -141,5 +146,5 @@ def service_account_email(self): Compute Engine metadata service. """ if self._service_account_email is None: - self._service_account_email = metadata.get_service_account_info()['email'] + self._service_account_email = _metadata.get_service_account_info()['email'] return self._service_account_email diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py index b8bb289b2..4451293fd 100644 --- a/tests/contrib/test_gce.py +++ b/tests/contrib/test_gce.py @@ -15,15 +15,18 @@ """Unit tests for oauth2client.contrib.gce.""" import datetime +import json +import mock import unittest2 -import mock +from six.moves import http_client from oauth2client.client import Credentials from oauth2client.client import save_to_well_known_file +from oauth2client.client import HttpAccessTokenRefreshError from oauth2client.contrib.gce import _SCOPES_WARNING from oauth2client.contrib.gce import AppAssertionCredentials - +from tests.contrib.test_metadata import request_mock __author__ = 'jcgregorio@google.com (Joe Gregorio)' @@ -53,7 +56,7 @@ def test_to_json_and_from_json(self): self.assertEqual(credentials.access_token, credentials_from_json.access_token) - @mock.patch('oauth2client.contrib.metadata.get_token', + @mock.patch('oauth2client.contrib._metadata.get_token', side_effect=[('A', datetime.datetime.min), ('B', datetime.datetime.max)]) def test_refresh_token(self, metadata): @@ -66,6 +69,17 @@ def test_refresh_token(self, metadata): self.assertEqual(credentials.access_token, 'B') self.assertFalse(credentials.access_token_expired) + def test_refresh_token_failed_fetch(self): + http_request = request_mock( + http_client.NOT_FOUND, + 'application/json', + json.dumps({'access_token': 'a', 'expires_in': 100}) + ) + credentials = AppAssertionCredentials() + + with self.assertRaises(HttpAccessTokenRefreshError): + credentials._refresh(http_request=http_request) + def test_serialization_data(self): credentials = AppAssertionCredentials() self.assertRaises(NotImplementedError, getattr, @@ -95,7 +109,7 @@ def test_sign_blob_not_implemented(self): with self.assertRaises(NotImplementedError): credentials.sign_blob(b'blob') - @mock.patch('oauth2client.contrib.metadata.get_service_account_info', + @mock.patch('oauth2client.contrib._metadata.get_service_account_info', return_value={'email': 'a@example.com'}) def test_service_account_email(self, metadata): credentials = AppAssertionCredentials() diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py index fd0ac7902..9b1b2fb18 100644 --- a/tests/contrib/test_metadata.py +++ b/tests/contrib/test_metadata.py @@ -22,12 +22,12 @@ from six.moves import http_client from oauth2client.client import HttpAccessTokenRefreshError -from oauth2client.contrib import metadata +from oauth2client.contrib import _metadata -PATH = ['a', 'b'] +PATH = ['instance', 'service-accounts', 'default'] DATA = {'foo': 'bar'} -EXPECTED_ARGS = ['http://metadata.google.internal/computeMetadata/v1/a/b/?recursive=true'] -EXPECTED_KWARGS = dict(headers=metadata.METADATA_HEADERS) +EXPECTED_ARGS = ['http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default'] +EXPECTED_KWARGS = dict(headers=_metadata.METADATA_HEADERS) def request_mock(status, content_type, content): @@ -45,7 +45,7 @@ def test_get_success_json(self): http_request = request_mock( http_client.OK, 'application/json', json.dumps(DATA)) self.assertEqual( - metadata.get(PATH, http_request=http_request), + _metadata.get(PATH, http_request=http_request), DATA ) http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) @@ -54,7 +54,7 @@ def test_get_success_string(self): http_request = request_mock( http_client.OK, 'text/html', '

Hello World!

') self.assertEqual( - metadata.get(PATH, http_request=http_request), + _metadata.get(PATH, http_request=http_request), '

Hello World!

' ) http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) @@ -63,46 +63,32 @@ def test_get_failure(self): http_request = request_mock( http_client.NOT_FOUND, 'text/html', '

Error

') with self.assertRaises(httplib2.HttpLib2Error): - metadata.get(PATH, http_request=http_request) + _metadata.get(PATH, http_request=http_request) http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) - @mock.patch('oauth2client.contrib.metadata._UTCNOW', return_value=datetime.datetime.min) + @mock.patch('oauth2client.contrib._metadata._UTCNOW', return_value=datetime.datetime.min) def test_get_token_success(self, now): http_request = request_mock( http_client.OK, 'application/json', json.dumps({'access_token': 'a', 'expires_in': 100}) ) - token, expiry = metadata.get_token(http_request=http_request) + token, expiry = _metadata.get_token(http_request=http_request) self.assertEqual(token, 'a') self.assertEqual(expiry, datetime.datetime.min + datetime.timedelta(seconds=100)) http_request.assert_called_once_with( - 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', + EXPECTED_ARGS[0]+'/token', **EXPECTED_KWARGS ) now.assert_called_once_with() - def test_get_token_failed_fetch(self): - http_request = request_mock( - http_client.NOT_FOUND, - 'application/json', - json.dumps({'access_token': 'a', 'expires_in': 100}) - ) - with self.assertRaises(HttpAccessTokenRefreshError): - metadata.get_token(http_request=http_request) - - http_request.assert_called_once_with( - 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', - **EXPECTED_KWARGS - ) - def test_service_account_info(self): http_request = request_mock( http_client.OK, 'application/json', json.dumps(DATA)) - info = metadata.get_service_account_info(http_request=http_request) + info = _metadata.get_service_account_info(http_request=http_request) self.assertEqual(info, DATA) http_request.assert_called_once_with( - 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/?recursive=true', + EXPECTED_ARGS[0]+'/?recursive=True', **EXPECTED_KWARGS ) From e10488b9c5cb1a6115c43d791efb9535ee0b3a5c Mon Sep 17 00:00:00 2001 From: elibixby Date: Thu, 9 Jun 2016 15:10:08 -0700 Subject: [PATCH 11/15] Python3 compat --- oauth2client/contrib/_metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py index 3146129b5..f07eddbaa 100644 --- a/oauth2client/contrib/_metadata.py +++ b/oauth2client/contrib/_metadata.py @@ -20,9 +20,9 @@ import datetime import httplib2 import json -import urllib from six.moves import http_client +from six.moves.urllib.parse import urlencode from oauth2client._helpers import _from_bytes from oauth2client.client import _UTCNOW @@ -39,7 +39,7 @@ def get(path, http_request=None, root=METADATA_ROOT, **kwargs): http_request = httplib2.Http().request if kwargs: - path.append('?' + urllib.urlencode(kwargs)) + path.append('?' + urlencode(kwargs)) full_path = root + '/'.join(path) response, content = http_request( From ac4dcd295ef6334215e1bac7ce0398ab43ad3366 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 10 Jun 2016 13:57:05 -0700 Subject: [PATCH 12/15] Fixing nits --- oauth2client/contrib/_metadata.py | 56 ++++++++++++++----------------- tests/contrib/test_metadata.py | 24 +++++++------ 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py index f07eddbaa..9b77b3595 100644 --- a/oauth2client/contrib/_metadata.py +++ b/oauth2client/contrib/_metadata.py @@ -22,30 +22,29 @@ import json from six.moves import http_client -from six.moves.urllib.parse import urlencode +from six.moves.urllib import parse as urlparse from oauth2client._helpers import _from_bytes from oauth2client.client import _UTCNOW +from oauth2client import util + METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/' METADATA_HEADERS = {'Metadata-Flavor': 'Google'} -def get(path, http_request=None, root=METADATA_ROOT, **kwargs): - if path is None: - path = [] - +def get(path, http_request=None, root=METADATA_ROOT, recursive=None): if not http_request: http_request = httplib2.Http().request - if kwargs: - path.append('?' + urlencode(kwargs)) + url = urlparse.urljoin(root, path) + url = util._add_query_parameter(url, 'recursive', recursive) - full_path = root + '/'.join(path) response, content = http_request( - full_path, + url, headers=METADATA_HEADERS ) + if response.status == http_client.OK: decoded = _from_bytes(content) if response['content-type'] == 'application/json': @@ -54,51 +53,48 @@ def get(path, http_request=None, root=METADATA_ROOT, **kwargs): return decoded else: raise httplib2.HttpLib2Error( - ( - 'Failed to retrieve {path} from the Google Compute Engine' - 'metadata service. Response:\n{error}' - ).format(path=full_path, error=response) - ) + 'Failed to retrieve {0} from the Google Compute Engine' + 'metadata service. Response:\n{1}'.format(url, response)) def get_service_account_info(service_account='default', http_request=None): - """ Get information about a service account from the metadata server. + """Get information about a service account from the metadata server. Args: service_account: An email specifying the service account for which to look up information. Default will be information for the "default" service account of the current compute engine instance. http_request: callable, a callable that matches the method - signature of httplib2.Http.request. Used to make the request to the metadata - server. + signature of httplib2.Http.request. Used to make the request to the + metadata server. Returns: A dictionary with information about the specified service account. """ return get( - ['instance', 'service-accounts', service_account], + 'instance/service-accounts/{0}'.format(service_account), recursive=True, - http_request=http_request - ) + http_request=http_request) def get_token(service_account='default', http_request=None): - """ Fetch an oauth token for the + """Fetch an oauth token for the Args: - service_account: An email specifying the service account this token should - represent. Default will be a token for the "default" service account - of the current compute engine instance. + service_account: An email specifying the service account this token + should represent. Default will be a token for the "default" service + account of the current compute engine instance. http_request: callable, a callable that matches the method - signature of httplib2.Http.request. Used to make the request to the metadata - server. + signature of httplib2.Http.request. Used to make the request to the + metadataserver. Returns: - A dictionary with information about the specified service account. + A tuple of (access token, token expiration), where access token is the + access token as a string and token expiration is a datetime object + that indicates when the access token will expire. """ token_json = get( - ['instance', 'service-accounts', service_account, 'token'], - http_request=http_request - ) + 'instance/service-accounts/{0}/token'.format(service_account), + http_request=http_request) token_expiry = _UTCNOW() + datetime.timedelta( seconds=token_json['expires_in']) return token_json['access_token'], token_expiry diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py index 9b1b2fb18..3b5318782 100644 --- a/tests/contrib/test_metadata.py +++ b/tests/contrib/test_metadata.py @@ -21,12 +21,13 @@ from six.moves import http_client -from oauth2client.client import HttpAccessTokenRefreshError from oauth2client.contrib import _metadata -PATH = ['instance', 'service-accounts', 'default'] +PATH = 'instance/service-accounts/default' DATA = {'foo': 'bar'} -EXPECTED_ARGS = ['http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default'] +EXPECTED_URL = ( + 'http://metadata.google.internal/computeMetadata/v1/instance' + '/service-accounts/default') EXPECTED_KWARGS = dict(headers=_metadata.METADATA_HEADERS) @@ -48,7 +49,7 @@ def test_get_success_json(self): _metadata.get(PATH, http_request=http_request), DATA ) - http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) + http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS) def test_get_success_string(self): http_request = request_mock( @@ -57,7 +58,7 @@ def test_get_success_string(self): _metadata.get(PATH, http_request=http_request), '

Hello World!

' ) - http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) + http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS) def test_get_failure(self): http_request = request_mock( @@ -65,9 +66,11 @@ def test_get_failure(self): with self.assertRaises(httplib2.HttpLib2Error): _metadata.get(PATH, http_request=http_request) - http_request.assert_called_once_with(*EXPECTED_ARGS, **EXPECTED_KWARGS) + http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS) - @mock.patch('oauth2client.contrib._metadata._UTCNOW', return_value=datetime.datetime.min) + @mock.patch( + 'oauth2client.contrib._metadata._UTCNOW', + return_value=datetime.datetime.min) def test_get_token_success(self, now): http_request = request_mock( http_client.OK, @@ -76,9 +79,10 @@ def test_get_token_success(self, now): ) token, expiry = _metadata.get_token(http_request=http_request) self.assertEqual(token, 'a') - self.assertEqual(expiry, datetime.datetime.min + datetime.timedelta(seconds=100)) + self.assertEqual( + expiry, datetime.datetime.min + datetime.timedelta(seconds=100)) http_request.assert_called_once_with( - EXPECTED_ARGS[0]+'/token', + EXPECTED_URL+'/token', **EXPECTED_KWARGS ) now.assert_called_once_with() @@ -89,6 +93,6 @@ def test_service_account_info(self): info = _metadata.get_service_account_info(http_request=http_request) self.assertEqual(info, DATA) http_request.assert_called_once_with( - EXPECTED_ARGS[0]+'/?recursive=True', + EXPECTED_URL+'?recursive=True', **EXPECTED_KWARGS ) From bbb9cf5885c7df93b40324724126759c622b3a6b Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 10 Jun 2016 14:00:24 -0700 Subject: [PATCH 13/15] Fixing docstring and final nit. --- oauth2client/contrib/_metadata.py | 5 ++++- oauth2client/contrib/gce.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py index 9b77b3595..43f88462c 100644 --- a/oauth2client/contrib/_metadata.py +++ b/oauth2client/contrib/_metadata.py @@ -68,7 +68,10 @@ def get_service_account_info(service_account='default', http_request=None): signature of httplib2.Http.request. Used to make the request to the metadata server. Returns: - A dictionary with information about the specified service account. + A dictionary with information about the specified service account, + for example: + + {'email': '...', 'scopes': ['scope', ...], 'aliases': 'default'} """ return get( 'instance/service-accounts/{0}'.format(service_account), diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index b19ec61f6..f3a897c6b 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -146,5 +146,6 @@ def service_account_email(self): Compute Engine metadata service. """ if self._service_account_email is None: - self._service_account_email = _metadata.get_service_account_info()['email'] + self._service_account_email = ( + _metadata.get_service_account_info()['email']) return self._service_account_email From ee9cbfda69e2022d2f0614a877f96a05865473b0 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 10 Jun 2016 14:03:51 -0700 Subject: [PATCH 14/15] removing extraneous six dependency in tox --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2706dce23..8e94c012b 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ basedeps = mock>=1.3.0 nose flask unittest2 - six>=1.10 deps = {[testenv]basedeps} django keyring From 88b268028dd929d77703712f9bd78e0018c91c3b Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 10 Jun 2016 14:09:41 -0700 Subject: [PATCH 15/15] Finishing docstrings. --- oauth2client/contrib/_metadata.py | 29 ++++++++++++++++++++++++++--- tests/contrib/test_metadata.py | 3 +-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py index 43f88462c..9987da753 100644 --- a/oauth2client/contrib/_metadata.py +++ b/oauth2client/contrib/_metadata.py @@ -34,6 +34,25 @@ def get(path, http_request=None, root=METADATA_ROOT, recursive=None): + """Fetch a resource from the metadata server. + + Args: + path: A string indicating the resource to retrieve. For example, + 'instance/service-accounts/defualt' + http_request: A callable that matches the method + signature of httplib2.Http.request. Used to make the request to the + metadataserver. + root: A string indicating the full path to the metadata server root. + recursive: A boolean indicating whether to do a recursive query of + metadata. See + https://cloud.google.com/compute/docs/metadata#aggcontents + + Returns: + A dictionary if the metadata server returns JSON, otherwise a string. + + Raises: + httplib2.Httplib2Error if an error corrured while retrieving metadata. + """ if not http_request: http_request = httplib2.Http().request @@ -64,14 +83,18 @@ def get_service_account_info(service_account='default', http_request=None): service_account: An email specifying the service account for which to look up information. Default will be information for the "default" service account of the current compute engine instance. - http_request: callable, a callable that matches the method + http_request: A callable that matches the method signature of httplib2.Http.request. Used to make the request to the metadata server. Returns: A dictionary with information about the specified service account, for example: - {'email': '...', 'scopes': ['scope', ...], 'aliases': 'default'} + { + 'email': '...', + 'scopes': ['scope', ...], + 'aliases': ['default', '...'] + } """ return get( 'instance/service-accounts/{0}'.format(service_account), @@ -86,7 +109,7 @@ def get_token(service_account='default', http_request=None): service_account: An email specifying the service account this token should represent. Default will be a token for the "default" service account of the current compute engine instance. - http_request: callable, a callable that matches the method + http_request: A callable that matches the method signature of httplib2.Http.request. Used to make the request to the metadataserver. diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py index 3b5318782..4e48387c4 100644 --- a/tests/contrib/test_metadata.py +++ b/tests/contrib/test_metadata.py @@ -1,4 +1,4 @@ -# Copyright 2014 Google Inc. All rights reserved. +# Copyright 2016 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Unit tests for oauth2client.contrib.metadata""" import datetime import httplib2 import json