Skip to content

Commit 38bd316

Browse files
authored
Merge pull request #542 from splitio/kerberos-proxy-support
added support for kerberos proxy
2 parents 6384570 + 22dad08 commit 38bd316

File tree

7 files changed

+183
-93
lines changed

7 files changed

+183
-93
lines changed

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
'pytest-asyncio==0.21.0',
1818
'aiohttp>=3.8.4',
1919
'aiofiles>=23.1.0',
20-
'requests-kerberos>=0.14.0'
20+
'requests-kerberos>=0.15.0'
2121
]
2222

2323
INSTALL_REQUIRES = [
@@ -48,7 +48,7 @@
4848
'uwsgi': ['uwsgi>=2.0.0'],
4949
'cpphash': ['mmh3cffi==0.2.1'],
5050
'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0'],
51-
'kerberos': ['requests-kerberos>=0.14.0']
51+
'kerberos': ['requests-kerberos>=0.15.0']
5252
},
5353
setup_requires=['pytest-runner', 'pluggy==1.0.0;python_version<"3.8"'],
5454
classifiers=[

splitio/api/client.py

Lines changed: 78 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import abc
66
import logging
77
import json
8-
from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL
8+
import threading
9+
from urllib3.util import parse_url
910

11+
from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL
1012
from splitio.client.config import AuthenticateScheme
1113
from splitio.optional.loaders import aiohttp
1214
from splitio.util.time import get_current_epoch_time_ms
@@ -69,6 +71,24 @@ def __init__(self, message):
6971
"""
7072
Exception.__init__(self, message)
7173

74+
class HTTPAdapterWithProxyKerberosAuth(requests.adapters.HTTPAdapter):
75+
"""HTTPAdapter override for Kerberos Proxy auth"""
76+
77+
def __init__(self, principal=None, password=None):
78+
requests.adapters.HTTPAdapter.__init__(self)
79+
self._principal = principal
80+
self._password = password
81+
82+
def proxy_headers(self, proxy):
83+
headers = {}
84+
if self._principal is not None:
85+
auth = HTTPKerberosAuth(principal=self._principal, password=self._password)
86+
else:
87+
auth = HTTPKerberosAuth()
88+
negotiate_details = auth.generate_request_header(None, parse_url(proxy).host, is_preemptive=True)
89+
headers['Proxy-Authorization'] = negotiate_details
90+
return headers
91+
7292
class HttpClientBase(object, metaclass=abc.ABCMeta):
7393
"""HttpClient wrapper template."""
7494

@@ -93,6 +113,11 @@ def set_telemetry_data(self, metric_name, telemetry_runtime_producer):
93113
self._telemetry_runtime_producer = telemetry_runtime_producer
94114
self._metric_name = metric_name
95115

116+
def _get_headers(self, extra_headers, sdk_key):
117+
headers = _build_basic_headers(sdk_key)
118+
if extra_headers is not None:
119+
headers.update(extra_headers)
120+
return headers
96121

97122
class HttpClient(HttpClientBase):
98123
"""HttpClient wrapper."""
@@ -112,10 +137,12 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t
112137
:param telemetry_url: Optional alternative telemetry URL.
113138
:type telemetry_url: str
114139
"""
140+
_LOGGER.debug("Initializing httpclient")
115141
self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
142+
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
116143
self._authentication_scheme = authentication_scheme
117144
self._authentication_params = authentication_params
118-
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
145+
self._lock = threading.RLock()
119146

120147
def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments
121148
"""
@@ -135,25 +162,22 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint:
135162
:return: Tuple of status_code & response text
136163
:rtype: HttpResponse
137164
"""
138-
headers = _build_basic_headers(sdk_key)
139-
if extra_headers is not None:
140-
headers.update(extra_headers)
141-
142-
authentication = self._get_authentication()
143-
start = get_current_epoch_time_ms()
144-
try:
145-
response = requests.get(
146-
_build_url(server, path, self._urls),
147-
params=query,
148-
headers=headers,
149-
timeout=self._timeout,
150-
auth=authentication
151-
)
152-
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
153-
return HttpResponse(response.status_code, response.text, response.headers)
154-
155-
except Exception as exc: # pylint: disable=broad-except
156-
raise HttpClientException('requests library is throwing exceptions') from exc
165+
with self._lock:
166+
start = get_current_epoch_time_ms()
167+
with requests.Session() as session:
168+
self._set_authentication(session)
169+
try:
170+
response = session.get(
171+
_build_url(server, path, self._urls),
172+
params=query,
173+
headers=self._get_headers(extra_headers, sdk_key),
174+
timeout=self._timeout
175+
)
176+
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
177+
return HttpResponse(response.status_code, response.text, response.headers)
178+
179+
except Exception as exc: # pylint: disable=broad-except
180+
raise HttpClientException('requests library is throwing exceptions') from exc
157181

158182
def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments
159183
"""
@@ -175,36 +199,37 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): #
175199
:return: Tuple of status_code & response text
176200
:rtype: HttpResponse
177201
"""
178-
headers = _build_basic_headers(sdk_key)
179-
180-
if extra_headers is not None:
181-
headers.update(extra_headers)
182-
183-
authentication = self._get_authentication()
184-
start = get_current_epoch_time_ms()
185-
try:
186-
response = requests.post(
187-
_build_url(server, path, self._urls),
188-
json=body,
189-
params=query,
190-
headers=headers,
191-
timeout=self._timeout,
192-
auth=authentication
193-
)
194-
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
195-
return HttpResponse(response.status_code, response.text, response.headers)
196-
197-
except Exception as exc: # pylint: disable=broad-except
198-
raise HttpClientException('requests library is throwing exceptions') from exc
199-
200-
def _get_authentication(self):
201-
authentication = None
202-
if self._authentication_scheme == AuthenticateScheme.KERBEROS:
203-
if self._authentication_params is not None:
204-
authentication = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL)
202+
with self._lock:
203+
start = get_current_epoch_time_ms()
204+
with requests.Session() as session:
205+
self._set_authentication(session)
206+
try:
207+
response = session.post(
208+
_build_url(server, path, self._urls),
209+
json=body,
210+
params=query,
211+
headers=self._get_headers(extra_headers, sdk_key),
212+
timeout=self._timeout,
213+
)
214+
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
215+
return HttpResponse(response.status_code, response.text, response.headers)
216+
except Exception as exc: # pylint: disable=broad-except
217+
raise HttpClientException('requests library is throwing exceptions') from exc
218+
219+
def _set_authentication(self, session):
220+
if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO:
221+
_LOGGER.debug("Using Kerberos Spnego Authentication")
222+
if self._authentication_params != [None, None]:
223+
session.auth = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL)
224+
else:
225+
session.auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
226+
elif self._authentication_scheme == AuthenticateScheme.KERBEROS_PROXY:
227+
_LOGGER.debug("Using Kerberos Proxy Authentication")
228+
if self._authentication_params != [None, None]:
229+
session.mount('https://', HTTPAdapterWithProxyKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1]))
205230
else:
206-
authentication = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
207-
return authentication
231+
session.mount('https://', HTTPAdapterWithProxyKerberosAuth())
232+
208233

