diff --git a/.travis.yml b/.travis.yml index 29bdd7a0..6ead16cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,3 +25,10 @@ after_success: before_install: - sudo apt-get install -y xmlsec1 libffi6 + +matrix: + include: + - python: 3.6 + env: TOXENV=flake8 + - python: 3.6 + env: TOXENV=isort diff --git a/requirements.txt b/requirements.txt index 4a8010de..ec4fad31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ passlib==1.7.1 lxml==4.2.3 Faker==0.8.16 exrex + +importlib-resources==1.0.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..d2559805 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[flake8] +exclude = *.egg-info + +[isort] +line_length = 79 +combine_as_imports = true +default_section = THIRDPARTY +known_first_party = testenv +not_skip = __init__.py diff --git a/spid-testenv.py b/spid-testenv.py index e2af195a..31f8fa6f 100644 --- a/spid-testenv.py +++ b/spid-testenv.py @@ -6,6 +6,7 @@ import os.path from flask import Flask + from testenv.exceptions import BadConfiguration from testenv.server import IdpServer from testenv.utils import get_config diff --git a/templates/login.html b/templates/login.html index 63e4fcb4..c3297456 100644 --- a/templates/login.html +++ b/templates/login.html @@ -7,7 +7,7 @@
{}'.format(_example, escape(line)) + if self._example: + _example = '
{}'.format(_example, escape(line)) + else: + _example = '' _error_msg = '{} {}'.format(_error_msg, _example) res['errors']['required_error'] = _error_msg self._errors.update(res['errors']) @@ -286,6 +293,8 @@ class SpidParser(object): """ def __init__(self, *args, **kwargs): + from testenv.parser import XMLValidator + self.xml_validator = XMLValidator() self.schema = None def get_schema(self, action, binding, **kwargs): @@ -453,10 +462,87 @@ def parse(self, obj, action, binding, schema=None, **kwargs): :param binding: :param schema: custom schema (None by default) """ + + errors = {} + # Validate xml against its XSD schema + validation_errors = self.xml_validator.validate_request(obj.xmlstr) + if validation_errors: + errors['validation_errors'] = validation_errors + # Validate xml against SPID rules _schema = self.get_schema(action, binding, **kwargs)\ if schema is None else schema self.observer = Observer() self.observer.attach(_schema) - validated = _schema.validate(obj) - errors = self.observer.evaluate() + validated = _schema.validate(obj.message) + spid_errors = self.observer.evaluate() + if spid_errors: + errors['spid_errors'] = spid_errors return validated, errors + + +class XMLSchemaFileLoader(object): + """ + Load XML Schema instances from the filesystem. + """ + + def __init__(self, import_path=None): + self._import_path = import_path or 'testenv.xsd' + + def load(self, name): + with importlib_resources.path(self._import_path, name) as path: + xmlschema_doc = etree.parse(str(path)) + return etree.XMLSchema(xmlschema_doc) + + +class XMLValidator(object): + """ + Validate XML fragments against XML Schema (XSD). + """ + + def __init__(self, schema_loader=None, parser=None, translator=None): + self._schema_loader = schema_loader or XMLSchemaFileLoader() + self._parser = parser or etree.XMLParser() + self._translator = translator or Libxml2Translator() + self._load_schemas() + + def _load_schemas(self): + self._schemas = { + type_: self._schema_loader.load(name) + for type_, name in XML_SCHEMAS.items() + } + + def validate_request(self, xml): + return self._run(xml, 'protocol') + + def _run(self, xml, schema_type): + xml_doc, parsing_errors = self._parse_xml(xml) + if parsing_errors: + return parsing_errors + return self._validate_xml(xml_doc, schema_type) + + def _parse_xml(self, xml): + xml_doc, errors = None, [] + try: + xml_doc = etree.fromstring(xml, parser=self._parser) + except SyntaxError: + error_log = self._parser.error_log + errors = self._handle_errors(error_log) + return xml_doc, errors + + def _validate_xml(self, xml_doc, schema_type): + schema = self._schemas[schema_type] + errors = [] + try: + schema.assertValid(xml_doc) + except Exception: + error_log = schema.error_log + errors = self._handle_errors(error_log) + return errors + + def _handle_errors(self, errors): + original_errors = [ + XMLError(err.line, err.column, err.domain_name, + err.type_name, err.message, err.path) + for err in errors + ] + return self._translator.translate_many(original_errors) diff --git a/testenv/server.py b/testenv/server.py index 8f3cf3bc..c4173c7f 100644 --- a/testenv/server.py +++ b/testenv/server.py @@ -22,6 +22,7 @@ from saml2.s_utils import UnknownSystemEntity, UnsupportedBinding from saml2.saml import NAME_FORMAT_BASIC, NAMEID_FORMAT_TRANSIENT, Attribute from saml2.sigver import verify_redirect_signature + from testenv.exceptions import BadConfiguration from testenv.parser import SpidParser from testenv.settings import (ALLOWED_SIG_ALGS, AUTH_NO_CONSENT, DIGEST_ALG, @@ -337,7 +338,7 @@ def _raise_error(self, msg, extra=None): def _check_spid_restrictions(self, msg, action, binding, **kwargs): parsed_msg, errors = self.spid_parser.parse( - msg.message, action, binding, **kwargs + msg, action, binding, **kwargs ) self.app.logger.debug('parsed authn_request: {}'.format(parsed_msg)) return parsed_msg, errors @@ -354,13 +355,14 @@ def _store_request(self, authnreq): self.ticket[key] = authnreq return key - def _handle_errors(self, errors, xmlstr): + def _handle_errors(self, xmlstr, errors={}): _escaped_xml = escape(prettify_xml(xmlstr.decode())) rendered_error_response = render_template_string( spid_error_table, **{ 'lines': _escaped_xml.splitlines(), - 'errors': errors + 'spid_errors': errors.get('spid_errors', []), + 'validation_errors': errors.get('validation_errors', []) } ) return rendered_error_response @@ -521,7 +523,7 @@ def single_sign_on_service(self): ) if errors: - return self._handle_errors(errors, req_info.xmlstr) + return self._handle_errors(req_info.xmlstr, errors=errors) if not req_info: self._raise_error('Processo di parsing del messaggio fallito.') @@ -954,7 +956,7 @@ def single_logout_service(self): ) if errors: - return self._handle_errors(errors, req_info.xmlstr) + return self._handle_errors(req_info.xmlstr, errors=errors) # Check if it is signed if _binding == BINDING_HTTP_REDIRECT: diff --git a/testenv/settings.py b/testenv/settings.py index dfc9391d..c71bc9b4 100644 --- a/testenv/settings.py +++ b/testenv/settings.py @@ -37,6 +37,9 @@ SIGN_ALG = ds.SIG_RSA_SHA512 DIGEST_ALG = ds.DIGEST_SHA512 +XML_SCHEMAS = { + 'protocol': 'saml-schema-protocol-2.0.xsd', +} spid_error_table = ''' @@ -57,7 +60,7 @@ - {% for err in errors %} + {% for err in spid_errors %}
+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +
++ See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +
++ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +
++ See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +
++ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.
+ ++ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +
++ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +
++ The union allows for the 'un-declaration' of xml:lang with + the empty string. +
++ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.
+ ++ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.
+ ++ See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +
++ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.
+ ++ See http://www.w3.org/TR/xml-id/ + for information about this attribute. +
++ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +
++++ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +
+
+ This schema defines attributes and an attribute group suitable
+ for use by schemas wishing to allow xml:base
,
+ xml:lang
, xml:space
or
+ xml:id
attributes on elements they define.
+
+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +
++ <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/xml.xsd"/> ++
+ or +
++ <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2009/01/xml.xsd"/> ++
+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +
++ <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> ++
+ will define a type which will schema-validate an instance element + with any of those attributes. +
++ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +
++ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +
++ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +
++ Previous dated (and unchanging) versions of this schema + document are at: +
+ +