From 30570bd89c04a56b35b91a0bc1d5fc00bb6ad266 Mon Sep 17 00:00:00 2001 From: kali Date: Mon, 24 Sep 2012 22:21:50 +0900 Subject: add schema to JSONLeapConfig classes and a jsonvalidate function too, that calls to jsonchemea.validate(self, data) with self.schema We're using the specs to both purposes now: * providing a type casting system for our config options (work in progress for the type casting) * json schema validation --- pkg/requirements.pip | 1 + src/leap/base/config.py | 58 ++++++++++++--- src/leap/base/specs.py | 98 ++++++++++++------------ src/leap/base/tests/test_config.py | 4 +- src/leap/eip/specs.py | 148 +++++++++++++++++++------------------ 5 files changed, 181 insertions(+), 128 deletions(-) diff --git a/pkg/requirements.pip b/pkg/requirements.pip index 5eeabf5c..78d8624a 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -4,3 +4,4 @@ requests ping netifaces python-gnutls==1.1.9 # see https://bugs.launchpad.net/ubuntu/+source/python-gnutls/+bug/1027129 +jsonschema diff --git a/src/leap/base/config.py b/src/leap/base/config.py index 79185976..3854c2c2 100644 --- a/src/leap/base/config.py +++ b/src/leap/base/config.py @@ -12,6 +12,7 @@ logger = logging.getLogger(name=__name__) logger.setLevel('DEBUG') import configuration +import jsonschema import requests from leap.base import exceptions @@ -46,30 +47,58 @@ class BaseLeapConfig(object): raise NotImplementedError("abstract base class") +class SchemaEncoder(json.JSONEncoder): + def default(self, obj): + if obj is str: + return 'string' + if obj is unicode: + return 'string' + if obj is int: + return 'int' + if obj is list: + return 'array' + if obj is dict: + return object + + class MetaConfigWithSpec(type): """ metaclass for JSONLeapConfig classes. It creates a configuration spec out of - the `spec` dictionary. + the `spec` dictionary. The `properties` attribute + of the spec dict is turn into the `schema` attribute + of the new class (which will be used to validate against). """ # XXX in the near future, this is the # place where we want to enforce - # singletons, read-only and stuff. + # singletons, read-only and similar stuff. # TODO: # - add a error handler for missing options that # we can act easily upon (sys.exit is ugly, for $deity's sake) def __new__(meta, classname, bases, classDict): - spec_options = classDict.get('spec', None) + schema_obj = classDict.get('spec', None) + if schema_obj: + spec_options = schema_obj.get('properties', None) + schema_json = SchemaEncoder().encode(schema_obj) + schema = json.loads(schema_json) + else: + spec_options = None + schema = None # not quite happy with this workaround. # I want to raise if missing spec dict, but only # for grand-children of this metaclass. # maybe should use abc module for this. abcderived = ("JSONLeapConfig",) if spec_options is None and classname not in abcderived: - raise exceptions.ImproperlyConfigured( - "missing spec dict on your derived class") + if not schema_obj: + raise exceptions.ImproperlyConfigured( + "missing spec dict on your derived class (%s)" % classname) + if schema_obj and not spec_options: + raise exceptions.ImproperlyConfigured( + "missing properties attr in spec dict " + "on your derived class (%s)" % classname) # we create a configuration spec attribute from the spec dict config_class = type( @@ -77,6 +106,8 @@ class MetaConfigWithSpec(type): (configuration.Configuration, object), {'options': spec_options}) classDict['spec'] = config_class + # A shipped json-schema for validation + classDict['schema'] = schema return type.__new__(meta, classname, bases, classDict) @@ -96,8 +127,8 @@ class MetaConfigWithSpec(type): # - get_config (returns a optparse.OptionParser object) # TODO: +# [done] raise validation errors # - have a good type cast repertory (uris, version, hashes...) -# - raise validation errors # - multilingual objects ########################################################## @@ -151,9 +182,14 @@ class JSONLeapConfig(BaseLeapConfig): return if fromfile is None: fromfile = self.filename - newconfig = self._config.deserialize(fromfile) - # XXX check for no errors, etc - self._config.config = newconfig + if os.path.isfile(fromfile): + newconfig = self._config.deserialize(fromfile) + # XXX check for no errors, etc + # XXX could validate here! + self._config.config = newconfig + else: + logger.error('tried to load config from non-existent path') + logger.error('Not Found: %s', fromfile) def fetch(self, uri, fetcher=None, verify=True): if not fetcher: @@ -187,6 +223,10 @@ class JSONLeapConfig(BaseLeapConfig): def filename(self): return self.get_filename() + def jsonvalidate(self, data): + jsonschema.validate(data, self.schema) + return True + # private def _loadtemp(self, filename): diff --git a/src/leap/base/specs.py b/src/leap/base/specs.py index d88dc63f..e75eca70 100644 --- a/src/leap/base/specs.py +++ b/src/leap/base/specs.py @@ -1,49 +1,53 @@ leap_provider_spec = { - 'serial': { - 'type': int, - 'default': 1, - 'required': True, - }, - 'version': { - 'type': unicode, - 'default': '0.1.0' - #'required': True - }, - 'domain': { - 'type': unicode, # XXX define uri type - 'default': 'testprovider.example.org' - #'required': True, - }, - 'display_name': { - 'type': unicode, # XXX multilingual object? - 'default': 'test provider' - #'required': True - }, - 'description': { - 'default': 'test provider' - }, - 'enrollment_policy': { - 'type': unicode, # oneof ?? - 'default': 'open' - }, - 'services': { - 'type': list, # oneof ?? - 'default': ['eip'] - }, - 'api_version': { - 'type': unicode, - 'default': '0.1.0' # version regexp - }, - 'api_uri': { - 'type': unicode # uri - }, - 'public_key': { - 'type': unicode # fingerprint - }, - 'ca_cert': { - 'type': unicode - }, - 'ca_cert_uri': { - 'type': unicode - }, + 'description': 'provider definition', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'default': 1, + 'required': True, + }, + 'version': { + 'type': unicode, + 'default': '0.1.0' + #'required': True + }, + 'domain': { + 'type': unicode, # XXX define uri type + 'default': 'testprovider.example.org' + #'required': True, + }, + 'display_name': { + 'type': unicode, # XXX multilingual object? + 'default': 'test provider' + #'required': True + }, + 'description': { + 'default': 'test provider' + }, + 'enrollment_policy': { + 'type': unicode, # oneof ?? + 'default': 'open' + }, + 'services': { + 'type': list, # oneof ?? + 'default': ['eip'] + }, + 'api_version': { + 'type': unicode, + 'default': '0.1.0' # version regexp + }, + 'api_uri': { + 'type': unicode # uri + }, + 'public_key': { + 'type': unicode # fingerprint + }, + 'ca_cert': { + 'type': unicode + }, + 'ca_cert_uri': { + 'type': unicode + } + } } diff --git a/src/leap/base/tests/test_config.py b/src/leap/base/tests/test_config.py index bede5ea1..d03149b2 100644 --- a/src/leap/base/tests/test_config.py +++ b/src/leap/base/tests/test_config.py @@ -38,14 +38,14 @@ class JSONLeapConfigTest(BaseLeapTest): class DummyTestConfig(config.JSONLeapConfig): __metaclass__ = config.MetaConfigWithSpec - spec = {} + spec = {'properties': {}} with self.assertRaises(exceptions.ImproperlyConfigured) as exc: DummyTestConfig() exc.startswith("missing slug") class DummyTestConfig(config.JSONLeapConfig): __metaclass__ = config.MetaConfigWithSpec - spec = {} + spec = {'properties': {}} slug = "foo" DummyTestConfig() diff --git a/src/leap/eip/specs.py b/src/leap/eip/specs.py index 05aef590..a10a9623 100644 --- a/src/leap/eip/specs.py +++ b/src/leap/eip/specs.py @@ -8,7 +8,7 @@ PROVIDER_CA_CERT = __branding.get( 'provider_ca_file', 'testprovider-ca-cert.pem') -provider_ca_path = lambda: unicode(os.path.join( +provider_ca_path = lambda: str(os.path.join( baseconfig.get_default_provider_path(), 'keys', 'ca', PROVIDER_CA_CERT @@ -24,78 +24,86 @@ client_cert_path = lambda: unicode(os.path.join( )) eipconfig_spec = { - 'provider': { - 'type': unicode, - 'default': u"%s" % PROVIDER_DOMAIN, - 'required': True, - }, - 'transport': { - 'type': unicode, - 'default': u"openvpn", - }, - 'openvpn_protocol': { - 'type': unicode, - 'default': u"tcp" - }, - 'openvpn_port': { - 'type': int, - 'default': 80 - }, - 'openvpn_ca_certificate': { - 'type': unicode, # path - 'default': provider_ca_path - }, - 'openvpn_client_certificate': { - 'type': unicode, # path - 'default': client_cert_path - }, - 'connect_on_login': { - 'type': bool, - 'default': True - }, - 'block_cleartext_traffic': { - 'type': bool, - 'default': True - }, - 'primary_gateway': { - 'type': unicode, - 'default': u"usa_west", - 'required': True - }, - 'secondary_gateway': { - 'type': unicode, - 'default': u"france" - }, - 'management_password': { - 'type': unicode + 'description': 'sample eipconfig', + 'type': 'object', + 'properties': { + 'provider': { + 'type': unicode, + 'default': u"%s" % PROVIDER_DOMAIN, + 'required': True, + }, + 'transport': { + 'type': unicode, + 'default': u"openvpn", + }, + 'openvpn_protocol': { + 'type': unicode, + 'default': u"tcp" + }, + 'openvpn_port': { + 'type': int, + 'default': 80 + }, + 'openvpn_ca_certificate': { + 'type': unicode, # path + 'default': provider_ca_path + }, + 'openvpn_client_certificate': { + 'type': unicode, # path + 'default': client_cert_path + }, + 'connect_on_login': { + 'type': bool, + 'default': True + }, + 'block_cleartext_traffic': { + 'type': bool, + 'default': True + }, + 'primary_gateway': { + 'type': unicode, + 'default': u"usa_west", + #'required': True + }, + 'secondary_gateway': { + 'type': unicode, + 'default': u"france" + }, + 'management_password': { + 'type': unicode + } } } eipservice_config_spec = { - 'serial': { - 'type': int, - 'required': True, - 'default': 1 - }, - 'version': { - 'type': unicode, - 'required': True, - 'default': "0.1.0" - }, - 'capabilities': { - 'type': dict, - 'default': { - "transport": ["openvpn"], - "ports": ["80", "53"], - "protocols": ["udp", "tcp"], - "static_ips": True, - "adblock": True} - }, - 'gateways': { - 'type': list, - 'default': [{"country_code": "us", - "label": {"en":"west"}, - "capabilities": {}, - "hosts": ["1.2.3.4", "1.2.3.5"]}] + 'description': 'sample eip service config', + 'type': 'object', + 'properties': { + 'serial': { + 'type': int, + 'required': True, + 'default': 1 + }, + 'version': { + 'type': unicode, + 'required': True, + 'default': "0.1.0" + }, + 'capabilities': { + 'type': dict, + 'default': { + "transport": ["openvpn"], + "ports": ["80", "53"], + "protocols": ["udp", "tcp"], + "static_ips": True, + "adblock": True} + }, + 'gateways': { + 'type': list, + 'default': [{"country_code": "us", + "label": {"en":"west"}, + "capabilities": {}, + "hosts": ["1.2.3.4", "1.2.3.5"]}] + } } } -- cgit v1.2.3