Skip to content

added support for kerberos proxy #542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
'pytest-asyncio==0.21.0',
'aiohttp>=3.8.4',
'aiofiles>=23.1.0',
'requests-kerberos>=0.14.0'
'requests-kerberos>=0.15.0'
]

INSTALL_REQUIRES = [
Expand Down Expand Up @@ -48,7 +48,7 @@
'uwsgi': ['uwsgi>=2.0.0'],
'cpphash': ['mmh3cffi==0.2.1'],
'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0'],
'kerberos': ['requests-kerberos>=0.14.0']
'kerberos': ['requests-kerberos>=0.15.0']
},
setup_requires=['pytest-runner', 'pluggy==1.0.0;python_version<"3.8"'],
classifiers=[
Expand Down
135 changes: 78 additions & 57 deletions splitio/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import abc
import logging
import json
from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL
import threading
from urllib3.util import parse_url

from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL
from splitio.client.config import AuthenticateScheme
from splitio.optional.loaders import aiohttp
from splitio.util.time import get_current_epoch_time_ms
Expand Down Expand Up @@ -69,6 +71,24 @@ def __init__(self, message):
"""
Exception.__init__(self, message)

class HTTPAdapterWithProxyKerberosAuth(requests.adapters.HTTPAdapter):
"""HTTPAdapter override for Kerberos Proxy auth"""

def __init__(self, principal=None, password=None):
requests.adapters.HTTPAdapter.__init__(self)
self._principal = principal
self._password = password

def proxy_headers(self, proxy):
headers = {}
if self._principal is not None:
auth = HTTPKerberosAuth(principal=self._principal, password=self._password)
else:
auth = HTTPKerberosAuth()
negotiate_details = auth.generate_request_header(None, parse_url(proxy).host, is_preemptive=True)
headers['Proxy-Authorization'] = negotiate_details
return headers

class HttpClientBase(object, metaclass=abc.ABCMeta):
"""HttpClient wrapper template."""

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

def _get_headers(self, extra_headers, sdk_key):
headers = _build_basic_headers(sdk_key)
if extra_headers is not None:
headers.update(extra_headers)
return headers

class HttpClient(HttpClientBase):
"""HttpClient wrapper."""
Expand All @@ -112,10 +137,12 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t
:param telemetry_url: Optional alternative telemetry URL.
:type telemetry_url: str
"""
_LOGGER.debug("Initializing httpclient")
self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
self._authentication_scheme = authentication_scheme
self._authentication_params = authentication_params
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
self._lock = threading.RLock()

def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments
"""
Expand All @@ -135,25 +162,22 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint:
:return: Tuple of status_code & response text
:rtype: HttpResponse
"""
headers = _build_basic_headers(sdk_key)
if extra_headers is not None:
headers.update(extra_headers)

authentication = self._get_authentication()
start = get_current_epoch_time_ms()
try:
response = requests.get(
_build_url(server, path, self._urls),
params=query,
headers=headers,
timeout=self._timeout,
auth=authentication
)
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
return HttpResponse(response.status_code, response.text, response.headers)

except Exception as exc: # pylint: disable=broad-except
raise HttpClientException('requests library is throwing exceptions') from exc
with self._lock:
start = get_current_epoch_time_ms()
with requests.Session() as session:
self._set_authentication(session)
try:
response = session.get(
_build_url(server, path, self._urls),
params=query,
headers=self._get_headers(extra_headers, sdk_key),
timeout=self._timeout
)
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
return HttpResponse(response.status_code, response.text, response.headers)

except Exception as exc: # pylint: disable=broad-except
raise HttpClientException('requests library is throwing exceptions') from exc

def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments
"""
Expand All @@ -175,36 +199,37 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): #
:return: Tuple of status_code & response text
:rtype: HttpResponse
"""
headers = _build_basic_headers(sdk_key)

if extra_headers is not None:
headers.update(extra_headers)

authentication = self._get_authentication()
start = get_current_epoch_time_ms()
try:
response = requests.post(
_build_url(server, path, self._urls),
json=body,
params=query,
headers=headers,
timeout=self._timeout,
auth=authentication
)
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
return HttpResponse(response.status_code, response.text, response.headers)

except Exception as exc: # pylint: disable=broad-except
raise HttpClientException('requests library is throwing exceptions') from exc

def _get_authentication(self):
authentication = None
if self._authentication_scheme == AuthenticateScheme.KERBEROS:
if self._authentication_params is not None:
authentication = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL)
with self._lock:
start = get_current_epoch_time_ms()
with requests.Session() as session:
self._set_authentication(session)
try:
response = session.post(
_build_url(server, path, self._urls),
json=body,
params=query,
headers=self._get_headers(extra_headers, sdk_key),
timeout=self._timeout,
)
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
return HttpResponse(response.status_code, response.text, response.headers)
except Exception as exc: # pylint: disable=broad-except
raise HttpClientException('requests library is throwing exceptions') from exc

