Skip to content

Commit 521cbec

Browse files
authored
30514: Upgrade Python to use API Session Key (#10)
* Issue 30514: Upgrade Python to use API Session Key -- Also update automated tests * Issue 30514: Upgrade Python to use API Session Key -- Code review: don't add apikey header if not specified -- Fix up automated tests * API Key: move to session header - remove SafeTLSAdapter in favor of normal TLS convention - improve exception messaging when raising ServerContextError * Update .gitignore
1 parent 8413072 commit 521cbec

File tree

6 files changed

+59
-35
lines changed

6 files changed

+59
-35
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Build Artifacts
22
*.py[cod]
33
build/
4-
dist/
4+
dist/
5+
6+
# Project Artifacts
7+
.idea/

labkey/exceptions.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
# base exception class for server responses
20-
class RequestError(exceptions.HTTPError):
20+
class RequestError(exceptions.RequestException):
2121
default_msg = 'Server Error'
2222

2323
def __init__(self, server_response, **kwargs):
@@ -62,14 +62,21 @@ class ServerNotFoundError(RequestError):
6262
default_msg = 'Server resource not found. Please verify context path and project path are valid'
6363

6464

65-
class ServerContextError(exceptions.HTTPError):
66-
def __init__(self, inner_exception=None):
67-
self.message = self._get_message(inner_exception)
65+
class ServerContextError(RequestError):
66+
67+
def __init__(self, server_context, inner_exception):
68+
self.message = self._get_message(server_context, inner_exception)
6869
self.exception = inner_exception
6970

70-
def _get_message(self, e):
71+
@staticmethod
72+
def _get_message(server_context, e):
7173
switcher = {
74+
exceptions.ConnectionError:
75+
'Failed to connect to server. Ensure the server_context domain, context_path, '
76+
'and SSL are configured correctly.',
77+
exceptions.InvalidURL:
78+
'Failed to parse URL. Context is ' + str(server_context),
7279
exceptions.SSLError:
7380
'Failed to match server SSL configuration. Ensure the server_context is configured correctly.'
7481
}
75-
return switcher.get(type(e), 'Please verify server_context is configured correctly')
82+
return switcher.get(type(e), 'Please verify server_context is configured correctly.')

labkey/utils.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,18 @@
1616
from __future__ import unicode_literals
1717

1818
import requests
19-
import ssl
2019

21-
from requests.adapters import HTTPAdapter
22-
from requests.exceptions import SSLError
23-
from requests.packages.urllib3.poolmanager import PoolManager
24-
from labkey.exceptions import RequestError, RequestAuthorizationError, QueryNotFoundError, \
25-
ServerContextError, ServerNotFoundError
20+
from requests.exceptions import RequestException
21+
from labkey.exceptions import RequestError, RequestAuthorizationError, QueryNotFoundError, ServerContextError, \
22+
ServerNotFoundError
2623

2724
__default_timeout = 60 * 5 # 5 minutes
25+
API_KEY_TOKEN = 'apikey'
2826
CSRF_TOKEN = 'X-LABKEY-CSRF'
2927
DISABLE_CSRF_CHECK = False # Used by tests to disable CSRF token check
3028

3129

32-
def create_server_context(domain, container_path, context_path=None, use_ssl=True):
30+
def create_server_context(domain, container_path, context_path=None, use_ssl=True, api_key=None):
3331
"""
3432
Create a LabKey server context. This context is used to encapsulate properties
3533
about the LabKey server that is being requested against. This includes, but is not limited to,
@@ -38,9 +36,17 @@ def create_server_context(domain, container_path, context_path=None, use_ssl=Tru
3836
:param container_path:
3937
:param context_path:
4038
:param use_ssl:
39+
:param api_key:
4140
:return:
4241
"""
43-
config = dict(domain=domain, container_path=container_path, context_path=context_path, use_ssl=use_ssl)
42+
config = dict(
43+
domain=domain,
44+
container_path=container_path,
45+
context_path=context_path,
46+
use_ssl=use_ssl,
47+
api_key=api_key
48+
)
49+
4450
return ServerContext(**config)
4551

4652

@@ -57,6 +63,12 @@ def build_url(server_context, controller, action, container_path=None):
5763
return server_context.build_url(controller, action, container_path=container_path)
5864

5965

66+
def handle_request_exception(e, server_context=None):
67+
if type(e) in [RequestAuthorizationError, QueryNotFoundError, ServerNotFoundError]:
68+
raise e
69+
raise ServerContextError(server_context, e)
70+
71+
6072
def handle_response(response):
6173
sc = response.status_code
6274

@@ -81,32 +93,23 @@ def handle_response(response):
8193
# could not decode response
8294
raise ServerNotFoundError(response)
8395
else:
96+
# consider response.raise_for_status()
8497
raise RequestError(response)
8598

8699

87-
# _ssl.c:504: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure
88-
# http://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
89-
class SafeTLSAdapter(HTTPAdapter):
90-
def init_poolmanager(self, connections, maxsize, block=False):
91-
self.poolmanager = PoolManager(num_pools=connections,
92-
maxsize=maxsize,
93-
block=block,
94-
ssl_version=ssl.PROTOCOL_TLSv1)
95-
96-
97100
class ServerContext(object):
98101

99102
def __init__(self, **kwargs):
100103
self._container_path = kwargs.pop('container_path', None)
101104
self._context_path = kwargs.pop('context_path', None)
102105
self._domain = kwargs.pop('domain', None)
103106
self._use_ssl = kwargs.pop('use_ssl', True)
107+
self._api_key = kwargs.pop('api_key', None)
104108

105109
self._session = requests.Session()
106110

107111
if self._use_ssl:
108112
self._scheme = 'https://'
109-
self._session.mount(self._scheme, SafeTLSAdapter())
110113
else:
111114
self._scheme = 'http://'
112115

@@ -129,6 +132,14 @@ def build_url(self, controller, action, container_path=None):
129132

130133
def make_request(self, url, payload, headers=None, timeout=300, method='POST'):
131134

135+
if self._api_key is not None:
136+
global API_KEY_TOKEN
137+
138+
if self._session.headers.get(API_KEY_TOKEN) is not self._api_key:
139+
self._session.headers.update({
140+
API_KEY_TOKEN: self._api_key
141+
})
142+
132143
if not DISABLE_CSRF_CHECK:
133144
global CSRF_TOKEN
134145

@@ -140,14 +151,14 @@ def make_request(self, url, payload, headers=None, timeout=300, method='POST'):
140151
self._session.headers.update({
141152
CSRF_TOKEN: response['CSRF']
142153
})
143-
except SSLError as e:
144-
raise ServerContextError(e)
154+
except RequestException as e:
155+
handle_request_exception(e, server_context=self)
145156

146157
try:
147158
if method is 'GET':
148159
raw_response = self._session.get(url, params=payload, headers=headers, timeout=timeout)
149160
else:
150161
raw_response = self._session.post(url, data=payload, headers=headers, timeout=timeout)
151162
return handle_response(raw_response)
152-
except SSLError as e:
153-
raise ServerContextError(e)
163+
except RequestException as e:
164+
handle_request_exception(e, server_context=self)

test/test_query_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def setUp(self):
5959
self.expected_kwargs = {
6060
'expected_args': [self.service.get_server_url()],
6161
'data': '{"queryName": "' + query + '", "rows": "{id:1234}", "schemaName": "' + schema + '"}',
62-
'headers': {u'Content-Type': u'application/json'},
62+
'headers': {'Content-Type': 'application/json'},
6363
'timeout': 300
6464
}
6565

@@ -101,7 +101,7 @@ def setUp(self):
101101
self.expected_kwargs = {
102102
'expected_args': [self.service.get_server_url()],
103103
'data': '{"queryName": "' + query + '", "rows": "{id:1234}", "schemaName": "' + schema + '"}',
104-
'headers': {u'Content-Type': u'application/json'},
104+
'headers': {'Content-Type': 'application/json'},
105105
'timeout': 300
106106
}
107107

@@ -143,7 +143,7 @@ def setUp(self):
143143
self.expected_kwargs = {
144144
'expected_args': [self.service.get_server_url()],
145145
'data': '{"queryName": "' + query + '", "rows": "{id:1234}", "schemaName": "' + schema + '"}',
146-
'headers': {u'Content-Type': u'application/json'},
146+
'headers': {'Content-Type': 'application/json'},
147147
'timeout': 300
148148
}
149149

test/test_security.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def setUp(self):
509509
}
510510

