From d99bee3693eb55046a970bdd26508076a7ed919f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cz=C3=A9m=C3=A1n=20Arnold?= Date: Sat, 15 Apr 2017 23:42:49 +0200 Subject: Fix some ECP problems --- src/saml2/ecp.py | 50 ++++++++++++++++++++++++--------------------- src/saml2/ecp_client.py | 29 ++++++++++---------------- src/saml2/entity.py | 5 +++-- src/saml2/profile/samlec.py | 14 +++++++++++++ 4 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 src/saml2/profile/samlec.py diff --git a/src/saml2/ecp.py b/src/saml2/ecp.py index f15a259c..5817cda4 100644 --- a/src/saml2/ecp.py +++ b/src/saml2/ecp.py @@ -24,6 +24,8 @@ from saml2.schema import soapenv from saml2.response import authn_response +from saml2 import saml + logger = logging.getLogger(__name__) @@ -53,7 +55,7 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): # ---------------------------------------- # # ---------------------------------------- - my_url = cls.service_url(BINDING_PAOS) + my_url = cls.service_urls(BINDING_PAOS)[0] # must_understand and actor according to the standard # @@ -63,6 +65,19 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): eelist.append(element_to_extension_element(paos_request)) + # ---------------------------------------- + # + # ---------------------------------------- + + logger.info("entityid: %s, binding: %s" % (entityid, BINDING_SOAP)) + + location = cls._sso_location(entityid, binding=BINDING_SOAP) + req_id, authn_req = cls.create_authn_request( + location, binding=BINDING_PAOS, service_url_binding=BINDING_PAOS) + + body = soapenv.Body() + body.extension_elements = [element_to_extension_element(authn_req)] + # ---------------------------------------- # # ---------------------------------------- @@ -74,14 +89,16 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): # ) # # idp_list = samlp.IDPList(idp_entry= [idp]) -# -# ecp_request = ecp.Request( -# actor = ACTOR, must_understand = "1", -# provider_name = "Example Service Provider", -# issuer=saml.Issuer(text="https://sp.example.org/entity"), -# idp_list = idp_list) -# -# eelist.append(element_to_extension_element(ecp_request)) + + idp_list = None + ecp_request = ecp.Request( + actor=ACTOR, + must_understand="1", + provider_name=None, + issuer=saml.Issuer(text=authn_req.issuer.text), + idp_list=idp_list) + + eelist.append(element_to_extension_element(ecp_request)) # ---------------------------------------- # @@ -95,19 +112,6 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): header = soapenv.Header() header.extension_elements = eelist - # ---------------------------------------- - # - # ---------------------------------------- - - logger.info("entityid: %s, binding: %s" % (entityid, BINDING_SOAP)) - - location = cls._sso_location(entityid, binding=BINDING_SOAP) - req_id, authn_req = cls.create_authn_request( - location, binding=BINDING_PAOS, service_url_binding=BINDING_PAOS) - - body = soapenv.Body() - body.extension_elements = [element_to_extension_element(authn_req)] - # ---------------------------------------- # The SOAP envelope # ---------------------------------------- @@ -126,7 +130,7 @@ def handle_ecp_authn_response(cls, soap_message, outstanding=None): if item.c_tag == "RelayState" and item.c_namespace == ecp.NAMESPACE: _relay_state = item - response = authn_response(cls.config, cls.service_url(), outstanding, + response = authn_response(cls.config, cls.service_urls(), outstanding, allow_unsolicited=True) response.loads("%s" % rdict["body"], False, soap_message) diff --git a/src/saml2/ecp_client.py b/src/saml2/ecp_client.py index 90c3b44c..788d252d 100644 --- a/src/saml2/ecp_client.py +++ b/src/saml2/ecp_client.py @@ -119,7 +119,7 @@ class Client(Entity): if response.status_code != 200: raise SAMLError( "Request to IdP failed (%s): %s" % (response.status_code, - response.error)) + response.text)) # SAMLP response in a SOAP envelope body, ecp response in headers respdict = self.parse_soap_message(response.text) @@ -200,22 +200,19 @@ class Client(Entity): ht_args = self.use_soap(idp_response, args["rc_url"], [args["relay_state"]]) - + ht_args["headers"][0] = ('Content-Type', MIME_PAOS) logger.debug("[P3] Post to SP: %s", ht_args["data"]) - ht_args["headers"].append(('Content-Type', 'application/vnd.paos+xml')) - # POST the package from the IdP to the SP - response = self.send(args["rc_url"], "POST", **ht_args) + response = self.send(**ht_args) if response.status_code == 302: # ignore where the SP is redirecting us to and go for the # url I started off with. pass else: - print(response.error) raise SAMLError( - "Error POSTing package to SP: %s" % response.error) + "Error POSTing package to SP: %s" % response.text) logger.debug("[P3] SP response: %s", response.text) @@ -255,8 +252,7 @@ class Client(Entity): :param opargs: Arguments to the HTTP call :return: The page """ - if url not in opargs: - url = self._sp + sp_url = self._sp # ******************************************** # Phase 1 - First conversation with the SP @@ -264,13 +260,13 @@ class Client(Entity): # headers needed to indicate to the SP that I'm ECP enabled opargs["headers"] = self.add_paos_headers(opargs["headers"]) - - response = self.send(url, op, **opargs) - logger.debug("[Op] SP response: %s", response) + response = self.send(sp_url, op, **opargs) + logger.debug("[Op] SP response: %s" % response) + print(response.text) if response.status_code != 200: raise SAMLError( - "Request to SP failed: %s" % response.error) + "Request to SP failed: %s" % response.text) # The response might be a AuthnRequest instance in a SOAP envelope # body. If so it's the start of the ECP conversation @@ -282,7 +278,6 @@ class Client(Entity): # header blocks may also be present try: respdict = self.parse_soap_message(response.text) - self.ecp_conversation(respdict, idp_entity_id) # should by now be authenticated so this should go smoothly @@ -290,11 +285,9 @@ class Client(Entity): except (soap.XmlParseError, AssertionError, KeyError): pass - #print("RESP",response, self.http.response) - - if response.status_code != 404: + if response.status_code >= 400: raise SAMLError("Error performing operation: %s" % ( - response.error,)) + response.text,)) return response diff --git a/src/saml2/entity.py b/src/saml2/entity.py index b24c6210..1276b80d 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -8,7 +8,7 @@ from binascii import hexlify from hashlib import sha1 from saml2.metadata import ENDPOINTS -from saml2.profile import paos, ecp +from saml2.profile import paos, ecp, samlec from saml2.soap import parse_soap_enveloped_saml_artifact_resolve from saml2.soap import class_instances_from_soap_enveloped_saml_thingies from saml2.soap import open_soap_envelope @@ -407,7 +407,8 @@ class Entity(HTTPBase): """ return class_instances_from_soap_enveloped_saml_thingies(text, [paos, ecp, - samlp]) + samlp, + samlec]) @staticmethod def unpack_soap_message(text): diff --git a/src/saml2/profile/samlec.py b/src/saml2/profile/samlec.py new file mode 100644 index 00000000..b90f6d3d --- /dev/null +++ b/src/saml2/profile/samlec.py @@ -0,0 +1,14 @@ +from saml2 import SamlBase + + +NAMESPACE = 'urn:ietf:params:xml:ns:samlec' + + +class GeneratedKey(SamlBase): + c_tag = 'GeneratedKey' + c_namespace = NAMESPACE + + +ELEMENT_BY_TAG = { + 'GeneratedKey': GeneratedKey, +} -- cgit v1.2.1 From 4c6e454ee50d8d55adcdd9a5c1741b4df99a30d9 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Fri, 21 Apr 2017 14:21:21 +0200 Subject: Allow to configuration option name_id_format_allow_create for sp config --- src/saml2/client_base.py | 15 ++++++---- src/saml2/config.py | 2 ++ tests/sp_conf_nameidpolicy.py | 64 +++++++++++++++++++++++++++++++++++++++++++ tests/test_51_client.py | 20 ++++++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 tests/sp_conf_nameidpolicy.py diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 55e5b1fc..c4797fd0 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -207,7 +207,7 @@ class Base(Entity): nameid_format=None, service_url_binding=None, message_id=0, consent=None, extensions=None, sign=None, - allow_create=False, sign_prepare=False, sign_alg=None, + allow_create=None, sign_prepare=False, sign_alg=None, digest_alg=None, **kwargs): """ Creates an authentication request. @@ -288,10 +288,15 @@ class Base(Entity): args["name_id_policy"] = kwargs["name_id_policy"] del kwargs["name_id_policy"] except KeyError: - if allow_create: - allow_create = "true" - else: - allow_create = "false" + if allow_create is None: + allow_create = self.config.getattr("name_id_format_allow_create", "sp") + if allow_create is None: + allow_create = "false" + else: + if allow_create is True: + allow_create = "true" + else: + allow_create = "false" if nameid_format == "": name_id_policy = None diff --git a/src/saml2/config.py b/src/saml2/config.py index 9fc3e708..50d61c57 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -73,6 +73,7 @@ SP_ARGS = [ "allow_unsolicited", "ecp", "name_id_format", + "name_id_format_allow_create", "logout_requests_signed", "requested_attribute_name_format" ] @@ -187,6 +188,7 @@ class Config(object): self.contact_person = None self.name_form = None self.name_id_format = None + self.name_id_format_allow_create = None self.virtual_organization = None self.logger = None self.only_use_keys_in_metadata = True diff --git a/tests/sp_conf_nameidpolicy.py b/tests/sp_conf_nameidpolicy.py new file mode 100644 index 00000000..d15989c2 --- /dev/null +++ b/tests/sp_conf_nameidpolicy.py @@ -0,0 +1,64 @@ +from pathutils import full_path +from pathutils import xmlsec_path + +CONFIG = { + "entityid": "urn:mace:example.com:saml:roland:sp", + "name": "urn:mace:example.com:saml:roland:sp", + "description": "My own SP", + "service": { + "sp": { + "endpoints": { + "assertion_consumer_service": [ + "http://lingon.catalogix.se:8087/"], + }, + "required_attributes": ["surName", "givenName", "mail"], + "optional_attributes": ["title"], + "idp": ["urn:mace:example.com:saml:roland:idp"], + "name_id_format": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "name_id_format_allow_create": "true" + } + }, + "debug": 1, + "key_file": full_path("test.key"), + "cert_file": full_path("test.pem"), + "encryption_keypairs": [{"key_file": full_path("test_1.key"), "cert_file": full_path("test_1.crt")}, + {"key_file": full_path("test_2.key"), "cert_file": full_path("test_2.crt")}], + "ca_certs": full_path("cacerts.txt"), + "xmlsec_binary": xmlsec_path, + "metadata": [{ + "class": "saml2.mdstore.MetaDataFile", + "metadata": [(full_path("idp.xml"), ), (full_path("vo_metadata.xml"), )], + }], + "virtual_organization": { + "urn:mace:example.com:it:tek": { + "nameid_format": "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID", + "common_identifier": "umuselin", + } + }, + "subject_data": "subject_data.db", + "accepted_time_diff": 60, + "attribute_map_dir": full_path("attributemaps"), + "valid_for": 6, + "organization": { + "name": ("AB Exempel", "se"), + "display_name": ("AB Exempel", "se"), + "url": "http://www.example.org", + }, + "contact_person": [{ + "given_name": "Roland", + "sur_name": "Hedberg", + "telephone_number": "+46 70 100 0000", + "email_address": ["tech@eample.com", + "tech@example.org"], + "contact_type": "technical" + }, + ], + "logger": { + "rotating": { + "filename": full_path("sp.log"), + "maxBytes": 100000, + "backupCount": 5, + }, + "loglevel": "info", + } +} diff --git a/tests/test_51_client.py b/tests/test_51_client.py index 13cef7cc..7e42045b 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -280,6 +280,26 @@ class TestClient: assert nid_policy.allow_create == "false" assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT + def test_create_auth_request_nameid_policy_allow_create(self): + conf = config.SPConfig() + conf.load_file("sp_conf_nameidpolicy") + client = Saml2Client(conf) + ar_str = "%s" % client.create_authn_request( + "http://www.example.com/sso", message_id="id1")[1] + + ar = samlp.authn_request_from_string(ar_str) + print(ar) + assert ar.assertion_consumer_service_url == ("http://lingon.catalogix" + ".se:8087/") + assert ar.destination == "http://www.example.com/sso" + assert ar.protocol_binding == BINDING_HTTP_POST + assert ar.version == "2.0" + assert ar.provider_name == "urn:mace:example.com:saml:roland:sp" + assert ar.issuer.text == "urn:mace:example.com:saml:roland:sp" + nid_policy = ar.name_id_policy + assert nid_policy.allow_create == "true" + assert nid_policy.format == saml.NAMEID_FORMAT_PERSISTENT + def test_create_auth_request_vo(self): assert list(self.client.config.vorg.keys()) == [ "urn:mace:example.com:it:tek"] -- cgit v1.2.1 From a1cef866e267f2b3f0ba3bbdd92482f00221eb31 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Fri, 21 Apr 2017 15:50:48 +0200 Subject: Missing test dependency --- tests/test_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_requirements.txt b/tests/test_requirements.txt index 2c1ebbbc..33301079 100644 --- a/tests/test_requirements.txt +++ b/tests/test_requirements.txt @@ -2,3 +2,4 @@ mock==2.0.0 pymongo==3.0.1 pytest==3.0.3 responses==0.5.0 +pyasn1==0.2.3 -- cgit v1.2.1 From 3ee85092e34cf637398b32741dda063b5c4bf316 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Mon, 24 Apr 2017 15:45:46 +0200 Subject: 'scoping' not i kwargs --- src/saml2/client_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 55e5b1fc..62410430 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -246,8 +246,8 @@ class Base(Entity): del kwargs["assertion_consumer_service_url"] except KeyError: try: - args["assertion_consumer_service_index"] = str(kwargs[ - "assertion_consumer_service_index"]) + args["assertion_consumer_service_index"] = str( + kwargs["assertion_consumer_service_index"]) del kwargs["assertion_consumer_service_index"] except KeyError: if service_url_binding is None: @@ -268,7 +268,7 @@ class Base(Entity): # all of these have cardinality 0..1 _msg = AuthnRequest() for param in ["scoping", "requested_authn_context", "conditions", - "subject", "scoping"]: + "subject"]: try: _item = kwargs[param] except KeyError: -- cgit v1.2.1 From e2adbb523c37643453fbe62469ca7fb2bf1b43f0 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Mon, 24 Apr 2017 15:46:07 +0200 Subject: Removed while investigating pyasn1 usage --- tests/test_40_sigver.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_40_sigver.py b/tests/test_40_sigver.py index e2ba952f..48cf19f2 100644 --- a/tests/test_40_sigver.py +++ b/tests/test_40_sigver.py @@ -18,6 +18,8 @@ from saml2.saml import EncryptedAssertion from saml2.samlp import response_from_string from saml2.s_utils import factory, do_attribute_statement +#from pyasn1.codec.der import decoder + from py.test import raises from pathutils import full_path @@ -67,7 +69,6 @@ Yj4cAafWaYfjBU2zi1ElwStIaJ5nyp/s/8B8SAPK2T79McMyccP3wSW13LHkmM1j wKe3ACFXBvqGQN0IbcH49hu0FKhYFM/GPDJcIHFBsiyMBXChpye9vBaTNEBCtU3K jjyG0hRT2mAQ9h+bkPmOvlEo/aH0xR68Z9hw4PF13w==""" -from pyasn1.codec.der import decoder def test_cert_from_instance_1(): @@ -80,16 +81,16 @@ def test_cert_from_instance_1(): assert certs[0] == CERT1 -def test_cert_from_instance_ssp(): - xml_response = open(SIMPLE_SAML_PHP_RESPONSE).read() - response = samlp.response_from_string(xml_response) - assertion = response.assertion[0] - certs = sigver.cert_from_instance(assertion) - assert len(certs) == 1 - assert certs[0] == CERT_SSP - der = base64.b64decode(certs[0]) - print(str(decoder.decode(der)).replace('.', "\n.")) - assert decoder.decode(der) +# def test_cert_from_instance_ssp(): +# xml_response = open(SIMPLE_SAML_PHP_RESPONSE).read() +# response = samlp.response_from_string(xml_response) +# assertion = response.assertion[0] +# certs = sigver.cert_from_instance(assertion) +# assert len(certs) == 1 +# assert certs[0] == CERT_SSP +# der = base64.b64decode(certs[0]) +# print(str(decoder.decode(der)).replace('.', "\n.")) +# assert decoder.decode(der) class FakeConfig(): -- cgit v1.2.1 From 8aa80e9e67f76ef230cb377dfe8b2050fc1f82bf Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Mon, 24 Apr 2017 16:09:52 +0200 Subject: Updated README --- README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 6aa3e23f..638b9133 100644 --- a/README.rst +++ b/README.rst @@ -38,3 +38,15 @@ testing. To run the tests on your system's version of python To run tests in multiple python environments, you can use `pyenv `_ with `tox `_. + + +Please contribute! +================== + +To help out, you could: + +1. Test and report any bugs or other difficulties. +2. Implement missing features. +3. Write more unit tests. + +**If you have the time and inclination I'm looking for Collaborators** -- cgit v1.2.1 From 0d2e0baf5277f9dd9248c64bf5a42fea3ff7be58 Mon Sep 17 00:00:00 2001 From: Scott Koranda Date: Mon, 15 May 2017 12:54:15 -0500 Subject: Enable deployer to signal no name format in authn request Enable a deployer to configure name_id_format with the string 'None' to signal that no Format attribute should be included in the that is sent with the . A yaml null is still converted to a Python None that then results in the default of Format being set to transient, so this patch does not change default behavior. --- src/saml2/client_base.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 4b1b350e..f740cb07 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -304,12 +304,21 @@ class Base(Entity): if nameid_format is None: nameid_format = self.config.getattr("name_id_format", "sp") + # If no nameid_format has been set in the configuration + # or passed in then transient is the default. if nameid_format is None: nameid_format = NAMEID_FORMAT_TRANSIENT + + # If a list has been configured or passed in choose the + # first since NameIDPolicy can only have one format specified. elif isinstance(nameid_format, list): - # NameIDPolicy can only have one format specified nameid_format = nameid_format[0] + # Allow a deployer to signal that no format should be specified + # in the NameIDPolicy by passing in or configuring the string 'None'. + elif nameid_format == 'None': + nameid_format = None + name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) -- cgit v1.2.1 From 4012711f8d510b62caa94392b91886f9c1423a6d Mon Sep 17 00:00:00 2001 From: william Date: Thu, 25 May 2017 16:28:19 +0800 Subject: fixbug: 'NoneType' object has no attribute 'get_signer' --- src/saml2/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 1276b80d..27b30fe9 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -224,7 +224,7 @@ class Entity(HTTPBase): info["method"] = "POST" elif binding == BINDING_HTTP_REDIRECT: logger.info("HTTP REDIRECT") - if 'sigalg' in kwargs: + if kwargs.get('sigalg', ''): signer = self.sec.sec_backend.get_signer(kwargs['sigalg']) else: signer = None -- cgit v1.2.1 From 979b6b28a3b11275782b11518d5a7621fdb9285c Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Mon, 29 May 2017 11:15:41 +0200 Subject: fix issue with satosa and encrypted assertions --- src/saml2/response.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/saml2/response.py b/src/saml2/response.py index 13323509..70ca93a8 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -910,7 +910,8 @@ class AuthnResponse(StatusResponse): else: # This is a saml2int limitation try: assert len(self.response.assertion) == 1 or \ - len(self.response.encrypted_assertion) == 1 + len(self.response.encrypted_assertion) == 1 or \ + self.assertion is not None except AssertionError: raise Exception("No assertion part") -- cgit v1.2.1 From bf96c83b2da7b9046495783510dc76a320a229cd Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Mon, 29 May 2017 17:35:03 +0200 Subject: fix for bad xsi:nil on complex attributevalue --- src/saml2/saml.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/saml2/saml.py b/src/saml2/saml.py index 35b7bd1a..c53aab95 100644 --- a/src/saml2/saml.py +++ b/src/saml2/saml.py @@ -139,10 +139,12 @@ class AttributeValueBase(SamlBase): if self._extatt: self.extension_attributes = self._extatt - if not text: - self.extension_attributes = {XSI_NIL: 'true'} - else: + if text: self.set_text(text) + elif not extension_elements: + self.extension_attributes = {XSI_NIL: 'true'} + elif XSI_TYPE in self.extension_attributes: + del self.extension_attributes[XSI_TYPE] def __setattr__(self, key, value): if key == "text": -- cgit v1.2.1 From 9d0cc9cdb6c692ff97dc7a6e890aa7b1445f7af1 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 12 Jun 2017 14:58:09 +0300 Subject: Add failing test for filtering attributes Added a test that fails when the friendlyName of the requested attribute is not the same with the name of the internal attribute (even though the OIDs and the internal representation names of the attribute are the same) --- tests/test_20_assertion.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_20_assertion.py b/tests/test_20_assertion.py index ae661d53..b53c9960 100644 --- a/tests/test_20_assertion.py +++ b/tests/test_20_assertion.py @@ -81,6 +81,18 @@ def test_filter_on_attributes_1(): assert ava["serialNumber"] == ["12345"] +def test_filter_on_attributes_2(): + + a = to_dict(Attribute(friendly_name="surName",name="urn:oid:2.5.4.4", + name_format=NAME_FORMAT_URI), ONTS) + required = [a] + ava = {"sn":["kakavas"]} + + ava = filter_on_attributes(ava,required,acs=ac_factory()) + assert list(ava.keys()) == ['sn'] + assert ava["sn"] == ["kakavas"] + + def test_filter_on_attributes_without_friendly_name(): ava = {"eduPersonTargetedID": "test@example.com", "eduPersonAffiliation": "test", @@ -923,3 +935,4 @@ def test_assertion_with_authn_instant(): if __name__ == "__main__": test_assertion_2() + -- cgit v1.2.1 From c24b47cebacb89191c1cea29e1dd21964398e91d Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 12 Jun 2017 16:57:28 +0300 Subject: Base attribute filtering on internal representation names Instead on relying on the FriendlyName from metadatata, use the name of the internal representation of an attribute in order to perform filtering. Resolves #422 --- src/saml2/assertion.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 64944d11..a9e3372a 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -79,10 +79,15 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None, def _match_attr_name(attr, ava): try: - friendly_name = attr["friendly_name"] - except KeyError: - friendly_name = get_local_name(acs, attr["name"], + friendly_name = get_local_name(acs, attr["name"], attr["name_format"]) + except: + friendly_name = None + if not friendly_name: + try: + friendly_name = attr["friendly_name"] + except KeyError: + pass _fn = _match(friendly_name, ava) if not _fn: # In the unlikely case that someone has provided us with @@ -91,6 +96,7 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None, return _fn + def _apply_attr_value_restrictions(attr, res, must=False): try: values = [av["text"] for av in attr["attribute_value"]] @@ -105,7 +111,6 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None, return _filter_values(ava[_fn], values, must) res = {} - if required is None: required = [] -- cgit v1.2.1 From 695e2f0a98d4df6f690e12226283577920e5406f Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 13 Jun 2017 12:04:51 +0300 Subject: Updated test cases As explained in https://github.com/rohe/pysaml2/pull/423#issuecomment-308053607 , ava cannot contain an 'surName' key, it should be named 'sn' --- tests/test_50_server.py | 26 +++++++++++++------------- tests/test_51_client.py | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/test_50_server.py b/tests/test_50_server.py index 4aa834c5..f0dcae3c 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -96,7 +96,7 @@ class TestServer1(): self.client = client.Saml2Client(conf) self.name_id = self.server.ident.transient_nameid( "urn:mace:example.com:saml:roland:sp", "id12") - self.ava = {"givenName": ["Derek"], "surName": ["Jeter"], + self.ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": "The man"} def teardown_class(self): @@ -110,7 +110,7 @@ class TestServer1(): assert ava ==\ {'mail': ['derek@nyy.mlb.com'], 'givenName': ['Derek'], - 'surName': ['Jeter'], 'title': ['The man']} + 'sn': ['Jeter'], 'title': ['The man']} def verify_encrypted_assertion(self, assertion, decr_text): @@ -145,7 +145,7 @@ class TestServer1(): format=saml.NAMEID_FORMAT_TRANSIENT)), attribute_statement=do_attribute_statement( { - ("", "", "surName"): ("Jeter", ""), + ("", "", "sn"): ("Jeter", ""), ("", "", "givenName"): ("Derek", ""), } ), @@ -164,12 +164,12 @@ class TestServer1(): attr1 = attribute_statement.attribute[1] if attr0.attribute_value[0].text == "Derek": assert attr0.friendly_name == "givenName" - assert attr1.friendly_name == "surName" + assert attr1.friendly_name == "sn" assert attr1.attribute_value[0].text == "Jeter" else: assert attr1.friendly_name == "givenName" assert attr1.attribute_value[0].text == "Derek" - assert attr0.friendly_name == "surName" + assert attr0.friendly_name == "sn" assert attr0.attribute_value[0].text == "Jeter" # subject = assertion.subject @@ -187,7 +187,7 @@ class TestServer1(): name_id=saml.NAMEID_FORMAT_TRANSIENT), attribute_statement=do_attribute_statement( { - ("", "", "surName"): ("Jeter", ""), + ("", "", "sn"): ("Jeter", ""), ("", "", "givenName"): ("Derek", ""), } ), @@ -277,7 +277,7 @@ class TestServer1(): resp = self.server.create_authn_response( { "eduPersonEntitlement": "Short stop", - "surName": "Jeter", + "sn": "Jeter", "givenName": "Derek", "mail": "derek.jeter@nyy.mlb.com", "title": "The man" @@ -394,7 +394,7 @@ class TestServer1(): conf.load_file("server_conf") self.client = client.Saml2Client(conf) - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": "The man"} npolicy = samlp.NameIDPolicy(format=saml.NAMEID_FORMAT_TRANSIENT, @@ -425,7 +425,7 @@ class TestServer1(): def test_signed_response(self): name_id = self.server.ident.transient_nameid( "urn:mace:example.com:saml:roland:sp", "id12") - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": "The man"} signed_resp = self.server.create_authn_response( @@ -1139,7 +1139,7 @@ class TestServer1(): "not_on_or_after": soon, "user": { "givenName": "Leo", - "surName": "Laport", + "sn": "Laport", } } self.client.users.add_information_about_person(sinfo) @@ -1163,7 +1163,7 @@ class TestServer1(): "not_on_or_after": soon, "user": { "givenName": "Leo", - "surName": "Laport", + "sn": "Laport", } } @@ -1188,7 +1188,7 @@ class TestServer1(): #------------------------------------------------------------------------ IDENTITY = {"eduPersonAffiliation": ["staff", "member"], - "surName": ["Jeter"], "givenName": ["Derek"], + "sn": ["Jeter"], "givenName": ["Derek"], "mail": ["foo@gmail.com"], "title": "The man"} @@ -1234,7 +1234,7 @@ def _logout_request(conf_file): "not_on_or_after": soon, "user": { "givenName": "Leo", - "surName": "Laport", + "sn": "Laport", } } sp.users.add_information_about_person(sinfo) diff --git a/tests/test_51_client.py b/tests/test_51_client.py index 7e42045b..1806de41 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -366,7 +366,7 @@ class TestClient: def test_response_1(self): IDP = "urn:mace:example.com:saml:roland:idp" - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": ["The man"]} nameid_policy = samlp.NameIDPolicy(allow_create="false", @@ -414,7 +414,7 @@ class TestClient: # --- authenticate another person - ava = {"givenName": ["Alfonson"], "surName": ["Soriano"], + ava = {"givenName": ["Alfonson"], "sn": ["Soriano"], "mail": ["alfonson@chc.mlb.com"], "title": ["outfielder"]} resp_str = "%s" % self.server.create_authn_response( @@ -732,7 +732,7 @@ class TestClient: def setup_verify_authn_response(self): idp = "urn:mace:example.com:saml:roland:idp" - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": ["The man"]} ava_verify = {'mail': ['derek@nyy.mlb.com'], 'givenName': ['Derek'], 'sn': ['Jeter'], 'title': ["The man"]} @@ -781,7 +781,7 @@ class TestClient: format=saml.NAMEID_FORMAT_TRANSIENT)), attribute_statement=do_attribute_statement( { - ("", "", "surName"): ("Jeter", ""), + ("", "", "sn"): ("Jeter", ""), ("", "", "givenName"): ("Derek", ""), } ), @@ -845,7 +845,7 @@ class TestClient: nameid_policy = samlp.NameIDPolicy(allow_create="false", format=saml.NAMEID_FORMAT_PERSISTENT) - asser = Assertion({"givenName": "Derek", "surName": "Jeter"}) + asser = Assertion({"givenName": "Derek", "sn": "Jeter"}) farg = add_path( {}, ['assertion', 'subject', 'subject_confirmation', 'method', @@ -916,7 +916,7 @@ class TestClient: nameid_policy = samlp.NameIDPolicy(allow_create="false", format=saml.NAMEID_FORMAT_PERSISTENT) - asser = Assertion({"givenName": "Derek", "surName": "Jeter"}) + asser = Assertion({"givenName": "Derek", "sn": "Jeter"}) subject_confirmation_specs = { 'recipient': "http://lingon.catalogix.se:8087/", @@ -1047,7 +1047,7 @@ class TestClient: name_id=name_id, farg=farg['assertion']) - asser_2 = Assertion({"surName": "Jeter"}) + asser_2 = Assertion({"sn": "Jeter"}) assertion_2 = asser_2.construct( self.client.config.entityid, @@ -1333,7 +1333,7 @@ class TestClient: "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" } } @@ -1370,7 +1370,7 @@ class TestClient: "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" }, "session_index": SessionIndex("_foo") @@ -1400,7 +1400,7 @@ class TestClient: "not_on_or_after": a_while_ago(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" }, "session_index": SessionIndex("_foo") @@ -1493,7 +1493,7 @@ class TestClientWithDummy(): "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" } } -- cgit v1.2.1 From 0a6df49f1a1cdd2c9379cd086f0df3db7c46d06e Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Mon, 26 Jun 2017 13:49:09 -0400 Subject: Fix dsa-sha1 signature URLs. --- src/saml2/algsupport.py | 2 +- src/saml2/xmldsig/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/saml2/algsupport.py b/src/saml2/algsupport.py index f9bc06b8..72036b40 100644 --- a/src/saml2/algsupport.py +++ b/src/saml2/algsupport.py @@ -23,7 +23,7 @@ SIGNING_METHODS = { "rsa-sha256": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', "rsa-sha384": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384', "rsa-sha512": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512', - "dsa-sha1": 'http,//www.w3.org/2000/09/xmldsig#dsa-sha1', + "dsa-sha1": 'http://www.w3.org/2000/09/xmldsig#dsa-sha1', 'dsa-sha256': 'http://www.w3.org/2009/xmldsig11#dsa-sha256', 'ecdsa_sha1': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha1', 'ecdsa_sha224': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha224', diff --git a/src/saml2/xmldsig/__init__.py b/src/saml2/xmldsig/__init__.py index e00f199d..144cdf54 100644 --- a/src/saml2/xmldsig/__init__.py +++ b/src/saml2/xmldsig/__init__.py @@ -29,7 +29,7 @@ DIGEST_ALLOWED_ALG = (('DIGEST_SHA1', DIGEST_SHA1), ('DIGEST_RIPEMD160', DIGEST_RIPEMD160)) DIGEST_AVAIL_ALG = DIGEST_ALLOWED_ALG + (('DIGEST_MD5', DIGEST_MD5),) -SIG_DSA_SHA1 = 'http,//www.w3.org/2000/09/xmldsig#dsa-sha1' +SIG_DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' SIG_DSA_SHA256 = 'http://www.w3.org/2009/xmldsig11#dsa-sha256' SIG_ECDSA_SHA1 = 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha1' SIG_ECDSA_SHA224 = 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha224' -- cgit v1.2.1 From 7368a49b9114153ba53c28e54d594fe49e5de310 Mon Sep 17 00:00:00 2001 From: Bogdan Despotov Date: Mon, 3 Jul 2017 14:59:33 +0300 Subject: Using the binary response content of requests in order to avoid the metadata xml being saved with incorrect encoding --- src/saml2/mdstore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index eff75c8b..60cd5a51 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -750,7 +750,7 @@ class MetaDataExtern(InMemoryMetaData): """ response = self.http.send(self.url) if response.status_code == 200: - _txt = response.text.encode("utf-8") + _txt = response.content return self.parse_and_check_signature(_txt) else: logger.info("Response status: %s", response.status_code) -- cgit v1.2.1 From 5bcb6ac24a15c40a74cfd0756a5dfc045ad155cf Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 5 Jul 2017 19:05:22 +0300 Subject: Minor fixes --- src/saml2/assertion.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index a9e3372a..0db4b723 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -78,18 +78,15 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None, """ def _match_attr_name(attr, ava): - try: - friendly_name = get_local_name(acs, attr["name"], - attr["name_format"]) - except: - friendly_name = None - if not friendly_name: + + local_name = get_local_name(acs, attr["name"], attr["name_format"]) + if not local_name: try: - friendly_name = attr["friendly_name"] + local_name = attr["friendly_name"] except KeyError: pass - _fn = _match(friendly_name, ava) + _fn = _match(local_name, ava) if not _fn: # In the unlikely case that someone has provided us with # URIs as attribute names _fn = _match(attr["name"], ava) -- cgit v1.2.1 From f00e0dda06f660dcf8d039ffa2b9c15912fd80c2 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 6 Jul 2017 16:43:43 +0300 Subject: Modified test cases to include acs in the args Since acs can't be None ( it get's a value in __init__() https://github.com/rohe/pysaml2/blob/master/src/saml2/assertion.py#L319) there is no reason to test for it. So we add a default value to acs using ac_factory() before passing it to filter_on_attributes --- tests/test_20_assertion.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_20_assertion.py b/tests/test_20_assertion.py index b53c9960..5fc36f6b 100644 --- a/tests/test_20_assertion.py +++ b/tests/test_20_assertion.py @@ -64,7 +64,7 @@ def test_filter_on_attributes_0(): required = [a] ava = {"serialNumber": ["12345"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] @@ -76,7 +76,7 @@ def test_filter_on_attributes_1(): required = [a] ava = {"serialNumber": ["12345"], "givenName": ["Lars"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] @@ -118,7 +118,7 @@ def test_filter_on_attributes_with_missing_required_attribute(): name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10", name_format=NAME_FORMAT_URI), ONTS) with pytest.raises(MissingValue): - filter_on_attributes(ava, required=[eptid]) + filter_on_attributes(ava, required=[eptid], acs=ac_factory()) def test_filter_on_attributes_with_missing_optional_attribute(): @@ -127,7 +127,7 @@ def test_filter_on_attributes_with_missing_optional_attribute(): friendly_name="eduPersonTargetedID", name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10", name_format=NAME_FORMAT_URI), ONTS) - assert filter_on_attributes(ava, optional=[eptid]) == {} + assert filter_on_attributes(ava, optional=[eptid], acs=ac_factory()) == {} # ---------------------------------------------------------------------- @@ -432,7 +432,7 @@ def test_filter_values_req_2(): required = [a1, a2] ava = {"serialNumber": ["12345"], "givenName": ["Lars"]} - raises(MissingValue, filter_on_attributes, ava, required) + raises(MissingValue, filter_on_attributes, ava, required, acs=ac_factory()) def test_filter_values_req_3(): @@ -444,7 +444,7 @@ def test_filter_values_req_3(): required = [a] ava = {"serialNumber": ["12345"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] @@ -458,7 +458,7 @@ def test_filter_values_req_4(): required = [a] ava = {"serialNumber": ["12345"]} - raises(MissingValue, filter_on_attributes, ava, required) + raises(MissingValue, filter_on_attributes, ava, required, acs=ac_factory()) def test_filter_values_req_5(): @@ -470,7 +470,7 @@ def test_filter_values_req_5(): required = [a] ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] @@ -484,7 +484,7 @@ def test_filter_values_req_6(): required = [a] ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["54321"] @@ -501,7 +501,7 @@ def test_filter_values_req_opt_0(): ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, [r], [o]) + ava = filter_on_attributes(ava, [r], [o], acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert _eq(ava["serialNumber"], ["12345", "54321"]) @@ -519,7 +519,7 @@ def test_filter_values_req_opt_1(): ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, [r], [o]) + ava = filter_on_attributes(ava, [r], [o], acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert _eq(ava["serialNumber"], ["12345", "54321"]) @@ -555,7 +555,7 @@ def test_filter_values_req_opt_2(): ava = {"surname": ["Hedberg"], "givenName": ["Roland"], "eduPersonAffiliation": ["staff"], "uid": ["rohe0002"]} - raises(MissingValue, "filter_on_attributes(ava, r, o)") + raises(MissingValue, "filter_on_attributes(ava, r, o, acs=ac_factory())") # --------------------------------------------------------------------------- -- cgit v1.2.1 From 7129d9002305b359596c2b26507a2f3a3c60da33 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 11 Jul 2017 14:53:07 +0300 Subject: Remove allow_unsolicited attribute from config object --- src/saml2/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/saml2/config.py b/src/saml2/config.py index 50d61c57..9b7cd508 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -207,7 +207,6 @@ class Config(object): self.crypto_backend = 'xmlsec1' self.scope = "" self.allow_unknown_attributes = False - self.allow_unsolicited = False self.extension_schema = {} self.cert_handler_extra_class = None self.verify_encrypt_cert_advice = None -- cgit v1.2.1 From b4f8c5478eb750ccd491162e27dcb098ba21ddd8 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 12 Jul 2017 16:09:04 +0300 Subject: Allow testers to specify test Until now the command `tox` would run all of the tests. With this change one can specify a test to be run by running: tox -- path/to/test.py --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d834512d..fcdbdde5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,4 +3,4 @@ envlist = py27,py34 [testenv] deps = -rtests/test_requirements.txt -commands = py.test tests/ +commands = py.test {posargs:tests/} -- cgit v1.2.1 From fe8f11913ff08341b2f924b79bd7266cc8449ea1 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 12 Jul 2017 16:51:24 +0300 Subject: Fix pytest warnings about deprecated pytest_funcarg__ prefix --- tests/conftest.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3a895627..5048394c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,18 @@ import os +import pytest #TODO: On my system this function seems to be returning an incorrect location -def pytest_funcarg__xmlsec(request): +@pytest.fixture +def xmlsec(request): for path in os.environ["PATH"].split(":"): fil = os.path.join(path, "xmlsec1") if os.access(fil,os.X_OK): return fil raise Exception("Can't find xmlsec1") - -def pytest_funcarg__AVA(request): + +@pytest.fixture +def AVA(request): return [ { "surName": ["Jeter"], @@ -27,4 +30,4 @@ def pytest_funcarg__AVA(request): "surName": ["Hedberg"], "givenName": ["Roland"], }, - ] + ] -- cgit v1.2.1 From ee17e8f9b732f5b08f2b94a67ef92ccc33f19b01 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 12 Jul 2017 09:53:52 +0300 Subject: Add force_authn sp configuration option If the value is truthy, "true" is given as the ForceAuthn value. The value is derived from the 'force_authn' keyword argument as passed to 'create_authn_request()' method otherwise it fallbacks to the configuration value. --- src/saml2/client_base.py | 8 ++++++++ src/saml2/config.py | 3 ++- tests/test_31_config.py | 11 +++++++++++ tests/test_51_client.py | 11 +++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index f740cb07..a5957f1d 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -335,6 +335,14 @@ class Base(Entity): except KeyError: nsprefix = None + try: + force_authn = kwargs['force_authn'] + except KeyError: + force_authn = self.config.getattr('force_authn', 'sp') + finally: + if force_authn: + args['force_authn'] = 'true' + if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) diff --git a/src/saml2/config.py b/src/saml2/config.py index 50d61c57..df567117 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -75,7 +75,8 @@ SP_ARGS = [ "name_id_format", "name_id_format_allow_create", "logout_requests_signed", - "requested_attribute_name_format" + "requested_attribute_name_format", + "force_authn", ] AA_IDP_ARGS = [ diff --git a/tests/test_31_config.py b/tests/test_31_config.py index 623c944f..eb8480c6 100644 --- a/tests/test_31_config.py +++ b/tests/test_31_config.py @@ -68,6 +68,7 @@ sp2 = { }, "authn_requests_signed": True, "logout_requests_signed": True, + "force_authn": True, } }, #"xmlsec_binary" : "/opt/local/bin/xmlsec1", @@ -408,5 +409,15 @@ def test_crypto_backend(): sec = security_context(idpc) assert isinstance(sec.crypto, CryptoBackendXMLSecurity) +def test_unset_force_authn(): + cnf = SPConfig().load(sp1) + assert bool(cnf.getattr('force_authn', 'sp')) == False + + +def test_set_force_authn(): + cnf = SPConfig().load(sp2) + assert bool(cnf.getattr('force_authn', 'sp')) == True + + if __name__ == "__main__": test_crypto_backend() diff --git a/tests/test_51_client.py b/tests/test_51_client.py index 1806de41..937e0e20 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -280,6 +280,17 @@ class TestClient: assert nid_policy.allow_create == "false" assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT + def test_create_auth_request_unset_force_authn(self): + req_id, req = self.client.create_authn_request( + "http://www.example.com/sso", sign=False, message_id="id1") + assert bool(req.force_authn) == False + + def test_create_auth_request_set_force_authn(self): + req_id, req = self.client.create_authn_request( + "http://www.example.com/sso", sign=False, message_id="id1", + force_authn="true") + assert bool(req.force_authn) == True + def test_create_auth_request_nameid_policy_allow_create(self): conf = config.SPConfig() conf.load_file("sp_conf_nameidpolicy") -- cgit v1.2.1 From 7ea88803b6a785e0f4ea51463ae85c5fa1df3598 Mon Sep 17 00:00:00 2001 From: Bogdan Despotov Date: Mon, 17 Jul 2017 10:51:53 +0300 Subject: Adding test to expose issue with validating XML signature due to encoding issues --- tests/test_30_mdstore.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py index aadd7726..c4fb6d69 100644 --- a/tests/test_30_mdstore.py +++ b/tests/test_30_mdstore.py @@ -7,12 +7,13 @@ from collections import OrderedDict from future.backports.urllib.parse import quote_plus from saml2.config import Config -from saml2.mdstore import MetadataStore +from saml2.mdstore import MetadataStore, MetaDataExtern from saml2.mdstore import MetaDataMDX from saml2.mdstore import SAML_METADATA_CONTENT_TYPE from saml2.mdstore import destinations from saml2.mdstore import name from saml2 import sigver +from saml2.httpbase import HTTPBase from saml2 import BINDING_SOAP from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_POST @@ -385,6 +386,14 @@ def test_load_local(): assert cfg +def test_load_remote_encoding(): + crypto = sigver._get_xmlsec_cryptobackend() + sc = sigver.SecurityContext(crypto, key_type="", cert_type="") + httpc = HTTPBase() + mds = MetaDataExtern(ATTRCONV, 'http://metadata.aai.switch.ch/metadata.aaitest.xml', sc, '/tmp/SWITCHaaiRootCA.crt.pem', httpc) + mds.load() + + def test_load_string(): sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) mds = MetadataStore(ATTRCONV, sec_config, -- cgit v1.2.1 From 9de2347f231913371b7911d4197d48162a53df32 Mon Sep 17 00:00:00 2001 From: Bogdan Despotov Date: Mon, 17 Jul 2017 13:31:46 +0300 Subject: Added certificate file and referenced it in the corresponding test. Patched MetaDataMDX to avoid same issue there --- src/saml2/mdstore.py | 2 +- tests/SWITCHaaiRootCA.crt.pem | 22 ++++++++++++++++++++++ tests/test_30_mdstore.py | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/SWITCHaaiRootCA.crt.pem diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index 60cd5a51..72825ea8 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -814,7 +814,7 @@ class MetaDataMDX(InMemoryMetaData): response = requests.get(mdx_url, headers={ 'Accept': SAML_METADATA_CONTENT_TYPE}) if response.status_code == 200: - _txt = response.text.encode("utf-8") + _txt = response.content if self.parse_and_check_signature(_txt): return self.entity[item] diff --git a/tests/SWITCHaaiRootCA.crt.pem b/tests/SWITCHaaiRootCA.crt.pem new file mode 100644 index 00000000..66c9e5d0 --- /dev/null +++ b/tests/SWITCHaaiRootCA.crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgINSWITCHaai+Root+CAzANBgkqhkiG9w0BAQUFADBrMQsw +CQYDVQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVu +c3RlIGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFp +IFJvb3QgQ0EwHhcNMDgwNTE1MDYzMDAwWhcNMjgwNTE1MDYyOTU5WjBrMQswCQYD +VQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVuc3Rl +IGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFpIFJv +b3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUSWbn/rhWew/s +LJRyciyRKDGyFXSgiDO/EohYuZLw6EAKLLlhZorNtEHQbbn0Oo13S33MclHMvGWT +KJM0u1hG+6gLy78EPmJbqAE1Uv23wVEH4SX0VJfl3JVqIebiAH/CjuLubgMUspDI +jOdQHNLS7pthTbm7Tgh7zMsiLPyMTZJep5CGbqv8NoK6bMaF0Z+Bt7e1JRlhHFCV +iJJaR/+hfpzLsJ8NWVivvrpRGaGJ1XR+9FGsTkjNdMCirNJJZ6XvUOe5w7pHSd9M +cppFP0eyLs02AMzMXI4iz6PK/w3EdzXGXpK+gSgvLxWYct4xHpv1e2NXhNgdJOSN +9ra/wJLVAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG +MB0GA1UdDgQWBBTpmuIGWOsP14EDXVyXubG1k307hDANBgkqhkiG9w0BAQUFAAOC +AQEAMV/eIW6pFB+mbk7rD7hUPTWDRaoca3kHqmFGFnHfuY8+c0/Mqjh8Y/jyX1yb +f58crTSWrbyGbUZ3oxDGQ34tuZSkmeR32NqryiX3sP5qlNSozVguQKt8o4vhS1Qe +WPsXALs3em2pdKuIGSOpbuDnopPcmU2g5Zi2R5P7qpKDKAKtNUEwV+LW7GBMEksO +Nj7BFXk4AFBFBijaYJGgHmoKSImVgeNIvsV+BSv5HJ4q6vcxfnwuvvGHM0AGphYO +6f5qtHMUgvAblI8M/2QsBgethaGrirtKJ3aCRLdaR2R1QfaGRpck/Ron5/MpMxiJ +wLT8YlW/zjx2yNABhPSAjfzeMw== +-----END CERTIFICATE----- diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py index c4fb6d69..2a79c86a 100644 --- a/tests/test_30_mdstore.py +++ b/tests/test_30_mdstore.py @@ -390,7 +390,7 @@ def test_load_remote_encoding(): crypto = sigver._get_xmlsec_cryptobackend() sc = sigver.SecurityContext(crypto, key_type="", cert_type="") httpc = HTTPBase() - mds = MetaDataExtern(ATTRCONV, 'http://metadata.aai.switch.ch/metadata.aaitest.xml', sc, '/tmp/SWITCHaaiRootCA.crt.pem', httpc) + mds = MetaDataExtern(ATTRCONV, 'http://metadata.aai.switch.ch/metadata.aaitest.xml', sc, full_path('SWITCHaaiRootCA.crt.pem'), httpc) mds.load() -- cgit v1.2.1 From 47cbd128516d5994591e9ae8d3068bf43196f018 Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 14 Jul 2017 11:32:45 +0300 Subject: Add option to hide assertion consumer service on authn requests When 'hide_assertion_consumer_service' is set to 'true', then the AuthnRequest will not include the 'AssertionConsumerServiceURL' and 'ProtocolBinding' attributes. --- src/saml2/client_base.py | 34 +++++++++++++++++++--------------- src/saml2/config.py | 3 ++- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index f740cb07..2a5d45cf 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -235,26 +235,30 @@ class Base(Entity): args = {} - try: - args["assertion_consumer_service_url"] = kwargs[ - "assertion_consumer_service_urls"][0] - del kwargs["assertion_consumer_service_urls"] - except KeyError: + if self.config.getattr('hide_assertion_consumer_service', 'sp'): + args["assertion_consumer_service_url"] = None + binding = None + else: try: args["assertion_consumer_service_url"] = kwargs[ - "assertion_consumer_service_url"] - del kwargs["assertion_consumer_service_url"] + "assertion_consumer_service_urls"][0] + del kwargs["assertion_consumer_service_urls"] except KeyError: try: - args["assertion_consumer_service_index"] = str( - kwargs["assertion_consumer_service_index"]) - del kwargs["assertion_consumer_service_index"] + args["assertion_consumer_service_url"] = kwargs[ + "assertion_consumer_service_url"] + del kwargs["assertion_consumer_service_url"] except KeyError: - if service_url_binding is None: - service_urls = self.service_urls(binding) - else: - service_urls = self.service_urls(service_url_binding) - args["assertion_consumer_service_url"] = service_urls[0] + try: + args["assertion_consumer_service_index"] = str( + kwargs["assertion_consumer_service_index"]) + del kwargs["assertion_consumer_service_index"] + except KeyError: + if service_url_binding is None: + service_urls = self.service_urls(binding) + else: + service_urls = self.service_urls(service_url_binding) + args["assertion_consumer_service_url"] = service_urls[0] try: args["provider_name"] = kwargs["provider_name"] diff --git a/src/saml2/config.py b/src/saml2/config.py index 50d61c57..235bf91e 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -75,7 +75,8 @@ SP_ARGS = [ "name_id_format", "name_id_format_allow_create", "logout_requests_signed", - "requested_attribute_name_format" + "requested_attribute_name_format", + "hide_assertion_consumer_service", ] AA_IDP_ARGS = [ -- cgit v1.2.1 From d267b23196581b995d5cec1b53c26b60ec5a494e Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 20 Jul 2017 11:50:29 +0300 Subject: Change log level for no attribute statements. Resolves #329 --- src/saml2/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saml2/response.py b/src/saml2/response.py index 13323509..5ca75bf1 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -666,7 +666,7 @@ class AuthnResponse(StatusResponse): _attr_statem = _assertion.attribute_statement[0] ava.update(self.read_attribute_statement(_attr_statem)) if not ava: - logger.error("Missing Attribute Statement") + logger.debug("Assertion contains no attribute statements") return ava def _bearer_confirmed(self, data): -- cgit v1.2.1 From 29b586df481421430193a234309f620f2f50dfbd Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 4 Aug 2017 16:03:34 +0300 Subject: attr might not be in _fro. Resolves #441 --- src/saml2/attribute_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saml2/attribute_converter.py b/src/saml2/attribute_converter.py index 3d52a816..3d32d226 100644 --- a/src/saml2/attribute_converter.py +++ b/src/saml2/attribute_converter.py @@ -246,7 +246,7 @@ def get_local_name(acs, attr, name_format): for aconv in acs: #print(ac.format, name_format) if aconv.name_format == name_format: - return aconv._fro[attr] + return aconv._fro.get(attr) def d_to_local_name(acs, attr): -- cgit v1.2.1 From dda8b025e390eb8aa244146dadc2c0a901289636 Mon Sep 17 00:00:00 2001 From: r2h2 Date: Tue, 29 Aug 2017 22:16:29 +0200 Subject: make exception not_before/notonorafter messages more explicit (formatted time, slack as different value) --- src/saml2/validate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/saml2/validate.py b/src/saml2/validate.py index de68fc00..9fe12c4d 100644 --- a/src/saml2/validate.py +++ b/src/saml2/validate.py @@ -3,6 +3,7 @@ from six.moves.urllib.parse import urlparse import re import struct import base64 +import time from saml2 import time_util @@ -42,8 +43,8 @@ NCNAME = re.compile("(?P[a-zA-Z_](\w|[_.-])*)") def valid_ncname(name): match = NCNAME.match(name) - if not match: - raise NotValid("NCName") + #if not match: # hack for invalid authnRequest/ID from meteor saml lib + # raise NotValid("NCName") return True @@ -90,8 +91,10 @@ def validate_on_or_after(not_on_or_after, slack): now = time_util.utc_now() nooa = calendar.timegm(time_util.str_to_time(not_on_or_after)) if now > nooa + slack: + now_str=time.strftime('%Y-%M-%dT%H:%M:%SZ', time.gmtime(now)) raise ResponseLifetimeExceed( - "Can't use it, it's too old %d > %d" % (now - slack, nooa)) + "Can't use repsonse, too old (now=%s + slack=%d > " \ + "not_on_or_after=%s" % (now_str, slack, not_on_or_after)) return nooa else: return False @@ -102,8 +105,9 @@ def validate_before(not_before, slack): now = time_util.utc_now() nbefore = calendar.timegm(time_util.str_to_time(not_before)) if nbefore > now + slack: - raise ToEarly("Can't use it yet %d <= %d" % (now + slack, nbefore)) - + now_str = time.strftime('%Y-%M-%dT%H:%M:%SZ', time.gmtime(now)) + raise ToEarly("Can't use response yet: (now=%s + slack=%d) " + "<= notbefore=%s" % (now_str, slack, not_before)) return True -- cgit v1.2.1 From 1952da2596f733d2aa80f1c9b0f95daa77dfcfd2 Mon Sep 17 00:00:00 2001 From: Scott Koranda Date: Sat, 23 Sep 2017 10:00:33 -0500 Subject: Fix for 459 HTTP_POST form nonconforming and shows submit Fix for issue 459 "Form used with HTTP_POST binding nonconforming and shows submit button". The fix introduces an HTML5 DOCTYPE declaration and uses noscript tags appropriately to hide the submit button when Javascript is enabled. Modification of tests were necessary because the tests unecessarily relied on the response being a list of strings with the
element being the fourth item in the list, in order to unpack the form and pull out the SAMLResponse and relay state for comparison. The new tests do not require the response to be arbitrarily broken up as a list of strings. --- src/saml2/pack.py | 49 ++++++++++++++++++++++++++++++------------- tests/test_51_client.py | 12 +++++------ tests/test_65_authn_query.py | 2 +- tests/test_68_assertion_id.py | 2 +- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/saml2/pack.py b/src/saml2/pack.py index 728a516f..3bf39fc8 100644 --- a/src/saml2/pack.py +++ b/src/saml2/pack.py @@ -40,12 +40,35 @@ except ImportError: import defusedxml.ElementTree NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/" -FORM_SPEC = """ - - - -
""" +FORM_SPEC = """\ + + + + + + + + +
+
+ + + +
+ +
+ +""" def http_form_post_message(message, location, relay_state="", typ="SAMLRequest", **kwargs): @@ -58,8 +81,6 @@ def http_form_post_message(message, location, relay_state="", :param relay_state: for preserving and conveying state information :return: A tuple containing header information and a HTML message. """ - response = ["", """SAML 2.0 POST""", ""] - if not isinstance(message, six.string_types): message = str(message) if not isinstance(message, six.binary_type): @@ -71,17 +92,17 @@ def http_form_post_message(message, location, relay_state="", _msg = message _msg = _msg.decode('ascii') - response.append(FORM_SPEC % (location, typ, _msg, relay_state)) + args = { + 'action' : location, + 'saml_type' : typ, + 'relay_state' : relay_state, + 'saml_response' : _msg + } - response.append("""""") - response.append("") + response = FORM_SPEC.format(**args) return {"headers": [("Content-type", "text/html")], "data": response} - def http_post_message(message, relay_state="", typ="SAMLRequest", **kwargs): """ diff --git a/tests/test_51_client.py b/tests/test_51_client.py index 937e0e20..bcc535af 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -1398,7 +1398,7 @@ class TestClient: binding, info = resp[entity_ids[0]] assert binding == BINDING_HTTP_POST - _dic = unpack_form(info["data"][3]) + _dic = unpack_form(info["data"]) res = self.server.parse_logout_request(_dic["SAMLRequest"], BINDING_HTTP_POST) assert b'_foo' in res.xmlstr @@ -1428,7 +1428,7 @@ class TestClient: binding, info = resp[entity_ids[0]] assert binding == BINDING_HTTP_POST - _dic = unpack_form(info["data"][3]) + _dic = unpack_form(info["data"]) res = self.server.parse_logout_request(_dic["SAMLRequest"], BINDING_HTTP_POST) assert b'_foo' in res.xmlstr @@ -1525,7 +1525,7 @@ class TestClientWithDummy(): sid, http_args = self.client.prepare_for_authenticate( "urn:mace:example.com:saml:roland:idp", relay_state="really", binding=binding, response_binding=response_binding) - _dic = unpack_form(http_args["data"][3]) + _dic = unpack_form(http_args["data"]) req = self.server.parse_authn_request(_dic["SAMLRequest"], binding) resp_args = self.server.response_args(req.message, [response_binding]) @@ -1543,7 +1543,7 @@ class TestClientWithDummy(): response = self.client.send(**http_args) print(response.text) - _dic = unpack_form(response.text[3], "SAMLResponse") + _dic = unpack_form(response.text, "SAMLResponse") resp = self.client.parse_authn_request_response(_dic["SAMLResponse"], BINDING_HTTP_POST, {sid: "/"}) @@ -1558,7 +1558,7 @@ class TestClientWithDummy(): sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate( "urn:mace:example.com:saml:roland:idp", relay_state="really", binding=binding, response_binding=response_binding) - _dic = unpack_form(http_args["data"][3]) + _dic = unpack_form(http_args["data"]) assert binding == auth_binding @@ -1578,7 +1578,7 @@ class TestClientWithDummy(): response = self.client.send(**http_args) print(response.text) - _dic = unpack_form(response.text[3], "SAMLResponse") + _dic = unpack_form(response.text, "SAMLResponse") resp = self.client.parse_authn_request_response(_dic["SAMLResponse"], BINDING_HTTP_POST, {sid: "/"}) diff --git a/tests/test_65_authn_query.py b/tests/test_65_authn_query.py index 54d529f8..68af10a1 100644 --- a/tests/test_65_authn_query.py +++ b/tests/test_65_authn_query.py @@ -28,7 +28,7 @@ def get_msg(hinfo, binding): if binding == BINDING_SOAP: xmlstr = hinfo["data"] elif binding == BINDING_HTTP_POST: - _inp = hinfo["data"][3] + _inp = hinfo["data"] i = _inp.find(TAG1) i += len(TAG1) + 1 j = _inp.find('"', i) diff --git a/tests/test_68_assertion_id.py b/tests/test_68_assertion_id.py index 52959f3a..283b4da6 100644 --- a/tests/test_68_assertion_id.py +++ b/tests/test_68_assertion_id.py @@ -27,7 +27,7 @@ def get_msg(hinfo, binding, response=False): if binding == BINDING_SOAP: msg = hinfo["data"] elif binding == BINDING_HTTP_POST: - _inp = hinfo["data"][3] + _inp = hinfo["data"] i = _inp.find(TAG1) i += len(TAG1) + 1 j = _inp.find('"', i) -- cgit v1.2.1 From 701bdacd43bdc3a0a15a6097b9c0b9ae4064993a Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 14 Jul 2017 17:23:00 +0300 Subject: Add eIDAS namespace and attributes --- src/saml2/attributemaps/saml_uri.py | 17 +++++++ tests/test_19_attribute_converter.py | 95 +++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 24 deletions(-) diff --git a/src/saml2/attributemaps/saml_uri.py b/src/saml2/attributemaps/saml_uri.py index ca6dfd84..e97090ff 100644 --- a/src/saml2/attributemaps/saml_uri.py +++ b/src/saml2/attributemaps/saml_uri.py @@ -13,10 +13,19 @@ SCHAC = 'urn:oid:1.3.6.1.4.1.25178.1.2.' SIS = 'urn:oid:1.2.752.194.10.2.' UMICH = 'urn:oid:1.3.6.1.4.1.250.1.57.' OPENOSI_OID = 'urn:oid:1.3.6.1.4.1.27630.2.1.1.' #openosi-0.82.schema http://www.openosi.org/osi/display/ldap/Home +EIDAS_NATURALPERSON = 'http://eidas.europa.eu/attributes/naturalperson/' MAP = { 'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'fro': { + EIDAS_NATURALPERSON+'PersonIdentifier': 'PersonIdentifier', + EIDAS_NATURALPERSON+'FamilyName': 'FamilyName', + EIDAS_NATURALPERSON+'FirstName': 'FirstName', + EIDAS_NATURALPERSON+'DateOfBirth': 'DateOfBirth', + EIDAS_NATURALPERSON+'BirthName': 'BirthName', + EIDAS_NATURALPERSON+'PlaceOfBirth': 'PlaceOfBirth', + EIDAS_NATURALPERSON+'CurrentAddress': 'CurrentAddress', + EIDAS_NATURALPERSON+'Gender': 'Gender', EDUCOURSE_OID+'1': 'eduCourseOffering', EDUCOURSE_OID+'2': 'eduCourseMember', EDUMEMBER1_OID+'1': 'isMemberOf', @@ -161,6 +170,14 @@ MAP = { X500ATTR_OID+'65': 'pseudonym', }, 'to': { + 'PersonIdentifier': EIDAS_NATURALPERSON+'PersonIdentifier', + 'FamilyName': EIDAS_NATURALPERSON+'FamilyName', + 'FirstName': EIDAS_NATURALPERSON+'FirstName', + 'DateOfBirth': EIDAS_NATURALPERSON+'DateOfBirth', + 'BirthName': EIDAS_NATURALPERSON+'BirthName', + 'PlaceOfBirth': EIDAS_NATURALPERSON+'PlaceOfBirth', + 'CurrentAddress': EIDAS_NATURALPERSON+'CurrentAddress', + 'Gender': EIDAS_NATURALPERSON+'Gender', 'associatedDomain': UCL_DIR_PILOT+'37', 'authorityRevocationList': X500ATTR_OID+'38', 'businessCategory': X500ATTR_OID+'15', diff --git a/tests/test_19_attribute_converter.py b/tests/test_19_attribute_converter.py index 0fa807b7..8662feee 100644 --- a/tests/test_19_attribute_converter.py +++ b/tests/test_19_attribute_converter.py @@ -10,6 +10,7 @@ from saml2.attribute_converter import AttributeConverter from saml2.attribute_converter import to_local from saml2.saml import attribute_from_string, name_id_from_string, NameID, NAMEID_FORMAT_PERSISTENT from saml2.saml import attribute_statement_from_string +import saml2.attributemaps.saml_uri as saml_map def _eq(l1, l2): @@ -139,12 +140,14 @@ class TestAC(): def test_to_local_name_from_unspecified(self): _xml = """ - - foo@bar.com -""" + + foo@bar.com + + + """ attr = attribute_statement_from_string(_xml) ava = attribute_converter.to_local(self.acs, attr) @@ -236,26 +239,70 @@ def test_noop_attribute_conversion(): assert attr.attribute_value[0].text == "Roland" -ava = """ - - - uu.se - -""" +class BuilderAVA(): + def __init__(self, name, friendly_name, name_format): + template = """ + + + uu.se + + + """ + + self.ava = template.format( + attr_name=name, + attr_friendly_name=friendly_name, + attr_name_format=name_format) + + +class TestSchac(): + def test(self): + failures = 0 + friendly_name = "schacHomeOrganization" + ava_schac = BuilderAVA( + "urn:oid:1.3.6.1.4.1.25178.1.2.9", + friendly_name, + saml_map.MAP['identifier']) + + attr = attribute_from_string(ava_schac.ava) + acs = attribute_converter.ac_factory() + + for ac in acs: + try: + res = ac.ava_from(attr) + except KeyError: + failures += 1 + else: + assert res[0] == "schacHomeOrganization" + assert failures != len(acs) -def test_schac(): - attr = attribute_from_string(ava) - acs = attribute_converter.ac_factory() - for ac in acs: - try: - res = ac.ava_from(attr) - assert res[0] == "schacHomeOrganization" - except KeyError: - pass + +class TestEIDAS(): + def test(self): + failures = 0 + friendly_name = 'PersonIdentifier' + ava_eidas = BuilderAVA( + saml_map.EIDAS_NATURALPERSON + friendly_name, + friendly_name, + saml_map.MAP['identifier']) + + attr = attribute_from_string(ava_eidas.ava) + acs = attribute_converter.ac_factory() + + for ac in acs: + try: + res = ac.ava_from(attr) + except KeyError: + failures += 1 + else: + assert res[0] == friendly_name + + assert failures != len(acs) if __name__ == "__main__": -- cgit v1.2.1 From 144248f968603c1483c56fcbbddab0edfd61613f Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 14 Jul 2017 17:23:29 +0300 Subject: Add eIDAS SPType node support --- src/saml2/client_base.py | 10 ++++++++ src/saml2/config.py | 2 ++ src/saml2/extension/sp_type.py | 54 ++++++++++++++++++++++++++++++++++++++++++ src/saml2/metadata.py | 12 +++++++++- tests/sp_mdext_conf.py | 2 ++ tests/test_83_md_extensions.py | 12 +++++++++- tools/data/sp_type.xsd | 16 +++++++++++++ 7 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/saml2/extension/sp_type.py create mode 100644 tools/data/sp_type.xsd diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 50b457d1..88a9bd11 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -18,6 +18,8 @@ from saml2.samlp import NameIDMappingRequest from saml2.samlp import AttributeQuery from saml2.samlp import AuthzDecisionQuery from saml2.samlp import AuthnRequest +from saml2.samlp import Extensions +from saml2.extension import sp_type import saml2 import time @@ -347,6 +349,14 @@ class Base(Entity): if force_authn: args['force_authn'] = 'true' + conf_sp_type = self.config.getattr('sp_type', 'sp') + conf_sp_type_in_md = self.config.getattr('sp_type_in_metadata', 'sp') + if conf_sp_type and conf_sp_type_in_md is False: + if not extensions: + extensions = Extensions() + item = sp_type.SPType(text=conf_sp_type) + extensions.add_extension_element(item) + if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) diff --git a/src/saml2/config.py b/src/saml2/config.py index e508a954..3c8618f4 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -78,6 +78,8 @@ SP_ARGS = [ "requested_attribute_name_format", "hide_assertion_consumer_service", "force_authn", + "sp_type", + "sp_type_in_metadata", ] AA_IDP_ARGS = [ diff --git a/src/saml2/extension/sp_type.py b/src/saml2/extension/sp_type.py new file mode 100644 index 00000000..8ffb2cea --- /dev/null +++ b/src/saml2/extension/sp_type.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +# +# Generated Tue Jul 18 15:03:44 2017 by parse_xsd.py version 0.5. +# + +import saml2 +from saml2 import SamlBase + + +NAMESPACE = 'http://eidas.europa.eu/saml-extensions' + +class SPTypeType_(SamlBase): + """The http://eidas.europa.eu/saml-extensions:SPTypeType element """ + + c_tag = 'SPTypeType' + c_namespace = NAMESPACE + c_value_type = {'base': 'xsd:string', 'enumeration': ['public', 'private']} + c_children = SamlBase.c_children.copy() + c_attributes = SamlBase.c_attributes.copy() + c_child_order = SamlBase.c_child_order[:] + c_cardinality = SamlBase.c_cardinality.copy() + +def sp_type_type__from_string(xml_string): + return saml2.create_class_from_xml_string(SPTypeType_, xml_string) + + +class SPType(SPTypeType_): + """The http://eidas.europa.eu/saml-extensions:SPType element """ + + c_tag = 'SPType' + c_namespace = NAMESPACE + c_children = SPTypeType_.c_children.copy() + c_attributes = SPTypeType_.c_attributes.copy() + c_child_order = SPTypeType_.c_child_order[:] + c_cardinality = SPTypeType_.c_cardinality.copy() + +def sp_type_from_string(xml_string): + return saml2.create_class_from_xml_string(SPType, xml_string) + + +ELEMENT_FROM_STRING = { + SPType.c_tag: sp_type_from_string, + SPTypeType_.c_tag: sp_type_type__from_string, +} + +ELEMENT_BY_TAG = { + 'SPType': SPType, + 'SPTypeType': SPTypeType_, +} + + +def factory(tag, **kwargs): + return ELEMENT_BY_TAG[tag](**kwargs) diff --git a/src/saml2/metadata.py b/src/saml2/metadata.py index 50ec0bae..de2e6e75 100644 --- a/src/saml2/metadata.py +++ b/src/saml2/metadata.py @@ -9,6 +9,7 @@ from saml2.extension import mdui from saml2.extension import idpdisc from saml2.extension import shibmd from saml2.extension import mdattr +from saml2.extension import sp_type from saml2.saml import NAME_FORMAT_URI from saml2.saml import AttributeValue from saml2.saml import Attribute @@ -722,7 +723,8 @@ def entity_descriptor(confd): entd.contact_person = do_contact_person_info(confd.contact_person) if confd.entity_category: - entd.extensions = md.Extensions() + if not entd.extensions: + entd.extensions = md.Extensions() ava = [AttributeValue(text=c) for c in confd.entity_category] attr = Attribute(attribute_value=ava, name="http://macedir.org/entity-category") @@ -734,6 +736,14 @@ def entity_descriptor(confd): entd.extensions = md.Extensions() entd.extensions.add_extension_element(item) + conf_sp_type = confd.getattr('sp_type', 'sp') + conf_sp_type_in_md = confd.getattr('sp_type_in_metadata', 'sp') + if conf_sp_type and conf_sp_type_in_md is True: + if not entd.extensions: + entd.extensions = md.Extensions() + item = sp_type.SPType(text=conf_sp_type) + entd.extensions.add_extension_element(item) + serves = confd.serves if not serves: raise SAMLError( diff --git a/tests/sp_mdext_conf.py b/tests/sp_mdext_conf.py index 67e33414..b1f0cf42 100644 --- a/tests/sp_mdext_conf.py +++ b/tests/sp_mdext_conf.py @@ -6,6 +6,8 @@ CONFIG = { "description": "My own SP", "service": { "sp": { + "sp_type": "public", + "sp_type_in_metadata": True, "endpoints": { "assertion_consumer_service": [ "http://lingon.catalogix.se:8087/"], diff --git a/tests/test_83_md_extensions.py b/tests/test_83_md_extensions.py index 71f98868..dace10a5 100644 --- a/tests/test_83_md_extensions.py +++ b/tests/test_83_md_extensions.py @@ -1,5 +1,6 @@ from saml2.config import Config from saml2.metadata import entity_descriptor +from saml2.extension.sp_type import SPType __author__ = 'roland' @@ -14,4 +15,13 @@ assert ed.spsso_descriptor.extensions assert len(ed.spsso_descriptor.extensions.extension_elements) == 3 assert ed.extensions -assert len(ed.extensions.extension_elements) > 1 \ No newline at end of file +assert len(ed.extensions.extension_elements) > 1 + +assert any(e.tag is SPType.c_tag for e in ed.extensions.extension_elements) + +cnf.setattr('sp', 'sp_type_in_metadata', False) +ed = entity_descriptor(cnf) + +print(ed) + +assert all(e.tag is not SPType.c_tag for e in ed.extensions.extension_elements) diff --git a/tools/data/sp_type.xsd b/tools/data/sp_type.xsd new file mode 100644 index 00000000..dbb1418d --- /dev/null +++ b/tools/data/sp_type.xsd @@ -0,0 +1,16 @@ + + + + + + + + + + -- cgit v1.2.1 From 20c961045cb1e2b7b56b2c7eb72595897d1a1477 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 18 Jul 2017 15:14:28 +0300 Subject: Add eIDAS RequestedAttributes node support --- src/saml2/client_base.py | 56 ++++++++++++ src/saml2/config.py | 1 + src/saml2/extension/requested_attributes.py | 131 ++++++++++++++++++++++++++++ tests/server_conf.py | 13 +++ tests/test_51_client.py | 16 ++++ tools/data/requested_attributes.xsd | 28 ++++++ 6 files changed, 245 insertions(+) create mode 100644 src/saml2/extension/requested_attributes.py create mode 100644 tools/data/requested_attributes.xsd diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 88a9bd11..531ddea5 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -10,6 +10,8 @@ import six from saml2.entity import Entity +import saml2.attributemaps as attributemaps + from saml2.mdstore import destinations from saml2.profile import paos, ecp from saml2.saml import NAMEID_FORMAT_TRANSIENT @@ -20,6 +22,7 @@ from saml2.samlp import AuthzDecisionQuery from saml2.samlp import AuthnRequest from saml2.samlp import Extensions from saml2.extension import sp_type +from saml2.extension import requested_attributes import saml2 import time @@ -357,6 +360,59 @@ class Base(Entity): item = sp_type.SPType(text=conf_sp_type) extensions.add_extension_element(item) + requested_attrs = self.config.getattr('requested_attributes', 'sp') + if requested_attrs: + if not extensions: + extensions = Extensions() + + attributemapsmods = [] + for modname in attributemaps.__all__: + attributemapsmods.append(getattr(attributemaps, modname)) + + items = [] + for attr in requested_attrs: + friendly_name = attr.get('friendly_name') + name = attr.get('name') + name_format = attr.get('name_format') + is_required = str(attr.get('required', False)).lower() + + if not name and not friendly_name: + raise ValueError( + "Missing required attribute: '{}' or '{}'".format( + 'name', 'friendly_name')) + + if not name: + for mod in attributemapsmods: + try: + name = mod.MAP['to'][friendly_name] + except KeyError: + continue + else: + if not name_format: + name_format = mod.MAP['identifier'] + break + + if not friendly_name: + for mod in attributemapsmods: + try: + friendly_name = mod.MAP['fro'][name] + except KeyError: + continue + else: + if not name_format: + name_format = mod.MAP['identifier'] + break + + items.append(requested_attributes.RequestedAttribute( + is_required=is_required, + name_format=name_format, + friendly_name=friendly_name, + name=name)) + + item = requested_attributes.RequestedAttributes( + extension_elements=items) + extensions.add_extension_element(item) + if kwargs: _args, extensions = self._filter_args(AuthnRequest(), extensions, **kwargs) diff --git a/src/saml2/config.py b/src/saml2/config.py index 3c8618f4..296f0e85 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -80,6 +80,7 @@ SP_ARGS = [ "force_authn", "sp_type", "sp_type_in_metadata", + "requested_attributes", ] AA_IDP_ARGS = [ diff --git a/src/saml2/extension/requested_attributes.py b/src/saml2/extension/requested_attributes.py new file mode 100644 index 00000000..3d574f15 --- /dev/null +++ b/src/saml2/extension/requested_attributes.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python + +# +# Generated Tue Jul 18 14:58:29 2017 by parse_xsd.py version 0.5. +# + +import saml2 +from saml2 import SamlBase + +from saml2 import saml + + +NAMESPACE = 'http://eidas.europa.eu/saml-extensions' + +class RequestedAttributeType_(SamlBase): + """The http://eidas.europa.eu/saml-extensions:RequestedAttributeType element """ + + c_tag = 'RequestedAttributeType' + c_namespace = NAMESPACE + c_children = SamlBase.c_children.copy() + c_attributes = SamlBase.c_attributes.copy() + c_child_order = SamlBase.c_child_order[:] + c_cardinality = SamlBase.c_cardinality.copy() + c_children['{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'] = ('attribute_value', [saml.AttributeValue]) + c_cardinality['attribute_value'] = {"min":0} + c_attributes['Name'] = ('name', 'None', True) + c_attributes['NameFormat'] = ('name_format', 'None', True) + c_attributes['FriendlyName'] = ('friendly_name', 'None', False) + c_attributes['isRequired'] = ('is_required', 'None', False) + c_child_order.extend(['attribute_value']) + + def __init__(self, + attribute_value=None, + name=None, + name_format=None, + friendly_name=None, + is_required=None, + text=None, + extension_elements=None, + extension_attributes=None, + ): + SamlBase.__init__(self, + text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + ) + self.attribute_value=attribute_value or [] + self.name=name + self.name_format=name_format + self.friendly_name=friendly_name + self.is_required=is_required + +def requested_attribute_type__from_string(xml_string): + return saml2.create_class_from_xml_string(RequestedAttributeType_, xml_string) + + +class RequestedAttribute(RequestedAttributeType_): + """The http://eidas.europa.eu/saml-extensions:RequestedAttribute element """ + + c_tag = 'RequestedAttribute' + c_namespace = NAMESPACE + c_children = RequestedAttributeType_.c_children.copy() + c_attributes = RequestedAttributeType_.c_attributes.copy() + c_child_order = RequestedAttributeType_.c_child_order[:] + c_cardinality = RequestedAttributeType_.c_cardinality.copy() + +def requested_attribute_from_string(xml_string): + return saml2.create_class_from_xml_string(RequestedAttribute, xml_string) + + +class RequestedAttributesType_(SamlBase): + """The http://eidas.europa.eu/saml-extensions:RequestedAttributesType element """ + + c_tag = 'RequestedAttributesType' + c_namespace = NAMESPACE + c_children = SamlBase.c_children.copy() + c_attributes = SamlBase.c_attributes.copy() + c_child_order = SamlBase.c_child_order[:] + c_cardinality = SamlBase.c_cardinality.copy() + c_children['{http://eidas.europa.eu/saml-extensions}RequestedAttribute'] = ('requested_attribute', [RequestedAttribute]) + c_cardinality['requested_attribute'] = {"min":0} + c_child_order.extend(['requested_attribute']) + + def __init__(self, + requested_attribute=None, + text=None, + extension_elements=None, + extension_attributes=None, + ): + SamlBase.__init__(self, + text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + ) + self.requested_attribute=requested_attribute or [] + +def requested_attributes_type__from_string(xml_string): + return saml2.create_class_from_xml_string(RequestedAttributesType_, xml_string) + + +class RequestedAttributes(RequestedAttributesType_): + """The http://eidas.europa.eu/saml-extensions:RequestedAttributes element """ + + c_tag = 'RequestedAttributes' + c_namespace = NAMESPACE + c_children = RequestedAttributesType_.c_children.copy() + c_attributes = RequestedAttributesType_.c_attributes.copy() + c_child_order = RequestedAttributesType_.c_child_order[:] + c_cardinality = RequestedAttributesType_.c_cardinality.copy() + +def requested_attributes_from_string(xml_string): + return saml2.create_class_from_xml_string(RequestedAttributes, xml_string) + + +ELEMENT_FROM_STRING = { + RequestedAttributes.c_tag: requested_attributes_from_string, + RequestedAttributesType_.c_tag: requested_attributes_type__from_string, + RequestedAttribute.c_tag: requested_attribute_from_string, + RequestedAttributeType_.c_tag: requested_attribute_type__from_string, +} + +ELEMENT_BY_TAG = { + 'RequestedAttributes': RequestedAttributes, + 'RequestedAttributesType': RequestedAttributesType_, + 'RequestedAttribute': RequestedAttribute, + 'RequestedAttributeType': RequestedAttributeType_, +} + + +def factory(tag, **kwargs): + return ELEMENT_BY_TAG[tag](**kwargs) diff --git a/tests/server_conf.py b/tests/server_conf.py index aa34d8f7..4b528119 100644 --- a/tests/server_conf.py +++ b/tests/server_conf.py @@ -14,6 +14,19 @@ CONFIG = { "required_attributes": ["surName", "givenName", "mail"], "optional_attributes": ["title"], "idp": ["urn:mace:example.com:saml:roland:idp"], + "requested_attributes": [ + { + "name": "http://eidas.europa.eu/attributes/naturalperson/DateOfBirth", + "required": False, + }, + { + "friendly_name": "PersonIdentifier", + "required": True, + }, + { + "friendly_name": "PlaceOfBirth", + }, + ], } }, "debug": 1, diff --git a/tests/test_51_client.py b/tests/test_51_client.py index bcc535af..2bd4d7cf 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -22,6 +22,8 @@ from saml2 import samlp from saml2 import sigver from saml2 import s_utils from saml2.assertion import Assertion +from saml2.extension.requested_attributes import RequestedAttributes +from saml2.extension.requested_attributes import RequestedAttribute from saml2.authn_context import INTERNETPROTOCOLPASSWORD from saml2.client import Saml2Client @@ -280,6 +282,20 @@ class TestClient: assert nid_policy.allow_create == "false" assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT + node_requested_attributes = None + for e in ar.extensions.extension_elements: + if e.tag == RequestedAttributes.c_tag: + node_requested_attributes = e + break + assert node_requested_attributes is not None + + for c in node_requested_attributes.children: + assert c.tag == RequestedAttribute.c_tag + assert c.attributes['isRequired'] in ['true', 'false'] + assert c.attributes['Name'] + assert c.attributes['FriendlyName'] + assert c.attributes['NameFormat'] + def test_create_auth_request_unset_force_authn(self): req_id, req = self.client.create_authn_request( "http://www.example.com/sso", sign=False, message_id="id1") diff --git a/tools/data/requested_attributes.xsd b/tools/data/requested_attributes.xsd new file mode 100644 index 00000000..b796f3d3 --- /dev/null +++ b/tools/data/requested_attributes.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.1