209234
def _record_telemetry(self, status_code, elapsed):
210235
"""
@@ -220,8 +245,8 @@ def _record_telemetry(self, status_code, elapsed):
220245
if 200 <= status_code < 300:
221246
self._telemetry_runtime_producer.record_successful_sync(self._metric_name, get_current_epoch_time_ms())
222247
return
223-
self._telemetry_runtime_producer.record_sync_error(self._metric_name, status_code)
224248

249+
self._telemetry_runtime_producer.record_sync_error(self._metric_name, status_code)
225250

226251
class HttpClientAsync(HttpClientBase):
227252
"""HttpClientAsync wrapper."""
@@ -260,10 +285,8 @@ async def get(self, server, path, apikey, query=None, extra_headers=None): # py
260285
:return: Tuple of status_code & response text
261286
:rtype: HttpResponse
262287
"""
263-
headers = _build_basic_headers(apikey)
264-
if extra_headers is not None:
265-
headers.update(extra_headers)
266288
start = get_current_epoch_time_ms()
289+
headers = self._get_headers(extra_headers, apikey)
267290
try:
268291
url = _build_url(server, path, self._urls)
269292
_LOGGER.debug("GET request: %s", url)
@@ -303,9 +326,7 @@ async def post(self, server, path, apikey, body, query=None, extra_headers=None)
303326
:return: Tuple of status_code & response text
304327
:rtype: HttpResponse
305328
"""
306-
headers = _build_basic_headers(apikey)
307-
if extra_headers is not None:
308-
headers.update(extra_headers)
329+
headers = self._get_headers(extra_headers, apikey)
309330
start = get_current_epoch_time_ms()
310331
try:
311332
headers['Accept-Encoding'] = 'gzip'

splitio/client/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
class AuthenticateScheme(Enum):
1313
"""Authentication Scheme."""
1414
NONE = 'NONE'
15-
KERBEROS = 'KERBEROS'
16-
15+
KERBEROS_SPNEGO = 'KERBEROS_SPNEGO'
16+
KERBEROS_PROXY = 'KERBEROS_PROXY'
1717

1818
DEFAULT_CONFIG = {
1919
'operationMode': 'standalone',
@@ -164,7 +164,7 @@ def sanitize(sdk_key, config):
164164
except (ValueError, AttributeError):
165165
authenticate_scheme = AuthenticateScheme.NONE
166166
_LOGGER.warning('You passed an invalid HttpAuthenticationScheme, HttpAuthenticationScheme should be ' \
167-
'one of the following values: `none` or `kerberos`. '
167+
'one of the following values: `none`, `kerberos_proxy` or `kerberos_spnego`. '
168168
' Defaulting to `none` mode.')
169169
processed["httpAuthenticateScheme"] = authenticate_scheme
170170

splitio/client/factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl
509509
telemetry_init_producer = telemetry_producer.get_telemetry_init_producer()
510510

511511
authentication_params = None
512-
if cfg.get("httpAuthenticateScheme") == AuthenticateScheme.KERBEROS:
512+
if cfg.get("httpAuthenticateScheme") in [AuthenticateScheme.KERBEROS_SPNEGO, AuthenticateScheme.KERBEROS_PROXY]:
513513
authentication_params = [cfg.get("kerberosPrincipalUser"),
514514
cfg.get("kerberosPrincipalPassword")]
515515

splitio/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '10.1.0rc1'
1+
__version__ = '10.1.0rc2'

0 commit comments

Comments
 (0)