Skip to content

Commit 862c400

Browse files
committed
ModuleRouter: support paths in BASE
If Satosa is installed under a path which is not the root of the webserver (ie. "https://example.com/satosa"), then endpoint routing must take the base path into consideration. Some modules registered some of their endpoints with the base path included, but other times the base path was omitted, thus it made the routing fail. Now all endpoint registrations include the base path in their endpoint map. Additionally, DEBUG logging was configured for the tests so that the debug logs are accessible during testing.
1 parent 6a4a83b commit 862c400

File tree

14 files changed

+127
-45
lines changed

14 files changed

+127
-45
lines changed

src/satosa/backends/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __init__(self, auth_callback_func, internal_attributes, base_url, name):
2929
self.auth_callback_func = auth_callback_func
3030
self.internal_attributes = internal_attributes
3131
self.converter = AttributeMapper(internal_attributes)
32-
self.base_url = base_url
32+
self.base_url = base_url.rstrip("/") if base_url else ""
3333
self.name = name
3434

3535
def start_auth(self, context, internal_request):

src/satosa/base.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import uuid
77

88
from saml2.s_utils import UnknownSystemEntity
9+
from urllib.parse import urlparse
910

1011
from satosa import util
1112
from .context import Context
@@ -39,6 +40,8 @@ def __init__(self, config):
3940
"""
4041
self.config = config
4142

43+
base_path = urlparse(self.config["BASE"]).path.lstrip("/")
44+
4245
logger.info("Loading backend modules...")
4346
backends = load_backends(self.config, self._auth_resp_callback_func,
4447
self.config["INTERNAL_ATTRIBUTES"])
@@ -64,8 +67,10 @@ def __init__(self, config):
6467
self.config["BASE"]))
6568
self._link_micro_services(self.response_micro_services, self._auth_resp_finish)
6669

67-
self.module_router = ModuleRouter(frontends, backends,
68-
self.request_micro_services + self.response_micro_services)
70+
self.module_router = ModuleRouter(frontends,
71+
backends,
72+
self.request_micro_services + self.response_micro_services,
73+
base_path)
6974

7075
def _link_micro_services(self, micro_services, finisher):
7176
if not micro_services:

src/satosa/context.py

-4
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,6 @@ def path(self, p):
7676
raise ValueError("path can't start with '/'")
7777
self._path = p
7878

79-
def target_entity_id_from_path(self):
80-
target_entity_id = self.path.split("/")[1]
81-
return target_entity_id
82-
8379
def decorate(self, key, value):
8480
"""
8581
Add information to the context

src/satosa/frontends/base.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"""
44
from ..attribute_mapping import AttributeMapper
55

6+
import os.path
7+
from urllib.parse import urlparse
8+
69

710
class FrontendModule(object):
811
"""
@@ -23,8 +26,10 @@ def __init__(self, auth_req_callback_func, internal_attributes, base_url, name):
2326
self.auth_req_callback_func = auth_req_callback_func
2427
self.internal_attributes = internal_attributes
2528
self.converter = AttributeMapper(internal_attributes)
26-
self.base_url = base_url
29+
self.base_url = base_url.rstrip("/") if base_url else ""
2730
self.name = name
31+
self.endpoint_baseurl = os.path.join(self.base_url, self.name)
32+
self.endpoint_basepath = urlparse(self.endpoint_baseurl).path.lstrip("/")
2833

2934
def handle_authn_response(self, context, internal_resp):
3035
"""

src/satosa/frontends/openid_connect.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ def __init__(self, auth_req_callback_func, internal_attributes, conf, base_url,
8989
else:
9090
cdb = {}
9191

92-
self.endpoint_baseurl = "{}/{}".format(self.base_url, self.name)
9392
self.provider = _create_provider(
9493
provider_config,
9594
self.endpoint_baseurl,
@@ -165,6 +164,9 @@ def register_endpoints(self, backend_names):
165164
:rtype: list[(str, ((satosa.context.Context, Any) -> satosa.response.Response, Any))]
166165
:raise ValueError: if more than one backend is configured
167166
"""
167+
provider_config = ("^.well-known/openid-configuration$", self.provider_config)
168+
jwks_uri = ("^{}/jwks$".format(self.endpoint_basepath), self.jwks)
169+
168170
backend_name = None
169171
if len(backend_names) != 1:
170172
# only supports one backend since there currently is no way to publish multiple authorization endpoints
@@ -181,16 +183,13 @@ def register_endpoints(self, backend_names):
181183
else:
182184
backend_name = backend_names[0]
183185

