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 @@

Login

+ id="username" name="username" aria-required="true" minlength="4" type="text" required autofocus> @@ -26,4 +26,4 @@

Login

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/testenv/parser.py b/testenv/parser.py index 5f4dd2ef..69d915e5 100644 --- a/testenv/parser.py +++ b/testenv/parser.py @@ -4,12 +4,16 @@ from datetime import datetime, timedelta from functools import reduce +import importlib_resources from flask import escape +from lxml import etree from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT from saml2.saml import NAMEID_FORMAT_ENTITY, NAMEID_FORMAT_TRANSIENT -from testenv.settings import COMPARISONS, SPID_LEVELS, TIMEDELTA + +from testenv.settings import COMPARISONS, SPID_LEVELS, TIMEDELTA, XML_SCHEMAS from testenv.spid import Observer -from testenv.utils import check_url, check_utc_date, str_to_time +from testenv.translation import Libxml2Translator +from testenv.utils import XMLError, check_url, check_utc_date, str_to_time class Attr(object): @@ -239,10 +243,13 @@ def validate(self, data): if self._required and data is None: # check if the element is required, if not provide and example _error_msg = self.MANDATORY_ERROR - _example = '
Esempio:
' - lines = self._example.splitlines() - for line in lines: - _example = '{}
{}
'.format(_example, escape(line)) + if self._example: + _example = '
Esempio:
' + lines = self._example.splitlines() + for line in lines: + _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 %} {{err.1}} @@ -79,6 +82,14 @@ {% endfor %} + {% for err in validation_errors %} + + {{err.path}} + + {{err.message}} + + + {% endfor %} diff --git a/testenv/spid.py b/testenv/spid.py index 4a213da3..a6ccad4b 100644 --- a/testenv/spid.py +++ b/testenv/spid.py @@ -12,6 +12,7 @@ from saml2.attribute_converter import AttributeConverter from saml2.entity import UnknownBinding from saml2.request import AuthnRequest, LogoutRequest +from saml2.response import IncorrectlySigned from saml2.s_utils import (UnravelError, decode_base64_and_inflate, do_ava, factory) from saml2.saml import Attribute @@ -36,18 +37,44 @@ def evaluate(self): return _errors -class SpidAuthnRequest(AuthnRequest): - def verify(self): - # TODO: move here a bit of parsing flow - return self +class RequestMixin(object): + + # TODO: refactor a bit this flow, maybe writing from scratch custom components using pysaml2 + # atomic components... + def _loads(self, xmldata, binding=None, origdoc=None, must=None, + only_valid_cert=False): + # See https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/request.py#L39 + self.xmlstr = xmldata[:] -class SpidLogoutRequest(LogoutRequest): + try: + self.message = self.signature_check(xmldata, origdoc=origdoc, + must=must, + only_valid_cert=only_valid_cert) + except TypeError as e: + raise + except Exception as excp: + pass + + if not self.message: + raise IncorrectlySigned() + + return self + def verify(self): + # https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/request.py#L98 # TODO: move here a bit of parsing flow return self +class SpidAuthnRequest(RequestMixin, AuthnRequest): + pass + + +class SpidLogoutRequest(RequestMixin, LogoutRequest): + pass + + class SpidServer(Server): def parse_authn_request(self, enc_request, binding=BINDING_HTTP_REDIRECT): """Parse a Authentication Request @@ -56,6 +83,8 @@ def parse_authn_request(self, enc_request, binding=BINDING_HTTP_REDIRECT): :param binding: Which binding that was used to transport the message to this entity. :return: A request instance + + See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/server.py#L221 """ return self._parse_request(enc_request, SpidAuthnRequest, @@ -69,6 +98,8 @@ def parse_logout_request(self, xmlstr, binding=BINDING_HTTP_REDIRECT): :return: None if the reply doesn't contain a valid SAML LogoutResponse, otherwise the reponse if the logout was successful and None if it was not. + + See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/entity.py#L1183 """ return self._parse_request(xmlstr, SpidLogoutRequest, @@ -83,6 +114,8 @@ def unravel(txt, binding, msgtype="response"): :param binding: :param msgtype: :return: + + See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/entity.py#L368 """ if binding not in [ BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, None @@ -104,6 +137,7 @@ def unravel(txt, binding, msgtype="response"): class SpidPolicy(Policy): def __init__(self, restrictions=None, index=None): + # See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/assertion.py#L325 super(SpidPolicy, self).__init__(restrictions=restrictions) self.index = index @@ -114,6 +148,8 @@ def restrict(self, ava, sp_entity_id, metadata=None): :return: A filtered ava according to the IdPs/AAs rules and the list of required/optional attributes according to the SP. If the requirements can't be met an exception is raised. + + See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/assertion.py#L539 """ if metadata: spec = metadata.attribute_requirement( @@ -132,6 +168,8 @@ def ac_factory(path="", **kwargs): :param path: The path to a directory where the attribute maps are expected to reside. :return: A AttributeConverter instance + + See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/attribute_converter.py#L52 """ acs = [] @@ -175,6 +213,7 @@ def ac_factory(path="", **kwargs): class SpidAttributeConverter(AttributeConverter): def __init__(self, name_format="", special_cases={}): + # See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/attribute_converter.py#L274 super(SpidAttributeConverter, self).__init__(name_format) self._special_cases = special_cases @@ -183,6 +222,8 @@ def to_(self, attrvals): :param attrvals: A dictionary of attributes and values :return: A list of Attribute instances + + See: https://github.com/IdentityPython/pysaml2/blob/master/src/saml2/attribute_converter.py#L486 """ attributes = [] for key, value in attrvals.items(): diff --git a/testenv/tests/data/__init__.py b/testenv/tests/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testenv/tests/data/sample_saml_requests.py b/testenv/tests/data/sample_saml_requests.py new file mode 100644 index 00000000..58232a9b --- /dev/null +++ b/testenv/tests/data/sample_saml_requests.py @@ -0,0 +1,57 @@ +valid = [ + + """\ + + http://sp.example.com/demo1/metadata.php + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + +""", + + """\ + + https://localhost/ + + + https://www.spid.gov.it/SpidL1 + +""", + +] + +invalid_id_attr = """\ + + https://localhost/ + + + https://www.spid.gov.it/SpidL1 + +""" + +missing_issue_instant_attr = """\ + + https://localhost/ + + + https://www.spid.gov.it/SpidL1 + +""" + +duplicate_version_attr = """\ + + https://localhost/ + + + https://www.spid.gov.it/SpidL1 + +""" + +multiple_errors = """\ + + https://localhost/ + + + https://www.spid.gov.it/SpidL1 + +""" diff --git a/testenv/tests/test_saml_xsd_validation.py b/testenv/tests/test_saml_xsd_validation.py new file mode 100644 index 00000000..905722a7 --- /dev/null +++ b/testenv/tests/test_saml_xsd_validation.py @@ -0,0 +1,79 @@ +import unittest + +from testenv.parser import XMLValidator +from testenv.tests.data import sample_saml_requests as sample_requests + + +class FakeTranslator(object): + + def translate_many(self, errors): + return errors + + +class AuthnRequestValidationTestCase(unittest.TestCase): + + def test_valid_requests(self): + validator = XMLValidator(translator=FakeTranslator()) + for request in sample_requests.valid: + errors = validator.validate_request(request) + self.assertEqual(errors, []) + + def test_empty_request(self): + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request('') + self.assertEqual(len(errors), 1) + self.assertIn('Document is empty', errors[0].message) + + def test_not_xml(self): + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request('{"this": "is JSON"}') + self.assertEqual(len(errors), 1) + self.assertIn("Start tag expected, '<' not found", errors[0].message) + + def test_invalid_xml(self): + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request('') + self.assertEqual(len(errors), 1) + self.assertIn( + 'Opening and ending tag mismatch: a line 1 and b', + errors[0].message + ) + + def test_invalid_attribute_format(self): + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request(sample_requests.invalid_id_attr) + self.assertEqual(len(errors), 1) + self.assertIn( + "is not a valid value of the atomic type 'xs:ID'", + errors[0].message + ) + + def test_missing_mandatory_attribute(self): + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request( + sample_requests.missing_issue_instant_attr) + self.assertEqual(len(errors), 1) + self.assertIn( + "The attribute 'IssueInstant' is required but missing.", + errors[0].message + ) + + def test_duplicate_attribute(self): + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request( + sample_requests.duplicate_version_attr) + self.assertEqual(len(errors), 1) + self.assertIn('Attribute Version redefined', errors[0].message) + + def test_multiple_errors(self): + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request(sample_requests.multiple_errors) + self.assertEqual(len(errors), 2) + self.assertIn( + "is not a valid value of the atomic type 'xs:ID'", + errors[0].message + ) + self.assertIn( + "The attribute 'Version' is required but missing.", + errors[1].message + ) diff --git a/testenv/tests/test_spid_testenv.py b/testenv/tests/test_spid_testenv.py index 56faee17..9df98012 100644 --- a/testenv/tests/test_spid_testenv.py +++ b/testenv/tests/test_spid_testenv.py @@ -10,16 +10,16 @@ import unittest import xml.etree.ElementTree as ET -from OpenSSL import crypto - import flask from freezegun import freeze_time +from OpenSSL import crypto from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT from saml2.s_utils import deflate_and_base64_encode from saml2.saml import NAMEID_FORMAT_ENTITY, NAMEID_FORMAT_TRANSIENT from saml2.sigver import REQ_ORDER, import_rsa_key_from_file from saml2.xmldsig import SIG_RSA_SHA1, SIG_RSA_SHA256 from six.moves.urllib.parse import urlencode + from testenv.utils import get_config sys.path.insert(0, '../') @@ -55,7 +55,7 @@ def generate_certificate(fname, path=DATA_DIR): def generate_authn_request(data={}, acs_level=0): - _id = data.get('id') if data.get('id') else '123456' + _id = data.get('id') if data.get('id') else 'test_123456' version = data.get('version') if data.get('version') else '2.0' issue_instant = data.get('issue_instant') if data.get('issue_instant') else '2018-07-16T09:38:29Z' destination = data.get('destination') if data.get('destination') else 'http://spid-testenv:8088/sso-test' @@ -72,7 +72,7 @@ def generate_authn_request(data={}, acs_level=0): if acs_level == 0: _acs = ''' ProtocolBinding="%s" - AssertionConsumerServiceURL="%s"> + AssertionConsumerServiceURL="%s" ''' % (protocol_binding, acsu) elif acs_level == 1: _acs = ''' @@ -391,7 +391,7 @@ def test_in_response_to(self, unravel, verified): self.assertEqual(response.status_code, 200) self.assertEqual(len(self.idp_server.ticket), 0) self.assertEqual(len(self.idp_server.responses), 0) - with patch('testenv.spid.SpidServer.unravel', return_value = generate_authn_request({'id': '9999'})) as mocked: + with patch('testenv.spid.SpidServer.unravel', return_value = generate_authn_request({'id': 'test_9999'})) as mocked: response = self.test_client.get( '/sso-test?SAMLRequest=b64encodedrequest&SigAlg={}&Signature=sign'.format(quote(SIG_RSA_SHA256)), follow_redirects=True diff --git a/testenv/tests/test_xsd_error_translation.py b/testenv/tests/test_xsd_error_translation.py new file mode 100644 index 00000000..cc5918d1 --- /dev/null +++ b/testenv/tests/test_xsd_error_translation.py @@ -0,0 +1,47 @@ +# coding: utf-8 +import unittest + +from testenv.translation import Libxml2Translator +from testenv.utils import XMLError + + +class Libxml2ItalianTranslationTestCase(unittest.TestCase): + samples = { + ('PARSER', 'ERR_DOCUMENT_END', + 'Extra content at the end of the document'): + 'Contenuto extra alla fine del documento.', + + ('PARSER', 'ERR_DOCUMENT_EMPTY', 'Document is empty'): + 'Il documento è vuoto.', + + ('SCHEMASV', 'SCHEMAV_CVC_COMPLEX_TYPE_4', + "Element '{urn:oasis:names:tc:SAML:2.0:protocol}AuthnRequest': " + "The attribute 'IssueInstant' is required but missing."): + "Elemento '{urn:oasis:names:tc:SAML:2.0:protocol}AuthnRequest': " + "L'attributo 'IssueInstant' è mandatorio ma non presente.", + + ('SCHEMASV', 'SCHEMAV_CVC_DATATYPE_VALID_1_2_1', + "Element '{urn:oasis:names:tc:SAML:2.0:protocol}AuthnRequest', " + "attribute 'ID': '123456' is not a valid value of the atomic type " + "'xs:ID'."): + "Elemento '{urn:oasis:names:tc:SAML:2.0:protocol}AuthnRequest', " + "attributo 'ID': '123456' non è un valore valido di tipo atomico " + "'xs:ID'.", + } + + def test_translations(self): + translator = Libxml2Translator() + for input_data, it_message in self.samples.items(): + domain, type_, en_message = input_data + en_error = XMLError(1, 2, domain, type_, en_message, '') + it_error = translator.translate(en_error) + self.assertEqual(it_error.message, it_message) + + def test_multiple_error_translation(self): + translator = Libxml2Translator() + errors = [ + XMLError(1, 2, 'domain1', 'type1', 'an error occured', 'path1'), + XMLError(3, 4, 'domain2', 'type2', 'another error occured', 'path2') + ] + actual = translator.translate_many(errors) + self.assertEqual(actual, errors) diff --git a/testenv/translation.py b/testenv/translation.py new file mode 100644 index 00000000..bc58c87a --- /dev/null +++ b/testenv/translation.py @@ -0,0 +1,60 @@ +# coding: utf-8 +import re + +from testenv.utils import XMLError + +_c = re.compile + + +class Libxml2Translator(object): + + _mapping = { + 'PARSER': { + 'ERR_DOCUMENT_END': { + _c(r'Extra content at the end of the document'): + 'Contenuto extra alla fine del documento.', + }, + 'ERR_DOCUMENT_EMPTY': { + _c(r'Document is empty'): + 'Il documento è vuoto.' + } + }, + + 'SCHEMASV': { + 'SCHEMAV_CVC_COMPLEX_TYPE_4': { + _c(r"Element '(?P.*)': The attribute '(?P.*)' is required but missing."): + "Elemento '{element}': L'attributo '{attribute}' è mandatorio ma non presente." + }, + 'SCHEMAV_CVC_DATATYPE_VALID_1_2_1': { + _c(r"Element '(?P.*)', attribute '(?P.*)': '(?P.*)' is not a valid value of the atomic type '(?P.*)'."): + "Elemento '{element}', attributo '{attribute}': '{value}' non è un valore valido di tipo atomico '{type}'." + } + } + } + + def translate_many(self, errors): + return [ + self.translate(error) for error in errors + ] + + def translate(self, error): + message = self._get_replacement_message(error) + return XMLError( + error.line, error.column, error.domain_name, + error.type_name, message, error.path, + ) + + def _get_replacement_message(self, error): + try: + translation = self._search_translation(error) + return translation or error.message + except KeyError: + return error.message + + def _search_translation(self, error): + regexp_group = self._mapping[error.domain_name][error.type_name] + for regexp, translation in regexp_group.items(): + match = re.match(regexp, error.message) + if match: + return translation.format(**match.groupdict()) + return None diff --git a/testenv/utils.py b/testenv/utils.py index 4d6b748a..95e5f009 100644 --- a/testenv/utils.py +++ b/testenv/utils.py @@ -3,13 +3,13 @@ import json import re +from collections import namedtuple from datetime import datetime import lxml.etree as etree import yaml -from six.moves.urllib.parse import urlparse - from saml2 import time_util + from testenv.settings import SPID_ERRORS @@ -64,3 +64,9 @@ def prettify_xml(msg): encoding='utf-8' ) return msg.decode('utf-8') + + +XMLError = namedtuple( + 'XMLError', + ['line', 'column', 'domain_name', 'type_name', 'message', 'path'] +) diff --git a/testenv/xsd/__init__.py b/testenv/xsd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testenv/xsd/saml-schema-assertion-2.0.xsd b/testenv/xsd/saml-schema-assertion-2.0.xsd new file mode 100644 index 00000000..a1ef536c --- /dev/null +++ b/testenv/xsd/saml-schema-assertion-2.0.xsd @@ -0,0 +1,283 @@ + + + + + + + Document identifier: saml-schema-assertion-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New assertion schema for SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testenv/xsd/saml-schema-metadata-2.0.xsd b/testenv/xsd/saml-schema-metadata-2.0.xsd new file mode 100644 index 00000000..f052721c --- /dev/null +++ b/testenv/xsd/saml-schema-metadata-2.0.xsd @@ -0,0 +1,337 @@ + + + + + + + + + Document identifier: saml-schema-metadata-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + Schema for SAML metadata, first published in SAML 2.0. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testenv/xsd/saml-schema-protocol-2.0.xsd b/testenv/xsd/saml-schema-protocol-2.0.xsd new file mode 100644 index 00000000..08879a02 --- /dev/null +++ b/testenv/xsd/saml-schema-protocol-2.0.xsd @@ -0,0 +1,302 @@ + + + + + + + Document identifier: saml-schema-protocol-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New protocol schema based in a SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testenv/xsd/xenc-schema.xsd b/testenv/xsd/xenc-schema.xsd new file mode 100644 index 00000000..3d27af69 --- /dev/null +++ b/testenv/xsd/xenc-schema.xsd @@ -0,0 +1,146 @@ + + + + + + ]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testenv/xsd/xml.xsd b/testenv/xsd/xml.xsd new file mode 100644 index 00000000..97754f5f --- /dev/null +++ b/testenv/xsd/xml.xsd @@ -0,0 +1,287 @@ + + + + + + +
+

