Skip to content

Commit 0fc3ef3

Browse files
vkadamc00kiemon5ter
authored andcommitted
Added test cases for expose co-frontend entity endpoints and duplicate entity id fix
1 parent 87f4082 commit 0fc3ef3

File tree

5 files changed

+104
-43
lines changed

5 files changed

+104
-43
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ _build
1414
build/
1515
dist/
1616
.coverage
17+
venv/

example/plugins/frontends/saml2_virtualcofrontend.yaml.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ config:
4949
metadata:
5050
local: [sp.xml]
5151

52-
entityid: <base_url>/<name>/proxy.xml
52+
# Available placeholders to use while constructing entityid,
53+
# <backend_name>: Backend name
54+
# <co_name>: collaborative_organizations encodeable_name
55+
# <base_url>: Base url of installation
56+
# <name>: Name of this virtual co-frontend
57+
entityid: <base_url>/<backend_name>/idp/<co_name>
5358
accepted_time_diff: 60
5459
service:
5560
idp:

src/satosa/frontends/saml2.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,10 @@ class SAMLVirtualCoFrontend(SAMLFrontend):
788788
KEY_ORGANIZATION = 'organization'
789789
KEY_ORGANIZATION_KEYS = ['display_name', 'name', 'url']
790790

791+
def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name):
792+
self.has_multiple_backends = False
793+
super().__init__(auth_req_callback_func, internal_attributes, config, base_url, name)
794+
791795
def handle_authn_request(self, context, binding_in):
792796
"""
793797
See super class
@@ -984,6 +988,9 @@ def _add_entity_id(self, config, co_name, backend_name):
984988
:return: config with updated entity ID
985989
"""
986990
base_entity_id = config['entityid']
991+
# If not using template for entityId and does not has multiple backends, then for backward compatibility append co_name at end
992+
if "<co_name>" not in base_entity_id and not self.has_multiple_backends:
993+
base_entity_id = "{}/{}".format(base_entity_id, "<co_name>")
987994

988995
replace = [
989996
("<backend_name>", quote_plus(backend_name)),
@@ -1110,10 +1117,22 @@ def _register_endpoints(self, backend_names):
11101117
:param backend_names: A list of backend names
11111118
:return: A list of url and endpoint function pairs
11121119
"""
1120+
1121+
# Throw exception if there is possibility of duplicate entity ids when using co_names with multiple backends
1122+
self.has_multiple_backends = len(backend_names) > 1
1123+
co_names = self._co_names_from_config()
1124+
all_entity_ids = []
1125+
for backend_name in backend_names:
1126+
for co_name in co_names:
1127+
all_entity_ids.append(self._add_entity_id(copy.deepcopy(self.idp_config), co_name, backend_name)['entityid'])
1128+
1129+
if len(all_entity_ids) != len(set(all_entity_ids)):
1130+
raise ValueError("Duplicate entities ids would be created for co-frontends, please make sure to make entity ids unique. "
1131+
"You can use <backend_name> and <co_name> to achieve it. See example yaml file.")
1132+
11131133
# Create a regex pattern that will match any of the CO names. We
11141134
# escape special characters like '+' and '.' that are valid
11151135
# characters in an URL encoded string.
1116-
co_names = self._co_names_from_config()
11171136
url_encoded_co_names = [re.escape(quote_plus(name)) for name in
11181137
co_names]
11191138
co_name_pattern = "|".join(url_encoded_co_names)

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def sp_conf(cert_and_key):
6464

6565
@pytest.fixture
6666
def idp_conf(cert_and_key):
67-
idp_base = "http://idp.example.com"
67+
idp_base = BASE_URL
6868

6969
idpconfig = {
7070
"entityid": "{}/{}/proxy.xml".format(idp_base, "Saml2IDP"),

tests/satosa/frontends/test_saml2.py

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Tests for the SAML frontend module src/frontends/saml2.py.
33
"""
4+
import copy
45
import itertools
56
import re
67
from collections import Counter
@@ -28,7 +29,6 @@
2829
from satosa.internal import AuthenticationInformation
2930
from satosa.internal import InternalData
3031
from satosa.state import State
31-
from satosa.context import Context
3232
from tests.users import USERS
3333
from tests.util import FakeSP, create_metadata_from_config_dict
3434

@@ -298,14 +298,14 @@ def test_acr_mapping_per_idp_in_authn_response(self, context, idp_conf, sp_conf,
298298
]
299299
)
300300
def test_respect_sp_entity_categories(
301-
self,
302-
context,
303-
entity_category,
304-
entity_category_module,
305-
expected_attributes,
306-
idp_conf,
307-
sp_conf,
308-
internal_response
301+
self,
302+
context,
303+
entity_category,
304+
entity_category_module,
305+
expected_attributes,
306+
idp_conf,
307+
sp_conf,
308+
internal_response
309309
):
310310
idp_metadata_str = create_metadata_from_config_dict(idp_conf)
311311
idp_conf["service"]["idp"]["policy"]["default"]["entity_categories"] = [entity_category_module]
@@ -365,7 +365,7 @@ def test_metadata_endpoint(self, context, idp_conf):
365365
assert idp_conf["entityid"] in resp.message
366366

