Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions splitio/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Auth API module."""

import logging
import json

from future.utils import raise_from

from splitio.api import APIException, headers_from_metadata
from splitio.api.client import HttpClientException
from splitio.models.token import from_raw


class AuthAPI(object): #pylint: disable=too-few-public-methods
"""Class that uses an httpClient to communicate with the SDK Auth Service API."""

def __init__(self, client, apikey, sdk_metadata):
"""
Class constructor.

:param client: HTTP Client responsble for issuing calls to the backend.
:type client: HttpClient
:param apikey: User apikey token.
:type apikey: string
:param sdk_metadata: SDK version & machine name & IP.
:type sdk_metadata: splitio.client.util.SdkMetadata
"""
self._logger = logging.getLogger(self.__class__.__name__)
self._client = client
self._apikey = apikey
self._metadata = headers_from_metadata(sdk_metadata)

def authenticate(self):
"""
Performs authentication.

:return: Json representation of an authentication.
:rtype: dict
"""
try:
response = self._client.get(
'auth',
'/auth',
self._apikey,
extra_headers=self._metadata
)
if 200 <= response.status_code < 300:
payload = json.loads(response.body)
return from_raw(payload)
else:
raise APIException(response.body, response.status_code)
except HttpClientException as exc:
self._logger.error('Exception raised while authenticating')
self._logger.debug('Exception information: ', exc_info=True)
raise_from(APIException('Could not perform authentication.'), exc)
8 changes: 6 additions & 2 deletions splitio/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ class HttpClient(object):

SDK_URL = 'https://sdk.split.io/api'
EVENTS_URL = 'https://events.split.io/api'
AUTH_URL = 'https://auth.split.io/api'

def __init__(self, timeout=None, sdk_url=None, events_url=None):
def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None):
"""
Class constructor.

Expand All @@ -38,11 +39,14 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None):
: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
"""
self._timeout = timeout / 1000 if timeout else None # Convert ms to seconds.
self._urls = {
'sdk': sdk_url if sdk_url is not None else self.SDK_URL,
'events': events_url if events_url is not None else self.EVENTS_URL,
'auth': auth_url if auth_url is not None else self.AUTH_URL,
}

def _build_url(self, server, path):
Expand Down Expand Up @@ -76,7 +80,7 @@ def get(self, server, path, apikey, query=None, extra_headers=None): #pylint: d
"""
Issue a get request.

:param server: Whether the request is for SDK server or Events server.
: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
Expand Down
89 changes: 89 additions & 0 deletions splitio/models/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Token module"""

import base64
import json

class Token(object):
"""Token object class."""

def __init__(self, push_enabled, token, channels, exp, iat):
"""
Class constructor.

:param push_enabled: flag push enabled.
:type push_enabled: bool

:param token: Token from auth.
:type token: str

:param channels: Channels parsed from token.
:type channels: str

:param exp: exp parsed from token.
:type exp: int

:param iat: iat parsed from token.
:type iat: int
"""
self._push_enabled = push_enabled
self._token = token
self._channels = channels
self._exp = exp
self._iat = iat

@property
def push_enabled(self):
"""Return push_enabled"""
return self._push_enabled

@property
def token(self):
"""Return token"""
return self._token

@property
def channels(self):
"""Return channels"""
return self._channels

@property
def exp(self):
"""Return exp"""
return self._exp

@property
def iat(self):
"""Return iat"""
return self._iat


def decode_token(raw_token):
"""Decode token"""
if not 'pushEnabled' in raw_token or not 'token' in raw_token:
return None, None, None

token = raw_token['token']
push_enabled = raw_token['pushEnabled']
if not push_enabled or len(token.strip()) == 0:
return None, None, None

token_parts = token.split('.')
if len(token_parts) < 2:
return None, None, None

to_decode = token_parts[1]
decoded_payload = base64.b64decode(to_decode + '='*(-len(to_decode) % 4))
return push_enabled, token, json.loads(decoded_payload)

def from_raw(raw_token):
"""
Parse a new token from a raw token response.

:param raw_token: Token parsed from auth response.
:type raw_token: dict

:return: New token model object
:rtype: splitio.models.token.Token
"""
push_enabled, token, decoded_token = decode_token(raw_token)
return None if push_enabled is None else Token(push_enabled, token, json.loads(decoded_token['x-ably-capability']), decoded_token['exp'], decoded_token['iat'])
47 changes: 47 additions & 0 deletions tests/api/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Split API tests module."""

import pytest
from splitio.api import auth, client, APIException
from splitio.client.util import get_metadata
from splitio.client.config import DEFAULT_CONFIG
from splitio.version import __version__


class AuthAPITests(object):
"""Auth API test cases."""