About the XML namespace

+ +
+

+ 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. +

+
+
+ + + + + + +
+ +

lang (as an attribute name)

+

+ 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.

+ +
+
+

Notes

+

+ 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. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ 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.

+ +
+
+
+ + + + + + +
+ + + +
+ +

base (as an attribute name)

+

+ 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. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ 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. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ 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". +

+
+
+
+
+
+ + + +
+

About this schema document

+ +
+

+ 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. +

+
+
+
+
+ + + +
+

Versioning policy for this schema document

+
+

+ 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: +

+ +
+
+
+
+ + + diff --git a/testenv/xsd/xmldsig-core-schema.xsd b/testenv/xsd/xmldsig-core-schema.xsd new file mode 100644 index 00000000..6a8854c0 --- /dev/null +++ b/testenv/xsd/xmldsig-core-schema.xsd @@ -0,0 +1,318 @@ + + + + + + ]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tox.ini b/tox.ini index fff51270..5b1f93a5 100644 --- a/tox.ini +++ b/tox.ini @@ -11,13 +11,14 @@ skipsdist = True [testenv] setenv = + PYTHONDONTWRITEBYTECODE = True PYTHONWARNINGS = all deps = -rrequirements_test.txt commands = python -V python -b -m coverage run \ - --include spid-testenv.py \ + --source testenv,spid-testenv \ --parallel-mode -m pytest -v {posargs} [testenv:coverage-report] @@ -29,10 +30,10 @@ skip_install = true [testenv:flake8] deps = flake8 -commands = flake8 spid-testenv.py testenv/*.py +commands = flake8 spid-testenv.py testenv skip_install = True [testenv:isort] deps = isort -commands = isort -c -rc -df spid-testenv.py testenv/*.py +commands = isort -c -rc -df spid-testenv.py testenv skip_install = true