Skip to content

Commit 1719b7f

Browse files
author
Bilal Al
committed
added support for spnego/kerberos auth
1 parent 325a0f6 commit 1719b7f

6 files changed

Lines changed: 117 additions & 22 deletions

File tree

setup.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,22 @@
66

77
TESTS_REQUIRES = [
88
'flake8',
9-
'pytest==7.1.0',
10-
'pytest-mock==3.11.1',
11-
'coverage==7.2.7',
9+
'pytest==7.0.1',
10+
'pytest-mock==3.13.0',
11+
'coverage==6.2',
1212
'pytest-cov',
13-
'importlib-metadata==6.7',
14-
'tomli',
15-
'iniconfig',
16-
'attrs'
13+
'importlib-metadata==4.2',
14+
'tomli==1.2.3',
15+
'iniconfig==1.1.1',
16+
'attrs==22.1.0'
1717
]
1818

1919
INSTALL_REQUIRES = [
2020
'requests',
2121
'pyyaml',
2222
'docopt>=0.6.2',
23-
'bloom-filter2>=2.0.0'
23+
'bloom-filter2>=2.0.0',
24+
'requests-kerberos>=0.14.0'
2425
]
2526

2627
with open(path.join(path.abspath(path.dirname(__file__)), 'splitio', 'version.py')) as f:

splitio/api/client.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
import requests
55
import logging
6+
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
7+
8+
from splitio.client.config import AuthenticateScheme
9+
610
_LOGGER = logging.getLogger(__name__)
711

812
HttpResponse = namedtuple('HttpResponse', ['status_code', 'body'])
@@ -28,7 +32,7 @@ class HttpClient(object):
2832
AUTH_URL = 'https://auth.split.io/api'
2933
TELEMETRY_URL = 'https://telemetry.split.io/api'
3034

31-
def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None):
35+
def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None):
3236
"""
3337
Class constructor.
3438
@@ -50,6 +54,8 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t
5054
'auth': auth_url if auth_url is not None else self.AUTH_URL,
5155
'telemetry': telemetry_url if telemetry_url is not None else self.TELEMETRY_URL,
5256
}
57+
self._authentication_scheme = authentication_scheme
58+
self._authentication_params = authentication_params
5359

5460
def _build_url(self, server, path):
5561
"""
@@ -100,14 +106,17 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint:
100106
if extra_headers is not None:
101107
headers.update(extra_headers)
102108

109+
authentication = self._get_authentication()
103110
try:
104111
response = requests.get(
105112
self._build_url(server, path),
106113
params=query,
107114
headers=headers,
108-
timeout=self._timeout
115+
timeout=self._timeout,
116+
auth=authentication
109117
)
110118
return HttpResponse(response.status_code, response.text)
119+
111120
except Exception as exc: # pylint: disable=broad-except
112121
raise HttpClientException('requests library is throwing exceptions') from exc
113122

@@ -136,14 +145,25 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): #
136145
if extra_headers is not None:
137146
headers.update(extra_headers)
138147

148+
authentication = self._get_authentication()
139149
try:
140150
response = requests.post(
141151
self._build_url(server, path),
142152
json=body,
143153
params=query,
144154
headers=headers,
145-
timeout=self._timeout
155+
timeout=self._timeout,
156+
auth=authentication
146157
)
147158
return HttpResponse(response.status_code, response.text)
148159
except Exception as exc: # pylint: disable=broad-except
149160
raise HttpClientException('requests library is throwing exceptions') from exc
161+
162+
def _get_authentication(self):
163+
authentication = None
164+
if self._authentication_scheme == AuthenticateScheme.KERBEROS:
165+
if self._authentication_params is not None:
166+
authentication = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL)
167+
else:
168+
authentication = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
169+
return authentication

splitio/client/config.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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
@@ -9,6 +10,12 @@
910
_LOGGER = logging.getLogger(__name__)
1011
DEFAULT_DATA_SAMPLING = 1
1112

13+
class AuthenticateScheme(Enum):
14+
"""Authentication Scheme."""
15+
NONE = 'NONE'
16+
KERBEROS = 'KERBEROS'
17+
18+
1219
DEFAULT_CONFIG = {
1320
'operationMode': 'standalone',
1421
'connectionTimeout': 1500,
@@ -60,7 +67,10 @@
6067
'storageWrapper': None,
6168
'storagePrefix': None,
6269
'storageType': None,
63-
'flagSetsFilter': None
70+
'flagSetsFilter': None,
71+
'httpAuthenticateScheme': AuthenticateScheme.NONE,
72+
'kerberosPrincipalUser': None,
73+
'kerberosPrincipalPassword': None
6474
}
6575

6676
def _parse_operation_mode(sdk_key, config):
@@ -149,4 +159,14 @@ def sanitize(sdk_key, config):
149159
else:
150160
processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None
151161

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

