Skip to content

Commit 6384570

Browse files
authored
Merge pull request #534 from splitio/kerberose-auth
added support for spnego/kerberos auth
2 parents 718a98b + cecabd8 commit 6384570

File tree

9 files changed

+119
-18
lines changed

9 files changed

+119
-18
lines changed

Diff for: .github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535

3636
- name: Install dependencies
3737
run: |
38+
sudo apt-get install -y libkrb5-dev
3839
pip install -U setuptools pip wheel
3940
pip install -e .[cpphash,redis,uwsgi]
4041

Diff for: setup.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
'attrs==22.1.0',
1717
'pytest-asyncio==0.21.0',
1818
'aiohttp>=3.8.4',
19-
'aiofiles>=23.1.0'
19+
'aiofiles>=23.1.0',
20+
'requests-kerberos>=0.14.0'
2021
]
2122

2223
INSTALL_REQUIRES = [
@@ -46,7 +47,8 @@
4647
'redis': ['redis>=2.10.5'],
4748
'uwsgi': ['uwsgi>=2.0.0'],
4849
'cpphash': ['mmh3cffi==0.2.1'],
49-
'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0']
50+
'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0'],
51+
'kerberos': ['requests-kerberos>=0.14.0']
5052
},
5153
setup_requires=['pytest-runner', 'pluggy==1.0.0;python_version<"3.8"'],
5254
classifiers=[

Diff for: splitio/api/client.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import abc
66
import logging
77
import json
8+
from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL
89

10+
from splitio.client.config import AuthenticateScheme
911
from splitio.optional.loaders import aiohttp
1012
from splitio.util.time import get_current_epoch_time_ms
1113

@@ -95,7 +97,7 @@ def set_telemetry_data(self, metric_name, telemetry_runtime_producer):
9597
class HttpClient(HttpClientBase):
9698
"""HttpClient wrapper."""
9799

98-
def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None):
100+
def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None):
99101
"""
100102
Class constructor.
101103
@@ -111,6 +113,8 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t
111113
:type telemetry_url: str
112114
"""
113115
self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
116+
self._authentication_scheme = authentication_scheme
117+
self._authentication_params = authentication_params
114118
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
115119

116120
def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments
@@ -135,13 +139,15 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint:
135139
if extra_headers is not None:
136140
headers.update(extra_headers)
137141

142+
authentication = self._get_authentication()
138143
start = get_current_epoch_time_ms()
139144
try:
140145
response = requests.get(
141146
_build_url(server, path, self._urls),
142147
params=query,
143148
headers=headers,
144-
timeout=self._timeout
149+
timeout=self._timeout,
150+
auth=authentication
145151
)
146152
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
147153
return HttpResponse(response.status_code, response.text, response.headers)
@@ -174,21 +180,32 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): #
174180
if extra_headers is not None:
175181
headers.update(extra_headers)
176182

183+
authentication = self._get_authentication()
177184
start = get_current_epoch_time_ms()
178185
try:
179186
response = requests.post(
180187
_build_url(server, path, self._urls),
181188
json=body,
182189
params=query,
183190
headers=headers,
184-
timeout=self._timeout
191+
timeout=self._timeout,
192+
auth=authentication
185193
)
186194
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
187195
return HttpResponse(response.status_code, response.text, response.headers)
188196

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

