From 0f26f04b8ee0bbcd91c6d0c90baaa7e374a4cca1 Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Fri, 10 Aug 2018 11:50:27 +0200 Subject: [PATCH 01/29] Improve error FE (move spid_errors template from settings to an html file) --- templates/spid_error.html | 81 ++++++++++++++++++++++++++++----------- testenv/server.py | 8 ++-- testenv/settings.py | 66 ------------------------------- 3 files changed, 62 insertions(+), 93 deletions(-) diff --git a/templates/spid_error.html b/templates/spid_error.html index 6ade5f21..563010bd 100644 --- a/templates/spid_error.html +++ b/templates/spid_error.html @@ -1,39 +1,74 @@ {% extends 'main_page.html' %} {% block content %} +
{% for line in lines %}
{{line}}
{% endfor %}
- - - - - - - - - {% for err in errors %} +

Validazione tramite XSD

+{% if validation_errors %} +
ElementoDettagli errore
+ - + + + + + + {% for err in validation_errors %} + + {% endfor %} - -
{{err.1}}ElementoDettagli errore
{{err.path}} -
    - {% for name, msgs in err.2.items() %} -
  • {{name}} -
      - {% for type, msg in msgs.items() %} -
    • {{msg|safe}}
    • - {% endfor %} -
    -
  • - {% endfor %} -
+ {{err.message}}
+ + +{% else %} + +{% endif %} + +

Validazione Regole spid

+{% if spid_errors %} + + + + + + + + + {% for err in spid_errors %} + + + + + {% endfor %} + +
ElementoDettagli errore
{{err.1}} +
    + {% for name, msgs in err.2.items() %} +
  • {{name}} +
      + {% if msgs is mapping %} + {% for type, msg in msgs.items() %} +
    • {{msg|safe}}
    • + {% endfor %} + {% else %} +
    • {{msgs}}
    • + {% endif %} +
    +
  • + {% endfor %} +
+
+{% else %} + +{% endif %} {% endblock %} + {% block js %} - - -
- {% for line in lines %} -
{{line}}
- {% endfor %} -
- - - - - - - - - {% for err in spid_errors %} - - - - - {% endfor %} - {% for err in validation_errors %} - - - - - {% endfor %} - -
ElementoDettagli errore
{{err.1}} -
    - {% for name, msgs in err.2.items() %} -
  • {{name}} -
      - {% if msgs is mapping %} - {% for type, msg in msgs.items() %} -
    • {{msg|safe}}
    • - {% endfor %} - {% else %} -
    • {{msgs}}
    • - {% endif %} -
    -
  • - {% endfor %} -
