Skip to content

Commit d3cf340

Browse files
author
Matias Melograno
committed
added auth client
1 parent 69892f6 commit d3cf340

5 files changed

Lines changed: 240 additions & 2 deletions

File tree

splitio/api/auth.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Splits API module."""
2+
3+
import logging
4+
import json
5+
6+
from future.utils import raise_from
7+
8+
from splitio.api import APIException, headers_from_metadata
9+
from splitio.api.client import HttpClientException
10+
from splitio.models.token import from_raw
11+
12+
13+
class AuthAPI(object): #pylint: disable=too-few-public-methods
14+
"""Class that uses an httpClient to communicate with the SDK Auth Service API."""
15+
16+
def __init__(self, client, apikey, sdk_metadata):
17+
"""
18+
Class constructor.
19+
20+
:param client: HTTP Client responsble for issuing calls to the backend.
21+
:type client: HttpClient
22+
:param apikey: User apikey token.
23+
:type apikey: string
24+
:param sdk_metadata: SDK version & machine name & IP.
25+
:type sdk_metadata: splitio.client.util.SdkMetadata
26+
"""
27+
self._logger = logging.getLogger(self.__class__.__name__)
28+
self._client = client
29+
self._apikey = apikey
30+
self._metadata = headers_from_metadata(sdk_metadata)
31+
32+
def authenticate(self):
33+
"""
34+
Performs authentication.
35+
36+
:return: Json representation of an authentication.
37+
:rtype: dict
38+
"""
39+
try:
40+
response = self._client.get(
41+
'auth',
42+
'/auth',
43+
self._apikey,
44+
extra_headers=self._metadata
45+
)
46+
if 200 <= response.status_code < 300:
47+
payload = json.loads(response.body)
48+
return from_raw(payload)
49+
else:
50+
raise APIException(response.body, response.status_code)
51+
except HttpClientException as exc:
52+
self._logger.error('Exception raised while authenticating')
53+
self._logger.debug('Exception information: ', exc_info=True)
54+
raise_from(APIException('Could not perform authentication.'), exc)

splitio/api/client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ class HttpClient(object):
2727

2828
SDK_URL = 'https://sdk.split.io/api'
2929
EVENTS_URL = 'https://events.split.io/api'
30+
AUTH_URL = 'https://auth.split.io/api'
3031

31-
def __init__(self, timeout=None, sdk_url=None, events_url=None):
32+
def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None):
3233
"""
3334
Class constructor.
3435
@@ -38,11 +39,14 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None):
3839
:type sdk_url: str
3940
:param events_url: Optional alternative events URL.
4041
:type events_url: str
42+
:param events_url: Optional alternative auth URL.
43+
:type auth_url: str
4144
"""
4245
self._timeout = timeout / 1000 if timeout else None # Convert ms to seconds.
4346
self._urls = {
4447
'sdk': sdk_url if sdk_url is not None else self.SDK_URL,
4548
'events': events_url if events_url is not None else self.EVENTS_URL,
49+
'auth': auth_url if auth_url is not None else self.AUTH_URL,
4650
}
4751

4852
def _build_url(self, server, path):
@@ -76,7 +80,7 @@ def get(self, server, path, apikey, query=None, extra_headers=None): #pylint: d
7680
"""
7781
Issue a get request.
7882
79-
:param server: Whether the request is for SDK server or Events server.
83+
:param server: Whether the request is for SDK server, Events server or Auth server.
8084
:typee server: str
8185
:param path: path to append to the host url.
8286
:type path: str