200+
def _get_authentication(self):
201+
authentication = None
202+
if self._authentication_scheme == AuthenticateScheme.KERBEROS:
203+
if self._authentication_params is not None:
204+
authentication = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL)
205+
else:
206+
authentication = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
207+
return authentication
208+
192209
def _record_telemetry(self, status_code, elapsed):
193210
"""
194211
Record Telemetry info
@@ -333,4 +350,4 @@ async def _record_telemetry(self, status_code, elapsed):
333350

334351
async def close_session(self):
335352
if not self._session.closed:
336-
await self._session.close()
353+
await self._session.close()

Diff for: splitio/client/config.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
"""Default settings for the Split.IO SDK Python client."""
22
import os.path
33
import logging
4+
from enum import Enum
45

56
from splitio.engine.impressions import ImpressionsMode
67
from splitio.client.input_validator import validate_flag_sets
78

89
_LOGGER = logging.getLogger(__name__)
910
DEFAULT_DATA_SAMPLING = 1
1011

12+
class AuthenticateScheme(Enum):
13+
"""Authentication Scheme."""
14+
NONE = 'NONE'
15+
KERBEROS = 'KERBEROS'
16+
17+
1118
DEFAULT_CONFIG = {
1219
'operationMode': 'standalone',
1320
'connectionTimeout': 1500,
@@ -59,7 +66,10 @@
5966
'storageWrapper': None,
6067
'storagePrefix': None,
6168
'storageType': None,
62-
'flagSetsFilter': None
69+
'flagSetsFilter': None,
70+
'httpAuthenticateScheme': AuthenticateScheme.NONE,
71+
'kerberosPrincipalUser': None,
72+
'kerberosPrincipalPassword': None
6373
}
6474

6575
def _parse_operation_mode(sdk_key, config):
@@ -148,4 +158,14 @@ def sanitize(sdk_key, config):
148158
else:
149159
processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None
150160

161+
if config.get('httpAuthenticateScheme') is not None:
162+
try:
163+
authenticate_scheme = AuthenticateScheme(config['httpAuthenticateScheme'].upper())
164+
except (ValueError, AttributeError):
165+
authenticate_scheme = AuthenticateScheme.NONE
166+
_LOGGER.warning('You passed an invalid HttpAuthenticationScheme, HttpAuthenticationScheme should be ' \
167+
'one of the following values: `none` or `kerberos`. '
168+
' Defaulting to `none` mode.')
169+
processed["httpAuthenticateScheme"] = authenticate_scheme
170+
151171
return processed

Diff for: splitio/client/factory.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from splitio.optional.loaders import asyncio
88
from splitio.client.client import Client, ClientAsync
99
from splitio.client import input_validator
10+
from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING, AuthenticateScheme
1011
from splitio.client.manager import SplitManager, SplitManagerAsync
11-
from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING
1212
from splitio.client import util
1313
from splitio.client.listener import ImpressionListenerWrapper, ImpressionListenerWrapperAsync
1414
from splitio.engine.impressions.impressions import Manager as ImpressionsManager
@@ -508,12 +508,19 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl
508508
telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer()
509509
telemetry_init_producer = telemetry_producer.get_telemetry_init_producer()
510510

511+
authentication_params = None
512+
if cfg.get("httpAuthenticateScheme") == AuthenticateScheme.KERBEROS:
513+
authentication_params = [cfg.get("kerberosPrincipalUser"),
514+
cfg.get("kerberosPrincipalPassword")]
515+
511516
http_client = HttpClient(
512517
sdk_url=sdk_url,
513518
events_url=events_url,
514519
auth_url=auth_api_base_url,
515520
telemetry_url=telemetry_api_base_url,
516-
timeout=cfg.get('connectionTimeout')
521+
timeout=cfg.get('connectionTimeout'),
522+
authentication_scheme = cfg.get("httpAuthenticateScheme"),
523+
authentication_params = authentication_params
517524
)
518525

519526
sdk_metadata = util.get_metadata(cfg)

Diff for: splitio/optional/loaders.py

+12
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,17 @@ def missing_asyncio_dependencies(*_, **__):
1414
asyncio = missing_asyncio_dependencies
1515
aiofiles = missing_asyncio_dependencies
1616

17+
try:
18+
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
19+
except ImportError:
20+
def missing_auth_dependencies(*_, **__):
21+
"""Fail if missing dependencies are used."""
22+
raise NotImplementedError(
23+
'Missing kerberos auth dependency. '
24+
'Please use `pip install splitio_client[kerberos]` to install the sdk with kerberos auth support'
25+
)
26+
HTTPKerberosAuth = missing_auth_dependencies
27+
OPTIONAL = missing_auth_dependencies
28+
1729
async def _anext(it):
1830
return await it.__anext__()

Diff for: splitio/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '10.0.1'
1+
__version__ = '10.1.0rc1'

Diff for: tests/api/test_httpclient.py

+40-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""HTTPClient test module."""
2+
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
23
import pytest
34
import unittest.mock as mock
45