def test_auth(self, mocker):
"""Test auth API call."""
token = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X3NlZ21lbnRzXCI6W1wic3Vic2NyaWJlXCJdLFwiTnpNMk1ESTVNemMwX01UZ3lOVGcxTVRnd05nPT1fc3BsaXRzXCI6W1wic3Vic2NyaWJlXCJdLFwiY29udHJvbF9wcmlcIjpbXCJzdWJzY3JpYmVcIixcImNoYW5uZWwtbWV0YWRhdGE6cHVibGlzaGVyc1wiXSxcImNvbnRyb2xfc2VjXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl19IiwieC1hYmx5LWNsaWVudElkIjoiY2xpZW50SWQiLCJleHAiOjE2MDIwODgxMjcsImlhdCI6MTYwMjA4NDUyN30.5_MjWonhs6yoFhw44hNJm3H7_YMjXpSW105DwjjppqE"
httpclient = mocker.Mock(spec=client.HttpClient)
payload = '{{"pushEnabled": true, "token": "{token}"}}'.format(token=token)
cfg = DEFAULT_CONFIG.copy()
cfg.update({'IPAddressesEnabled': True, 'machineName': 'some_machine_name', 'machineIp': '123.123.123.123'})
sdk_metadata = get_metadata(cfg)
httpclient.get.return_value = client.HttpResponse(200, payload)
auth_api = auth.AuthAPI(httpclient, 'some_api_key', sdk_metadata)
response = auth_api.authenticate()

assert response.push_enabled == True
assert response.token == token

call_made = httpclient.get.mock_calls[0]

# validate positional arguments
assert call_made[1] == ('auth', '/auth', 'some_api_key')

# validate key-value args (headers)
assert call_made[2]['extra_headers'] == {
'SplitSDKVersion': 'python-%s' % __version__,
'SplitSDKMachineIP': '123.123.123.123',
'SplitSDKMachineName': 'some_machine_name'
}

httpclient.reset_mock()
def raise_exception(*args, **kwargs):
raise client.HttpClientException('some_message')
httpclient.get.side_effect = raise_exception
with pytest.raises(APIException) as exc_info:
response = auth_api.authenticate()
assert exc_info.type == APIException
assert exc_info.value.message == 'some_message'
45 changes: 45 additions & 0 deletions tests/models/test_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Split model tests module."""

from splitio.models import token
from splitio.models.grammar.condition import Condition


class TokenTests(object):
"""Token model tests."""
raw_false = {
'pushEnabled': False,
'token': 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X3NlZ21lbnRzXCI6W1wic3Vic2NyaWJlXCJdLFwiTnpNMk1ESTVNemMwX01UZ3lOVGcxTVRnd05nPT1fc3BsaXRzXCI6W1wic3Vic2NyaWJlXCJdLFwiY29udHJvbF9wcmlcIjpbXCJzdWJzY3JpYmVcIixcImNoYW5uZWwtbWV0YWRhdGE6cHVibGlzaGVyc1wiXSxcImNvbnRyb2xfc2VjXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl19IiwieC1hYmx5LWNsaWVudElkIjoiY2xpZW50SWQiLCJleHAiOjE2MDIwODgxMjcsImlhdCI6MTYwMjA4NDUyN30.5_MjWonhs6yoFhw44hNJm3H7_YMjXpSW105DwjjppqE',
}

def test_from_raw_false(self):
"""Test token model parsing."""
parsed = token.from_raw(self.raw_false)
assert parsed == None

raw_empty = {
'pushEnabled': True,
'token': '',
}

def test_from_raw_empty(self):
"""Test token model parsing."""
parsed = token.from_raw(self.raw_empty)
assert parsed == None

raw_ok = {
'pushEnabled': True,
'token': 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X3NlZ21lbnRzXCI6W1wic3Vic2NyaWJlXCJdLFwiTnpNMk1ESTVNemMwX01UZ3lOVGcxTVRnd05nPT1fc3BsaXRzXCI6W1wic3Vic2NyaWJlXCJdLFwiY29udHJvbF9wcmlcIjpbXCJzdWJzY3JpYmVcIixcImNoYW5uZWwtbWV0YWRhdGE6cHVibGlzaGVyc1wiXSxcImNvbnRyb2xfc2VjXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl19IiwieC1hYmx5LWNsaWVudElkIjoiY2xpZW50SWQiLCJleHAiOjE2MDIwODgxMjcsImlhdCI6MTYwMjA4NDUyN30.5_MjWonhs6yoFhw44hNJm3H7_YMjXpSW105DwjjppqE',
}

def test_from_raw(self):
"""Test token model parsing."""
parsed = token.from_raw(self.raw_ok)
assert isinstance(parsed, token.Token)
assert parsed.push_enabled == True
assert parsed.iat == 1602084527
assert parsed.exp == 1602088127
assert parsed.channels['NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_segments'] == ['subscribe']
assert parsed.channels['NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits'] == ['subscribe']
assert parsed.channels['control_pri'] == ['subscribe', 'channel-metadata:publishers']
assert parsed.channels['control_sec'] == ['subscribe', 'channel-metadata:publishers']