Skip to content

Commit 612e205

Browse files
committed
Separate disco handling from backend decision
Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent db6d9b8 commit 612e205

File tree

6 files changed

+105
-106
lines changed

6 files changed

+105
-106
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module: satosa.micro_services.disco.DiscoToTargetIssuer
2+
name: DiscoToTargetIssuer
3+
config:
4+
# the regex that will intercept http requests to be handled with this microservice
5+
disco_endpoints:
6+
- ".*/disco"

example/plugins/microservices/target_based_routing.yaml.example

-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ name: TargetRouter
33
config:
44
default_backend: Saml2
55

6-
# the regex that will intercept http requests to be handled with this microservice
7-
endpoint_paths:
8-
- ".*/disco"
9-
106
target_mapping:
117
"http://idpspid.testunical.it:8088": "spidSaml2" # map SAML entity with entity id 'target_id' to backend name
128
"http://eidas.testunical.it:8081/saml2/metadata": "eidasSaml2"

src/satosa/micro_services/custom_routing.py

+4-49
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,17 @@
77
from .base import RequestMicroService
88
from ..exception import SATOSAConfigurationError
99
from ..exception import SATOSAError
10-
from ..exception import SATOSAStateError
1110

1211

1312
logger = logging.getLogger(__name__)
1413

1514

1615
class CustomRoutingError(SATOSAError):
17-
"""
18-
SATOSA exception raised by CustomRouting rules
19-
"""
16+
"""SATOSA exception raised by CustomRouting rules"""
2017
pass
2118

2219

23-
class DecideBackendByTargetIdP(RequestMicroService):
20+
class DecideBackendByTargetIssuer(RequestMicroService):
2421
"""
2522
Select target backend based on the target issuer.
2623
"""
@@ -38,14 +35,11 @@ def __init__(self, config:dict, *args, **kwargs):
3835
self.default_backend = config['default_backend']
3936

4037
def process(self, context:Context, data:InternalData):
41-
"""
42-
Set context.target_backend based on the target issuer (context.target_entity_id)
38+
"""Set context.target_backend based on the target issuer"""
4339

44-
:param context: request context
45-
:param data: the internal request
46-
"""
4740
target_issuer = context.get_decoration(Context.KEY_TARGET_ENTITYID)
4841
if not target_issuer:
42+
logger.info('skipping backend decision because no target_issuer was found')
4943
return super().process(context, data)
5044