splitio/models/token.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Token module"""
2+
3+
import base64
4+
import json
5+
6+
class Token(object):
7+
"""Token object class."""
8+
9+
def __init__(self, push_enabled, token, channels, exp, iat):
10+
"""
11+
Class constructor.
12+
13+
:param push_enabled: flag push enabled.
14+
:type push_enabled: bool
15+
16+
:param token: Token from auth.
17+
:type token: str
18+
19+
:param channels: Channels parsed from token.
20+
:type channels: str
21+
22+
:param exp: exp parsed from token.
23+
:type exp: int
24+
25+
:param iat: iat parsed from token.
26+
:type iat: int
27+
"""
28+
self._push_enabled = push_enabled
29+
self._token = token
30+
self._channels = channels
31+
self._exp = exp
32+
self._iat = iat
33+
34+
@property
35+
def push_enabled(self):
36+
"""Return push_enabled"""
37+
return self._push_enabled
38+
39+
@property
40+
def token(self):
41+
"""Return token"""
42+
return self._token
43+
44+
@property
45+
def channels(self):
46+
"""Return channels"""
47+
return self._channels
48+
49+
@property
50+
def exp(self):
51+
"""Return exp"""
52+
return self._exp
53+
54+
@property
55+
def iat(self):
56+
"""Return iat"""
57+
return self._iat
58+
59+
60+
def decode_token(push_enabled, token):
61+
"""Return channel_list"""
62+
if not push_enabled or len(token.strip()) == 0:
63+
return None
64+
65+
token_parts = token.split('.')
66+
if len(token_parts) < 2:
67+
return None
68+
69+
to_decode = token_parts[1]
70+
decoded_payload = base64.b64decode(to_decode + '='*(-len(to_decode) % 4))
71+
return json.loads(decoded_payload)
72+
73+
def from_raw(raw_token):
74+
"""
75+
Parse a new token from a raw token response.
76+
77+
:param raw_token: Token parsed from auth response.
78+
:type raw_token: dict
79+
80+
:return: New token model object
81+
:rtype: splitio.models.token.Token
82+
"""
83+
decoded_token = decode_token(raw_token['pushEnabled'], raw_token['token'])
84+
if decoded_token is None:
85+
return None
86+
return Token(raw_token['pushEnabled'], raw_token['token'], json.loads(decoded_token['x-ably-capability']), decoded_token['exp'], decoded_token['iat'])
87+

tests/api/test_auth.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Split API tests module."""
2+
3+
import pytest
4+
from splitio.api import auth, client, APIException
5+
from splitio.client.util import get_metadata
6+
from splitio.client.config import DEFAULT_CONFIG
7+
from splitio.version import __version__
8+
9+
10+
class AuthAPITests(object):
11+
"""Auth API test cases."""
12+
13+
def test_auth(self, mocker):
14+
"""Test auth API call."""
15+
token = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X3NlZ21lbnRzXCI6W1wic3Vic2NyaWJlXCJdLFwiTnpNMk1ESTVNemMwX01UZ3lOVGcxTVRnd05nPT1fc3BsaXRzXCI6W1wic3Vic2NyaWJlXCJdLFwiY29udHJvbF9wcmlcIjpbXCJzdWJzY3JpYmVcIixcImNoYW5uZWwtbWV0YWRhdGE6cHVibGlzaGVyc1wiXSxcImNvbnRyb2xfc2VjXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl19IiwieC1hYmx5LWNsaWVudElkIjoiY2xpZW50SWQiLCJleHAiOjE2MDIwODgxMjcsImlhdCI6MTYwMjA4NDUyN30.5_MjWonhs6yoFhw44hNJm3H7_YMjXpSW105DwjjppqE"
16+
httpclient = mocker.Mock(spec=client.HttpClient)
17+
payload = '{{"pushEnabled": true, "token": "{token}"}}'.format(token=token)
18+
cfg = DEFAULT_CONFIG.copy()
19+
cfg.update({'IPAddressesEnabled': True, 'machineName': 'some_machine_name', 'machineIp': '123.123.123.123'})
20+
sdk_metadata = get_metadata(cfg)
21+
httpclient.get.return_value = client.HttpResponse(200, payload)
22+
auth_api = auth.AuthAPI(httpclient, 'some_api_key', sdk_metadata)
23+
response = auth_api.authenticate()
24+
25+
assert response.push_enabled == True
26+
assert response.token == token
27+
28+
call_made = httpclient.get.mock_calls[0]
29+
30+
# validate positional arguments
31+
assert call_made[1] == ('auth', '/auth', 'some_api_key')
32+
33+
# validate key-value args (headers)
34+
assert call_made[2]['extra_headers'] == {
35+
'SplitSDKVersion': 'python-%s' % __version__,
36+
'SplitSDKMachineIP': '123.123.123.123',
37+
'SplitSDKMachineName': 'some_machine_name'
38+
}
39+
# assert httpclient.get.mock_calls == [mocker.call('auth', '/auth', 'some_api_key', )]
40+
41+
httpclient.reset_mock()
42+
def raise_exception(*args, **kwargs):
43+
raise client.HttpClientException('some_message')
44+
httpclient.get.side_effect = raise_exception
45+
with pytest.raises(APIException) as exc_info:
46+
response = auth_api.authenticate()
47+
assert exc_info.type == APIException
48+
assert exc_info.value.message == 'some_message'

