summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkali <kali@leap.se>2012-09-24 22:21:50 +0900
committerkali <kali@leap.se>2012-09-24 22:21:50 +0900
commit30570bd89c04a56b35b91a0bc1d5fc00bb6ad266 (patch)
tree10364a8c906b34474f0e453fd5a3b29c4d6fba92
parent5c32cc7b5e00853b3cc28b5003b92ab009418dff (diff)
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
-rw-r--r--pkg/requirements.pip1
-rw-r--r--src/leap/base/config.py58
-rw-r--r--src/leap/base/specs.py98
-rw-r--r--src/leap/base/tests/test_config.py4
-rw-r--r--src/leap/eip/specs.py148
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"]}]
+ }
}
}