5145
target_backend = (
@@ -64,45 +58,6 @@ def process(self, context:Context, data:InternalData):
6458
return super().process(context, data)
6559

6660

67-
class DecideBackendByDiscoIdP(DecideBackendByTargetIdP):
68-
def __init__(self, config:dict, *args, **kwargs):
69-
super().__init__(config, *args, **kwargs)
70-
71-
self.disco_endpoints = config['disco_endpoints']
72-
if not isinstance(self.disco_endpoints, list):
73-
raise CustomRoutingError('disco_endpoints must be a list of str')
74-
75-
def register_endpoints(self):
76-
"""
77-
URL mapping of additional endpoints this micro service needs to register for callbacks.
78-
79-
Example of a mapping from the url path '/callback' to the callback() method of a micro service:
80-
reg_endp = [
81-
("^/callback1$", self.callback),
82-
]
83-
84-
:rtype List[Tuple[str, Callable[[satosa.context.Context, Any], satosa.response.Response]]]
85-
86-
:return: A list with functions and args bound to a specific endpoint url,
87-
[(regexp, Callable[[satosa.context.Context], satosa.response.Response]), ...]
88-
"""
89-
90-
return [
91-
(path , self._handle_disco_response)
92-
for path in self.disco_endpoints
93-
]
94-
95-
def _handle_disco_response(self, context:Context):
96-
target_issuer_from_disco = context.request.get('entityID')
97-
if not target_issuer_from_disco:
98-
raise CustomRoutingError('no valid entity_id in the disco response')
99-
100-
context.decorate(Context.KEY_TARGET_ENTITYID, target_issuer_from_disco)
101-
data_serialized = context.state.get(self.name, {}).get('internal', {})
102-
data = InternalData.from_dict(data_serialized)
103-
return self.process(context, data)
104-
105-
10661
class DecideBackendByRequester(RequestMicroService):
10762
"""
10863
Select which backend should be used based on who the requester is.

src/satosa/micro_services/disco.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from satosa.context import Context
2+
from satosa.internal import InternalData
3+
4+
from .base import RequestMicroService
5+
from ..exception import SATOSAError
6+
7+
8+
class DiscoToTargetIssuerError(SATOSAError):
9+
"""SATOSA exception raised by CustomRouting rules"""
10+
11+
12+
class DiscoToTargetIssuer(RequestMicroService):
13+
def __init__(self, config:dict, *args, **kwargs):
14+
super().__init__(*args, **kwargs)
15+
16+
self.disco_endpoints = config['disco_endpoints']
17+
if not isinstance(self.disco_endpoints, list) or not self.disco_endpoints:
18+
raise DiscoToTargetIssuerError('disco_endpoints must be a list of str')
19+
20+
def register_endpoints(self):
21+
"""
22+
URL mapping of additional endpoints this micro service needs to register for callbacks.
23+
24+
Example of a mapping from the url path '/callback' to the callback() method of a micro service:
25+
reg_endp = [
26+
("^/callback1$", self.callback),
27+
]
28+
29+
:rtype List[Tuple[str, Callable[[satosa.context.Context, Any], satosa.response.Response]]]
30+
31+
:return: A list with functions and args bound to a specific endpoint url,
32+
[(regexp, Callable[[satosa.context.Context], satosa.response.Response]), ...]
33+
"""
34+
35+
return [
36+
(path , self._handle_disco_response)
37+
for path in self.disco_endpoints
38+
]
39+
40+
def _handle_disco_response(self, context:Context):
41+
target_issuer = context.request.get('entityID')
42+
if not target_issuer:
43+
raise DiscoToTargetIssuerError('no valid entity_id in the disco response')
44+
45+
data_serialized = context.state.get(self.name, {}).get('internal', {})
46+
data = InternalData.from_dict(data_serialized)
47+
context.decorate(Context.KEY_TARGET_ENTITYID, target_issuer)
48+
return super().process(context, data)

tests/satosa/micro_services/test_custom_routing.py

+3-53
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
from satosa.exception import SATOSAError, SATOSAConfigurationError, SATOSAStateError
99
from satosa.internal import InternalData
1010
from satosa.micro_services.custom_routing import DecideIfRequesterIsAllowed
11-
from satosa.micro_services.custom_routing import DecideBackendByDiscoIdP
12-
from satosa.micro_services.custom_routing import DecideBackendByTargetIdP
11+
from satosa.micro_services.custom_routing import DecideBackendByTargetIssuer
1312
from satosa.micro_services.custom_routing import CustomRoutingError
1413

1514

@@ -164,7 +163,7 @@ def test_missing_target_entity_id_from_context(self, context):
164163
decide_service.process(context, req)
165164

166165

167-
class TestDecideBackendByTargetIdP(TestCase):
166+
class TestDecideBackendByTargetIssuer(TestCase):
168167
def setUp(self):
169168
context = Context()
170169
context.state = State()
@@ -174,12 +173,9 @@ def setUp(self):
174173
'target_mapping': {
175174
'mapped_idp.example.org': 'mapped_backend',
176175
},
177-
'disco_endpoints': [
178-
'.*/disco',
179-
],
180176
}
181177

182-
plugin = DecideBackendByTargetIdP(
178+
plugin = DecideBackendByTargetIssuer(
183179
config=config,
184180
name='test_decide_service',
185181
base_url='https://satosa.example.org',
@@ -207,49 +203,3 @@ def test_when_target_is_mapped_choose_mapping_backend(self):
207203
data.requester = 'somebody else'
208204
newctx, newdata = self.plugin.process(self.context, data)
209205
assert newctx.target_backend == 'mapped_backend'
210-
211-
212-
class TestDecideBackendByDiscoIdP(TestCase):
213-
def setUp(self):
214-
context = Context()
215-
context.state = State()
216-
217-
config = {
218-
'default_backend': 'default_backend',
219-
'target_mapping': {
220-
'mapped_idp.example.org': 'mapped_backend',
221-
},
222-
'disco_endpoints': [
223-
'.*/disco',
224-
],
225-
}
226-
227-
plugin = DecideBackendByDiscoIdP(
228-
config=config,
229-
name='test_decide_service',
230-
base_url='https://satosa.example.org',
231-
)
232-
plugin.next = lambda ctx, data: (ctx, data)
233-
234-
self.config = config
235-
self.context = context
236-
self.plugin = plugin
237-
238-
def test_when_target_is_not_set_raise_error(self):
239-
self.context.request = {}
240-
with pytest.raises(CustomRoutingError):
241-
self.plugin._handle_disco_response(self.context)
242-
243-
def test_when_target_is_not_mapped_choose_default_backend(self):
244-
self.context.request = {
245-
'entityID': 'idp.example.org',
246-
}
247-
newctx, newdata = self.plugin._handle_disco_response(self.context)
248-
assert newctx.target_backend == 'default_backend'
249-
250-
def test_when_target_is_mapped_choose_mapping_backend(self):
251-
self.context.request = {
252-
'entityID': 'mapped_idp.example.org',
253-
}
254-
newctx, newdata = self.plugin._handle_disco_response(self.context)
255-
assert newctx.target_backend == 'mapped_backend'
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from unittest import TestCase
2+
3+
import pytest
4+
5+
from satosa.context import Context
6+
from satosa.state import State
7+
from satosa.micro_services.disco import DiscoToTargetIssuer
8+
from satosa.micro_services.disco import DiscoToTargetIssuerError
9+
10+
11+
class TestDiscoToTargetIssuer(TestCase):
12+
def setUp(self):
13+
context = Context()
14+
context.state = State()
15+
16+
config = {
17+
'disco_endpoints': [
18+
'.*/disco',
19+
],
20+
}
21+
22+
plugin = DiscoToTargetIssuer(
23+
config=config,
24+
name='test_disco_to_target_issuer',
25+
base_url='https://satosa.example.org',
26+
)
27+
plugin.next = lambda ctx, data: (ctx, data)
28+
29+
self.config = config
30+
self.context = context
31+
self.plugin = plugin
32+
33+
def test_when_entity_id_is_not_set_raise_error(self):
34+
self.context.request = {}
35+
with pytest.raises(DiscoToTargetIssuerError):
36+
self.plugin._handle_disco_response(self.context)
37+
38+
def test_when_entity_id_is_set_target_issuer_is_set(self):
39+
entity_id = 'idp.example.org'
40+
self.context.request = {
41+
'entityID': entity_id,
42+
}
43+
newctx, newdata = self.plugin._handle_disco_response(self.context)
44+
assert newctx.get_decoration(Context.KEY_TARGET_ENTITYID) == entity_id

0 commit comments

Comments
 (0)