From 1719b7f97cd0d4049d730ae4231dc8d33369559a Mon Sep 17 00:00:00 2001 From: Bilal Al Date: Wed, 5 Jun 2024 19:57:55 -0700 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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]