367367
def test_custom_attribute_release_with_less_attributes_than_entity_category(
368-
self, context, idp_conf, sp_conf, internal_response
368+
self, context, idp_conf, sp_conf, internal_response
369369
):
370370
idp_metadata_str = create_metadata_from_config_dict(idp_conf)
371371
idp_conf["service"]["idp"]["policy"]["default"]["entity_categories"] = ["swamid"]
@@ -387,8 +387,8 @@ def test_custom_attribute_release_with_less_attributes_than_entity_category(
387387
internal_response.requester = sp_conf["entityid"]
388388
resp = self.get_auth_response(samlfrontend, context, internal_response, sp_conf, idp_metadata_str)
389389
assert len(resp.ava.keys()) == (
390-
len(expected_attributes)
391-
- len(custom_attributes[internal_response.auth_info.issuer][internal_response.requester]["exclude"])
390+
len(expected_attributes)
391+
- len(custom_attributes[internal_response.auth_info.issuer][internal_response.requester]["exclude"])
392392
)
393393

394394

@@ -431,6 +431,7 @@ def test_load_idp_dynamic_entity_id(self, idp_conf):
431431

432432
class TestSAMLVirtualCoFrontend(TestSAMLFrontend):
433433
BACKEND = "test_backend"
434+
BACKEND_1 = "test_backend_1"
434435
CO = "MESS"
435436
CO_O = "organization"
436437
CO_C = "countryname"
@@ -442,7 +443,7 @@ class TestSAMLVirtualCoFrontend(TestSAMLFrontend):
442443
CO_C: ["US"],
443444
CO_CO: ["United States"],
444445
CO_NOREDUORGACRONYM: ["MESS"]
445-
}
446+
}
446447
KEY_SSO = "single_sign_on_service"
447448

448449
@pytest.fixture
@@ -471,10 +472,10 @@ def frontend(self, idp_conf, sp_conf):
471472
# endpoints, and the collaborative organization configuration to
472473
# create the configuration for the frontend.
473474
conf = {
474-
"idp_config": idp_conf,
475-
"endpoints": ENDPOINTS,
476-
"collaborative_organizations": [collab_org]
477-
}
475+
"idp_config": idp_conf,
476+
"endpoints": ENDPOINTS,
477+
"collaborative_organizations": [collab_org]
478+
}
478479