184-
provider_config = ("^.well-known/openid-configuration$", self.provider_config)
185-
jwks_uri = ("^{}/jwks$".format(self.name), self.jwks)
186-
187186
if backend_name:
188187
# if there is only one backend, include its name in the path so the default routing can work
189188
auth_endpoint = "{}/{}/{}/{}".format(self.base_url, backend_name, self.name, AuthorizationEndpoint.url)
190189
self.provider.configuration_information["authorization_endpoint"] = auth_endpoint
191190
auth_path = urlparse(auth_endpoint).path.lstrip("/")
192191
else:
193-
auth_path = "{}/{}".format(self.name, AuthorizationEndpoint.url)
192+
auth_path = "{}/{}".format(self.endpoint_basepath, AuthorizationEndpoint.url)
194193

195194
authentication = ("^{}$".format(auth_path), self.handle_authn_request)
196195
url_map = [provider_config, jwks_uri, authentication]
@@ -200,21 +199,21 @@ def register_endpoints(self, backend_names):
200199
self.endpoint_baseurl, TokenEndpoint.url
201200
)
202201
token_endpoint = (
203-
"^{}/{}".format(self.name, TokenEndpoint.url), self.token_endpoint
202+
"^{}/{}".format(self.endpoint_basepath, TokenEndpoint.url), self.token_endpoint
204203
)
205204
url_map.append(token_endpoint)
206205

207206
self.provider.configuration_information["userinfo_endpoint"] = (
208207
"{}/{}".format(self.endpoint_baseurl, UserinfoEndpoint.url)
209208
)
210209
userinfo_endpoint = (
211-
"^{}/{}".format(self.name, UserinfoEndpoint.url), self.userinfo_endpoint
210+
"^{}/{}".format(self.endpoint_basepath, UserinfoEndpoint.url), self.userinfo_endpoint
212211
)
213212
url_map.append(userinfo_endpoint)
214213

215214
if "registration_endpoint" in self.provider.configuration_information:
216215
client_registration = (
217-
"^{}/{}".format(self.name, RegistrationEndpoint.url),
216+
"^{}/{}".format(self.endpoint_basepath, RegistrationEndpoint.url),
218217
self.client_registration,
219218
)
220219
url_map.append(client_registration)

src/satosa/frontends/ping.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os.path
23

34
import satosa.logging_util as lu
45
import satosa.micro_services.base
@@ -44,7 +45,7 @@ def register_endpoints(self, backend_names):
4445
:rtype: list[(str, ((satosa.context.Context, Any) -> satosa.response.Response, Any))]
4546
:raise ValueError: if more than one backend is configured
4647
"""
47-
url_map = [("^{}".format(self.name), self.ping_endpoint)]
48+
url_map = [("^{}".format(os.path.join(self.endpoint_basepath, self.name)), self.ping_endpoint)]
4849

4950
return url_map
5051

src/satosa/frontends/saml2.py

+28-14
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def register_endpoints(self, backend_names):
117117

118118
if self.enable_metadata_reload():
119119
url_map.append(
120-
("^%s/%s$" % (self.name, "reload-metadata"), self._reload_metadata))
120+
("^%s/%s$" % (self.endpoint_basepath, "reload-metadata"), self._reload_metadata))
121121

122122
self.idp_config = self._build_idp_config_endpoints(
123123
self.config[self.KEY_IDP_CONFIG], backend_names)
@@ -512,15 +512,19 @@ def _register_endpoints(self, providers):
512512
"""
513513
url_map = []
514514

515+
backend_providers = "|".join(providers)
516+
base_path = urlparse(self.base_url).path.lstrip("/")
517+
if base_path:
518+
base_path = base_path + "/"
515519
for endp_category in self.endpoints:
516520
for binding, endp in self.endpoints[endp_category].items():
517-
valid_providers = ""
518-
for provider in providers:
519-
valid_providers = "{}|^{}".format(valid_providers, provider)
520-
valid_providers = valid_providers.lstrip("|")
521-
parsed_endp = urlparse(endp)
522-
url_map.append(("(%s)/%s$" % (valid_providers, parsed_endp.path),
523-
functools.partial(self.handle_authn_request, binding_in=binding)))
521+
endp_path = urlparse(endp).path
522+
url_map.append(
523+
(
524+
"^{}({})/{}$".format(base_path, backend_providers, endp_path),
525+
functools.partial(self.handle_authn_request, binding_in=binding)
526+
)
527+
)
524528

