From c0470894ff1b68ff521bd09c5bb7cf86f4d7291d Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 10 Sep 2014 11:42:39 -0500 Subject: [PATCH 1/3] Makes Authentication More Flexible --- SoftLayer/API.py | 20 ++++++++++++------- SoftLayer/auth.py | 32 ++++++++++++++---------------- SoftLayer/tests/api_tests.py | 6 +++--- SoftLayer/tests/auth_tests.py | 32 ++++++++++++++++++------------ SoftLayer/tests/transport_tests.py | 8 ++++++-- SoftLayer/transports.py | 13 ++++++++---- 6 files changed, 65 insertions(+), 46 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 61851a35a..f2d8bf75c 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -146,9 +146,6 @@ def call(self, service, method, *args, **kwargs): headers = kwargs.get('headers', {}) - if self.auth: - headers.update(self.auth.get_headers()) - if kwargs.get('id') is not None: headers[service + 'InitParameters'] = {'id': kwargs.get('id')} @@ -178,11 +175,18 @@ def call(self, service, method, *args, **kwargs): http_headers.update(kwargs.get('raw_headers')) uri = '/'.join([self.endpoint_url, service]) + options = { + 'headers': headers, + 'http_headers': http_headers, + 'timeout': self.timeout, + 'proxy': self.proxy, + } + + if self.auth: + options = self.auth.get_options(options) + return transports.make_xml_rpc_api_call(uri, method, args, - headers=headers, - http_headers=http_headers, - timeout=self.timeout, - proxy=self.proxy) + **options) __call__ = call @@ -326,6 +330,8 @@ def call(self, name, *args, **kwargs): :param int offset: (optional) offset results by this many :param boolean iter: (optional) if True, returns a generator with the results + :param bool verify: verify SSL cert + :param cert: client certificate path Usage: >>> import SoftLayer diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index e430839a7..a0b1daf0b 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -10,8 +10,8 @@ class AuthenticationBase(object): """A base authentication class intended to be overridden.""" - def get_headers(self): - """Return a dictionary of headers to be inserted for authentication.""" + def get_options(self, options): + """Receives request options and returns request options.""" raise NotImplementedError @@ -26,15 +26,14 @@ def __init__(self, user_id, auth_token): self.user_id = user_id self.auth_token = auth_token - def get_headers(self): - """Returns token-based auth headers.""" - return { - 'authenticate': { - 'complexType': 'PortalLoginToken', - 'userId': self.user_id, - 'authToken': self.auth_token, - } + def get_options(self, options): + """Sets token-based auth headers.""" + options['headers']['authenticate'] = { + 'complexType': 'PortalLoginToken', + 'userId': self.user_id, + 'authToken': self.auth_token, } + return options def __repr__(self): return "" % (self.user_id, self.auth_token) @@ -50,14 +49,13 @@ def __init__(self, username, api_key): self.username = username self.api_key = api_key - def get_headers(self): - """Returns token-based auth headers.""" - return { - 'authenticate': { - 'username': self.username, - 'apiKey': self.api_key, - } + def get_options(self, options): + """Sets token-based auth headers.""" + options['headers']['authenticate'] = { + 'username': self.username, + 'apiKey': self.api_key, } + return options def __repr__(self): return "" % (self.username) diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index d9408cdc9..8260f4e62 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -17,9 +17,9 @@ def test_init(self): client = SoftLayer.Client(username='doesnotexist', api_key='issurelywrong', timeout=10) - auth_headers = {'authenticate': {'username': 'doesnotexist', - 'apiKey': 'issurelywrong'}} - self.assertEqual(client.auth.get_headers(), auth_headers) + self.assertIsInstance(client.auth, SoftLayer.BasicAuthentication) + self.assertEqual(client.auth.username, 'doesnotexist') + self.assertEqual(client.auth.api_key, 'issurelywrong') self.assertEqual(client.endpoint_url, SoftLayer.API_PUBLIC_ENDPOINT.rstrip('/')) self.assertEqual(client.timeout, 10) diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py index 2a3af4b1f..3e1fe74ee 100644 --- a/SoftLayer/tests/auth_tests.py +++ b/SoftLayer/tests/auth_tests.py @@ -9,9 +9,9 @@ class TestAuthenticationBase(testing.TestCase): - def test_get_headers(self): + def test_get_options(self): auth_base = auth.AuthenticationBase() - self.assertRaises(NotImplementedError, auth_base.get_headers) + self.assertRaises(NotImplementedError, auth_base.get_options, {}) class TestBasicAuthentication(testing.TestCase): @@ -22,11 +22,14 @@ def test_attribs(self): self.assertEqual(self.auth.username, 'USERNAME') self.assertEqual(self.auth.api_key, 'APIKEY') - def test_get_headers(self): - self.assertEqual(self.auth.get_headers(), { - 'authenticate': { - 'username': 'USERNAME', - 'apiKey': 'APIKEY', + def test_get_options(self): + headers = {'headers': {}} + self.assertEqual(self.auth.get_options(headers), { + 'headers': { + 'authenticate': { + 'username': 'USERNAME', + 'apiKey': 'APIKEY', + } } }) @@ -44,12 +47,15 @@ def test_attribs(self): self.assertEqual(self.auth.user_id, 12345) self.assertEqual(self.auth.auth_token, 'TOKEN') - def test_get_headers(self): - self.assertEqual(self.auth.get_headers(), { - 'authenticate': { - 'complexType': 'PortalLoginToken', - 'userId': 12345, - 'authToken': 'TOKEN', + def test_get_options(self): + headers = {'headers': {}} + self.assertEqual(self.auth.get_options(headers), { + 'headers': { + 'authenticate': { + 'complexType': 'PortalLoginToken', + 'userId': 12345, + 'authToken': 'TOKEN', + } } }) diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index a0e82f558..e65ca6595 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -56,7 +56,9 @@ def test_call(self, request): headers=None, proxies=None, data=data, - timeout=None) + timeout=None, + cert=None, + verify=True) self.assertEqual(resp, []) def test_proxy_without_protocol(self): @@ -81,7 +83,9 @@ def test_valid_proxy(self, request): proxies={'https': 'http://localhost:3128', 'http': 'http://localhost:3128'}, data=mock.ANY, - timeout=None) + timeout=None, + cert=None, + verify=True) class TestRestAPICall(testing.TestCase): diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 44fc9812c..332ae9a98 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -23,8 +23,9 @@ def _proxies_dict(proxy): return {'http': proxy, 'https': proxy} -def make_xml_rpc_api_call(uri, method, args=None, headers=None, - http_headers=None, timeout=None, proxy=None): +def make_xml_rpc_api_call(url, method, args=None, headers=None, + http_headers=None, timeout=None, proxy=None, + verify=True, cert=None): """Makes a SoftLayer API call against the XML-RPC endpoint. :param string uri: endpoint URL @@ -32,6 +33,8 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, :param dict headers: XML-RPC headers to use for the request :param dict http_headers: HTTP headers to use for the request :param int timeout: number of seconds to use as a timeout + :param bool verify: verify SSL cert + :param cert: client certificate path """ if args is None: args = tuple() @@ -43,14 +46,16 @@ def make_xml_rpc_api_call(uri, method, args=None, headers=None, methodname=method, allow_none=True) LOGGER.debug("=== REQUEST ===") - LOGGER.info('POST %s', uri) + LOGGER.info('POST %s', url) LOGGER.debug(http_headers) LOGGER.debug(payload) - response = requests.request('POST', uri, + response = requests.request('POST', url, data=payload, headers=http_headers, timeout=timeout, + verify=verify, + cert=cert, proxies=_proxies_dict(proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(response.headers) From e115be7233cfb56f87ea3b0a04f446f44b5b0067 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 18 Sep 2014 18:18:03 -0500 Subject: [PATCH 2/3] Adds backwards compat for old style auth objects --- SoftLayer/API.py | 10 ++++++++- SoftLayer/auth.py | 17 +++++++++++++-- SoftLayer/tests/auth_tests.py | 3 ++- SoftLayer/tests/deprecated_tests.py | 33 +++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 SoftLayer/tests/deprecated_tests.py diff --git a/SoftLayer/API.py b/SoftLayer/API.py index f2d8bf75c..c306e1ae2 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -6,6 +6,7 @@ :license: MIT, see LICENSE for more details. """ import time +import warnings from SoftLayer import auth as slauth from SoftLayer import config @@ -183,7 +184,14 @@ def call(self, service, method, *args, **kwargs): } if self.auth: - options = self.auth.get_options(options) + if getattr(self.auth, "get_headers", None): + warnings.warn("auth.get_headers() is deprecation and will be " + "removed in the next major version", + DeprecationWarning) + headers.update(self.auth.get_headers()) + + if getattr(self.auth, "get_options", None): + options = self.auth.get_options(options) return transports.make_xml_rpc_api_call(uri, method, args, **options) diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index a0b1daf0b..931042e72 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -10,9 +10,22 @@ class AuthenticationBase(object): """A base authentication class intended to be overridden.""" + def get_options(self, options): - """Receives request options and returns request options.""" - raise NotImplementedError + """Receives request options and returns request options. + + :param options dict: dictionary of request options + + """ + return options + + def get_headers(self): + """Return a dictionary of headers to be inserted for authentication. + + .. deprecated:: 3.3.0 + Use :func:`get_options` instead. + """ + return {} class TokenAuthentication(AuthenticationBase): diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py index 3e1fe74ee..45405c7e8 100644 --- a/SoftLayer/tests/auth_tests.py +++ b/SoftLayer/tests/auth_tests.py @@ -11,7 +11,8 @@ class TestAuthenticationBase(testing.TestCase): def test_get_options(self): auth_base = auth.AuthenticationBase() - self.assertRaises(NotImplementedError, auth_base.get_options, {}) + self.assertEqual(auth_base.get_options({}), {}) + self.assertEqual(auth_base.get_headers(), {}) class TestBasicAuthentication(testing.TestCase): diff --git a/SoftLayer/tests/deprecated_tests.py b/SoftLayer/tests/deprecated_tests.py new file mode 100644 index 000000000..096b1c28d --- /dev/null +++ b/SoftLayer/tests/deprecated_tests.py @@ -0,0 +1,33 @@ +""" + SoftLayer.tests.depecated_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +import mock + +import SoftLayer +import SoftLayer.API +from SoftLayer import testing + + +class DeprecatedAuth(SoftLayer.AuthenticationBase): + """Auth that only implements get_headers().""" + + def get_headers(self): + return {'deprecated': 'header'} + + +class APIClient(testing.TestCase): + def set_up(self): + self.client = SoftLayer.Client(auth=DeprecatedAuth()) + + @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') + def test_simple_call(self, make_xml_rpc_api_call): + self.client['SERVICE'].METHOD() + make_xml_rpc_api_call.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, + headers={'deprecated': 'header'}, + proxy=mock.ANY, + timeout=mock.ANY, + http_headers=mock.ANY) From fc98a222070874871bf4889ec3f1687da6413fd8 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 19 Sep 2014 11:36:54 -0500 Subject: [PATCH 3/3] Adds better test/implementation for deprecated functionality --- SoftLayer/API.py | 10 +++++----- SoftLayer/tests/deprecated_tests.py | 24 +++++++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index c306e1ae2..e1956c92a 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -184,14 +184,14 @@ def call(self, service, method, *args, **kwargs): } if self.auth: - if getattr(self.auth, "get_headers", None): - warnings.warn("auth.get_headers() is deprecation and will be " + extra_headers = self.auth.get_headers() + if extra_headers: + warnings.warn("auth.get_headers() is deprecated and will be " "removed in the next major version", DeprecationWarning) - headers.update(self.auth.get_headers()) + headers.update(extra_headers) - if getattr(self.auth, "get_options", None): - options = self.auth.get_options(options) + options = self.auth.get_options(options) return transports.make_xml_rpc_api_call(uri, method, args, **options) diff --git a/SoftLayer/tests/deprecated_tests.py b/SoftLayer/tests/deprecated_tests.py index 096b1c28d..207d8e0ed 100644 --- a/SoftLayer/tests/deprecated_tests.py +++ b/SoftLayer/tests/deprecated_tests.py @@ -4,6 +4,8 @@ :license: MIT, see LICENSE for more details. """ +import warnings + import mock import SoftLayer @@ -24,10 +26,18 @@ def set_up(self): @mock.patch('SoftLayer.transports.make_xml_rpc_api_call') def test_simple_call(self, make_xml_rpc_api_call): - self.client['SERVICE'].METHOD() - make_xml_rpc_api_call.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, - headers={'deprecated': 'header'}, - proxy=mock.ANY, - timeout=mock.ANY, - http_headers=mock.ANY) + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + self.client['SERVICE'].METHOD() + + make_xml_rpc_api_call.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, + headers={'deprecated': 'header'}, + proxy=mock.ANY, + timeout=mock.ANY, + http_headers=mock.ANY) + self.assertEqual(len(w), 1) + self.assertEqual(w[0].category, DeprecationWarning) + self.assertIn("deprecated", str(w[0].message))