479480
# Use a richer set of internal attributes than what is provided
480481
# for the parent class so that we can test for the static SAML
@@ -504,10 +505,13 @@ def context(self, context):
504505
that would be available during a SAML flow and that would include
505506
a path and target_backend that indicates the CO.
506507
"""
507-
context.path = "{}/{}/sso/redirect".format(self.BACKEND, self.CO)
508-
context.target_backend = self.BACKEND
508+
return self._make_context(context, self.BACKEND, self.CO)
509509

510-
return context
510+
def _make_context(self, context, backend, co_name):
511+
_context = copy.deepcopy(context)
512+
_context.path = "{}/{}/sso/redirect".format(backend, co_name)
513+
_context.target_backend = backend
514+
return _context
511515

512516
def test_create_state_data(self, frontend, context, idp_conf):
513517
frontend._create_co_virtual_idp(context)
@@ -542,6 +546,17 @@ def test_create_co_virtual_idp(self, frontend, context, idp_conf):
542546
assert idp_server.config.entityid == expected_entityid
543547
assert all(sso in sso_endpoints for sso in expected_endpoints)
544548

549+
def test_create_co_virtual_idp_with_entity_id_templates(self, frontend, context):
550+
frontend.idp_config['entityid'] = "{}/Saml2IDP/proxy.xml".format(BASE_URL)
551+
expected_entity_id = "{}/Saml2IDP/proxy.xml/{}".format(BASE_URL, self.CO)
552+
idp_server = frontend._create_co_virtual_idp(context)
553+
assert idp_server.config.entityid == expected_entity_id
554+
555+
frontend.idp_config['entityid'] = "{}/<backend_name>/idp/<co_name>".format(BASE_URL)
556+
expected_entity_id = "{}/{}/idp/{}".format(BASE_URL, context.target_backend, self.CO)
557+
idp_server = frontend._create_co_virtual_idp(context)
558+
assert idp_server.config.entityid == expected_entity_id
559+
545560
def test_register_endpoints(self, frontend, context):
546561
idp_server = frontend._create_co_virtual_idp(context)
547562
url_map = frontend.register_endpoints([self.BACKEND])
@@ -553,6 +568,28 @@ def test_register_endpoints(self, frontend, context):
553568
for endpoint in all_idp_endpoints:
554569
assert any(pat.match(endpoint) for pat in compiled_regex)
555570

571+
def test_register_endpoints_throws_error_in_case_duplicate_entity_ids(self, frontend):
572+
with pytest.raises(ValueError):
573+
frontend.register_endpoints([self.BACKEND, self.BACKEND_1])
574+
575+
def test_register_endpoints_with_metadata_endpoints(self, frontend, context):
576+
frontend.idp_config['entityid'] = "{}/<backend_name>/idp/<co_name>".format(BASE_URL)
577+
frontend.config['entityid_endpoint'] = True
578+
idp_server_1 = frontend._create_co_virtual_idp(context)
579+
context_2 = self._make_context(context, self.BACKEND_1, self.CO)
580+
idp_server_2 = frontend._create_co_virtual_idp(context_2)
581+
582+
url_map = frontend.register_endpoints([self.BACKEND, self.BACKEND_1])
583+
expected_idp_endpoints = [urlparse(endpoint[0]).path[1:] for server in [idp_server_1, idp_server_2]
584+
for endpoint in server.config._idp_endpoints[self.KEY_SSO]]
585+
for server in [idp_server_1, idp_server_2]:
586+
expected_idp_endpoints.append(urlparse(server.config.entityid).path[1:])
587+
588+
compiled_regex = [re.compile(regex) for regex, _ in url_map]
589+
590+
for endpoint in expected_idp_endpoints:
591+
assert any(pat.match(endpoint) for pat in compiled_regex)
592+
556593
def test_co_static_attributes(self, frontend, context, internal_response,
557594
idp_conf, sp_conf):
558595
# Use the frontend and context fixtures to dynamically create the
@@ -563,9 +600,8 @@ def test_co_static_attributes(self, frontend, context, internal_response,
563600
# and then use those to dynamically update the ipd_conf fixture.
564601
co_name = frontend._get_co_name(context)
565602
backend_name = context.target_backend
566-
idp_conf = frontend._add_endpoints_to_config(idp_conf, co_name,
567-
backend_name)
568-
idp_conf = frontend._add_entity_id(idp_conf, co_name)
603+
idp_conf = frontend._add_endpoints_to_config(idp_conf, co_name, backend_name)
604+
idp_conf = frontend._add_entity_id(idp_conf, co_name, backend_name)
569605

570606
# Use a utility function to serialize the idp_conf IdP configuration
571607
# fixture to a string and then dynamically update the sp_conf
@@ -597,9 +633,9 @@ def test_co_static_attributes(self, frontend, context, internal_response,
597633
"name_id_policy": NameIDPolicy(format=NAMEID_FORMAT_TRANSIENT),
598634
"in_response_to": None,
599635
"destination": sp_config.endpoint(
600-
"assertion_consumer_service",
601-
binding=BINDING_HTTP_REDIRECT
602-
)[0],
636+
"assertion_consumer_service",
637+
binding=BINDING_HTTP_REDIRECT
638+
)[0],
603639
"sp_entity_id": sp_conf["entityid"],
604640
"binding": BINDING_HTTP_REDIRECT
605641
}
@@ -616,42 +652,42 @@ def test_co_static_attributes(self, frontend, context, internal_response,
616652
class TestSubjectTypeToSamlNameIdFormat:
617653
def test_should_default_to_persistent(self):
618654
assert (
619-
subject_type_to_saml_nameid_format("unmatched")
620-
== NAMEID_FORMAT_PERSISTENT
655+
subject_type_to_saml_nameid_format("unmatched")
656+
== NAMEID_FORMAT_PERSISTENT
621657
)
622658

623659
def test_should_map_persistent(self):
624660
assert (
625-
subject_type_to_saml_nameid_format(NAMEID_FORMAT_PERSISTENT)
626-
== NAMEID_FORMAT_PERSISTENT
661+
subject_type_to_saml_nameid_format(NAMEID_FORMAT_PERSISTENT)
662+
== NAMEID_FORMAT_PERSISTENT
627663
)
628664

629665
def test_should_map_transient(self):
630666
assert (
631-
subject_type_to_saml_nameid_format(NAMEID_FORMAT_TRANSIENT)
632-
== NAMEID_FORMAT_TRANSIENT
667+
subject_type_to_saml_nameid_format(NAMEID_FORMAT_TRANSIENT)
668+
== NAMEID_FORMAT_TRANSIENT
633669
)
634670

635671
def test_should_map_emailaddress(self):
636672
assert (
637-
subject_type_to_saml_nameid_format(NAMEID_FORMAT_EMAILADDRESS)
638-
== NAMEID_FORMAT_EMAILADDRESS
673+
subject_type_to_saml_nameid_format(NAMEID_FORMAT_EMAILADDRESS)
674+
== NAMEID_FORMAT_EMAILADDRESS
639675
)
640676

641677
def test_should_map_unspecified(self):
642678
assert (
643-
subject_type_to_saml_nameid_format(NAMEID_FORMAT_UNSPECIFIED)
644-
== NAMEID_FORMAT_UNSPECIFIED
679+
subject_type_to_saml_nameid_format(NAMEID_FORMAT_UNSPECIFIED)
680+
== NAMEID_FORMAT_UNSPECIFIED
645681
)
646682

647683
def test_should_map_public(self):
648684
assert (
649-
subject_type_to_saml_nameid_format("public")
650-
== NAMEID_FORMAT_PERSISTENT
685+
subject_type_to_saml_nameid_format("public")
686+
== NAMEID_FORMAT_PERSISTENT
651687
)
652688

653689
def test_should_map_pairwise(self):
654690
assert (
655-
subject_type_to_saml_nameid_format("pairwise")
656-
== NAMEID_FORMAT_TRANSIENT
691+
subject_type_to_saml_nameid_format("pairwise")
692+
== NAMEID_FORMAT_TRANSIENT
657693
)

0 commit comments

Comments
 (0)