525529
if self.expose_entityid_endpoint():
526530
logger.debug("Exposing frontend entity endpoint = {}".format(self.idp.config.entityid))
@@ -676,11 +680,18 @@ def _load_idp_dynamic_endpoints(self, context):
676680
:param context:
677681
:return: An idp server
678682
"""
679-
target_entity_id = context.target_entity_id_from_path()
683+
target_entity_id = self._target_entity_id_from_path(context.path)
680684
idp_conf_file = self._load_endpoints_to_config(context.target_backend, target_entity_id)
681685
idp_config = IdPConfig().load(idp_conf_file)
682686
return Server(config=idp_config)
683687

688+
def _target_entity_id_from_path(self, request_path):
689+
path = request_path.lstrip("/")
690+
base_path = urlparse(self.base_url).path.lstrip("/")
691+
if base_path and path.startswith(base_path):
692+
path = path[len(base_path):].lstrip("/")
693+
return path.split("/")[1]
694+
684695
def _load_idp_dynamic_entity_id(self, state):
685696
"""
686697
Loads an idp server with the entity id saved in state
@@ -706,7 +717,7 @@ def handle_authn_request(self, context, binding_in):
706717
:type binding_in: str
707718
:rtype: satosa.response.Response
708719
"""
709-
target_entity_id = context.target_entity_id_from_path()
720+
target_entity_id = self._target_entity_id_from_path(context.path)
710721
target_entity_id = urlsafe_b64decode(target_entity_id).decode()
711722
context.decorate(Context.KEY_TARGET_ENTITYID, target_entity_id)
712723

@@ -724,7 +735,7 @@ def _create_state_data(self, context, resp_args, relay_state):
724735
:rtype: dict[str, dict[str, str] | str]
725736
"""
726737
state = super()._create_state_data(context, resp_args, relay_state)
727-
state["target_entity_id"] = context.target_entity_id_from_path()
738+
state["target_entity_id"] = self._target_entity_id_from_path(context.path)
728739
return state
729740

730741
def handle_backend_error(self, exception):
@@ -759,13 +770,16 @@ def _register_endpoints(self, providers):
759770
"""
760771
url_map = []
761772

773+
backend_providers = "|".join(providers)
774+
base_path = urlparse(self.base_url).path.lstrip("/")
775+
if base_path:
776+
base_path = base_path + "/"
762777
for endp_category in self.endpoints:
763778
for binding, endp in self.endpoints[endp_category].items():
764-
valid_providers = "|^".join(providers)
765-
parsed_endp = urlparse(endp)
779+
endp_path = urlparse(endp).path
766780
url_map.append(
767781
(
768-
r"(^{})/\S+/{}".format(valid_providers, parsed_endp.path),
782+
"^{}({})/\S+/{}$".format(base_path, backend_providers, endp_path),
769783
functools.partial(self.handle_authn_request, binding_in=binding)
770784
)
771785
)

src/satosa/micro_services/account_linking.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
import json
55
import logging
6+
import os.path
67

78
import requests
89
from jwkest.jwk import rsa_load, RSAKey
@@ -161,4 +162,13 @@ def register_endpoints(self):
161162
162163
:return: A list of endpoints bound to a function
163164
"""
164-
return [("^account_linking%s$" % self.endpoint, self._handle_al_response)]
165+
return [
166+
(
167+
"^{}$".format(
168+
os.path.join(
169+
self.base_path, "account_linking", self.endpoint.lstrip("/")
170+
)
171+
),
172+
self._handle_al_response,
173+
)
174+
]