tests/models/test_token.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Split model tests module."""
2+
3+
from splitio.models import token
4+
from splitio.models.grammar.condition import Condition
5+
6+
7+
class TokenTests(object):
8+
"""Token model tests."""
9+
raw_false = {
10+
'pushEnabled': False,
11+
'token': 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X3NlZ21lbnRzXCI6W1wic3Vic2NyaWJlXCJdLFwiTnpNMk1ESTVNemMwX01UZ3lOVGcxTVRnd05nPT1fc3BsaXRzXCI6W1wic3Vic2NyaWJlXCJdLFwiY29udHJvbF9wcmlcIjpbXCJzdWJzY3JpYmVcIixcImNoYW5uZWwtbWV0YWRhdGE6cHVibGlzaGVyc1wiXSxcImNvbnRyb2xfc2VjXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl19IiwieC1hYmx5LWNsaWVudElkIjoiY2xpZW50SWQiLCJleHAiOjE2MDIwODgxMjcsImlhdCI6MTYwMjA4NDUyN30.5_MjWonhs6yoFhw44hNJm3H7_YMjXpSW105DwjjppqE',
12+
}
13+
14+
def test_from_raw_false(self):
15+
"""Test token model parsing."""
16+
parsed = token.from_raw(self.raw_false)
17+
assert parsed == None
18+
19+
raw_empty = {
20+
'pushEnabled': True,
21+
'token': '',
22+
}
23+
24+
def test_from_raw_empty(self):
25+
"""Test token model parsing."""
26+
parsed = token.from_raw(self.raw_empty)
27+
assert parsed == None
28+
29+
raw_ok = {
30+
'pushEnabled': True,
31+
'token': 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X3NlZ21lbnRzXCI6W1wic3Vic2NyaWJlXCJdLFwiTnpNMk1ESTVNemMwX01UZ3lOVGcxTVRnd05nPT1fc3BsaXRzXCI6W1wic3Vic2NyaWJlXCJdLFwiY29udHJvbF9wcmlcIjpbXCJzdWJzY3JpYmVcIixcImNoYW5uZWwtbWV0YWRhdGE6cHVibGlzaGVyc1wiXSxcImNvbnRyb2xfc2VjXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl19IiwieC1hYmx5LWNsaWVudElkIjoiY2xpZW50SWQiLCJleHAiOjE2MDIwODgxMjcsImlhdCI6MTYwMjA4NDUyN30.5_MjWonhs6yoFhw44hNJm3H7_YMjXpSW105DwjjppqE',
32+
}
33+
34+
def test_from_raw(self):
35+
"""Test token model parsing."""
36+
parsed = token.from_raw(self.raw_ok)
37+
assert isinstance(parsed, token.Token)
38+
assert parsed.push_enabled == True
39+
assert parsed.iat == 1602084527
40+
assert parsed.exp == 1602088127
41+
assert parsed.channels['NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_segments'] == ['subscribe']
42+
assert parsed.channels['NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits'] == ['subscribe']
43+
assert parsed.channels['control_pri'] == ['subscribe', 'channel-metadata:publishers']
44+
assert parsed.channels['control_sec'] == ['subscribe', 'channel-metadata:publishers']
45+

0 commit comments

Comments
 (0)