diff options
-rw-r--r-- | src/saml2/client.py | 12 | ||||
-rw-r--r-- | src/saml2/client_base.py | 9 | ||||
-rw-r--r-- | src/saml2/entity.py | 20 | ||||
-rw-r--r-- | src/saml2/mdstore.py | 66 | ||||
-rw-r--r-- | tests/test_30_mdstore.py | 17 | ||||
-rw-r--r-- | tests/test_30_mdstore_old.py | 17 | ||||
-rw-r--r-- | tests/test_50_server.py | 24 | ||||
-rw-r--r-- | tests/test_76_metadata_in_mdb.py | 4 |
8 files changed, 109 insertions, 60 deletions
diff --git a/src/saml2/client.py b/src/saml2/client.py index e283420a..60b108ef 100644 --- a/src/saml2/client.py +++ b/src/saml2/client.py @@ -29,7 +29,7 @@ from saml2.client_base import Base from saml2.client_base import SignOnError from saml2.client_base import LogoutError from saml2.client_base import NoServiceDefined -from saml2.mdstore import destinations +from saml2.mdstore import locations import logging @@ -209,7 +209,7 @@ class Saml2Client(Base): logger.debug("No SLO '%s' service", binding) continue - destination = destinations(srvs)[0] + destination = next(locations(srvs), None) logger.info("destination to provider: %s", destination) try: session_info = self.users.get_info_from(name_id, @@ -374,7 +374,7 @@ class Saml2Client(Base): name_qualifier=name_qualifier)) srvs = self.metadata.authz_service(entity_id, BINDING_SOAP) - for dest in destinations(srvs): + for dest in locations(srvs): resp = self._use_soap(dest, "authz_decision_query", action=action, evidence=evidence, resource=resource, subject=subject) @@ -397,7 +397,7 @@ class Saml2Client(Base): _id_refs = [AssertionIDRef(_id) for _id in assertion_ids] - for destination in destinations(srvs): + for destination in locations(srvs): res = self._use_soap(destination, "assertion_id_request", assertion_id_refs=_id_refs, consent=consent, extensions=extensions, sign=sign) @@ -411,7 +411,7 @@ class Saml2Client(Base): srvs = self.metadata.authn_request_service(entity_id, BINDING_SOAP) - for destination in destinations(srvs): + for destination in locations(srvs): resp = self._use_soap(destination, "authn_query", consent=consent, extensions=extensions, sign=sign) if resp: @@ -461,7 +461,7 @@ class Saml2Client(Base): if srvs is []: raise SAMLError("No attribute service support at entity") - destination = destinations(srvs)[0] + destination = next(locations(srvs), None) if binding == BINDING_SOAP: return self._use_soap(destination, "attribute_query", diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 871f3f2c..51a3a574 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -12,7 +12,7 @@ import logging from saml2.entity import Entity -from saml2.mdstore import destinations +from saml2.mdstore import locations from saml2.profile import paos, ecp from saml2.saml import NAMEID_FORMAT_PERSISTENT from saml2.saml import NAMEID_FORMAT_TRANSIENT @@ -212,7 +212,7 @@ class Base(Entity): # verify that it's in the metadata srvs = self.metadata.single_sign_on_service(entityid, binding) if srvs: - return destinations(srvs)[0] + return next(locations(srvs), None) else: logger.info("_sso_location: %s, %s", entityid, binding) raise IdpUnspecified("No IdP to send to given the premises") @@ -224,9 +224,8 @@ class Base(Entity): raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: - srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], - binding) - return destinations(srvs)[0] + srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], binding) + return next(locations(srvs), None) except IndexError: raise IdpUnspecified("No IdP to send to given the premises") diff --git a/src/saml2/entity.py b/src/saml2/entity.py index fad9326a..c9572aef 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -53,7 +53,7 @@ from saml2.samlp import ArtifactResponse from saml2.samlp import Artifact from saml2.samlp import LogoutRequest from saml2.samlp import AttributeQuery -from saml2.mdstore import destinations, response_destinations +from saml2.mdstore import all_locations from saml2 import BINDING_HTTP_POST from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_SOAP @@ -249,8 +249,9 @@ class Entity(HTTPBase): return info - def pick_binding(self, service, bindings=None, descr_type="", request=None, - entity_id="", response=False): + def pick_binding( + self, service, bindings=None, descr_type="", request=None, entity_id="" + ): if request and not entity_id: entity_id = request.issuer.text.strip() @@ -284,10 +285,8 @@ class Entity(HTTPBase): if srv["index"] == _index: return binding, srv["location"] else: - if response: - return binding, response_destinations(srvs)[0] - else: - return binding, destinations(srvs)[0] + destination = next(all_locations(srvs), None) + return binding, destination except UnsupportedBinding: pass @@ -352,10 +351,9 @@ class Entity(HTTPBase): else: descr_type = "spsso" - binding, destination = self.pick_binding(rsrv, bindings, - descr_type=descr_type, - request=message, - response=True) + binding, destination = self.pick_binding( + rsrv, bindings, descr_type=descr_type, request=message + ) info["binding"] = binding info["destination"] = destination diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index 41e521ec..3dfd0e5a 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -5,6 +5,8 @@ import json import logging import os import sys +from itertools import chain +from warnings import warn as _warn from hashlib import sha1 from os.path import isfile @@ -26,7 +28,11 @@ from saml2 import BINDING_SOAP from saml2.httpbase import HTTPBase from saml2.extension.idpdisc import BINDING_DISCO from saml2.extension.idpdisc import DiscoveryResponse +from saml2.md import NAMESPACE as NS_MD from saml2.md import EntitiesDescriptor +from saml2.md import ArtifactResolutionService +from saml2.md import NameIDMappingService +from saml2.md import SingleSignOnService from saml2.mdie import to_dict from saml2.s_utils import UnsupportedBinding from saml2.s_utils import UnknownSystemEntity @@ -70,6 +76,9 @@ classnames = { ns=NS_MDUI, tag=PrivacyStatementURL.c_tag ), "mdui_uiinfo_logo": "{ns}&{tag}".format(ns=NS_MDUI, tag=Logo.c_tag), + "service_artifact_resolution": "{ns}&{tag}".format(ns=NS_MD, tag=ArtifactResolutionService.c_tag), + "service_single_sign_on": "{ns}&{tag}".format(ns=NS_MD, tag=SingleSignOnService.c_tag), + "service_nameid_mapping": "{ns}&{tag}".format(ns=NS_MD, tag=NameIDMappingService.c_tag), } ENTITY_CATEGORY = "http://macedir.org/entity-category" @@ -79,8 +88,6 @@ ASSURANCE_CERTIFICATION = "urn:oasis:names:tc:SAML:attribute:assurance-certifica SAML_METADATA_CONTENT_TYPE = "application/samlmetadata+xml" DEFAULT_FRESHNESS_PERIOD = "P0Y0M0DT12H0M0S" - - REQ2SRV = { # IDP "authn_request": "single_sign_on_service", @@ -149,17 +156,54 @@ def metadata_modules(): return _res -def response_destinations(srvs): - _res = [] - for s in srvs: - if "response_location" in s: - _res.append(s["response_location"]) - else: - _res.append(s["location"]) - return _res +def response_locations(srvs): + """ + Return the ResponseLocation attributes mapped to the services. + + ArtifactResolutionService, SingleSignOnService and NameIDMappingService MUST omit + the ResponseLocation attribute. This is enforced here, but metadata with such + service declarations and such attributes should not have been part of the metadata + store in the first place. + """ + values = ( + s["response_location"] + for s in srvs + if "response_location" in s + if s["__class__"] not in [ + classnames["service_artifact_resolution"], + classnames["service_single_sign_on"], + classnames["service_nameid_mapping"], + ] + ) + return values + + +def locations(srvs): + values = ( + s["location"] + for s in srvs + if "location" in s + ) + return values + def destinations(srvs): - return [s["location"] for s in srvs] + warn_msg = ( + "`saml2.mdstore.destinations` function is deprecated; " + "instead, use `saml2.mdstore.locations` or `saml2.mdstore.all_locations`." + ) + logger.warning(warn_msg) + _warn(warn_msg) + values = list(locations(srvs)) + return values + + +def all_locations(srvs): + values = chain( + response_locations(srvs), + locations(srvs), + ) + return values def attribute_requirement(entity, index=None): diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py index 59dc7da6..d712383f 100644 --- a/tests/test_30_mdstore.py +++ b/tests/test_30_mdstore.py @@ -15,7 +15,7 @@ from saml2.config import Config 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 locations from saml2.mdstore import name from saml2 import sigver from saml2.httpbase import HTTPBase @@ -177,8 +177,9 @@ def test_swami_1(): assert idps.keys() idpsso = mds.single_sign_on_service(UMU_IDP) assert len(idpsso) == 1 - assert destinations(idpsso) == [ - 'https://idp.umu.se/saml2/idp/SSOService.php'] + assert list(locations(idpsso)) == [ + 'https://idp.umu.se/saml2/idp/SSOService.php' + ] _name = name(mds[UMU_IDP]) assert _name == u'Umeå University (SAML2)' @@ -219,8 +220,9 @@ def test_incommon_1(): idpsso = mds.single_sign_on_service('urn:mace:incommon:alaska.edu') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO' + ] sps = mds.with_descriptor("spsso") @@ -279,8 +281,9 @@ def test_switch_1(): 'https://aai-demo-idp.switch.ch/idp/shibboleth') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO' + ] assert len(idps) > 30 aas = mds.with_descriptor("attribute_authority") print(aas.keys()) diff --git a/tests/test_30_mdstore_old.py b/tests/test_30_mdstore_old.py index 7ceb6653..d9f400ed 100644 --- a/tests/test_30_mdstore_old.py +++ b/tests/test_30_mdstore_old.py @@ -6,7 +6,7 @@ import os from unittest.mock import patch from saml2.mdstore import MetadataStore, MetaDataMDX -from saml2.mdstore import destinations +from saml2.mdstore import locations from saml2.mdstore import name from saml2 import md @@ -145,8 +145,9 @@ def test_swami_1(): assert idps.keys() idpsso = mds.single_sign_on_service(UMU_IDP) assert len(idpsso) == 1 - assert destinations(idpsso) == [ - 'https://idp.umu.se/saml2/idp/SSOService.php'] + assert list(locations(idpsso)) == [ + 'https://idp.umu.se/saml2/idp/SSOService.php' + ] _name = name(mds[UMU_IDP]) assert _name == u'Umeå University (SAML2)' @@ -187,8 +188,9 @@ def test_incommon_1(): idpsso = mds.single_sign_on_service('urn:mace:incommon:alaska.edu') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO' + ] sps = mds.with_descriptor("spsso") @@ -247,8 +249,9 @@ def test_switch_1(): 'https://aai-demo-idp.switch.ch/idp/shibboleth') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO' + ] assert len(idps) > 30 aas = mds.with_descriptor("attribute_authority") print(aas.keys()) diff --git a/tests/test_50_server.py b/tests/test_50_server.py index 93fbc6c6..2aee6d8a 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -2297,19 +2297,19 @@ def _logout_request(conf_file): class TestServerLogout(): - def test_1(self): with closing(Server("idp_slo_redirect_conf")) as server: req_id, request = _logout_request("sp_slo_redirect_conf") print(request) bindings = [BINDING_HTTP_REDIRECT] response = server.create_logout_response(request, bindings) - binding, destination = server.pick_binding("single_logout_service", - bindings, "spsso", - request, response=True) - http_args = server.apply_binding(binding, "%s" % response, destination, - "relay_state", response=True) + binding, destination = server.pick_binding( + "single_logout_service", bindings, "spsso", request + ) + http_args = server.apply_binding( + binding, "%s" % response, destination, "relay_state", response=True + ) assert len(http_args) == 4 assert http_args["headers"][0][0] == "Location" @@ -2322,18 +2322,20 @@ class TestServerLogout(): print(request) bindings = [BINDING_HTTP_POST] response = server.create_logout_response(request, bindings) - binding, destination = server.pick_binding("single_logout_service", - bindings, "spsso", - request, response=True) - http_args = server.apply_binding(binding, "%s" % response, destination, - "relay_state", response=True) + binding, destination = server.pick_binding( + "single_logout_service", bindings, "spsso", request + ) + http_args = server.apply_binding( + binding, "%s" % response, destination, "relay_state", response=True + ) assert len(http_args) == 4 assert len(http_args["data"]) > 0 assert http_args["method"] == "POST" assert http_args['url'] == 'http://lingon.catalogix.se:8087/slo' + if __name__ == "__main__": ts = TestServer1() ts.setup_class() diff --git a/tests/test_76_metadata_in_mdb.py b/tests/test_76_metadata_in_mdb.py index f1376b17..60c1ff1c 100644 --- a/tests/test_76_metadata_in_mdb.py +++ b/tests/test_76_metadata_in_mdb.py @@ -5,7 +5,7 @@ from saml2.attribute_converter import ac_factory from saml2.mongo_store import export_mdstore_to_mongo_db from saml2.mongo_store import MetadataMDB from saml2.mdstore import MetadataStore -from saml2.mdstore import destinations +from saml2.mdstore import locations from saml2.mdstore import name from saml2 import config from pathutils import full_path @@ -46,7 +46,7 @@ def test_metadata(): assert idps.keys() idpsso = mds.single_sign_on_service(umu_idp) assert len(idpsso) == 1 - assert destinations(idpsso) == [ + assert list(locations(idpsso)) == [ 'https://idp.umu.se/saml2/idp/SSOService.php'] _name = name(mds[umu_idp]) |