src/satosa/micro_services/base.py

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Micro service for SATOSA
33
"""
44
import logging
5+
from urllib.parse import urlparse
56

67
logger = logging.getLogger(__name__)
78

@@ -14,6 +15,7 @@ class MicroService(object):
1415
def __init__(self, name, base_url, **kwargs):
1516
self.name = name
1617
self.base_url = base_url
18+
self.base_path = urlparse(base_url).path.lstrip("/")
1719
self.next = None
1820

1921
def process(self, context, data):

src/satosa/micro_services/consent.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import hashlib
55
import json
66
import logging
7+
import os.path
78
from base64 import urlsafe_b64encode
89

910
import requests
@@ -238,4 +239,13 @@ def register_endpoints(self):
238239
239240
:return: A list of endpoints bound to a function
240241
"""
241-
return [("^consent%s$" % self.endpoint, self._handle_consent_response)]
242+
return [
243+
(
244+
"^{}$".format(
245+
os.path.join(
246+
self.base_path, "consent", self.endpoint.lstrip("/")
247+
)
248+
),
249+
self._handle_consent_response,
250+
)
251+
]

src/satosa/routing.py

+24-6
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,20 @@ class UnknownEndpoint(ValueError):
3838
and handles the internal routing between frontends and backends.
3939
"""
4040

41-
def __init__(self, frontends, backends, micro_services):
41+
def __init__(self, frontends, backends, micro_services, base_path=""):
4242
"""
4343
:type frontends: dict[str, satosa.frontends.base.FrontendModule]
4444
:type backends: dict[str, satosa.backends.base.BackendModule]
4545
:type micro_services: Sequence[satosa.micro_services.base.MicroService]
46+
:type base_path: str
4647
4748
:param frontends: All available frontends used by the proxy. Key as frontend name, value as
4849
module
4950
:param backends: All available backends used by the proxy. Key as backend name, value as
5051
module
5152
:param micro_services: All available micro services used by the proxy. Key as micro service name, value as
5253
module
54+
:param base_path: Base path for endpoint mapping
5355
"""
5456

5557
if not frontends or not backends:
@@ -68,6 +70,8 @@ def __init__(self, frontends, backends, micro_services):
6870
else:
6971
self.micro_services = {}
7072

73+
self.base_path = base_path
74+
7175
logger.debug("Loaded backends with endpoints: {}".format(backends))
7276
logger.debug("Loaded frontends with endpoints: {}".format(frontends))
7377
logger.debug("Loaded micro services with endpoints: {}".format(micro_services))
@@ -134,6 +138,19 @@ def _find_registered_endpoint(self, context, modules):
134138

135139
raise ModuleRouter.UnknownEndpoint(context.path)
136140

141+
def _find_backend(self, request_path):
142+
"""
143+
Tries to guess the backend in use from the request.
144+
Returns the backend name or None if the backend was not specified.
145+
"""
146+
request_path = request_path.lstrip("/")
147+
if self.base_path and request_path.startswith(self.base_path):
148+
request_path = request_path[len(self.base_path):].lstrip("/")
149+
backend_guess = request_path.split("/")[0]
150+
if backend_guess in self.backends:
151+
return backend_guess
152+
return None
153+
137154
def endpoint_routing(self, context):
138155
"""
139156
Finds and returns the endpoint function bound to the path
@@ -155,13 +172,12 @@ def endpoint_routing(self, context):
155172
msg = "Routing path: {path}".format(path=context.path)
156173
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
157174
logger.debug(logline)
158-
path_split = context.path.split("/")
159-
backend = path_split[0]
160175

161-
if backend in self.backends:
176+
backend = self._find_backend(context.path)
177+
if backend is not None:
162178
context.target_backend = backend
163179
else:
164-
msg = "Unknown backend {}".format(backend)
180+
msg = "No backend was specified in request or no such backend {}".format(backend)
165181
logline = lu.LOG_FMT.format(
166182
id=lu.get_session_id(context.state), message=msg
167183
)
@@ -170,6 +186,8 @@ def endpoint_routing(self, context):
170186
try:
171187
name, frontend_endpoint = self._find_registered_endpoint(context, self.frontends)
172188
except ModuleRouter.UnknownEndpoint:
189+
for frontend in self.frontends.values():
190+
logger.debug(f"Unable to find {context.path} in {frontend['endpoints']}")
173191
pass
174192
else:
175193
context.target_frontend = name
@@ -183,7 +201,7 @@ def endpoint_routing(self, context):
183201
context.target_micro_service = name
184202
return micro_service_endpoint
185203

186-
if backend in self.backends:
204+
if backend is not None:
187205
backend_endpoint = self._find_registered_backend_endpoint(context)
188206
if backend_endpoint:
189207
return backend_endpoint

0 commit comments

Comments
 (0)