From 1719b7f97cd0d4049d730ae4231dc8d33369559a Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Wed, 5 Jun 2024 19:57:55 -0700 Subject: [PATCH 01/14] added support for spnego/kerberos auth --- setup.py | 17 ++++++------ splitio/api/client.py | 26 ++++++++++++++++-- splitio/client/config.py | 22 ++++++++++++++- splitio/client/factory.py | 11 ++++++-- tests/api/test_httpclient.py | 53 ++++++++++++++++++++++++++++++------ tests/client/test_config.py | 10 +++++++ 6 files changed, 117 insertions(+), 22 deletions(-) diff --git a/setup.py b/setup.py index 766b88e2..86b1e832 100644 --- a/setup.py +++ b/setup.py @@ -6,21 +6,22 @@ TESTS_REQUIRES = [ 'flake8', - 'pytest==7.1.0', - 'pytest-mock==3.11.1', - 'coverage==7.2.7', + 'pytest==7.0.1', + 'pytest-mock==3.13.0', + 'coverage==6.2', 'pytest-cov', - 'importlib-metadata==6.7', - 'tomli', - 'iniconfig', - 'attrs' + 'importlib-metadata==4.2', + 'tomli==1.2.3', + 'iniconfig==1.1.1', + 'attrs==22.1.0' ] INSTALL_REQUIRES = [ 'requests', 'pyyaml', 'docopt>=0.6.2', - 'bloom-filter2>=2.0.0' + 'bloom-filter2>=2.0.0', + 'requests-kerberos>=0.14.0' ] with open(path.join(path.abspath(path.dirname(__file__)), 'splitio', 'version.py')) as f: diff --git a/splitio/api/client.py b/splitio/api/client.py index c58d14e9..2e289c13 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -3,6 +3,10 @@ import requests import logging +from requests_kerberos import HTTPKerberosAuth, OPTIONAL + +from splitio.client.config import AuthenticateScheme + _LOGGER = logging.getLogger(__name__) HttpResponse = namedtuple('HttpResponse', ['status_code', 'body']) @@ -28,7 +32,7 @@ class HttpClient(object): AUTH_URL = 'https://auth.split.io/api' TELEMETRY_URL = 'https://telemetry.split.io/api' - def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None): + def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None): """ Class constructor. @@ -50,6 +54,8 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t 'auth': auth_url if auth_url is not None else self.AUTH_URL, 'telemetry': telemetry_url if telemetry_url is not None else self.TELEMETRY_URL, } + self._authentication_scheme = authentication_scheme + self._authentication_params = authentication_params def _build_url(self, server, path): """ @@ -100,14 +106,17 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: if extra_headers is not None: headers.update(extra_headers) + authentication = self._get_authentication() try: response = requests.get( self._build_url(server, path), params=query, headers=headers, - timeout=self._timeout + timeout=self._timeout, + auth=authentication ) return HttpResponse(response.status_code, response.text) + except Exception as exc: # pylint: disable=broad-except raise HttpClientException('requests library is throwing exceptions') from exc @@ -136,14 +145,25 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # if extra_headers is not None: headers.update(extra_headers) + authentication = self._get_authentication() try: response = requests.post( self._build_url(server, path), json=body, params=query, headers=headers, - timeout=self._timeout + timeout=self._timeout, + auth=authentication ) return HttpResponse(response.status_code, response.text) 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) + else: + authentication = HTTPKerberosAuth(mutual_authentication=OPTIONAL) + return authentication \ No newline at end of file diff --git a/splitio/client/config.py b/splitio/client/config.py index 1789e0b9..55b7f936 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -1,6 +1,7 @@ """Default settings for the Split.IO SDK Python client.""" import os.path import logging +from enum import Enum from splitio.engine.impressions import ImpressionsMode from splitio.client.input_validator import validate_flag_sets @@ -9,6 +10,12 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 +class AuthenticateScheme(Enum): + """Authentication Scheme.""" + NONE = 'NONE' + KERBEROS = 'KERBEROS' + + DEFAULT_CONFIG = { 'operationMode': 'standalone', 'connectionTimeout': 1500, @@ -60,7 +67,10 @@ 'storageWrapper': None, 'storagePrefix': None, 'storageType': None, - 'flagSetsFilter': None + 'flagSetsFilter': None, + 'httpAuthenticateScheme': AuthenticateScheme.NONE, + 'kerberosPrincipalUser': None, + 'kerberosPrincipalPassword': None } def _parse_operation_mode(sdk_key, config): @@ -149,4 +159,14 @@ def sanitize(sdk_key, config): else: processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None + if config.get('httpAuthenticateScheme') is not None: + try: + authenticate_scheme = AuthenticateScheme(config['httpAuthenticateScheme'].upper()) + 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`. ' + ' Defaulting to `none` mode.') + processed["httpAuthenticateScheme"] = authenticate_scheme + return processed diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 5ac809cc..142063a6 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -8,7 +8,7 @@ from splitio.client.client import Client from splitio.client import input_validator from splitio.client.manager import SplitManager -from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING +from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING, AuthenticateScheme from splitio.client import util from splitio.client.listener import ImpressionListenerWrapper from splitio.engine.impressions.impressions import Manager as ImpressionsManager @@ -332,12 +332,19 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() telemetry_init_producer = telemetry_producer.get_telemetry_init_producer() + authentication_params = None + if cfg.get("httpAuthenticateScheme") == AuthenticateScheme.KERBEROS: + authentication_params = [cfg.get("kerberosPrincipalUser"), + cfg.get("kerberosPrincipalPassword")] + http_client = HttpClient( sdk_url=sdk_url, events_url=events_url, auth_url=auth_api_base_url, telemetry_url=telemetry_api_base_url, - timeout=cfg.get('connectionTimeout') + timeout=cfg.get('connectionTimeout'), + authentication_scheme = cfg.get("httpAuthenticateScheme"), + authentication_params = authentication_params ) sdk_metadata = util.get_metadata(cfg) diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index 694c9a22..94110b68 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -1,6 +1,8 @@ """HTTPClient test module.""" +from requests_kerberos import HTTPKerberosAuth, OPTIONAL from splitio.api import client +from splitio.client.config import AuthenticateScheme class HttpClientTests(object): """Http Client test cases.""" @@ -19,7 +21,8 @@ def test_get(self, mocker): client.HttpClient.SDK_URL + '/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -31,7 +34,8 @@ def test_get(self, mocker): client.HttpClient.EVENTS_URL + '/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert get_mock.mock_calls == [call] assert response.status_code == 200 @@ -51,7 +55,8 @@ def test_get_custom_urls(self, mocker): 'https://sdk.com/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert get_mock.mock_calls == [call] assert response.status_code == 200 @@ -63,7 +68,8 @@ def test_get_custom_urls(self, mocker): 'https://events.com/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -85,7 +91,8 @@ def test_post(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -98,7 +105,8 @@ def test_post(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -119,7 +127,8 @@ def test_post_custom_urls(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -132,8 +141,36 @@ def test_post_custom_urls(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None + timeout=None, + auth=None ) assert response.status_code == 200 assert response.body == 'ok' assert get_mock.mock_calls == [call] + + def test_authentication_scheme(self, mocker): + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.text = 'ok' + get_mock = mocker.Mock() + get_mock.return_value = response_mock + mocker.patch('splitio.api.client.requests.get', new=get_mock) + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS) + response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + call = mocker.call( + 'https://sdk.com/test1', + headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, + params={'param1': 123}, + timeout=None, + auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) + ) + + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS, authentication_params=['bilal', 'split']) + response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + call = mocker.call( + 'https://sdk.com/test1', + headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, + params={'param1': 123}, + timeout=None, + auth=HTTPKerberosAuth(principal='bilal', password='split',mutual_authentication=OPTIONAL) + ) diff --git a/tests/client/test_config.py b/tests/client/test_config.py index b4b9d9e9..19495eec 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -68,9 +68,19 @@ def test_sanitize(self): processed = config.sanitize('some', {}) assert processed['redisLocalCacheEnabled'] # check default is True assert processed['flagSetsFilter'] is None + assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE processed = config.sanitize('some', {'redisHost': 'x', 'flagSetsFilter': ['set']}) assert processed['flagSetsFilter'] is None processed = config.sanitize('some', {'storageType': 'pluggable', 'flagSetsFilter': ['set']}) assert processed['flagSetsFilter'] is None + + processed = config.sanitize('some', {'httpAuthenticateScheme': 'KERBEROS'}) + assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.KERBEROS + + processed = config.sanitize('some', {'httpAuthenticateScheme': 'anything'}) + assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE + + processed = config.sanitize('some', {'httpAuthenticateScheme': 'NONE'}) + assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE From 6573cc7063904653376fb4b8b7a4387e45a5292f Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Mon, 8 Jul 2024 14:27:47 -0700 Subject: [PATCH 02/14] moved kerberose import to loaders --- setup.py | 4 ++-- splitio/api/client.py | 2 +- splitio/optional/loaders.py | 12 ++++++++++++ splitio/version.py | 2 +- tests/api/test_httpclient.py | 2 ++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 462d1e4f..3573f835 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ 'requests', 'pyyaml', 'docopt>=0.6.2', - 'requests-kerberos>=0.14.0' 'enum34;python_version<"3.4"', 'bloom-filter2>=2.0.0' ] @@ -47,7 +46,8 @@ 'redis': ['redis>=2.10.5'], 'uwsgi': ['uwsgi>=2.0.0'], 'cpphash': ['mmh3cffi==0.2.1'], - 'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0'] + 'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0'], + 'kerberos': ['requests-kerberos>=0.14.0'] }, setup_requires=['pytest-runner', 'pluggy==1.0.0;python_version<"3.8"'], classifiers=[ diff --git a/splitio/api/client.py b/splitio/api/client.py index 0bacdb2c..b255baff 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -5,7 +5,7 @@ import abc import logging import json -from requests_kerberos import HTTPKerberosAuth, OPTIONAL +from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL from splitio.client.config import AuthenticateScheme from splitio.optional.loaders import aiohttp diff --git a/splitio/optional/loaders.py b/splitio/optional/loaders.py index 4c2e02d9..b5f11621 100644 --- a/splitio/optional/loaders.py +++ b/splitio/optional/loaders.py @@ -14,5 +14,17 @@ def missing_asyncio_dependencies(*_, **__): asyncio = missing_asyncio_dependencies aiofiles = missing_asyncio_dependencies +try: + from requests_kerberos import HTTPKerberosAuth, OPTIONAL +except ImportError: + def missing_auth_dependencies(*_, **__): + """Fail if missing dependencies are used.""" + raise NotImplementedError( + 'Missing kerberos auth dependency. ' + 'Please use `pip install splitio_client[kerberos]` to install the sdk with kerberos auth support' + ) + HTTPKerberosAuth = missing_auth_dependencies + OPTIONAL = missing_auth_dependencies + async def _anext(it): return await it.__anext__() diff --git a/splitio/version.py b/splitio/version.py index ffcd3342..8b73a574 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '10.0.1' \ No newline at end of file +__version__ = '10.1.0-rc1' \ No newline at end of file diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index d18effaf..c0530854 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -168,6 +168,7 @@ def test_authentication_scheme(self, mocker): get_mock.return_value = response_mock mocker.patch('splitio.api.client.requests.get', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS) + httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( 'https://sdk.com/test1', @@ -178,6 +179,7 @@ def test_authentication_scheme(self, mocker): ) httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS, authentication_params=['bilal', 'split']) + httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) def test_telemetry(self, mocker): From b919ad7875b150680bb1154b8f41e9cf7d580c43 Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Mon, 8 Jul 2024 15:04:18 -0700 Subject: [PATCH 03/14] fixed setup tests --- setup.py | 3 ++- splitio/version.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3573f835..ebc484dd 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,8 @@ 'attrs==22.1.0', 'pytest-asyncio==0.21.0', 'aiohttp>=3.8.4', - 'aiofiles>=23.1.0' + 'aiofiles>=23.1.0', + 'requests-kerberos>=0.14.0' ] INSTALL_REQUIRES = [ diff --git a/splitio/version.py b/splitio/version.py index 8b73a574..a671925d 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '10.1.0-rc1' \ No newline at end of file +__version__ = '10.1.0rc1' \ No newline at end of file From ce9bf50981f5be5dc3ec7a9b857520eb3fa31cb4 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Tue, 9 Jul 2024 09:23:20 -0700 Subject: [PATCH 04/14] Update ci.yml added kerberos dev lib --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52a7bf1c..26c92525 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: - name: Install dependencies run: | + apt-get install libkrb5-dev pip install -U setuptools pip wheel pip install -e .[cpphash,redis,uwsgi] From cecabd8f77302470a73a03a1cb0348e09530b5a9 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Tue, 9 Jul 2024 09:28:20 -0700 Subject: [PATCH 05/14] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26c92525..eafd6e2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | - apt-get install libkrb5-dev + sudo apt-get install -y libkrb5-dev pip install -U setuptools pip wheel pip install -e .[cpphash,redis,uwsgi] From 621d60b61566e24937d72dbcc0aec084c52a3a93 Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Tue, 16 Jul 2024 15:47:38 -0700 Subject: [PATCH 06/14] added support for kerberos proxy --- setup.py | 4 +- splitio/api/client.py | 136 ++++++++++++++++++++--------------- splitio/client/config.py | 6 +- splitio/client/factory.py | 2 +- splitio/version.py | 2 +- tests/api/test_httpclient.py | 120 ++++++++++++++++++++++++------- tests/client/test_config.py | 7 +- 7 files changed, 184 insertions(+), 93 deletions(-) diff --git a/setup.py b/setup.py index ebc484dd..10fa308f 100644 --- a/setup.py +++ b/setup.py @@ -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 = [ @@ -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=[ diff --git a/splitio/api/client.py b/splitio/api/client.py index b255baff..cafb3a84 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -5,8 +5,11 @@ import abc import logging import json -from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL +import time +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 @@ -69,6 +72,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.""" @@ -93,6 +114,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.""" @@ -112,10 +138,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 """ @@ -135,25 +163,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 """ @@ -175,36 +200,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 is not [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 is not [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): """ @@ -220,8 +246,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.""" @@ -260,10 +286,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) @@ -303,9 +327,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' diff --git a/splitio/client/config.py b/splitio/client/config.py index 60643a37..78d08b45 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -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', @@ -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 diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 27938ecd..fffb0212 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -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")] diff --git a/splitio/version.py b/splitio/version.py index a671925d..642e5ce1 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '10.1.0rc1' \ No newline at end of file +__version__ = '10.1.0rc2' \ No newline at end of file diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index c0530854..d95dcb5f 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -2,6 +2,7 @@ from requests_kerberos import HTTPKerberosAuth, OPTIONAL import pytest import unittest.mock as mock +import requests from splitio.client.config import AuthenticateScheme from splitio.api import client @@ -19,7 +20,7 @@ def test_get(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.get', new=get_mock) + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) httpclient = client.HttpClient() httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) @@ -27,8 +28,7 @@ def test_get(self, mocker): client.SDK_URL + '/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -40,8 +40,7 @@ def test_get(self, mocker): client.EVENTS_URL + '/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert get_mock.mock_calls == [call] assert response.status_code == 200 @@ -55,7 +54,7 @@ def test_get_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.get', new=get_mock) + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) @@ -63,8 +62,7 @@ def test_get_custom_urls(self, mocker): 'https://sdk.com/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert get_mock.mock_calls == [call] assert response.status_code == 200 @@ -76,8 +74,7 @@ def test_get_custom_urls(self, mocker): 'https://events.com/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -92,7 +89,7 @@ def test_post(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.post', new=get_mock) + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) httpclient = client.HttpClient() httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) @@ -101,8 +98,7 @@ def test_post(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -115,8 +111,7 @@ def test_post(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -130,7 +125,7 @@ def test_post_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.post', new=get_mock) + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) @@ -139,8 +134,7 @@ def test_post_custom_urls(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -153,8 +147,7 @@ def test_post_custom_urls(self, mocker): json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=None + timeout=None ) assert response.status_code == 200 assert response.body == 'ok' @@ -166,21 +159,94 @@ def test_authentication_scheme(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.get', new=get_mock) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS) + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + httpclient.set_telemetry_data("metric", mocker.Mock()) + response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + call = mocker.call( + 'https://sdk.com/test1', + headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, + params={'param1': 123}, + timeout=None +# auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) + ) + assert response.status_code == 200 + assert response.body == 'ok' + assert get_mock.mock_calls == [call] + get_mock.reset_mock() + + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) + httpclient.set_telemetry_data("metric", mocker.Mock()) + response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + call = mocker.call( + 'https://sdk.com/test1', + headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, + params={'param1': 123}, + timeout=None +# auth=HTTPKerberosAuth(principal='bilal', password='split', mutual_authentication=OPTIONAL) + ) + assert response.status_code == 200 + assert response.body == 'ok' + assert get_mock.mock_calls == [call] + get_mock.reset_mock() + + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( 'https://sdk.com/test1', headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, - timeout=None, - auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) + timeout=None +# auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) ) + assert response.status_code == 200 + assert response.body == 'ok' + assert get_mock.mock_calls == [call] + get_mock.reset_mock() - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS, authentication_params=['bilal', 'split']) + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + call = mocker.call( + 'https://sdk.com/test1', + headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, + params={'param1': 123}, + timeout=None + ) + assert response.status_code == 200 + assert response.body == 'ok' + assert get_mock.mock_calls == [call] + get_mock.reset_mock() + + # test auth settings + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) + my_session = requests.Session() + httpclient._set_authentication(my_session) + assert(my_session.auth.principal == 'bilal') + assert(my_session.auth.password == 'split') + assert(isinstance(my_session.auth, HTTPKerberosAuth)) + + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + my_session2 = requests.Session() + httpclient._set_authentication(my_session2) + assert(my_session2.auth.principal == None) + assert(my_session2.auth.password == None) + assert(isinstance(my_session2.auth, HTTPKerberosAuth)) + + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) + my_session = requests.Session() + httpclient._set_authentication(my_session) + assert(my_session.adapters['https://']._principal == 'bilal') + assert(my_session.adapters['https://']._password == 'split') + assert(isinstance(my_session.adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) + + httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + my_session2 = requests.Session() + httpclient._set_authentication(my_session2) + assert(my_session2.adapters['https://']._principal == None) + assert(my_session2.adapters['https://']._password == None) + assert(isinstance(my_session2.adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) def test_telemetry(self, mocker): telemetry_storage = InMemoryTelemetryStorage() @@ -193,7 +259,7 @@ def test_telemetry(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.post', new=get_mock) + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", telemetry_runtime_producer) @@ -231,7 +297,7 @@ def record_sync_error(metric_name, elapsed): assert (self.status == 400) # testing get call - mocker.patch('splitio.api.client.requests.get', new=get_mock) + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) self.metric1 = None self.cur_time = 0 self.metric2 = None diff --git a/tests/client/test_config.py b/tests/client/test_config.py index ddfd85b0..028736b3 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -76,8 +76,11 @@ def test_sanitize(self): processed = config.sanitize('some', {'storageType': 'pluggable', 'flagSetsFilter': ['set']}) assert processed['flagSetsFilter'] is None - processed = config.sanitize('some', {'httpAuthenticateScheme': 'KERBEROS'}) - assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.KERBEROS + processed = config.sanitize('some', {'httpAuthenticateScheme': 'KERBEROS_spnego'}) + assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.KERBEROS_SPNEGO + + processed = config.sanitize('some', {'httpAuthenticateScheme': 'kerberos_proxy'}) + assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.KERBEROS_PROXY processed = config.sanitize('some', {'httpAuthenticateScheme': 'anything'}) assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE From 22dad08f3a6fc9822e90497b699ce85c43ec3d7a Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Tue, 16 Jul 2024 16:09:31 -0700 Subject: [PATCH 07/14] polish --- splitio/api/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/splitio/api/client.py b/splitio/api/client.py index cafb3a84..02eff8c2 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -5,7 +5,6 @@ import abc import logging import json -import time import threading from urllib3.util import parse_url @@ -220,13 +219,13 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # def _set_authentication(self, session): if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO: _LOGGER.debug("Using Kerberos Spnego Authentication") - if self._authentication_params is not [None, None]: + 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 is not [None, None]: + if self._authentication_params != [None, None]: session.mount('https://', HTTPAdapterWithProxyKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1])) else: session.mount('https://', HTTPAdapterWithProxyKerberosAuth()) From 5900f26089f53582f7f5465e506f347f37859b9e Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Tue, 23 Jul 2024 13:34:38 -0700 Subject: [PATCH 08/14] refactored httpclient for kerberos auth --- splitio/api/client.py | 180 ++++++++++++++++++++++++++--------- splitio/client/factory.py | 29 +++--- tests/api/test_httpclient.py | 28 +++--- 3 files changed, 166 insertions(+), 71 deletions(-) diff --git a/splitio/api/client.py b/splitio/api/client.py index 02eff8c2..f516bf38 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -122,7 +122,7 @@ def _get_headers(self, extra_headers, sdk_key): class HttpClient(HttpClientBase): """HttpClient wrapper.""" - def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None): + def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None): """ Class constructor. @@ -140,8 +140,6 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t _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._lock = threading.RLock() def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments @@ -164,20 +162,18 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: """ 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 + try: + response = requests.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 """ @@ -201,35 +197,18 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # """ 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: - session.mount('https://', HTTPAdapterWithProxyKerberosAuth()) - + try: + response = requests.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 _record_telemetry(self, status_code, elapsed): """ @@ -372,3 +351,112 @@ async def _record_telemetry(self, status_code, elapsed): async def close_session(self): if not self._session.closed: await self._session.close() + +class HttpClientKerberos(HttpClient): + """HttpClient wrapper.""" + + def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None): + """ + Class constructor. + + :param timeout: How many milliseconds to wait until the server responds. + :type timeout: int + :param sdk_url: Optional alternative sdk URL. + :type sdk_url: str + :param events_url: Optional alternative events URL. + :type events_url: str + :param auth_url: Optional alternative auth URL. + :type auth_url: str + :param telemetry_url: Optional alternative telemetry URL. + :type telemetry_url: str + """ + _LOGGER.debug("Initializing httpclient for Kerberos auth") + HttpClient.__init__(self, timeout, sdk_url, events_url, auth_url, telemetry_url) + self._authentication_scheme = authentication_scheme + self._authentication_params = authentication_params + + def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments + """ + Issue a get request. + + :param server: Whether the request is for SDK server, Events server or Auth server. + :typee server: str + :param path: path to append to the host url. + :type path: str + :param sdk_key: sdk key. + :type sdk_key: str + :param query: Query string passed as dictionary. + :type query: dict + :param extra_headers: key/value pairs of possible extra headers. + :type extra_headers: dict + + :return: Tuple of status_code & response text + :rtype: HttpResponse + """ + 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 + """ + Issue a POST request. + + :param server: Whether the request is for SDK server or Events server. + :typee server: str + :param path: path to append to the host url. + :type path: str + :param sdk_key: sdk key. + :type sdk_key: str + :param body: body sent in the request. + :type body: str + :param query: Query string passed as dictionary. + :type query: dict + :param extra_headers: key/value pairs of possible extra headers. + :type extra_headers: dict + + :return: Tuple of status_code & response text + :rtype: HttpResponse + """ + 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: + session.mount('https://', HTTPAdapterWithProxyKerberosAuth()) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index fffb0212..8c3b7572 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -33,7 +33,7 @@ PluggableImpressionsStorageAsync, PluggableSegmentStorageAsync, PluggableSplitStorageAsync # APIs -from splitio.api.client import HttpClient, HttpClientAsync +from splitio.api.client import HttpClient, HttpClientAsync, HttpClientKerberos from splitio.api.splits import SplitsAPI, SplitsAPIAsync from splitio.api.segments import SegmentsAPI, SegmentsAPIAsync from splitio.api.impressions import ImpressionsAPI, ImpressionsAPIAsync @@ -512,16 +512,23 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl if cfg.get("httpAuthenticateScheme") in [AuthenticateScheme.KERBEROS_SPNEGO, AuthenticateScheme.KERBEROS_PROXY]: authentication_params = [cfg.get("kerberosPrincipalUser"), cfg.get("kerberosPrincipalPassword")] - - http_client = HttpClient( - sdk_url=sdk_url, - events_url=events_url, - auth_url=auth_api_base_url, - telemetry_url=telemetry_api_base_url, - timeout=cfg.get('connectionTimeout'), - authentication_scheme = cfg.get("httpAuthenticateScheme"), - authentication_params = authentication_params - ) + http_client = HttpClientKerberos( + sdk_url=sdk_url, + events_url=events_url, + auth_url=auth_api_base_url, + telemetry_url=telemetry_api_base_url, + timeout=cfg.get('connectionTimeout'), + authentication_scheme = cfg.get("httpAuthenticateScheme"), + authentication_params = authentication_params + ) + else: + http_client = HttpClient( + sdk_url=sdk_url, + events_url=events_url, + auth_url=auth_api_base_url, + telemetry_url=telemetry_api_base_url, + timeout=cfg.get('connectionTimeout'), + ) sdk_metadata = util.get_metadata(cfg) apis = { diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index d95dcb5f..0a3cb6b6 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -20,7 +20,7 @@ def test_get(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + mocker.patch('splitio.api.client.requests.get', new=get_mock) httpclient = client.HttpClient() httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) @@ -54,7 +54,7 @@ def test_get_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + mocker.patch('splitio.api.client.requests.get', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) @@ -89,7 +89,7 @@ def test_post(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + mocker.patch('splitio.api.client.requests.post', new=get_mock) httpclient = client.HttpClient() httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) @@ -125,7 +125,7 @@ def test_post_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + mocker.patch('splitio.api.client.requests.post', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) @@ -160,7 +160,7 @@ def test_authentication_scheme(self, mocker): get_mock = mocker.Mock() get_mock.return_value = response_mock mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( @@ -175,7 +175,7 @@ def test_authentication_scheme(self, mocker): assert get_mock.mock_calls == [call] get_mock.reset_mock() - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( @@ -190,7 +190,7 @@ def test_authentication_scheme(self, mocker): assert get_mock.mock_calls == [call] get_mock.reset_mock() - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( @@ -205,7 +205,7 @@ def test_authentication_scheme(self, mocker): assert get_mock.mock_calls == [call] get_mock.reset_mock() - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( @@ -220,28 +220,28 @@ def test_authentication_scheme(self, mocker): get_mock.reset_mock() # test auth settings - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) my_session = requests.Session() httpclient._set_authentication(my_session) assert(my_session.auth.principal == 'bilal') assert(my_session.auth.password == 'split') assert(isinstance(my_session.auth, HTTPKerberosAuth)) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) my_session2 = requests.Session() httpclient._set_authentication(my_session2) assert(my_session2.auth.principal == None) assert(my_session2.auth.password == None) assert(isinstance(my_session2.auth, HTTPKerberosAuth)) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) my_session = requests.Session() httpclient._set_authentication(my_session) assert(my_session.adapters['https://']._principal == 'bilal') assert(my_session.adapters['https://']._password == 'split') assert(isinstance(my_session.adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) my_session2 = requests.Session() httpclient._set_authentication(my_session2) assert(my_session2.adapters['https://']._principal == None) @@ -259,7 +259,7 @@ def test_telemetry(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + mocker.patch('splitio.api.client.requests.post', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", telemetry_runtime_producer) @@ -297,7 +297,7 @@ def record_sync_error(metric_name, elapsed): assert (self.status == 400) # testing get call - mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + mocker.patch('splitio.api.client.requests.get', new=get_mock) self.metric1 = None self.cur_time = 0 self.metric2 = None From 08d38a828f15183e8a1d7b252853630558ccda38 Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Tue, 23 Jul 2024 20:54:53 -0700 Subject: [PATCH 09/14] polish --- splitio/api/client.py | 16 ++++++++-------- tests/api/test_httpclient.py | 20 ++++++++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/splitio/api/client.py b/splitio/api/client.py index f516bf38..40d92efc 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -19,7 +19,7 @@ TELEMETRY_URL = 'https://telemetry.split.io/api' _LOGGER = logging.getLogger(__name__) - +_EXC_MSG = '{source} library is throwing exceptions' HttpResponse = namedtuple('HttpResponse', ['status_code', 'body', 'headers']) @@ -173,7 +173,7 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: 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 + raise HttpClientException(_EXC_MSG.format(source='request')) from exc def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -208,7 +208,7 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # 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 + raise HttpClientException(_EXC_MSG.format(source='request')) from exc def _record_telemetry(self, status_code, elapsed): """ @@ -285,7 +285,7 @@ async def get(self, server, path, apikey, query=None, extra_headers=None): # py return HttpResponse(response.status, body, response.headers) except aiohttp.ClientError as exc: # pylint: disable=broad-except - raise HttpClientException('aiohttp library is throwing exceptions') from exc + raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc async def post(self, server, path, apikey, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -329,7 +329,7 @@ async def post(self, server, path, apikey, body, query=None, extra_headers=None) return HttpResponse(response.status, body, response.headers) except aiohttp.ClientError as exc: # pylint: disable=broad-except - raise HttpClientException('aiohttp library is throwing exceptions') from exc + raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc async def _record_telemetry(self, status_code, elapsed): """ @@ -371,7 +371,7 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t :type telemetry_url: str """ _LOGGER.debug("Initializing httpclient for Kerberos auth") - HttpClient.__init__(self, timeout, sdk_url, events_url, auth_url, telemetry_url) + HttpClient.__init__(self, timeout=timeout, sdk_url=sdk_url, events_url=events_url, auth_url=auth_url, telemetry_url=telemetry_url) self._authentication_scheme = authentication_scheme self._authentication_params = authentication_params @@ -408,7 +408,7 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: 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 + raise HttpClientException(_EXC_MSG.format(source='request')) from exc def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -445,7 +445,7 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # 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 + raise HttpClientException(_EXC_MSG.format(source='request')) from exc def _set_authentication(self, session): if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO: diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index 0a3cb6b6..621e696a 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -168,7 +168,6 @@ def test_authentication_scheme(self, mocker): headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, timeout=None -# auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) ) assert response.status_code == 200 assert response.body == 'ok' @@ -183,28 +182,37 @@ def test_authentication_scheme(self, mocker): headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, timeout=None -# auth=HTTPKerberosAuth(principal='bilal', password='split', mutual_authentication=OPTIONAL) ) assert response.status_code == 200 assert response.body == 'ok' assert get_mock.mock_calls == [call] get_mock.reset_mock() - httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.headers = {} + response_mock.text = 'ok' + get_mock = mocker.Mock() + get_mock.return_value = response_mock + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', events_url='https://events.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) - response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + + response = httpclient.post('events', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) call = mocker.call( - 'https://sdk.com/test1', + 'https://events.com/test1', + json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, timeout=None -# auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) ) assert response.status_code == 200 assert response.body == 'ok' assert get_mock.mock_calls == [call] get_mock.reset_mock() + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) From 6db6c5418254e9f4877324a26c32e81baf5b9494 Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Tue, 23 Jul 2024 21:15:04 -0700 Subject: [PATCH 10/14] polish --- splitio/api/client.py | 55 ++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/splitio/api/client.py b/splitio/api/client.py index 40d92efc..3ec7ec15 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -160,20 +160,19 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: :return: Tuple of status_code & response text :rtype: HttpResponse """ - with self._lock: - start = get_current_epoch_time_ms() - try: - response = requests.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(_EXC_MSG.format(source='request')) from exc + start = get_current_epoch_time_ms() + try: + response = requests.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(_EXC_MSG.format(source='request')) from exc def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -195,20 +194,19 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # :return: Tuple of status_code & response text :rtype: HttpResponse """ - with self._lock: - start = get_current_epoch_time_ms() - try: - response = requests.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(_EXC_MSG.format(source='request')) from exc + start = get_current_epoch_time_ms() + try: + response = requests.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(_EXC_MSG.format(source='request')) from exc def _record_telemetry(self, status_code, elapsed): """ @@ -378,7 +376,6 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ Issue a get request. - :param server: Whether the request is for SDK server, Events server or Auth server. :typee server: str :param path: path to append to the host url. From dfd430de7c0b383c331d611b5e903c482639f0da Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Tue, 23 Jul 2024 21:32:31 -0700 Subject: [PATCH 11/14] polish --- splitio/api/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splitio/api/client.py b/splitio/api/client.py index 3ec7ec15..c7a37194 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -397,8 +397,8 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: try: response = session.get( _build_url(server, path, self._urls), - params=query, headers=self._get_headers(extra_headers, sdk_key), + params=query, timeout=self._timeout ) self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) @@ -434,9 +434,9 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # try: response = session.post( _build_url(server, path, self._urls), - json=body, params=query, headers=self._get_headers(extra_headers, sdk_key), + json=body, timeout=self._timeout, ) self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) From c1aa51fe187442c7fce1cfb8166ae6e0c4f2feb7 Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Mon, 5 Aug 2024 19:29:38 -0700 Subject: [PATCH 12/14] Used four sessions per split host and reconnect when timing out --- splitio/api/client.py | 193 ++++++++++++++++++-------- tests/api/test_httpclient.py | 261 ++++++++++++++++++++++++++--------- 2 files changed, 328 insertions(+), 126 deletions(-) diff --git a/splitio/api/client.py b/splitio/api/client.py index c7a37194..5db1cadb 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -119,6 +119,23 @@ def _get_headers(self, extra_headers, sdk_key): headers.update(extra_headers) return headers + def _record_telemetry(self, status_code, elapsed): + """ + Record Telemetry info + + :param status_code: http request status code + :type status_code: int + + :param elapsed: response time elapsed. + :type status_code: int + """ + self._telemetry_runtime_producer.record_sync_latency(self._metric_name, 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) + class HttpClient(HttpClientBase): """HttpClient wrapper.""" @@ -140,7 +157,6 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t _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._lock = threading.RLock() def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -208,23 +224,6 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # except Exception as exc: # pylint: disable=broad-except raise HttpClientException(_EXC_MSG.format(source='request')) from exc - def _record_telemetry(self, status_code, elapsed): - """ - Record Telemetry info - - :param status_code: http request status code - :type status_code: int - - :param elapsed: response time elapsed. - :type status_code: int - """ - self._telemetry_runtime_producer.record_sync_latency(self._metric_name, 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) - class HttpClientAsync(HttpClientBase): """HttpClientAsync wrapper.""" @@ -350,7 +349,7 @@ async def close_session(self): if not self._session.closed: await self._session.close() -class HttpClientKerberos(HttpClient): +class HttpClientKerberos(HttpClientBase): """HttpClient wrapper.""" def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None): @@ -367,11 +366,22 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t :type auth_url: str :param telemetry_url: Optional alternative telemetry URL. :type telemetry_url: str + :param authentication_scheme: Optional authentication scheme to use. + :type authentication_scheme: splitio.client.config.AuthenticateScheme + :param authentication_params: Optional authentication username and password to use. + :type authentication_params: [str, str] """ _LOGGER.debug("Initializing httpclient for Kerberos auth") - HttpClient.__init__(self, timeout=timeout, sdk_url=sdk_url, events_url=events_url, auth_url=auth_url, telemetry_url=telemetry_url) + 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._lock = threading.RLock() + self._sessions = {'sdk': requests.Session(), + 'events': requests.Session(), + 'auth': requests.Session(), + 'telemetry': requests.Session()} + self._set_authentication() def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -392,21 +402,49 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: """ with self._lock: start = get_current_epoch_time_ms() - with requests.Session() as session: - self._set_authentication(session) + try: + return self._do_get(server, path, sdk_key, query, extra_headers, start) + + except requests.exceptions.ProxyError as exc: + _LOGGER.debug("Proxy Exception caught, resetting the http session") + self._sessions[server].close() + self._sessions[server] = requests.Session() + self._set_authentication(server_name=server) try: - response = session.get( - _build_url(server, path, self._urls), - headers=self._get_headers(extra_headers, sdk_key), - params=query, - 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 + return self._do_get(server, path, sdk_key, query, extra_headers, start) + + except Exception as exc: raise HttpClientException(_EXC_MSG.format(source='request')) from exc + except Exception as exc: # pylint: disable=broad-except + raise HttpClientException(_EXC_MSG.format(source='request')) from exc + + def _do_get(self, server, path, sdk_key, query, extra_headers, start): + """ + Issue a get request. + :param server: Whether the request is for SDK server, Events server or Auth server. + :typee server: str + :param path: path to append to the host url. + :type path: str + :param sdk_key: sdk key. + :type sdk_key: str + :param query: Query string passed as dictionary. + :type query: dict + :param extra_headers: key/value pairs of possible extra headers. + :type extra_headers: dict + + :return: Tuple of status_code & response text + :rtype: HttpResponse + """ + with self._sessions[server].get( + _build_url(server, path, self._urls), + headers=self._get_headers(extra_headers, sdk_key), + params=query, + timeout=self._timeout + ) as response: + self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) + return HttpResponse(response.status_code, response.text, response.headers) + def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ Issue a POST request. @@ -429,31 +467,72 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # """ with self._lock: start = get_current_epoch_time_ms() - with requests.Session() as session: - self._set_authentication(session) + try: + return self._do_post(server, path, sdk_key, query, extra_headers, body, start) + + except requests.exceptions.ProxyError as exc: + _LOGGER.debug("Proxy Exception caught, resetting the http session") + self._sessions[server].close() + self._sessions[server] = requests.Session() + self._set_authentication(server_name=server) try: - response = session.post( - _build_url(server, path, self._urls), - params=query, - headers=self._get_headers(extra_headers, sdk_key), - json=body, - 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 + return self._do_post(server, path, sdk_key, query, extra_headers, body, start) + + except Exception as exc: raise HttpClientException(_EXC_MSG.format(source='request')) 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: - session.mount('https://', HTTPAdapterWithProxyKerberosAuth()) + except Exception as exc: # pylint: disable=broad-except + raise HttpClientException(_EXC_MSG.format(source='request')) from exc + + def _do_post(self, server, path, sdk_key, query, extra_headers, body, start): + """ + Issue a POST request. + + :param server: Whether the request is for SDK server or Events server. + :typee server: str + :param path: path to append to the host url. + :type path: str + :param sdk_key: sdk key. + :type sdk_key: str + :param body: body sent in the request. + :type body: str + :param query: Query string passed as dictionary. + :type query: dict + :param extra_headers: key/value pairs of possible extra headers. + :type extra_headers: dict + + :return: Tuple of status_code & response text + :rtype: HttpResponse + """ + with self._sessions[server].post( + _build_url(server, path, self._urls), + params=query, + headers=self._get_headers(extra_headers, sdk_key), + json=body, + timeout=self._timeout, + ) as response: + self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) + return HttpResponse(response.status_code, response.text, response.headers) + + def _set_authentication(self, server_name=None): + """ + Set the authentication for all self._sessions variables based on authentication scheme. + + :param server: If set, will only add the auth for its session variable, otherwise will set all sessions. + :typee server: str + """ + for server in ['sdk', 'events', 'auth', 'telemetry']: + if server_name is not None and server_name != server: + continue + if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO: + _LOGGER.debug("Using Kerberos Spnego Authentication") + if self._authentication_params != [None, None]: + self._sessions[server].auth = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL) + else: + self._sessions[server].auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) + elif self._authentication_scheme == AuthenticateScheme.KERBEROS_PROXY: + _LOGGER.debug("Using Kerberos Proxy Authentication") + if self._authentication_params != [None, None]: + self._sessions[server].mount('https://', HTTPAdapterWithProxyKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1])) + else: + self._sessions[server].mount('https://', HTTPAdapterWithProxyKerberosAuth()) diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index 621e696a..147eb897 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -1,5 +1,5 @@ """HTTPClient test module.""" -from requests_kerberos import HTTPKerberosAuth, OPTIONAL +from requests_kerberos import HTTPKerberosAuth import pytest import unittest.mock as mock import requests @@ -153,108 +153,233 @@ def test_post_custom_urls(self, mocker): assert response.body == 'ok' assert get_mock.mock_calls == [call] - def test_authentication_scheme(self, mocker): + def test_telemetry(self, mocker): + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + response_mock = mocker.Mock() response_mock.status_code = 200 + response_mock.headers = {} response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock + mocker.patch('splitio.api.client.requests.post', new=get_mock) + httpclient = client.HttpClient(timeout=1500, sdk_url='https://sdk.com', events_url='https://events.com') + httpclient.set_telemetry_data("metric", telemetry_runtime_producer) + + self.metric1 = None + self.cur_time = 0 + def record_successful_sync(metric_name, cur_time): + self.metric1 = metric_name + self.cur_time = cur_time + httpclient._telemetry_runtime_producer.record_successful_sync = record_successful_sync + + self.metric2 = None + self.elapsed = 0 + def record_sync_latency(metric_name, elapsed): + self.metric2 = metric_name + self.elapsed = elapsed + httpclient._telemetry_runtime_producer.record_sync_latency = record_sync_latency + + self.metric3 = None + self.status = 0 + def record_sync_error(metric_name, elapsed): + self.metric3 = metric_name + self.status = elapsed + httpclient._telemetry_runtime_producer.record_sync_error = record_sync_error + + httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) + assert (self.metric2 == "metric") + assert (self.metric1 == "metric") + assert (self.cur_time > self.elapsed) + + response_mock.status_code = 400 + response_mock.headers = {} + response_mock.text = 'ok' + httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) + assert (self.metric3 == "metric") + assert (self.status == 400) + + # testing get call + mocker.patch('splitio.api.client.requests.get', new=get_mock) + self.metric1 = None + self.cur_time = 0 + self.metric2 = None + self.elapsed = 0 + response_mock.status_code = 200 + httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + assert (self.metric2 == "metric") + assert (self.metric1 == "metric") + assert (self.cur_time > self.elapsed) + + self.metric3 = None + self.status = 0 + response_mock.status_code = 400 + httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + assert (self.metric3 == "metric") + assert (self.status == 400) + +class HttpClientKerberosTests(object): + """Http Client test cases.""" + + def test_authentication_scheme(self, mocker): + global turl + global theaders + global tparams + global ttimeout + global tjson + + turl = None + theaders = None + tparams = None + ttimeout = None + class get_mock(object): + def __init__(self, url, headers, params, timeout): + global turl + global theaders + global tparams + global ttimeout + turl = url + theaders = headers + tparams = params + ttimeout = timeout + + def __enter__(self): + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.text = 'ok' + return response_mock + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) - call = mocker.call( - 'https://sdk.com/test1', - headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, - params={'param1': 123}, - timeout=None - ) + assert turl == 'https://sdk.com/test1' + assert theaders == {'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'} + assert tparams == {'param1': 123} + assert ttimeout == None assert response.status_code == 200 assert response.body == 'ok' - assert get_mock.mock_calls == [call] - get_mock.reset_mock() + turl = None + theaders = None + tparams = None + ttimeout = None httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) - call = mocker.call( - 'https://sdk.com/test1', - headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, - params={'param1': 123}, - timeout=None - ) + assert turl == 'https://sdk.com/test1' + assert theaders == {'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'} + assert tparams == {'param1': 123} + assert ttimeout == None + assert response.status_code == 200 assert response.body == 'ok' - assert get_mock.mock_calls == [call] - get_mock.reset_mock() response_mock = mocker.Mock() response_mock.status_code = 200 response_mock.headers = {} response_mock.text = 'ok' - get_mock = mocker.Mock() - get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) - httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', events_url='https://events.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + turl = None + theaders = None + tparams = None + ttimeout = None + tjson = None + class post_mock(object): + def __init__(self, url, params, headers, json, timeout): + global turl + global theaders + global tparams + global ttimeout + global tjson + turl = url + theaders = headers + tparams = params + ttimeout = timeout + tjson = json + + def __enter__(self): + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.text = 'ok' + return response_mock + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + mocker.patch('splitio.api.client.requests.Session.post', new=post_mock) + + httpclient = client.HttpClientKerberos(timeout=1500, sdk_url='https://sdk.com', events_url='https://events.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.post('events', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) - call = mocker.call( - 'https://events.com/test1', - json={'p1': 'a'}, - headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, - params={'param1': 123}, - timeout=None - ) + assert turl == 'https://events.com/test1' + assert tjson == {'p1': 'a'} + assert theaders == {'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'} + assert tparams == {'param1': 123} + assert ttimeout == 1.5 + assert response.status_code == 200 assert response.body == 'ok' - assert get_mock.mock_calls == [call] - get_mock.reset_mock() + turl = None + theaders = None + tparams = None + ttimeout = None mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) - httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(timeout=1500, sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) - call = mocker.call( - 'https://sdk.com/test1', - headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, - params={'param1': 123}, - timeout=None - ) + assert turl == 'https://sdk.com/test1' + assert theaders == {'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'} + assert tparams == {'param1': 123} + assert ttimeout == 1.5 + assert response.status_code == 200 assert response.body == 'ok' - assert get_mock.mock_calls == [call] - get_mock.reset_mock() # test auth settings httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) - my_session = requests.Session() - httpclient._set_authentication(my_session) - assert(my_session.auth.principal == 'bilal') - assert(my_session.auth.password == 'split') - assert(isinstance(my_session.auth, HTTPKerberosAuth)) - - httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) - my_session2 = requests.Session() - httpclient._set_authentication(my_session2) - assert(my_session2.auth.principal == None) - assert(my_session2.auth.password == None) - assert(isinstance(my_session2.auth, HTTPKerberosAuth)) - - httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) - my_session = requests.Session() - httpclient._set_authentication(my_session) - assert(my_session.adapters['https://']._principal == 'bilal') - assert(my_session.adapters['https://']._password == 'split') - assert(isinstance(my_session.adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) - - httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) - my_session2 = requests.Session() - httpclient._set_authentication(my_session2) - assert(my_session2.adapters['https://']._principal == None) - assert(my_session2.adapters['https://']._password == None) - assert(isinstance(my_session2.adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) + httpclient._set_authentication('sdk') + for server in ['sdk', 'events', 'auth', 'telemetry']: + assert(httpclient._sessions[server].auth.principal == 'bilal') + assert(httpclient._sessions[server].auth.password == 'split') + assert(isinstance(httpclient._sessions[server].auth, HTTPKerberosAuth)) + + httpclient._sessions['sdk'].close() + httpclient._sessions['events'].close() + httpclient._sessions['sdk'] = requests.Session() + httpclient._sessions['events'] = requests.Session() + assert(httpclient._sessions['sdk'].auth == None) + assert(httpclient._sessions['events'].auth == None) + + httpclient._set_authentication('sdk') + assert(httpclient._sessions['sdk'].auth.principal == 'bilal') + assert(httpclient._sessions['sdk'].auth.password == 'split') + assert(isinstance(httpclient._sessions['sdk'].auth, HTTPKerberosAuth)) + assert(httpclient._sessions['events'].auth == None) + + httpclient2 = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + for server in ['sdk', 'events', 'auth', 'telemetry']: + assert(httpclient2._sessions[server].auth.principal == None) + assert(httpclient2._sessions[server].auth.password == None) + assert(isinstance(httpclient2._sessions[server].auth, HTTPKerberosAuth)) + + httpclient3 = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) + for server in ['sdk', 'events', 'auth', 'telemetry']: + assert(httpclient3._sessions[server].adapters['https://']._principal == 'bilal') + assert(httpclient3._sessions[server].adapters['https://']._password == 'split') + assert(isinstance(httpclient3._sessions[server].adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) + + httpclient4 = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + for server in ['sdk', 'events', 'auth', 'telemetry']: + assert(httpclient4._sessions[server].adapters['https://']._principal == None) + assert(httpclient4._sessions[server].adapters['https://']._password == None) + assert(isinstance(httpclient4._sessions[server].adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) def test_telemetry(self, mocker): telemetry_storage = InMemoryTelemetryStorage() @@ -268,7 +393,7 @@ def test_telemetry(self, mocker): get_mock = mocker.Mock() get_mock.return_value = response_mock mocker.patch('splitio.api.client.requests.post', new=get_mock) - httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') + httpclient = client.HttpClient(timeout=1500, sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", telemetry_runtime_producer) self.metric1 = None @@ -323,7 +448,6 @@ def record_sync_error(metric_name, elapsed): assert (self.metric3 == "metric") assert (self.status == 400) - class MockResponse: def __init__(self, text, status, headers): self._text = text @@ -412,7 +536,6 @@ async def test_get_custom_urls(self, mocker): assert response.body == 'ok' assert get_mock.mock_calls == [call] - @pytest.mark.asyncio async def test_post(self, mocker): """Test HTTP POST verb requests.""" From d187e8c157402057f713a62c18501ce96a80be35 Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Mon, 5 Aug 2024 20:41:54 -0700 Subject: [PATCH 13/14] added proxy exception test --- tests/api/test_httpclient.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index 147eb897..837997aa 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -381,6 +381,62 @@ def __exit__(self, exc_type, exc_val, exc_tb): assert(httpclient4._sessions[server].adapters['https://']._password == None) assert(isinstance(httpclient4._sessions[server].adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) + def test_proxy_exception(self, mocker): + global count + count = 0 + class get_mock(object): + def __init__(self, url, params, headers, timeout): + pass + + def __enter__(self): + global count + count += 1 + if count == 1: + raise requests.exceptions.ProxyError() + + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.text = 'ok' + return response_mock + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + httpclient.set_telemetry_data("metric", mocker.Mock()) + response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + assert response.status_code == 200 + assert response.body == 'ok' + + count = 0 + class post_mock(object): + def __init__(self, url, params, headers, json, timeout): + pass + + def __enter__(self): + global count + count += 1 + if count == 1: + raise requests.exceptions.ProxyError() + + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.text = 'ok' + return response_mock + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + mocker.patch('splitio.api.client.requests.Session.post', new=post_mock) + + httpclient = client.HttpClientKerberos(timeout=1500, sdk_url='https://sdk.com', events_url='https://events.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + httpclient.set_telemetry_data("metric", mocker.Mock()) + response = httpclient.post('events', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) + assert response.status_code == 200 + assert response.body == 'ok' + + + def test_telemetry(self, mocker): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) From 2d7bb11f2d4b84a2fd2e9274e570df33a11c0f95 Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Wed, 7 Aug 2024 08:26:36 -0700 Subject: [PATCH 14/14] updated changes and version --- CHANGES.txt | 3 +++ splitio/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index ffa2da1e..5b8e8646 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +10.1.0 (Aug 7, 2024) +- Added support for Kerberos authentication in Spnego and Proxy Kerberos server instances. + 10.0.1 (Jun 28, 2024) - Fixed failure to load lib issue in SDK startup for Python versions higher than or equal to 3.10 diff --git a/splitio/version.py b/splitio/version.py index 642e5ce1..953a047f 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '10.1.0rc2' \ No newline at end of file +__version__ = '10.1.0' \ No newline at end of file