511511
self.args = [
512-
mock_server_context(self.service),
512+
mock_server_context(self.service)
513513
]
514514

515515
def test_success(self):

test/test_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ def mock_server_context(mock_action):
2929
with mock.patch('labkey.utils.requests.sessions.Session.get') as mock_get:
3030

3131
mock_get.return_value = mock_action.get_csrf_response()
32-
return create_server_context(mock_action.server_name, mock_action.project_path, mock_action.context_path)
32+
return create_server_context(mock_action.server_name, mock_action.project_path, mock_action.context_path,
33+
api_key=mock_action.api_key)
3334

3435

3536
def success_test(test, expected_response, api_method, compare_response, *args, **expected_kwargs):
@@ -83,6 +84,7 @@ class MockLabKey:
8384
default_server_not_found_body = ''
8485
default_query_not_found_body = ''
8586
default_general_server_error_body = ''
87+
default_api_key = None
8688

8789
def __init__(self, **kwargs):
8890
self.protocol = kwargs.pop('protocol', self.default_protocol)
@@ -95,6 +97,7 @@ def __init__(self, **kwargs):
9597
self.server_not_found_body = kwargs.pop('server_not_found_body', self.default_server_not_found_body)
9698
self.query_not_found_body = kwargs.pop('query_not_found_body', self.default_query_not_found_body)
9799
self.general_server_error_body = kwargs.pop('general_server_error_body', self.default_general_server_error_body)
100+
self.api_key = kwargs.pop('api_key', self.default_api_key)
98101

99102
def _get_mock_response(self, code, url, body):
100103
mock_response = mock.Mock(requests.Response)

0 commit comments

Comments
 (0)