diff --git a/doc/README.md b/doc/README.md index cfdf02e06..1063445a7 100644 --- a/doc/README.md +++ b/doc/README.md @@ -151,6 +151,7 @@ Common configuration parameters: | `attribute_profile` | string | `saml` | attribute profile to use for mapping attributes from/to response | `entityid_endpoint` | bool | `true` | whether `entityid` should be used as a URL that serves the metadata xml document | `acr_mapping` | dict | `None` | custom Authentication Context Class Reference +| `metadata_endpoint` | string | `my/metadata/endpoint.xml` | metadata endpoint, used for serving the metadata xml at a path of your choice. This config option acts independently from `entityid_endpoint`. | The metadata could be loaded in multiple ways in the table above it's loaded from a static file by using the key "local". It's also possible to load read the metadata from a remote URL. diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index e74b00f07..ea73955e8 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -452,11 +452,7 @@ def register_endpoints(self): url_map.append( ("^%s$" % parsed_endp.path[1:], self.disco_response)) - if self.expose_entityid_endpoint(): - parsed_entity_id = urlparse(self.sp.config.entityid) - url_map.append(("^{0}".format(parsed_entity_id.path[1:]), - self._metadata_endpoint)) - + self.populate_entityid_urls(url_map, self.sp.config.entityid, self._metadata_endpoint) return url_map def get_metadata_desc(self): diff --git a/src/satosa/base.py b/src/satosa/base.py index a5724fdf5..67b347f0f 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -4,6 +4,7 @@ import json import logging import uuid +from urllib.parse import urlparse import warnings as _warnings from saml2.s_utils import UnknownSystemEntity @@ -306,6 +307,7 @@ def run(self, context): class SAMLBaseModule(object): KEY_ENTITYID_ENDPOINT = 'entityid_endpoint' + KEY_METADATA_ENDPOINT = 'metadata_endpoint' KEY_ATTRIBUTE_PROFILE = 'attribute_profile' KEY_ACR_MAPPING = 'acr_mapping' VALUE_ATTRIBUTE_PROFILE_DEFAULT = 'saml' @@ -321,6 +323,26 @@ def expose_entityid_endpoint(self): value = self.config.get(self.KEY_ENTITYID_ENDPOINT, False) return bool(value) + def metadata_endpoint(self): + value = self.config.get(self.KEY_METADATA_ENDPOINT, '') + return str(value) + + def populate_entityid_urls(self, url_map, entity_id, metadata_endpoint_fct): + """ + Populate the endpoints that return the metadata + + :param url_map: A list of tuples from endpoint to a metadata endpoint function + :param entity_id: The entity id defined in the config + :param metadata_endpoint_fct: The function that handles the metadata endpoint + """ + if self.expose_entityid_endpoint(): + parsed_entity_id = urlparse(entity_id) + url_map.append(("^{0}".format(parsed_entity_id.path[1:]), + metadata_endpoint_fct)) + metadata_endpoint = self.metadata_endpoint() + if metadata_endpoint: + url_map.append(("^{0}".format(metadata_endpoint), + metadata_endpoint_fct)) class SAMLEIDASBaseModule(SAMLBaseModule): VALUE_ATTRIBUTE_PROFILE_DEFAULT = 'eidas' diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index d866c3dd1..c97c1c589 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -458,12 +458,7 @@ def _register_endpoints(self, providers): parsed_endp = urlparse(endp) url_map.append(("(%s)/%s$" % (valid_providers, parsed_endp.path), functools.partial(self.handle_authn_request, binding_in=binding))) - - if self.expose_entityid_endpoint(): - parsed_entity_id = urlparse(self.idp.config.entityid) - url_map.append(("^{0}".format(parsed_entity_id.path[1:]), - self._metadata_endpoint)) - + self.populate_entityid_urls(url_map, self.idp.config.entityid, self._metadata_endpoint) return url_map def _set_common_domain_cookie(self, internal_response, http_args, context): diff --git a/src/satosa/routing.py b/src/satosa/routing.py index 317b047f9..9c6122c1c 100644 --- a/src/satosa/routing.py +++ b/src/satosa/routing.py @@ -3,6 +3,7 @@ """ import logging import re +import pprint from satosa.context import SATOSABadContextError from satosa.exception import SATOSAError @@ -38,6 +39,9 @@ class UnknownEndpoint(ValueError): and handles the internal routing between frontends and backends. """ + def __format_endpoint_urls(self, endpoints): + return pprint.pformat([{i : v['endpoints']} for i, v in endpoints.items()]) + def __init__(self, frontends, backends, micro_services): """ :type frontends: dict[str, satosa.frontends.base.FrontendModule] @@ -68,8 +72,8 @@ def __init__(self, frontends, backends, micro_services): else: self.micro_services = {} - logger.debug("Loaded backends with endpoints: {}".format(backends)) - logger.debug("Loaded frontends with endpoints: {}".format(frontends)) + logger.debug("Loaded backends with endpoints: {}".format(self.__format_endpoint_urls(self.backends))) + logger.debug("Loaded frontends with endpoints: {}".format(self.__format_endpoint_urls(self.frontends))) logger.debug("Loaded micro services with endpoints: {}".format(micro_services)) def backend_routing(self, context): diff --git a/tests/satosa/frontends/test_saml2.py b/tests/satosa/frontends/test_saml2.py index 3e89fd2fa..3b7f5ad5e 100644 --- a/tests/satosa/frontends/test_saml2.py +++ b/tests/satosa/frontends/test_saml2.py @@ -142,6 +142,26 @@ def get_path_from_url(url): for endp in all_idp_endpoints: assert any(p.match(endp) for p in compiled_regex) + def test_register_endpoints_metadata_endpoint_mapping(self, idp_conf): + """ + Tests the method register_endpoints when a metadata_endpoint is defined + """ + metadata_endpoint = "potato/test" + def get_path_from_url(url): + return urlparse(url).path.lstrip("/") + + config = {"idp_config": idp_conf, "endpoints": ENDPOINTS, + "metadata_endpoint": metadata_endpoint, "entityid_endpoint": True } + + base_url = self.construct_base_url_from_entity_id(idp_conf["entityid"]) + samlfrontend = SAMLFrontend(lambda context, internal_req: (context, internal_req), + INTERNAL_ATTRIBUTES, config, base_url, "saml_frontend") + + providers = ["foo", "bar"] + url_map = samlfrontend.register_endpoints(providers) + compiled_regex = [re.compile(regex) for regex, _ in url_map] + assert any(p.match(metadata_endpoint) for p in compiled_regex) + def test_handle_authn_request(self, context, idp_conf, sp_conf, internal_response): samlfrontend = self.setup_for_authn_req(context, idp_conf, sp_conf) _, internal_req = samlfrontend.handle_authn_request(context, BINDING_HTTP_REDIRECT)