def _set_authentication(self, session):
if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO:
_LOGGER.debug("Using Kerberos Spnego Authentication")
if self._authentication_params != [None, None]:
session.auth = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL)
else:
session.auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
elif self._authentication_scheme == AuthenticateScheme.KERBEROS_PROXY:
_LOGGER.debug("Using Kerberos Proxy Authentication")
if self._authentication_params != [None, None]:
session.mount('https://', HTTPAdapterWithProxyKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1]))
else:
authentication = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
return authentication
session.mount('https://', HTTPAdapterWithProxyKerberosAuth())


def _record_telemetry(self, status_code, elapsed):
"""
Expand All @@ -220,8 +245,8 @@ def _record_telemetry(self, status_code, elapsed):
if 200 <= status_code < 300:
self._telemetry_runtime_producer.record_successful_sync(self._metric_name, get_current_epoch_time_ms())
return
self._telemetry_runtime_producer.record_sync_error(self._metric_name, status_code)

self._telemetry_runtime_producer.record_sync_error(self._metric_name, status_code)

class HttpClientAsync(HttpClientBase):
"""HttpClientAsync wrapper."""
Expand Down Expand Up @@ -260,10 +285,8 @@ async def get(self, server, path, apikey, query=None, extra_headers=None): # py
:return: Tuple of status_code & response text
:rtype: HttpResponse
"""
headers = _build_basic_headers(apikey)
if extra_headers is not None:
headers.update(extra_headers)
start = get_current_epoch_time_ms()
headers = self._get_headers(extra_headers, apikey)
try:
url = _build_url(server, path, self._urls)
_LOGGER.debug("GET request: %s", url)
Expand Down Expand Up @@ -303,9 +326,7 @@ async def post(self, server, path, apikey, body, query=None, extra_headers=None)
:return: Tuple of status_code & response text
:rtype: HttpResponse
"""
headers = _build_basic_headers(apikey)
if extra_headers is not None:
headers.update(extra_headers)
headers = self._get_headers(extra_headers, apikey)
start = get_current_epoch_time_ms()
try:
headers['Accept-Encoding'] = 'gzip'
Expand Down
6 changes: 3 additions & 3 deletions splitio/client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
class AuthenticateScheme(Enum):
"""Authentication Scheme."""
NONE = 'NONE'
KERBEROS = 'KERBEROS'

KERBEROS_SPNEGO = 'KERBEROS_SPNEGO'
KERBEROS_PROXY = 'KERBEROS_PROXY'

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

Expand Down
2 changes: 1 addition & 1 deletion splitio/client/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl
telemetry_init_producer = telemetry_producer.get_telemetry_init_producer()

authentication_params = None
if cfg.get("httpAuthenticateScheme") == AuthenticateScheme.KERBEROS:
if cfg.get("httpAuthenticateScheme") in [AuthenticateScheme.KERBEROS_SPNEGO, AuthenticateScheme.KERBEROS_PROXY]:
authentication_params = [cfg.get("kerberosPrincipalUser"),
cfg.get("kerberosPrincipalPassword")]

Expand Down
2 changes: 1 addition & 1 deletion splitio/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '10.1.0rc1'
__version__ = '10.1.0rc2'
Loading
Loading