-
{{err.path}} - {{err.message}} -
- - - - -''' From ad3a277d025dfb4f6e9868a2ef6852a8a786e386 Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Fri, 10 Aug 2018 12:03:38 +0200 Subject: [PATCH 02/29] Add test to cover issue #79 --- testenv/tests/data/sample_saml_requests.py | 7 +++++++ testenv/tests/test_saml_xsd_validation.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/testenv/tests/data/sample_saml_requests.py b/testenv/tests/data/sample_saml_requests.py index 58232a9b..61ba7e7b 100644 --- a/testenv/tests/data/sample_saml_requests.py +++ b/testenv/tests/data/sample_saml_requests.py @@ -55,3 +55,10 @@ https://www.spid.gov.it/SpidL1 """ + +unexpected_element = """\ + + 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 index 905722a7..6b4a0c28 100644 --- a/testenv/tests/test_saml_xsd_validation.py +++ b/testenv/tests/test_saml_xsd_validation.py @@ -77,3 +77,15 @@ def test_multiple_errors(self): "The attribute 'Version' is required but missing.", errors[1].message ) + + def test_unexpected_element(self): + # See: https://github.com/italia/spid-testenv2/issues/79 + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request(sample_requests.unexpected_element) + self.assertEqual(len(errors), 1) + self.assertIn( + "Element '{urn:oasis:names:tc:SAML:2.0:assertion}AuthnContextClassRef': " + "This element is not expected. Expected is one of ( {urn:oasis:names:tc:SAML:2.0:assertion}" + "Conditions, {urn:oasis:names:tc:SAML:2.0:protocol}RequestedAuthnContext, {urn:oasis:names:tc:SAML:2.0:protocol}Scoping ).", + errors[0].message + ) From 3434f96297c8546b72d804f2aa75d6ba73142448 Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Fri, 10 Aug 2018 12:05:11 +0200 Subject: [PATCH 03/29] isort --- testenv/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testenv/server.py b/testenv/server.py index c4866ee3..1ab64fe6 100644 --- a/testenv/server.py +++ b/testenv/server.py @@ -9,8 +9,8 @@ from hashlib import sha1 from logging.handlers import RotatingFileHandler -from flask import (Response, abort, escape, redirect, render_template, - request, session, url_for) +from flask import (Response, abort, escape, redirect, render_template, request, + session, url_for) from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT from saml2.assertion import filter_on_demands from saml2.attribute_converter import list_to_local From 4cb89b6735463be330c24e9a5b9e2fd87e2883b3 Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Fri, 10 Aug 2018 12:10:14 +0200 Subject: [PATCH 04/29] Add link to #63 test --- testenv/tests/test_saml_xsd_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testenv/tests/test_saml_xsd_validation.py b/testenv/tests/test_saml_xsd_validation.py index 6b4a0c28..f4996f3e 100644 --- a/testenv/tests/test_saml_xsd_validation.py +++ b/testenv/tests/test_saml_xsd_validation.py @@ -40,6 +40,7 @@ def test_invalid_xml(self): ) def test_invalid_attribute_format(self): + # See: https://github.com/italia/spid-testenv2/issues/63 validator = XMLValidator(translator=FakeTranslator()) errors = validator.validate_request(sample_requests.invalid_id_attr) self.assertEqual(len(errors), 1) From a3dc81410aa1255162d7e57deee5f00d1336e270 Mon Sep 17 00:00:00 2001 From: Fabio Sangiovanni Date: Fri, 10 Aug 2018 12:13:41 +0200 Subject: [PATCH 05/29] add test to cover issue #97 --- testenv/tests/data/sample_saml_requests.py | 9 +++++++++ testenv/tests/test_saml_xsd_validation.py | 16 ++++++++++++++++ testenv/tests/test_xsd_error_translation.py | 8 ++++++++ testenv/translation.py | 4 ++++ 4 files changed, 37 insertions(+) diff --git a/testenv/tests/data/sample_saml_requests.py b/testenv/tests/data/sample_saml_requests.py index 61ba7e7b..9a7509f3 100644 --- a/testenv/tests/data/sample_saml_requests.py +++ b/testenv/tests/data/sample_saml_requests.py @@ -62,3 +62,12 @@ https://www.spid.gov.it/SpidL1 """ + +invalid_comparison_attr = """\ + + 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 index 6b4a0c28..16cd5635 100644 --- a/testenv/tests/test_saml_xsd_validation.py +++ b/testenv/tests/test_saml_xsd_validation.py @@ -89,3 +89,19 @@ def test_unexpected_element(self): "Conditions, {urn:oasis:names:tc:SAML:2.0:protocol}RequestedAuthnContext, {urn:oasis:names:tc:SAML:2.0:protocol}Scoping ).", errors[0].message ) + + def test_invalid_comparison_attribute(self): + # https://github.com/italia/spid-testenv2/issues/97 + validator = XMLValidator(translator=FakeTranslator()) + errors = validator.validate_request( + sample_requests.invalid_comparison_attr) + self.assertEqual(len(errors), 2) + self.assertIn( + "The value 'invalid' is not an element of the set " + "{'exact', 'minimum', 'maximum', 'better'}", + errors[0].message + ) + self.assertIn( + "'invalid' is not a valid value of the atomic type", + errors[1].message + ) diff --git a/testenv/tests/test_xsd_error_translation.py b/testenv/tests/test_xsd_error_translation.py index cc5918d1..46e34482 100644 --- a/testenv/tests/test_xsd_error_translation.py +++ b/testenv/tests/test_xsd_error_translation.py @@ -27,6 +27,14 @@ class Libxml2ItalianTranslationTestCase(unittest.TestCase): "Elemento '{urn:oasis:names:tc:SAML:2.0:protocol}AuthnRequest', " "attributo 'ID': '123456' non è un valore valido di tipo atomico " "'xs:ID'.", + + ('SCHEMASV', 'SCHEMAV_CVC_ENUMERATION_VALID', + "Element '{urn:oasis:names:tc:SAML:2.0:protocol}RequestedAuthnContext', " + "attribute 'Comparison': [facet 'enumeration'] The value 'invalid' is " + "not an element of the set {'exact', 'minimum', 'maximum', 'better'}."): + "Elemento '{urn:oasis:names:tc:SAML:2.0:protocol}RequestedAuthnContext', " + "attributo 'Comparison': [facet 'enumeration'] Il valore 'invalid' non è " + "un elemento dell'insieme {'exact', 'minimum', 'maximum', 'better'}.", } def test_translations(self): diff --git a/testenv/translation.py b/testenv/translation.py index bc58c87a..6e8153b6 100644 --- a/testenv/translation.py +++ b/testenv/translation.py @@ -28,6 +28,10 @@ class Libxml2Translator(object): '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}'." + }, + 'SCHEMAV_CVC_ENUMERATION_VALID': { + _c(r"Element '(?P.*)', attribute '(?P.*)': \[facet '(?P.*)'\] The value '(?P.*)' is not an element of the set (?P.*)."): + "Elemento '{element}', attributo '{attribute}': [facet '{facet}'] Il valore '{value}' non è un elemento dell'insieme {set}." } } } From 346112b88ec2ad606cf6feb08d97e66e03805044 Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Fri, 10 Aug 2018 13:02:44 +0200 Subject: [PATCH 06/29] Minfix --- testenv/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testenv/parser.py b/testenv/parser.py index 69d915e5..c8d07ad0 100644 --- a/testenv/parser.py +++ b/testenv/parser.py @@ -303,6 +303,7 @@ def get_schema(self, action, binding, **kwargs): """ _schema = None receivers = kwargs.get('receivers') + issuer = kwargs.get('issuer') if action == 'login': required_signature = False if binding == BINDING_HTTP_POST: @@ -315,7 +316,6 @@ def get_schema(self, action, binding, **kwargs): assertion_consumer_service_indexes = kwargs.get( 'assertion_consumer_service_indexes' ) - issuer = kwargs.get('issuer') _schema = Elem( name='auth_request', tag='samlp:AuthnRequest', From 0b5a2781ad47f7a9018bc75f0de1f34c4967cdeb Mon Sep 17 00:00:00 2001 From: Fabio Sangiovanni Date: Mon, 13 Aug 2018 10:07:00 +0200 Subject: [PATCH 07/29] refactor translation logic (first-match list of regexps) --- testenv/translation.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/testenv/translation.py b/testenv/translation.py index 6e8153b6..cb9b5717 100644 --- a/testenv/translation.py +++ b/testenv/translation.py @@ -10,29 +10,28 @@ 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.' - } + '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}'." - }, - 'SCHEMAV_CVC_ENUMERATION_VALID': { - _c(r"Element '(?P.*)', attribute '(?P.*)': \[facet '(?P.*)'\] The value '(?P.*)' is not an element of the set (?P.*)."): - "Elemento '{element}', attributo '{attribute}': [facet '{facet}'] Il valore '{value}' non è un elemento dell'insieme {set}." - } + '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}'.") + ], + 'SCHEMAV_CVC_ENUMERATION_VALID': [ + (_c(r"Element '(?P.*)', attribute '(?P.*)': \[facet '(?P.*)'\] The value '(?P.*)' is not an element of the set (?P.*)."), + "Elemento '{element}', attributo '{attribute}': [facet '{facet}'] Il valore '{value}' non è un elemento dell'insieme {set}.") + ] } } @@ -57,7 +56,7 @@ def _get_replacement_message(self, error): def _search_translation(self, error): regexp_group = self._mapping[error.domain_name][error.type_name] - for regexp, translation in regexp_group.items(): + for regexp, translation in regexp_group: match = re.match(regexp, error.message) if match: return translation.format(**match.groupdict()) From 42f42efe2fc785715c5271b1312628725d765d3e Mon Sep 17 00:00:00 2001 From: Fabio Sangiovanni Date: Mon, 13 Aug 2018 10:07:54 +0200 Subject: [PATCH 08/29] change wording of an error message --- testenv/tests/test_xsd_error_translation.py | 4 ++-- testenv/translation.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/testenv/tests/test_xsd_error_translation.py b/testenv/tests/test_xsd_error_translation.py index 46e34482..9c1ffcb8 100644 --- a/testenv/tests/test_xsd_error_translation.py +++ b/testenv/tests/test_xsd_error_translation.py @@ -25,8 +25,8 @@ class Libxml2ItalianTranslationTestCase(unittest.TestCase): "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'.", + "attributo 'ID': Il valore dell'attributo 'ID' può iniziare solo con " + "una lettera o con un underscore.", ('SCHEMASV', 'SCHEMAV_CVC_ENUMERATION_VALID', "Element '{urn:oasis:names:tc:SAML:2.0:protocol}RequestedAuthnContext', " diff --git a/testenv/translation.py b/testenv/translation.py index cb9b5717..ed0fd0e0 100644 --- a/testenv/translation.py +++ b/testenv/translation.py @@ -25,6 +25,8 @@ class Libxml2Translator(object): "Elemento '{element}': L'attributo '{attribute}' è mandatorio ma non presente.") ], 'SCHEMAV_CVC_DATATYPE_VALID_1_2_1': [ + (_c(r"Element '(?P.*)', attribute 'ID': '.*' is not a valid value of the atomic type 'xs:ID'."), + "Elemento '{element}', attributo 'ID': Il valore dell'attributo 'ID' può iniziare solo con una lettera o con un underscore."), (_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}'.") ], From a7ae100161acdd3c5bd1913369b2c1a7f70d7ddd Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Mon, 13 Aug 2018 10:36:41 +0200 Subject: [PATCH 09/29] Add tests for #88 (and add some test utilities) --- requirements_test.txt | 1 + testenv/spid.py | 1 - testenv/tests/data/sp-metadata.xml.example | 3 + testenv/tests/test_spid_testenv.py | 106 +++++++++++++++++++-- 4 files changed, 102 insertions(+), 9 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 88465b5a..026ed783 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,3 +3,4 @@ mock freezegun pytest coverage +BeautifulSoup4 \ No newline at end of file diff --git a/testenv/spid.py b/testenv/spid.py index a6ccad4b..dba40856 100644 --- a/testenv/spid.py +++ b/testenv/spid.py @@ -46,7 +46,6 @@ 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[:] - try: self.message = self.signature_check(xmldata, origdoc=origdoc, must=must, diff --git a/testenv/tests/data/sp-metadata.xml.example b/testenv/tests/data/sp-metadata.xml.example index 775171eb..12f2c37c 100644 --- a/testenv/tests/data/sp-metadata.xml.example +++ b/testenv/tests/data/sp-metadata.xml.example @@ -28,6 +28,9 @@ + urn:oasis:names:tc:SAML:2.0:nameid-format:transient diff --git a/testenv/tests/test_spid_testenv.py b/testenv/tests/test_spid_testenv.py index 9df98012..55770af3 100644 --- a/testenv/tests/test_spid_testenv.py +++ b/testenv/tests/test_spid_testenv.py @@ -8,18 +8,20 @@ import shutil import sys import unittest -import xml.etree.ElementTree as ET import flask +from bs4 import BeautifulSoup as BS from freezegun import freeze_time +from lxml import etree as ET from OpenSSL import crypto from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT -from saml2.s_utils import deflate_and_base64_encode +from saml2.s_utils import decode_base64_and_inflate, 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 six.moves.urllib.parse import parse_qs, quote, urlencode, urlparse +from testenv.spid import SpidServer from testenv.utils import get_config sys.path.insert(0, '../') @@ -30,15 +32,17 @@ except ImportError: from mock import patch -try: - from urllib import quote # Python 2.X -except ImportError: - from urllib.parse import quote # Python 3+ - DATA_DIR = 'testenv/tests/data/' +def _sp_single_logout_service(server, issuer_name, binding): + _slo = server.metadata.single_logout_service( + issuer_name, binding=binding, typ='spsso' + ) + return _slo + + def generate_certificate(fname, path=DATA_DIR): key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, 2048) @@ -117,6 +121,45 @@ def generate_authn_request(data={}, acs_level=0): return bytes(xmlstr.encode('utf-8')) +def generate_logout_request(data={}): + _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/slo-test' + issuer__format = data.get('issuer__format') if data.get('issuer__format') else NAMEID_FORMAT_ENTITY + issuer_url = data.get('issuer__url') if data.get('issuer__url') else 'https://spid.test:8000' + issuer__namequalifier = data.get('issuer__namequalifier') if data.get('issuer__namequalifier') else issuer_url + name_id__format = data.get('name_id__format') if data.get('name_id__format') else NAMEID_FORMAT_TRANSIENT + name_id__namequalifier = data.get('name_id__namequalifier') if data.get('name_id__namequalifier') else 'https://spid.test:8000' + name_id__value = data.get('name_id__value') if data.get('name_id__value') else 'name_id' + session_index__value = data.get('session_index__value') if data.get('session_index__value') else 'session_idx_123' + + xmlstr= ''' + %s + %s + %s + + ''' % ( + _id, + version, + issue_instant, + destination, + issuer__format, + issuer__namequalifier, + issuer_url, + name_id__format, + name_id__namequalifier, + name_id__value, + session_index__value + ) + return bytes(xmlstr.encode('utf-8')) + + class SpidTestenvTest(unittest.TestCase): maxDiff = None @@ -673,6 +716,53 @@ def test_wrong_issuer_namequalifier_not_an_url(self, unravel, verified): response_text ) + @freeze_time("2018-07-16T09:38:29Z") + @patch('testenv.spid.SpidServer.unravel', return_value=generate_logout_request()) + @patch('testenv.server.verify_redirect_signature', return_value=True) + def test_logout_response_http_redirect(self, unravel, verified): + # See: https://github.com/italia/spid-testenv2/issues/88 + with patch('testenv.server.IdpServer._sp_single_logout_service', return_value=_sp_single_logout_service(self.idp_server.server, 'https://spid.test:8000', BINDING_HTTP_REDIRECT)) as mocked: + response = self.test_client.get( + '/slo-test?SAMLRequest=b64encodedrequest&SigAlg={}&Signature=sign'.format(quote(SIG_RSA_SHA256)), + follow_redirects=False + ) + self.assertEqual(response.status_code, 302) + response_location = response.headers.get('Location') + url = urlparse(response_location) + query = parse_qs(url.query) + self.assertIn( + 'Signature', + query + ) + saml_response = query.get('SAMLResponse')[0] + response = decode_base64_and_inflate(saml_response) + xml = ET.fromstring(response) + signatures = xml.findall('.//{http://www.w3.org/2000/09/xmldsig#}Signature') + self.assertEqual(0, len(signatures)) + self.assertEqual(len(self.idp_server.ticket), 0) + self.assertEqual(len(self.idp_server.responses), 0) + + @freeze_time("2018-07-16T09:38:29Z") + @patch('testenv.spid.SpidServer.unravel', return_value=generate_logout_request()) + @patch('testenv.server.verify_redirect_signature', return_value=True) + def test_logout_response_http_post(self, unravel, verified): + # See: https://github.com/italia/spid-testenv2/issues/88 + with patch('testenv.server.IdpServer._sp_single_logout_service', return_value=_sp_single_logout_service(self.idp_server.server, 'https://spid.test:8000', BINDING_HTTP_POST)) as mocked: + response = self.test_client.get( + '/slo-test?SAMLRequest=b64encodedrequest&SigAlg={}&Signature=sign'.format(quote(SIG_RSA_SHA256)), + follow_redirects=False + ) + self.assertEqual(response.status_code, 200) + response_text = response.get_data(as_text=True) + soup = BS(response_text) + saml_response = soup.find('input', {'name': 'SAMLResponse'}).get('value') + response = base64.b64decode(saml_response) + xml = ET.fromstring(response) + signatures = xml.findall('.//{http://www.w3.org/2000/09/xmldsig#}Signature') + self.assertEqual(1, len(signatures)) + self.assertEqual(len(self.idp_server.ticket), 0) + self.assertEqual(len(self.idp_server.responses), 0) + if __name__ == '__main__': unittest.main() From 0c71502812cc652561613690f67725dbacb4412e Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Mon, 13 Aug 2018 10:53:32 +0200 Subject: [PATCH 10/29] Do not check value formt for Issuer text and namequalifier. Fix issue #121 --- testenv/parser.py | 8 ++++---- testenv/tests/test_spid_testenv.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testenv/parser.py b/testenv/parser.py index c8d07ad0..99974fe4 100644 --- a/testenv/parser.py +++ b/testenv/parser.py @@ -368,8 +368,8 @@ def get_schema(self, action, binding, **kwargs): tag='saml:Issuer', attributes=[ Attr('format', default=NAMEID_FORMAT_ENTITY), - Attr('name_qualifier', default=issuer, func=check_url), - Attr('text', func=check_url) + Attr('name_qualifier', default=issuer), + Attr('text', default=issuer) ], ), Elem( @@ -436,8 +436,8 @@ def get_schema(self, action, binding, **kwargs): tag='saml:Issuer', attributes=[ Attr('format', default=NAMEID_FORMAT_ENTITY), - Attr('name_qualifier', default=issuer, func=check_url), - Attr('text', func=check_url) + Attr('name_qualifier', default=issuer), + Attr('text', default=issuer) ], ), Elem( diff --git a/testenv/tests/test_spid_testenv.py b/testenv/tests/test_spid_testenv.py index 55770af3..4ba9d0ed 100644 --- a/testenv/tests/test_spid_testenv.py +++ b/testenv/tests/test_spid_testenv.py @@ -712,7 +712,7 @@ def test_wrong_issuer_namequalifier_not_an_url(self, unravel, verified): self.assertEqual(len(self.idp_server.responses), 0) response_text = response.get_data(as_text=True) self.assertIn( - 'la url non è in formato corretto', + 'something è diverso dal valore di riferimento https://spid.test:8000', response_text ) From 5b37041455bf70ad40e0649a661ae384b1a19176 Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Mon, 13 Aug 2018 11:47:15 +0200 Subject: [PATCH 11/29] Fix typo --- testenv/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testenv/parser.py b/testenv/parser.py index 99974fe4..01f14edd 100644 --- a/testenv/parser.py +++ b/testenv/parser.py @@ -391,7 +391,7 @@ def get_schema(self, action, binding, **kwargs): ), Elem( 'requested_authn_context', - tag='saml:AuthnContext', + tag='saml:RequestedAuthnContext', attributes=[ Attr('comparison', default=COMPARISONS), ], From 8bc615f356e18986b25d1d0ba841893da145fd7e Mon Sep 17 00:00:00 2001 From: Marco Federighi Date: Mon, 13 Aug 2018 15:15:24 +0200 Subject: [PATCH 12/29] Fix httpost-signature check --- testenv/spid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testenv/spid.py b/testenv/spid.py index dba40856..a43326a9 100644 --- a/testenv/spid.py +++ b/testenv/spid.py @@ -46,6 +46,7 @@ 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[:] + must = True if binding == BINDING_HTTP_POST else False try: self.message = self.signature_check(xmldata, origdoc=origdoc, must=must, @@ -57,7 +58,6 @@ def _loads(self, xmldata, binding=None, origdoc=None, must=None, if not self.message: raise IncorrectlySigned() - return self def verify(self): From 3f83920cbc8f10f9323a652ffb195ccb1f084586 Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Mon, 13 Aug 2018 21:10:59 +0200 Subject: [PATCH 13/29] Minor improvements to README --- README.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 82305417..72f66126 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,10 @@ Alternativamente alla procedura di installazione manuale riportata sopra, è pos Alternativamente alla procedura di installazione manuale è possible installare ed eseguire l'Identity Provider di test usando l'immagine presente su [Docker Hub](https://hub.docker.com/). -Per ottenere la persistenza della configurazione è necessario creare nell'host una directory. Tale directory sarà mappata in `conf/` all'interno del container. +Per ottenere la persistenza della configurazione è necessario creare nell'host una directory, da collocarsi in un percorso a piacere (di seguito un suggerimento). Tale directory sarà mappata in `conf/` all'interno del container. ``` -mkdir /path/to/testenv/conf +mkdir /etc/spid-testenv2 ``` Creare nella directory il file config.yaml e la coppia chiave/certificato per l'IdP, nonché eventuali metadata SP, come indicato nel paragrafo successivo. @@ -65,7 +65,7 @@ Creare il container con il seguente comando: ``` docker create --name spid-testenv2 -p 8088:8088 \ - --mount src="/path/to/testenv/conf",target="/app/conf",type=bind \ + --mount src="/etc/spid-testenv2",target="/app/conf",type=bind \ italia/spid-testenv2 ``` @@ -75,22 +75,20 @@ Avviare il container: docker start spid-testenv2 ``` +Il log si può visualizzare con il comando: + +``` +docker logs -f spid-testenv2 +``` + ## Configurazione Generare una chiave privata ed un certificato. -### Versione Docker ``` openssl req -x509 -nodes -sha256 -subj '/C=IT' -newkey rsa:2048 -keyout conf/idp.key -out conf/idp.crt ``` -### Versione manuale -``` -openssl req -x509 -nodes -sha256 -subj '/C=IT -newkey rsa:2048 -keyout idp.key -out idp.crt -``` - - - Creare e configurare il file config.yaml. ``` @@ -111,7 +109,7 @@ python spid-testenv.py ## Home page -Nella home page è presente una lista di Service Providers registrati sull'IdP di test. +Nella home page è presente la lista dei Service Providers registrati sull'IdP di test. ## Metadata IdP From 45998fc2d350cbb40b58fcf7c3249529b03e2946 Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Mon, 13 Aug 2018 21:33:08 +0200 Subject: [PATCH 14/29] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72f66126..f3c80aec 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Creare nella directory il file config.yaml e la coppia chiave/certificato per l' Creare il container con il seguente comando: ``` -docker create --name spid-testenv2 -p 8088:8088 \ +docker create --name spid-testenv2 -p 8088:8088 --restart=always \ --mount src="/etc/spid-testenv2",target="/app/conf",type=bind \ italia/spid-testenv2 ``` @@ -83,6 +83,8 @@ docker logs -f spid-testenv2 ## Configurazione +(In caso di installazione via Docker, sostituire `conf/` nei seguenti comandi con il percorso alla directory di configurazione creata nell'host.) + Generare una chiave privata ed un certificato. ``` From c531eb895ef087099450e95b4a66a2789ce9734a Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Fri, 17 Aug 2018 20:18:54 +0200 Subject: [PATCH 15/29] Publish the default port in base_url --- conf/config.yaml.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/config.yaml.example b/conf/config.yaml.example index 720d43a1..5b818f9f 100644 --- a/conf/config.yaml.example +++ b/conf/config.yaml.example @@ -3,7 +3,7 @@ # URL da usare per generare l'entityID dell'IdP e gli URL degli endpoint # SAML indicati nel metadata dell'IdP -base_url: "https://localhost" +base_url: "http://localhost:8088" # Chiave e certificato necessari per la firma dei messaggi SAML key_file: "./conf/idp.key" From 8d64376ea406e6689fd319d292fee01ebd8508c5 Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Fri, 17 Aug 2018 20:19:09 +0200 Subject: [PATCH 16/29] Add link to IdP metadata in menu. Fixes #123 --- templates/admin_area_base.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/admin_area_base.html b/templates/admin_area_base.html index 716b2ef8..ded7099a 100644 --- a/templates/admin_area_base.html +++ b/templates/admin_area_base.html @@ -40,6 +40,7 @@

Spid Idp Test

+ From a85fbb16b21153e510550ca8bbd791dc7400277f Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Fri, 17 Aug 2018 20:31:25 +0200 Subject: [PATCH 17/29] UI improvements to the users page --- templates/users.html | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/templates/users.html b/templates/users.html index 53c6b203..04a951fb 100644 --- a/templates/users.html +++ b/templates/users.html @@ -1,8 +1,29 @@ {% extends 'admin_area_base.html' %} {% block content %}
-

Lista utenti

+

Utenti SPID

+ + + + + + + + {% for user, info in users.items() %} + + + + + + + {% endfor %} +
UsernamePasswordSPAttributes
{{user}}{{info.pwd}}{{info.sp}} + {% for attr, value in info.attrs.items() %} + {{attr}}: {{value}}
+ {% endfor %} +
+
    {% for user, info in users.items() %}
  • {{user}} @@ -41,7 +62,7 @@

    Credenzi {% endfor %}

    Attributi Primari

    From 9ace83aa9a48dd6475f94e97ffc735c457ad068c Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Fri, 17 Aug 2018 21:07:02 +0200 Subject: [PATCH 18/29] Remove old users list --- templates/users.html | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/templates/users.html b/templates/users.html index 04a951fb..6a044536 100644 --- a/templates/users.html +++ b/templates/users.html @@ -23,18 +23,8 @@

    Utenti SPID

    {% endfor %} - -
      - {% for user, info in users.items() %} -
    • {{user}} - {% for attr, value in info.items() %} - {{attr}} - {{value}}
      - {% endfor %} -
    • - {% endfor %} -
    -
+

Crea nuovo utente

From 1d631c26d2c090bd4080bcb2011d80aee133c1d3 Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Fri, 17 Aug 2018 21:10:15 +0200 Subject: [PATCH 19/29] Minfix: redirect to users list after adding a new user --- testenv/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testenv/server.py b/testenv/server.py index 1ab64fe6..ca1c650f 100644 --- a/testenv/server.py +++ b/testenv/server.py @@ -592,7 +592,7 @@ def users(self): if spid_value: extra[spid_field] = spid_value self.user_manager.add(username, password, sp, extra) - return 'Added a new user', 200 + return redirect(url_for('users')) def index(self): rendered_form = render_template( From 2c1cf132fe372bfd3c7183ea7a93c6f4acf2a2cd Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Fri, 17 Aug 2018 21:13:00 +0200 Subject: [PATCH 20/29] Show 'tutti' instead of 'None' when an user is not limited to a single SP --- templates/users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/users.html b/templates/users.html index 6a044536..9e29ab71 100644 --- a/templates/users.html +++ b/templates/users.html @@ -14,7 +14,7 @@

Utenti SPID

{{user}} {{info.pwd}} - {{info.sp}} + {{info.sp if info.sp != None else 'tutti'}} {% for attr, value in info.attrs.items() %} {{attr}}: {{value}}
From 510b7d47ae3429f294fdeb73dd7e2353d11e7214 Mon Sep 17 00:00:00 2001 From: sanjioh <4040184+sanjioh@users.noreply.github.com> Date: Thu, 23 Aug 2018 14:54:17 +0200 Subject: [PATCH 21/29] WIP: refactoring/request-validation-flow (#126) * add request parsers * WIP: refactoring SPID rules parsing * DRY up error handling * wording * add deserializer * refactor validators * refactor deserializer error handling * WIP: Spid parsing rules refactoring * refactor XMLSchemaFileLoader * Move SpidValidator from parsers to validators module * parser.py cleanup * Remove old spidparser * Rename Invalid to ValidationDetail * Fix import * WIP: Integrate on server flow custom parsers and deserializer * Use custom Request instead of pysaml2 Request classes * add signature verifier for HTTP-Redirect requests * cleanup * add signature verifier for HTTP-Post requests * fix ValidationDetail creation * Handle logout response generation * add request deserializer factory functions * remove imports from pysaml2 * make normalize_x509 more robust * Handle signing for responses * Handle default None on getting attribute on request * handle request types with dedicated methods * handle lists of elements in SAMLTree * snake case names in SAMLTree * simplify SAMLTree * Fix attributes fetching * add digest validation * add initial_data attribute to DeserializationError * Enable new error handling flow * Minor fixes * fix existing validators tests * fix existing translation tests * add signxml to dependencies * Minor fixes * simplify XML format test for duplicate attribute * add tests for HTTPRedirectRequestParser * decode to utf-8 explicitly * add tests for HTTPPostRequestParser * refactor test utils * add tests for HTTPRequestDeserializer * py27 compat * linting * flake8 config * show warnings for our code only * handle RelayState * add FIXMEs * add tests to validators * add test for SAMLTree * Handle custom response generation * Move contants to settings * Move more constants into settings module * Isort * Cleanup * Cleanup * fix handling exceptions with same name * add SUPPORTED_ALGORITHMS constant * add tests for crypto * WIP: tests + minor fixes * refactor verifiers * WIP: Minor fixes and tests alignments * Normalize spid validator path * Remove pysaml2 dependencies from tests * take care of multiple-occurrences tags * Remove another dependency from pysaml2 * Remove zombie code * enforce required keys * handle AssertionConsumerService-related attributes * Cleanup * fix encoding errors on python2.7 * Minfix * remove ALLOW_EXTRA * linting * add ID, SessionIndex * linting --- requirements.txt | 5 +- setup.cfg | 8 +- templates/form_http_post.html | 25 + templates/spid_error.html | 49 +- testenv/attributemaps/spid_attributes.py | 42 -- testenv/crypto.py | 257 +++++++ testenv/exceptions.py | 49 +- testenv/parser.py | 791 +++++++------------- testenv/saml.py | 397 ++++++++++ testenv/server.py | 785 +++++++++---------- testenv/settings.py | 107 ++- testenv/spid.py | 242 ------ testenv/tests/data/sample_saml_requests.py | 38 +- testenv/tests/test_crypto.py | 263 +++++++ testenv/tests/test_parsers.py | 215 ++++++ testenv/tests/test_saml_xsd_validation.py | 108 --- testenv/tests/test_spid_testenv.py | 166 ++-- testenv/tests/test_validators.py | 148 ++++ testenv/tests/test_xsd_error_translation.py | 8 +- testenv/tests/utils.py | 7 + testenv/translation.py | 7 +- testenv/utils.py | 68 +- testenv/validators.py | 439 +++++++++++ tox.ini | 7 +- 24 files changed, 2660 insertions(+), 1571 deletions(-) create mode 100644 templates/form_http_post.html delete mode 100644 testenv/attributemaps/spid_attributes.py create mode 100644 testenv/crypto.py create mode 100644 testenv/saml.py delete mode 100644 testenv/spid.py create mode 100644 testenv/tests/test_crypto.py create mode 100644 testenv/tests/test_parsers.py delete mode 100644 testenv/tests/test_saml_xsd_validation.py create mode 100644 testenv/tests/test_validators.py create mode 100644 testenv/tests/utils.py create mode 100644 testenv/validators.py diff --git a/requirements.txt b/requirements.txt index ec4fad31..2d42a994 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ PyYAML==3.12 passlib==1.7.1 lxml==4.2.3 Faker==0.8.16 -exrex - +exrex==0.10.5 +voluptuous==0.11.5 importlib-resources==1.0.1 +signxml==2.5.2 diff --git a/setup.cfg b/setup.cfg index d2559805..272c874b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,15 @@ [flake8] exclude = *.egg-info +max-line-length = 119 [isort] -line_length = 79 +line_length = 119 combine_as_imports = true default_section = THIRDPARTY +include_trailing_comma = true known_first_party = testenv +multi_line_output = 5 not_skip = __init__.py + +[tool:pytest] +filterwarnings = default diff --git a/templates/form_http_post.html b/templates/form_http_post.html new file mode 100644 index 00000000..2414167f --- /dev/null +++ b/templates/form_http_post.html @@ -0,0 +1,25 @@ + + + + + + + + +
+ + +
+ + + +{{line}} {% endfor %} -

Validazione tramite XSD

-{% if validation_errors %} +

Errori di validazione

+{% if errors %} @@ -16,59 +16,18 @@

Validazione tramite XSD

- {% for err in validation_errors %} + {% for err in errors %} {% endfor %}
{{err.path}} - {{err.message}} + {{err.value}} {{err.message}}
-{% else %} - -{% endif %} - -

Validazione Regole spid

-{% if spid_errors %} - - - - - - - - - {% for err in spid_errors %} - - - - - {% endfor %} - -
ElementoDettagli errore
{{err.1}} -
    - {% for name, msgs in err.2.items() %} -
  • {{name}} -
      - {% if msgs is mapping %} - {% for type, msg in msgs.items() %} -
    • {{msg|safe}}
    • - {% endfor %} - {% else %} -
    • {{msgs}}
    • - {% endif %} -
    -
  • - {% endfor %} -
-
-{% else %} - {% endif %} {% endblock %} - {% block js %}