6+
from splitio.client.config import AuthenticateScheme
57
from splitio.api import client
68
from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync
79
from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemoryTelemetryStorageAsync
@@ -25,7 +27,8 @@ def test_get(self, mocker):
2527
client.SDK_URL + '/test1',
2628
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
2729
params={'param1': 123},
28-
timeout=None
30+
timeout=None,
31+
auth=None
2932
)
3033
assert response.status_code == 200
3134
assert response.body == 'ok'
@@ -37,7 +40,8 @@ def test_get(self, mocker):
3740
client.EVENTS_URL + '/test1',
3841
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
3942
params={'param1': 123},
40-
timeout=None
43+
timeout=None,
44+
auth=None
4145
)
4246
assert get_mock.mock_calls == [call]
4347
assert response.status_code == 200
@@ -59,7 +63,8 @@ def test_get_custom_urls(self, mocker):
5963
'https://sdk.com/test1',
6064
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
6165
params={'param1': 123},
62-
timeout=None
66+
timeout=None,
67+
auth=None
6368
)
6469
assert get_mock.mock_calls == [call]
6570
assert response.status_code == 200
@@ -71,7 +76,8 @@ def test_get_custom_urls(self, mocker):
7176
'https://events.com/test1',
7277
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
7378
params={'param1': 123},
74-
timeout=None
79+
timeout=None,
80+
auth=None
7581
)
7682
assert response.status_code == 200
7783
assert response.body == 'ok'
@@ -95,7 +101,8 @@ def test_post(self, mocker):
95101
json={'p1': 'a'},
96102
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
97103
params={'param1': 123},
98-
timeout=None
104+
timeout=None,
105+
auth=None
99106
)
100107
assert response.status_code == 200
101108
assert response.body == 'ok'
@@ -108,7 +115,8 @@ def test_post(self, mocker):
108115
json={'p1': 'a'},
109116
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
110117
params={'param1': 123},
111-
timeout=None
118+
timeout=None,
119+
auth=None
112120
)
113121
assert response.status_code == 200
114122
assert response.body == 'ok'
@@ -131,7 +139,8 @@ def test_post_custom_urls(self, mocker):
131139
json={'p1': 'a'},
132140
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
133141
params={'param1': 123},
134-
timeout=None
142+
timeout=None,
143+
auth=None
135144
)
136145
assert response.status_code == 200
137146
assert response.body == 'ok'
@@ -144,12 +153,35 @@ def test_post_custom_urls(self, mocker):
144153
json={'p1': 'a'},
145154
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
146155
params={'param1': 123},
147-
timeout=None
156+
timeout=None,
157+
auth=None
148158
)
149159
assert response.status_code == 200
150160
assert response.body == 'ok'
151161
assert get_mock.mock_calls == [call]
152162

163+
def test_authentication_scheme(self, mocker):
164+
response_mock = mocker.Mock()
165+
response_mock.status_code = 200
166+
response_mock.text = 'ok'
167+
get_mock = mocker.Mock()
168+
get_mock.return_value = response_mock
169+
mocker.patch('splitio.api.client.requests.get', new=get_mock)
170+
httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS)
171+
httpclient.set_telemetry_data("metric", mocker.Mock())
172+
response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'})
173+
call = mocker.call(
174+
'https://sdk.com/test1',
175+
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
176+
params={'param1': 123},
177+
timeout=None,
178+
auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL)
179+
)
180+
181+
httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS, authentication_params=['bilal', 'split'])
182+
httpclient.set_telemetry_data("metric", mocker.Mock())
183+
response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'})
184+
153185
def test_telemetry(self, mocker):
154186
telemetry_storage = InMemoryTelemetryStorage()
155187
telemetry_producer = TelemetryStorageProducer(telemetry_storage)

Diff for: tests/client/test_config.py

+10
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,19 @@ def test_sanitize(self):
6868
processed = config.sanitize('some', configs)
6969
assert processed['redisLocalCacheEnabled'] # check default is True
7070
assert processed['flagSetsFilter'] is None
71+
assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE
7172

7273
processed = config.sanitize('some', {'redisHost': 'x', 'flagSetsFilter': ['set']})
7374
assert processed['flagSetsFilter'] is None
7475

7576
processed = config.sanitize('some', {'storageType': 'pluggable', 'flagSetsFilter': ['set']})
7677
assert processed['flagSetsFilter'] is None
78+
79+
processed = config.sanitize('some', {'httpAuthenticateScheme': 'KERBEROS'})
80+
assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.KERBEROS
81+
82+
processed = config.sanitize('some', {'httpAuthenticateScheme': 'anything'})
83+
assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE
84+
85+
processed = config.sanitize('some', {'httpAuthenticateScheme': 'NONE'})
86+
assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE

0 commit comments

Comments
 (0)