From a4f4a3c4e074b6c970adeac8398d45e6831d43e1 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 6 Mar 2015 16:50:14 +0200 Subject: [PATCH] Replace the oauth library with oauthlib Because the oauth library we were using doesn't support Python 3, cloudbase-init could not work on Python 3 for the MaaS metadata service, the only place in the code base where OAuth is needed. This patch replaces oauth with oauthlib. oauthlib is better maintained and has support for Python 2.6-3.4. Change-Id: Iae2995420697bc305f2724ce038db2f2b3ab51e3 Closes-Bug: #1382572 --- .../metadata/services/maasservice.py | 39 ++++++------ .../metadata/services/test_maasservice.py | 62 ++++++++----------- requirements.txt | 2 +- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/cloudbaseinit/metadata/services/maasservice.py b/cloudbaseinit/metadata/services/maasservice.py index 6da6162e..58d5e14f 100644 --- a/cloudbaseinit/metadata/services/maasservice.py +++ b/cloudbaseinit/metadata/services/maasservice.py @@ -13,9 +13,8 @@ # under the License. import posixpath -import time -from oauth import oauth +from oauthlib import oauth1 from oslo.config import cfg from six.moves.urllib import error from six.moves.urllib import request @@ -43,6 +42,17 @@ CONF.register_opts(opts) LOG = logging.getLogger(__name__) +class _Realm(str): + # There's a bug in oauthlib which ignores empty realm strings, + # by checking that the given realm is always True. + # This string class always returns True in a boolean context, + # making sure that an empty realm can be used by oauthlib. + def __bool__(self): + return True + + __nonzero__ = __bool__ + + class MaaSHttpService(base.BaseMetadataService): _METADATA_2012_03_01 = '2012-03-01' @@ -76,22 +86,15 @@ class MaaSHttpService(base.BaseMetadataService): raise def _get_oauth_headers(self, url): - consumer = oauth.OAuthConsumer(CONF.maas_oauth_consumer_key, - CONF.maas_oauth_consumer_secret) - token = oauth.OAuthToken(CONF.maas_oauth_token_key, - CONF.maas_oauth_token_secret) - - parameters = {'oauth_version': "1.0", - 'oauth_nonce': oauth.generate_nonce(), - 'oauth_timestamp': int(time.time()), - 'oauth_token': token.key, - 'oauth_consumer_key': consumer.key} - - req = oauth.OAuthRequest(http_url=url, parameters=parameters) - req.sign_request(oauth.OAuthSignatureMethod_PLAINTEXT(), consumer, - token) - - return req.to_header() + client = oauth1.Client( + CONF.maas_oauth_consumer_key, + client_secret=CONF.maas_oauth_consumer_secret, + resource_owner_key=CONF.maas_oauth_token_key, + resource_owner_secret=CONF.maas_oauth_token_secret, + signature_method=oauth1.SIGNATURE_PLAINTEXT) + realm = _Realm("") + headers = client.sign(url, realm=realm)[1] + return headers def _get_data(self, path): norm_path = posixpath.join(CONF.maas_metadata_url, path) diff --git a/cloudbaseinit/tests/metadata/services/test_maasservice.py b/cloudbaseinit/tests/metadata/services/test_maasservice.py index 605f4851..46ffce9e 100644 --- a/cloudbaseinit/tests/metadata/services/test_maasservice.py +++ b/cloudbaseinit/tests/metadata/services/test_maasservice.py @@ -14,7 +14,6 @@ import os import posixpath -import sys import unittest try: @@ -25,12 +24,10 @@ from oslo.config import cfg from six.moves.urllib import error from cloudbaseinit.metadata.services import base +from cloudbaseinit.metadata.services import maasservice from cloudbaseinit.tests import testutils from cloudbaseinit.utils import x509constants -if sys.version_info < (3, 0): - # TODO(alexpilotti) replace oauth with a Python 3 compatible module - from cloudbaseinit.metadata.services import maasservice CONF = cfg.CONF @@ -38,12 +35,7 @@ CONF = cfg.CONF class MaaSHttpServiceTest(unittest.TestCase): def setUp(self): - if sys.version_info < (3, 0): - self.mock_oauth = mock.MagicMock() - maasservice.oauth = self.mock_oauth - self._maasservice = maasservice.MaaSHttpService() - else: - self.skipTest("Python 3 is not yet supported for maasservice") + self._maasservice = maasservice.MaaSHttpService() @mock.patch("cloudbaseinit.metadata.services.maasservice.MaaSHttpService" "._get_data") @@ -91,32 +83,32 @@ class MaaSHttpServiceTest(unittest.TestCase): 'test other error', {}, None) self._test_get_response(ret_val=err) - @mock.patch('time.time') - def test_get_oauth_headers(self, mock_time): - mock_token = mock.MagicMock() - mock_consumer = mock.MagicMock() - mock_req = mock.MagicMock() - self.mock_oauth.OAuthConsumer.return_value = mock_consumer - self.mock_oauth.OAuthToken.return_value = mock_token - self.mock_oauth.OAuthRequest.return_value = mock_req - mock_time.return_value = 0 - self.mock_oauth.generate_nonce.return_value = 'fake nounce' + @testutils.ConfPatcher('maas_oauth_consumer_key', 'consumer_key') + @testutils.ConfPatcher('maas_oauth_consumer_secret', 'consumer_secret') + @testutils.ConfPatcher('maas_oauth_token_key', 'token_key') + @testutils.ConfPatcher('maas_oauth_token_secret', 'token_secret') + def test_get_oauth_headers(self): response = self._maasservice._get_oauth_headers(url='196.254.196.254') - self.mock_oauth.OAuthConsumer.assert_called_once_with( - CONF.maas_oauth_consumer_key, CONF.maas_oauth_consumer_secret) - self.mock_oauth.OAuthToken.assert_called_once_with( - CONF.maas_oauth_token_key, CONF.maas_oauth_token_secret) - parameters = {'oauth_version': "1.0", - 'oauth_nonce': 'fake nounce', - 'oauth_timestamp': int(0), - 'oauth_token': mock_token.key, - 'oauth_consumer_key': mock_consumer.key} - self.mock_oauth.OAuthRequest.assert_called_once_with( - http_url='196.254.196.254', parameters=parameters) - mock_req.sign_request.assert_called_once_with( - self.mock_oauth.OAuthSignatureMethod_PLAINTEXT(), mock_consumer, - mock_token) - self.assertEqual(mock_req.to_header.return_value, response) + self.assertIsInstance(response, dict) + self.assertIn('Authorization', response) + + auth = response['Authorization'] + self.assertTrue(auth.startswith('OAuth')) + + auth = auth[6:] + parts = [item.strip() for item in auth.split(",")] + auth_parts = dict(part.split("=") for part in parts) + + required_headers = { + 'oauth_token', + 'oauth_consumer_key', + 'oauth_signature', + } + self.assertTrue(required_headers.issubset(set(auth_parts))) + self.assertEqual('"token_key"', auth_parts['oauth_token']) + self.assertEqual('"consumer_key"', auth_parts['oauth_consumer_key']) + self.assertEqual('"consumer_secret%26token_secret"', + auth_parts['oauth_signature']) @mock.patch("cloudbaseinit.metadata.services.maasservice.MaaSHttpService" "._get_oauth_headers") diff --git a/requirements.txt b/requirements.txt index 065baebf..5ae64e81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ pyserial oslo.config six>=1.7.0 Babel>=1.3 -oauth +oauthlib netifaces PyYAML \ No newline at end of file