diff --git a/kaylee/__init__.py b/kaylee/__init__.py index 9c12d7e..b4388d4 100644 --- a/kaylee/__init__.py +++ b/kaylee/__init__.py @@ -17,7 +17,7 @@ __version__ = '0.3' from . import loader -kl = loader.LazyKaylee() + from .core import Kaylee, Applications from .node import Node, NodeID, NodesRegistry @@ -33,6 +33,7 @@ NodeRequestRejectedError, ApplicationCompletedError,) +kl = loader.LazyKaylee() def setup(settings): #pylint: disable-msg=W0212 diff --git a/kaylee/client/kaylee.coffee b/kaylee/client/kaylee.coffee index aaa40f0..3df0b8a 100644 --- a/kaylee/client/kaylee.coffee +++ b/kaylee/client/kaylee.coffee @@ -20,7 +20,7 @@ # CONSTANTS # #-----------# -SESSION_DATA_ATTRIBUTE = '__kl_sd__' +SESSION_DATA_ATTRIBUTE = '__kl_session_data__' WORKER_SCRIPT_URL = ((scripts) -> scripts = document.getElementsByTagName('script') @@ -29,7 +29,7 @@ WORKER_SCRIPT_URL = ((scripts) -> if not script.getAttribute.length? path = script.src - # replace 'http://address/kaylee.js' with 'http://address/klworker.js' + # replace 'http://path/to/kaylee.js' with 'http://path/to/klworker.js' path = script.getAttribute('src', -1) return path[..path.lastIndexOf('/')] + 'klworker.js' )() diff --git a/kaylee/controller.py b/kaylee/controller.py index 74934dd..b888e7e 100644 --- a/kaylee/controller.py +++ b/kaylee/controller.py @@ -8,10 +8,10 @@ :copyright: (c) 2012 by Zaur Nasibov. :license: MIT, see LICENSE for more details. """ - import re from abc import ABCMeta, abstractmethod + #: The Application name regular expression pattern which can be used in #: e.g. web frameworks' URL dispatchers. app_name_pattern = r'[a-zA-Z\.\d_-]+' @@ -55,7 +55,7 @@ class Controller(object, metaclass=ABCMeta): _app_name_re = re.compile('^{}$'.format(app_name_pattern)) def __init__(self, name, project, permanent_storage, - temporal_storage=None): + temporal_storage=None, **kwargs): if Controller._app_name_re.match(name) is None: raise ValueError('Invalid application name: {}' .format(name)) diff --git a/kaylee/core.py b/kaylee/core.py index 1995638..61ec5ae 100644 --- a/kaylee/core.py +++ b/kaylee/core.py @@ -20,8 +20,8 @@ from functools import wraps from .node import Node, NodeID -from .errors import (KayleeError, InvalidResultError, NodeRequestRejectedError, - InvalidConfigurationError) +from .errors import (KayleeError, InvalidResultError, NodeRequestRejectedError) + from .controller import KL_RESULT from .util import DictAsObjectWrapper @@ -114,7 +114,7 @@ def register(self, remote_host): node = Node(NodeID.for_host(remote_host)) self.registry.add(node) return json.dumps ({ 'node_id' : str(node.id), - 'config' : self.config.to_client_dict(), + 'config' : self.config.client_config(), 'applications' : self._applications.names } ) @json_error_handler @@ -262,23 +262,10 @@ class Config(DictAsObjectWrapper): """The ``Config`` object maintains the run-time Kaylee configuration options (see :ref:`configuration` for full description). """ - client_config_fields = [ - 'AUTO_GET_ACTION', - ] - def __init__(self, **kwargs): super(Config, self).__init__(**kwargs) self._dirty = True self._cached_dict = {} - self._validate() - - def _validate(self): - #pylint: disable-msg=E1101 - if not isinstance(self.AUTO_GET_ACTION, bool): - raise InvalidConfigurationError('AUTO_GET_ACTION is not a boolean') - #pylint: disable-msg=E1101 - if not isinstance(self.SECRET_KEY, str): - raise InvalidConfigurationError('SECRET_KEY is not a string object') def __setattr__(self, name, value): if name != '_dirty': @@ -287,10 +274,14 @@ def __setattr__(self, name, value): else: self.__dict__[name] = value - def to_client_dict(self): + def client_config(self): + client_config_fields = [ + 'AUTO_GET_ACTION', + ] + if self._dirty: - self._cached_dict = { k : getattr(self, k) - for k in self.client_config_fields } + self._cached_dict = {k : getattr(self, k) + for k in client_config_fields} self._dirty = False return self._cached_dict diff --git a/kaylee/errors.py b/kaylee/errors.py index 11e6003..1292ad4 100644 --- a/kaylee/errors.py +++ b/kaylee/errors.py @@ -72,12 +72,11 @@ def __init__(self, application): .format(application.name) ) -class InvalidConfigurationError(KayleeError): - """Raised when :class:`Kaylee object ` configuration is - configured improperly.""" +class SettingsError(KayleeError): + """Raised when Kaylee settings are invalid""" def __init__(self, message): - super(InvalidConfigurationError, self).__init__( - 'Invalid configuration: ' + message) + super(SettingsError, self).__init__( + 'Invalid settings: ' + message) class SessionKeyNameError(KayleeError): diff --git a/kaylee/loader.py b/kaylee/loader.py index 7375446..dba167d 100644 --- a/kaylee/loader.py +++ b/kaylee/loader.py @@ -19,16 +19,14 @@ import kaylee.contrib from .core import Kaylee -from .errors import KayleeError -from .util import LazyObject, is_strong_subclass +from .errors import KayleeError, SettingsError +from .util import (LazyObject, is_strong_subclass, MIN_SECRET_KEY_LENGTH,) from . import storage, controller, project, node, session import logging log = logging.getLogger(__name__) -_default_settings = { - 'AUTO_GET_ACTION' : True, -} + class LazyKaylee(LazyObject): @@ -75,10 +73,9 @@ def load(settings): str.__name__, type(settings).__name__)) - new_settings = deepcopy(_default_settings) - new_settings.update(settings) try: - loader = Loader(new_settings) + SettingsValidator.validate(settings) + loader = Loader(settings) registry = loader.registry sdm = loader.session_data_manager apps = loader.applications @@ -91,8 +88,32 @@ def load(settings): **settings) - -class Loader(object): +class SettingsValidator: + @staticmethod + def validate(settings): + SettingsValidator.validate_AUTO_GET_ACTION(settings) + SettingsValidator.validate_SECRET_KEY(settings) + + @staticmethod + def validate_AUTO_GET_ACTION(settings): + val = settings['AUTO_GET_ACTION'] + if not isinstance(val, bool): + raise SettingsError('AUTO_GET_ACTION is not a boolean') + + @staticmethod + def validate_SECRET_KEY(settings): + if 'SECRET_KEY' not in settings: + return + val = settings['SECRET_KEY'] + if not isinstance(val, str): + raise SettingsError('SECRET_KEY is not a string object') + if len(val) < MIN_SECRET_KEY_LENGTH: + raise SettingsError('SECRET_KEY is too short (at least {} ' + 'characters are required)' + .format(MIN_SECRET_KEY_LENGTH)) + + +class Loader: _loadable_base_classes = [ project.Project, controller.Controller, @@ -105,7 +126,6 @@ class Loader(object): def __init__(self, settings): self._classes = defaultdict(dict) self._settings = settings - # load classes from contrib (non-refreshable) self._update_classes(kaylee.contrib) # load session data managers @@ -116,6 +136,7 @@ def __init__(self, settings): for mod in find_modules(projects_dir): self._update_classes(mod) + @property def registry(self): settings = self._settings diff --git a/kaylee/manager/commands/start_env.py b/kaylee/manager/commands/start_env.py index c198008..d7a3a84 100644 --- a/kaylee/manager/commands/start_env.py +++ b/kaylee/manager/commands/start_env.py @@ -2,8 +2,7 @@ import stat from jinja2 import Template from kaylee.manager import AdminCommand -from kaylee.util import random_string - +from kaylee.util import generate_sercret_key class StartEnvCommand(AdminCommand): name = 'startenv' @@ -36,7 +35,7 @@ def start_env(opts): os.mkdir(dest_path) render_args = { - 'SECRET_KEY' : random_string(32), + 'SECRET_KEY' : generate_sercret_key(), 'PROJECTS_DIR' : dest_path, } diff --git a/kaylee/manager/commands/templates/env_template/settings.py b/kaylee/manager/commands/templates/env_template/settings.py index d8c4896..57be879 100644 --- a/kaylee/manager/commands/templates/env_template/settings.py +++ b/kaylee/manager/commands/templates/env_template/settings.py @@ -2,7 +2,8 @@ # when a result is accepted from a node. AUTO_GET_ACTION = True -# The key used for session encryption etc. +# A string that can be explicitly used in all the configurations +# which require a secret key (for encryption, signing etc). SECRET_KEY = '{{ SECRET_KEY }}' # A directory in which Kaylee searches for user projects diff --git a/kaylee/session.py b/kaylee/session.py index 337dd91..a9efde8 100644 --- a/kaylee/session.py +++ b/kaylee/session.py @@ -22,11 +22,11 @@ from Crypto.Cipher import AES from abc import ABCMeta, abstractmethod -from .util import get_secret_key +from .util import random_string from .errors import KayleeError, SessionKeyNameError -SESSION_DATA_ATTRIBUTE = '__kl_sd__' +SESSION_DATA_ATTRIBUTE = '__kl_session_data__' class SessionDataManager(object, metaclass=ABCMeta): @@ -94,6 +94,22 @@ def remove_session_data_from_task(session_data_keys, task): del task[key] + +class EncryptedSessionDataManager(SessionDataManager): + """The implementation of this abstract class is a + session data manager which encrypts all stored session data. + + :param secret_key: A key used to encrypt the data. Generated automatically + if not supplied + (see :attr:`EncryptedSessionDataManager.SECRET_KEY_LENGTH`). + :type secret_key: str + """ + + def __init__(self, secret_key): + self.secret_key = secret_key + super(EncryptedSessionDataManager, self).__init__() + + class PhonySessionDataManager(SessionDataManager): """The default session data manager which throws :class:`KayleeError` if any session variables are encountered in an outgoing task.""" @@ -130,7 +146,7 @@ def restore(self, node, result): result.update(session_data) -class ClientSessionDataManager(SessionDataManager): +class ClientSessionDataManager(EncryptedSessionDataManager): """Stores encrypted session variables in task and restores them from the results. For example, the following task data:: @@ -144,10 +160,10 @@ class ClientSessionDataManager(SessionDataManager): task = { id: 'i1', - '__kl_sd__': 'yn/fCyEcW8AFrPps7XoxunC...' # 143 chars in total + '#__kl_sd__': 'yn/fCyEcW8AFrPps7XoxunC...' # 143 chars in total } - The Kaylee client-side engine automatically attaches the ``'__kl_sd__`` + The Kaylee client-side engine automatically attaches the ``'#__kl_sd__`` data to the JSON result sent to the server, so that the session data could be decrypted and restored, e.g.:: @@ -156,16 +172,13 @@ class ClientSessionDataManager(SessionDataManager): '#s1': 10, '#s2': [1, 2, 3] } - - :param secret_key: An override of the global :config:`SECRET_KEY` - parameter. """ - def __init__(self, secret_key=None): + def __init__(self, secret_key): #pylint: disable-msg=W0231 #W0231: __init__ method from base class 'SessionDataManager' # is not called. - self._secret_key = secret_key self.SESSION_DATA_ATTRIBUTE = SESSION_DATA_ATTRIBUTE + super(ClientSessionDataManager, self).__init__(secret_key) def store(self, node, task): session_data = self.get_session_data(task) @@ -183,14 +196,6 @@ def restore(self, node, result): del result[self.SESSION_DATA_ATTRIBUTE] result.update(sd) - @property - def secret_key(self): - if self._secret_key is None: - self._secret_key = get_secret_key() - return self._secret_key - - - def _encrypt(data, secret_key): """Encrypt the data and return its base64 representation. diff --git a/kaylee/testsuite/loader_tests.py b/kaylee/testsuite/loader_tests.py index 4ce2a23..4a8ebdd 100644 --- a/kaylee/testsuite/loader_tests.py +++ b/kaylee/testsuite/loader_tests.py @@ -6,12 +6,14 @@ from kaylee.testsuite import KayleeTest, load_tests, PROJECTS_DIR import os -from kaylee import loader, Kaylee +from kaylee import loader, Kaylee, KayleeError +from kaylee.errors import SettingsError from kaylee.contrib import (MemoryTemporalStorage, MemoryPermanentStorage, MemoryNodesRegistry) from kaylee.session import ClientSessionDataManager -from kaylee.loader import Loader +from kaylee.loader import Loader, SettingsValidator +from kaylee.util import generate_sercret_key _test_REGISTRY = { 'name' : 'MemoryNodesRegistry', @@ -26,11 +28,13 @@ class TestSettings(object): AUTO_GET_ACTION = True - SECRET_KEY = '1234' + SECRET_KEY = generate_sercret_key() SESSION_DATA_MANAGER = { 'name' : 'ClientSessionDataManager', - 'config' : {} + 'config' : { + 'secret_key' : SECRET_KEY + } } @@ -39,7 +43,7 @@ class TestSettingsWithApps(object): AUTO_GET_ACTION = True - SECRET_KEY = '1234' + SECRET_KEY = generate_sercret_key() APPLICATIONS = [ { @@ -132,4 +136,16 @@ def test_kaylee_setup(self): self.assertEqual(app.project.__class__.__name__, 'AutoTestProject') + def test_settings_validator(self): + sv = SettingsValidator + self.assertRaises(SettingsError, sv.validate_AUTO_GET_ACTION, {'AUTO_GET_ACTION': 10}) + # self.assertRaises(KayleeError, Settings, SECRET_KEY=123) + # self.assertRaises(KayleeError, Settings, SECRET_KEY='abc') + + # self.assertIsNone(_validate({ + # 'SECRET_KEY': ' ', + # 'AUTO_GET_ACTION' : True + # })) + + kaylee_suite = load_tests([KayleeLoaderTests]) diff --git a/kaylee/testsuite/session_tests.py b/kaylee/testsuite/session_tests.py index fe7320b..de5d510 100644 --- a/kaylee/testsuite/session_tests.py +++ b/kaylee/testsuite/session_tests.py @@ -4,7 +4,7 @@ from kaylee import KayleeError from kaylee.session import (_encrypt, _decrypt, ClientSessionDataManager, ServerSessionDataManager, PhonySessionDataManager, - SESSION_DATA_ATTRIBUTE, + SESSION_DATA_ATTRIBUTE, EncryptedSessionDataManager, SessionDataManager,) from kaylee.errors import SessionKeyNameError @@ -121,6 +121,10 @@ def test_json_session_data_manager(self): jsdm.restore(node, task) self.assertEqual(task, orig_task) + jsdm2 = ClientSessionDataManager(secret_key='abc') + self.assertIsInstance(jsdm2, EncryptedSessionDataManager) + self.assertEqual(len(jsdm2.secret_key), (len('abc'))) + def test_phony_session_data_manager(self): node = Node(NodeID.for_host('127.0.0.1')) task1 = { diff --git a/kaylee/testsuite/util_tests.py b/kaylee/testsuite/util_tests.py index aa8ffd0..bb9f3e1 100644 --- a/kaylee/testsuite/util_tests.py +++ b/kaylee/testsuite/util_tests.py @@ -10,7 +10,7 @@ import kaylee from kaylee.testsuite import KayleeTest, load_tests from kaylee.util import (parse_timedelta, LazyObject, random_string, - get_secret_key, DictAsObjectWrapper, + DictAsObjectWrapper, RecursiveDictAsObjectWrapper) from kaylee import KayleeError @@ -125,32 +125,6 @@ def test_random_string(self): for c in s: self.assertIn(c, special) - def test_get_secret_key(self): - sk = get_secret_key('abc') - self.assertEqual(sk, 'abc') - - # test when config is not loaded - self.assertRaises(KayleeError, get_secret_key) - - # test loading from config - from kaylee.testsuite import test_settings - from kaylee import setup - setup(test_settings) - sk = get_secret_key() - self.assertEqual(sk, test_settings.SECRET_KEY) - - # test if default parameter works after previous call - sk = get_secret_key('abc') - self.assertEqual(sk, 'abc') - - # test for proper behaviour after releasing the object from proxy - setup(None) - self.assertRaises(KayleeError, get_secret_key) - - # and the final test :) - sk = get_secret_key('abc') - self.assertEqual(sk, 'abc') - def test_dict_as_object_wrapper(self): #pylint: disable-msg=E1101 #E1101 Instance of 'DictAsObjectWrapper' has no 'A' member diff --git a/kaylee/util.py b/kaylee/util.py index 6098127..cb1f120 100644 --- a/kaylee/util.py +++ b/kaylee/util.py @@ -26,6 +26,9 @@ from datetime import timedelta from .errors import KayleeError + +MIN_SECRET_KEY_LENGTH = 32 + def parse_timedelta(s): try: match = parse_timedelta.timeout_regex.match(s) @@ -150,21 +153,8 @@ def random_string(length, alphabet=None, lowercase=True, uppercase=True, return ''.join(random.choice(src) for x in range(length)) -def get_secret_key(key=None): - if key is not None: - return key - else: - from kaylee import kl - if kl._wrapped is None: - raise KayleeError('Cannot locate a valid secret key because ' - 'Kaylee proxy object has not beet set up.') - key = kl.config.SECRET_KEY - if key is None: - raise KayleeError('SECRET_KEY configuration option is not ' - 'defined.') - return key - - +def generate_sercret_key(): + return random_string(MIN_SECRET_KEY_LENGTH) def is_strong_subclass(C, B): """Return whether class C is a subclass (i.e., a derived class) of class B