Skip to content

Commit 712ddea

Browse files
Merge branch 'master' into ali/update_logs_for_tr
2 parents 0e69287 + 93689b9 commit 712ddea

File tree

5 files changed

+127
-18
lines changed

5 files changed

+127
-18
lines changed

optimizely/config_manager.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ def get_config(self):
150150
class PollingConfigManager(StaticConfigManager):
151151
""" Config manager that polls for the datafile and updated ProjectConfig based on an update interval. """
152152

153+
DATAFILE_URL_TEMPLATE = enums.ConfigManager.DATAFILE_URL_TEMPLATE
154+
153155
def __init__(
154156
self,
155157
sdk_key=None,
@@ -192,7 +194,7 @@ def __init__(
192194
skip_json_validation=skip_json_validation,
193195
)
194196
self.datafile_url = self.get_datafile_url(
195-
sdk_key, url, url_template or enums.ConfigManager.DATAFILE_URL_TEMPLATE
197+
sdk_key, url, url_template or self.DATAFILE_URL_TEMPLATE
196198
)
197199
self.set_update_interval(update_interval)
198200
self.set_blocking_timeout(blocking_timeout)
@@ -368,3 +370,46 @@ def start(self):
368370
""" Start the config manager and the thread to periodically fetch datafile. """
369371
if not self.is_running:
370372
self._polling_thread.start()
373+
374+
375+
class AuthDatafilePollingConfigManager(PollingConfigManager):
376+
""" Config manager that polls for authenticated datafile using access token. """
377+
378+
DATAFILE_URL_TEMPLATE = enums.ConfigManager.AUTHENTICATED_DATAFILE_URL_TEMPLATE
379+
380+
def __init__(
381+
self,
382+
access_token,
383+
*args,
384+
**kwargs
385+
):
386+
""" Initialize config manager. One of sdk_key or url has to be set to be able to use.
387+
388+
Args:
389+
access_token: String to be attached to the request header to fetch the authenticated datafile.
390+
*args: Refer to arguments descriptions in PollingConfigManager.
391+
**kwargs: Refer to keyword arguments descriptions in PollingConfigManager.
392+
"""
393+
self._set_access_token(access_token)
394+
super(AuthDatafilePollingConfigManager, self).__init__(*args, **kwargs)
395+
396+
def _set_access_token(self, access_token):
397+
""" Checks for valid access token input and sets it. """
398+
if not access_token:
399+
raise optimizely_exceptions.InvalidInputException(
400+
'access_token cannot be empty or None.')
401+
self.access_token = access_token
402+
403+
def fetch_datafile(self):
404+
""" Fetch authenticated datafile and set ProjectConfig. """
405+
request_headers = {}
406+
request_headers[enums.HTTPHeaders.AUTHORIZATION] = \
407+
enums.ConfigManager.AUTHORIZATION_HEADER_DATA_TEMPLATE.format(access_token=self.access_token)
408+
409+
if self.last_modified:
410+
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified
411+
412+
response = requests.get(
413+
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
414+
)
415+
self._handle_response(response)

optimizely/helpers/enums.py

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class RolloutRuleAudienceEvaluationLogs(CommonAudienceEvaluationLogs):
5757

5858

5959
class ConfigManager(object):
60+
AUTHENTICATED_DATAFILE_URL_TEMPLATE = 'https://config.optimizely.com/datafiles/auth/{sdk_key}.json'
61+
AUTHORIZATION_HEADER_DATA_TEMPLATE = 'Bearer {access_token}'
6062
DATAFILE_URL_TEMPLATE = 'https://cdn.optimizely.com/datafiles/{sdk_key}.json'
6163
# Default time in seconds to block the 'get_config' method call until 'config' instance has been initialized.
6264
DEFAULT_BLOCKING_TIMEOUT = 10
@@ -112,6 +114,7 @@ class Errors(object):
112114

113115

114116
class HTTPHeaders(object):
117+
AUTHORIZATION = 'Authorization'
115118
IF_MODIFIED_SINCE = 'If-Modified-Since'
116119
LAST_MODIFIED = 'Last-Modified'
117120

optimizely/optimizely.py

+18-15
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from . import event_builder
1818
from . import exceptions
1919
from . import logger as _logging
20+
from .config_manager import AuthDatafilePollingConfigManager
2021
from .config_manager import PollingConfigManager
2122
from .config_manager import StaticConfigManager
2223
from .error_handler import NoOpErrorHandler as noop_error_handler
@@ -43,6 +44,7 @@ def __init__(
4344
config_manager=None,
4445
notification_center=None,
4546
event_processor=None,
47+
access_token=None,
4648
):
4749
""" Optimizely init method for managing Custom projects.
4850
@@ -65,6 +67,7 @@ def __init__(
6567
By default optimizely.event.event_processor.ForwardingEventProcessor is used
6668
which simply forwards events to the event dispatcher.
6769
To enable event batching configure and use optimizely.event.event_processor.BatchEventProcessor.
70+
access_token: Optional string used to fetch authenticated datafile for a secure project environment.
6871
"""
6972
self.logger_name = '.'.join([__name__, self.__class__.__name__])
7073
self.is_valid = True
@@ -87,24 +90,24 @@ def __init__(
8790
self.logger.exception(str(error))
8891
return
8992

93+
config_manager_options = {
94+
'datafile': datafile,
95+
'logger': self.logger,
96+
'error_handler': self.error_handler,
97+
'notification_center': self.notification_center,
98+
'skip_json_validation': skip_json_validation,
99+
}
100+
90101
if not self.config_manager:
91102
if sdk_key:
92-
self.config_manager = PollingConfigManager(
93-
sdk_key=sdk_key,
94-
datafile=datafile,
95-
logger=self.logger,
96-
error_handler=self.error_handler,
97-
notification_center=self.notification_center,
98-
skip_json_validation=skip_json_validation,
99-
)
103+
config_manager_options['sdk_key'] = sdk_key
104+
if access_token:
105+
config_manager_options['access_token'] = access_token
106+
self.config_manager = AuthDatafilePollingConfigManager(**config_manager_options)
107+
else:
108+
self.config_manager = PollingConfigManager(**config_manager_options)
100109
else:
101-
self.config_manager = StaticConfigManager(
102-
datafile=datafile,
103-
logger=self.logger,
104-
error_handler=self.error_handler,
105-
notification_center=self.notification_center,
106-
skip_json_validation=skip_json_validation,
107-
)
110+
self.config_manager = StaticConfigManager(**config_manager_options)
108111

109112
self.event_builder = event_builder.EventBuilder()
110113
self.decision_service = decision_service.DecisionService(self.logger, user_profile_service)

tests/test_config_manager.py

+50-2
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,10 @@ def test_set_last_modified(self, _):
365365

366366
def test_fetch_datafile(self, _):
367367
""" Test that fetch_datafile sets config and last_modified based on response. """
368+
sdk_key = 'some_key'
368369
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
369-
project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key')
370-
expected_datafile_url = 'https://cdn.optimizely.com/datafiles/some_key.json'
370+
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key)
371+
expected_datafile_url = enums.ConfigManager.DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
371372
test_headers = {'Last-Modified': 'New Time'}
372373
test_datafile = json.dumps(self.config_dict_with_features)
373374
test_response = requests.Response()
@@ -397,3 +398,50 @@ def test_is_running(self, _):
397398
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
398399
project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key')
399400
self.assertTrue(project_config_manager.is_running)
401+
402+
403+
@mock.patch('requests.get')
404+
class AuthDatafilePollingConfigManagerTest(base.BaseTest):
405+
def test_init__access_token_none__fails(self, _):
406+
""" Test that initialization fails if access_token is None. """
407+
self.assertRaisesRegexp(
408+
optimizely_exceptions.InvalidInputException,
409+
'access_token cannot be empty or None.',
410+
config_manager.AuthDatafilePollingConfigManager,
411+
access_token=None
412+
)
413+
414+
def test_set_access_token(self, _):
415+
""" Test that access_token is properly set as instance variable. """
416+
access_token = 'some_token'
417+
sdk_key = 'some_key'
418+
with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile'):
419+
project_config_manager = config_manager.AuthDatafilePollingConfigManager(
420+
access_token=access_token, sdk_key=sdk_key)
421+
422+
self.assertEqual(access_token, project_config_manager.access_token)
423+
424+
def test_fetch_datafile(self, _):
425+
""" Test that fetch_datafile sets authorization header in request header and sets config based on response. """
426+
access_token = 'some_token'
427+
sdk_key = 'some_key'
428+
with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile'):
429+
project_config_manager = config_manager.AuthDatafilePollingConfigManager(
430+
access_token=access_token, sdk_key=sdk_key)
431+
expected_datafile_url = enums.ConfigManager.AUTHENTICATED_DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
432+
test_datafile = json.dumps(self.config_dict_with_features)
433+
test_response = requests.Response()
434+
test_response.status_code = 200
435+
test_response._content = test_datafile
436+
437+
# Call fetch_datafile and assert that request was sent with correct authorization header
438+
with mock.patch('requests.get', return_value=test_response) as mock_request:
439+
project_config_manager.fetch_datafile()
440+
441+
mock_request.assert_called_once_with(
442+
expected_datafile_url,
443+
headers={'Authorization': 'Bearer {access_token}'.format(access_token=access_token)},
444+
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
445+
)
446+
447+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)

tests/test_optimizely.py

+10
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,16 @@ def test_init__sdk_key_and_datafile(self):
252252

253253
self.assertIs(type(opt_obj.config_manager), config_manager.PollingConfigManager)
254254

255+
def test_init__sdk_key_and_access_token(self):
256+
""" Test that if both sdk_key and access_token is provided then AuthDatafilePollingConfigManager is used. """
257+
258+
with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager._set_config'), mock.patch(
259+
'threading.Thread.start'
260+
):
261+
opt_obj = optimizely.Optimizely(access_token='test_access_token', sdk_key='test_sdk_key')
262+
263+
self.assertIs(type(opt_obj.config_manager), config_manager.AuthDatafilePollingConfigManager)
264+
255265
def test_invalid_json_raises_schema_validation_off(self):
256266
""" Test that invalid JSON logs error if schema validation is turned off. """
257267

0 commit comments

Comments
 (0)