splitio/client/factory.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from splitio.client.client import Client
99
from splitio.client import input_validator
1010
from splitio.client.manager import SplitManager
11-
from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING
11+
from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING, AuthenticateScheme
1212
from splitio.client import util
1313
from splitio.client.listener import ImpressionListenerWrapper
1414
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
332332
telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer()
333333
telemetry_init_producer = telemetry_producer.get_telemetry_init_producer()
334334

335+
authentication_params = None
336+
if cfg.get("httpAuthenticateScheme") == AuthenticateScheme.KERBEROS:
337+
authentication_params = [cfg.get("kerberosPrincipalUser"),
338+
cfg.get("kerberosPrincipalPassword")]
339+
335340
http_client = HttpClient(
336341
sdk_url=sdk_url,
337342
events_url=events_url,
338343
auth_url=auth_api_base_url,
339344
telemetry_url=telemetry_api_base_url,
340-
timeout=cfg.get('connectionTimeout')
345+
timeout=cfg.get('connectionTimeout'),
346+
authentication_scheme = cfg.get("httpAuthenticateScheme"),
347+
authentication_params = authentication_params
341348
)
342349

343350
sdk_metadata = util.get_metadata(cfg)

tests/api/test_httpclient.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""HTTPClient test module."""
2+
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
23

34
from splitio.api import client
5+
from splitio.client.config import AuthenticateScheme
46

57
class HttpClientTests(object):
68
"""Http Client test cases."""
@@ -19,7 +21,8 @@ def test_get(self, mocker):
1921
client.HttpClient.SDK_URL + '/test1',
2022
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
2123
params={'param1': 123},
22-
timeout=None
24+
timeout=None,
25+
auth=None
2326
)
2427
assert response.status_code == 200
2528
assert response.body == 'ok'
@@ -31,7 +34,8 @@ def test_get(self, mocker):
3134
client.HttpClient.EVENTS_URL + '/test1',
3235
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
3336
params={'param1': 123},
34-
timeout=None
37+
timeout=None,
38+
auth=None
3539
)
3640
assert get_mock.mock_calls == [call]
3741
assert response.status_code == 200
@@ -51,7 +55,8 @@ def test_get_custom_urls(self, mocker):
5155
'https://sdk.com/test1',
5256
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
5357
params={'param1': 123},
54-
timeout=None
58+
timeout=None,
59+
auth=None
5560
)
5661
assert get_mock.mock_calls == [call]
5762
assert response.status_code == 200
@@ -63,7 +68,8 @@ def test_get_custom_urls(self, mocker):
6368
'https://events.com/test1',
6469
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
6570
params={'param1': 123},
66-
timeout=None
71+
timeout=None,
72+
auth=None
6773
)
6874
assert response.status_code == 200
6975
assert response.body == 'ok'
@@ -85,7 +91,8 @@ def test_post(self, mocker):
8591
json={'p1': 'a'},
8692
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
8793
params={'param1': 123},
88-
timeout=None
94+
timeout=None,
95+
auth=None
8996
)
9097
assert response.status_code == 200
9198
assert response.body == 'ok'
@@ -98,7 +105,8 @@ def test_post(self, mocker):
98105
json={'p1': 'a'},
99106
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
100107
params={'param1': 123},
101-
timeout=None
108+
timeout=None,
109+
auth=None
102110
)
103111
assert response.status_code == 200
104112
assert response.body == 'ok'
@@ -119,7 +127,8 @@ def test_post_custom_urls(self, mocker):
119127
json={'p1': 'a'},
120128
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
121129
params={'param1': 123},
122-
timeout=None
130+
timeout=None,
131+
auth=None
123132
)
124133
assert response.status_code == 200
125134
assert response.body == 'ok'
@@ -132,8 +141,36 @@ def test_post_custom_urls(self, mocker):
132141
json={'p1': 'a'},
133142
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
134143
params={'param1': 123},
135-
timeout=None
144+
timeout=None,
145+
auth=None
136146
)
137147
assert response.status_code == 200
138148
assert response.body == 'ok'
139149
assert get_mock.mock_calls == [call]
150+
151+
def test_authentication_scheme(self, mocker):
152+
response_mock = mocker.Mock()
153+
response_mock.status_code = 200
154+
response_mock.text = 'ok'
155+
get_mock = mocker.Mock()
156+
get_mock.return_value = response_mock
157+
mocker.patch('splitio.api.client.requests.get', new=get_mock)
158+
httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS)
159+
response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'})
160+
call = mocker.call(
161+
'https://sdk.com/test1',
162+
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
163+
params={'param1': 123},
164+
timeout=None,
165+
auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL)
166+
)
167+
168+
httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS, authentication_params=['bilal', 'split'])
169+
response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'})
170+
call = mocker.call(
171+
'https://sdk.com/test1',
172+
headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'},
173+
params={'param1': 123},
174+
timeout=None,
175+
auth=HTTPKerberosAuth(principal='bilal', password='split',mutual_authentication=OPTIONAL)
176+
)

tests/client/test_config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,19 @@ def test_sanitize(self):
6868
processed = config.sanitize('some', {})
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)