Skip to content

Commit 2e54657

Browse files
committed
Adds multiple validators for eIDAS IdP config
- Adds inherited validators for eIDAS IdP config. Specifically the following rules related to the metadata: 1. entityid MUST be HTTPs URL 2. SingleLogoutElementService SHOULD NOT be declared 3. ArtifactResolutionService SHOULD NOT be declared 4. ManageNameIDService SHOULD NOT be declared 5. KeyDescriptor MUST be declared 6. Organization with minimal info (name/display name/url) SHOULD be provided 7. Contact Person of contactType technical with an email address SHOULD be provided 8. Contact Person of contactType support with an email address SHOULD be provided 9. eIDAS protocol version implemented SHOULD be provided 10. eIDAS application identifier SHOULD be provided and MUST have a specific format 11. Node country information MUST be declared and MUST follow the ISO 3166-1 alpha-2 format All the above are specified in eIDAS SAML Message Format v.1.2 spec document.
1 parent 8518822 commit 2e54657

File tree

3 files changed

+274
-17
lines changed

3 files changed

+274
-17
lines changed

src/saml2/config.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
"name_qualifier",
129129
"edu_person_targeted_id",
130130
"node_country",
131-
"application_identifier"
131+
"application_identifier",
132132
"protocol_version"
133133
]
134134

@@ -717,8 +717,28 @@ def __init__(self):
717717
Config.__init__(self)
718718

719719

720-
class eIDASIdPConfig(IdPConfig):
721-
pass
720+
class eIDASIdPConfig(IdPConfig, eIDASConfig):
721+
def get_endpoint_element(self, element):
722+
return getattr(self, "_idp_endpoints", {}).get(element, None)
723+
724+
def get_application_identifier(self):
725+
return getattr(self, "_idp_application_identifier", None)
726+
727+
def get_protocol_version(self):
728+
return getattr(self, "_idp_protocol_version", None)
729+
730+
def get_node_country(self):
731+
return getattr(self, "_idp_node_country", None)
732+
733+
@property
734+
def warning_validators(self):
735+
idp_warning_validators = {}
736+
return {**super().warning_validators, **idp_warning_validators}
737+
738+
@property
739+
def error_validators(self):
740+
idp_error_validators = {}
741+
return {**super().error_validators, **idp_error_validators}
722742

723743

724744
def config_factory(_type, config):

tests/eidas/idp_conf.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,13 @@
1010
BASE = "http://localhost:8088"
1111

1212
CONFIG = {
13-
"entityid": "urn:mace:example.com:saml:roland:idp",
13+
"entityid": "https://example.org",
1414
"name": "Rolands IdP",
1515
"service": {
1616
"idp": {
1717
"endpoints": {
1818
"single_sign_on_service": [
1919
("%s/sso" % BASE, BINDING_HTTP_REDIRECT)],
20-
"single_logout_service": [
21-
("%s/slo" % BASE, BINDING_SOAP),
22-
("%s/slop" % BASE, BINDING_HTTP_POST)]
2320
},
2421
"policy": {
2522
"default": {
@@ -38,7 +35,9 @@
3835
}
3936
},
4037
"subject_data": full_path("subject_data.db"),
41-
"node_country": "GR"
38+
"node_country": "GR",
39+
"application_identifier": "CEF:eIDAS-ref:2.0",
40+
"protocol_version": [1.1, 2.2]
4241
},
4342
},
4443
"debug": 1,
@@ -53,16 +52,25 @@
5352
}],
5453
"attribute_map_dir": full_path("attributemaps"),
5554
"organization": {
56-
"name": "Exempel AB",
57-
"display_name": [("Exempel AB", "se"), ("Example Co.", "en")],
58-
"url": "http://www.example.com/roland",
55+
"name": ("AB Exempel", "se"),
56+
"display_name": ("AB Exempel", "se"),
57+
"url": "http://www.example.org",
5958
},
6059
"contact_person": [
6160
{
62-
"given_name": "John",
63-
"sur_name": "Smith",
64-
"email_address": ["john.smith@example.com"],
65-
"contact_type": "technical",
61+
"given_name": "Roland",
62+
"sur_name": "Hedberg",
63+
"telephone_number": "+46 70 100 0000",
64+
"email_address": ["tech@eample.com",
65+
"tech@example.org"],
66+
"contact_type": "technical"
6667
},
68+
{
69+
"given_name": "Roland",
70+
"sur_name": "Hedberg",
71+
"telephone_number": "+46 70 100 0000",
72+
"email_address": ["tech@eample.com",
73+
"tech@example.org"],
74+
"contact_type": "support"}
6775
],
6876
}

tests/eidas/test_idp.py

+231-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,242 @@
1+
import pytest
2+
import copy
3+
from saml2 import BINDING_HTTP_POST
14
from saml2 import metadata
2-
from saml2.config import IdPConfig
5+
from saml2 import samlp
6+
from saml2.client import Saml2Client
7+
from saml2.server import Server
8+
from saml2.config import eIDASSPConfig, eIDASIdPConfig
9+
from eidas.idp_conf import CONFIG
10+
from saml2.utility.config import ConfigValidationError
311

412

513
class TestIdP:
614
def setup_class(self):
7-
self.conf = IdPConfig()
15+
self.server = Server("idp_conf")
16+
17+
self.conf = eIDASIdPConfig()
818
self.conf.load_file("idp_conf")
919

20+
sp_conf = eIDASSPConfig()
21+
sp_conf.load_file("sp_conf")
22+
23+
self.client = Saml2Client(sp_conf)
24+
25+
def teardown_class(self):
26+
self.server.close()
27+
28+
@pytest.fixture(scope="function")
29+
def config(self):
30+
return copy.deepcopy(CONFIG)
31+
1032
def test_node_country_in_metadata(self):
1133
entd = metadata.entity_descriptor(self.conf)
1234
assert any(filter(lambda x: x.tag == "NodeCountry",
1335
entd.extensions.extension_elements))
36+
37+
def test_application_identifier_in_metadata(self):
38+
entd = metadata.entity_descriptor(self.conf)
39+
entity_attributes = next(filter(lambda x: x.tag == "EntityAttributes",
40+
entd.extensions.extension_elements))
41+
app_identifier = [
42+
x for x in entity_attributes.children
43+
if getattr(x, "attributes", {}).get("Name") ==
44+
"http://eidas.europa.eu/entity-attributes/application-identifier"
45+
]
46+
assert len(app_identifier) == 1
47+
assert self.conf._idp_application_identifier \
48+
== next(x.text for y in app_identifier for x in y.children)
49+
50+
def test_multiple_protocol_version_in_metadata(self):
51+
entd = metadata.entity_descriptor(self.conf)
52+
entity_attributes = next(filter(lambda x: x.tag == "EntityAttributes",
53+
entd.extensions.extension_elements))
54+
protocol_version = next(
55+
x for x in entity_attributes.children
56+
if getattr(x, "name", "") ==
57+
"http://eidas.europa.eu/entity-attributes/protocol-version"
58+
)
59+
assert len(protocol_version.attribute_value) == 2
60+
assert set(str(x) for x in self.conf._idp_protocol_version) \
61+
== set([x.text for x in protocol_version.attribute_value])
62+
63+
def test_protocol_version_in_metadata(self, config):
64+
config["service"]["idp"]["protocol_version"] = 1.2
65+
66+
conf = eIDASIdPConfig()
67+
conf.load(config)
68+
69+
entd = metadata.entity_descriptor(conf)
70+
entity_attributes = next(filter(lambda x: x.tag == "EntityAttributes",
71+
entd.extensions.extension_elements))
72+
protocol_version = next(
73+
x for x in entity_attributes.children
74+
if getattr(x, "name", "") ==
75+
"http://eidas.europa.eu/entity-attributes/protocol-version"
76+
)
77+
assert len(protocol_version.attribute_value) == 1
78+
assert {str(conf._idp_protocol_version)} \
79+
== set([x.text for x in protocol_version.attribute_value])
80+
81+
82+
class TestIdPConfig:
83+
@staticmethod
84+
def assert_validation_error(config):
85+
conf = eIDASIdPConfig()
86+
conf.load(config)
87+
with pytest.raises(ConfigValidationError):
88+
conf.validate()
89+
90+
@pytest.fixture(scope="function")
91+
def technical_contacts(self, config):
92+
return [
93+
x for x in config["contact_person"]
94+
if x["contact_type"] == "technical"
95+
]
96+
97+
@pytest.fixture(scope="function")
98+
def support_contacts(self, config):
99+
return [
100+
x for x in config["contact_person"]
101+
if x["contact_type"] == "support"
102+
]
103+
104+
@pytest.fixture(scope="function")
105+
def raise_error_on_warning(self, monkeypatch):
106+
def r(*args, **kwargs):
107+
raise ConfigValidationError()
108+
monkeypatch.setattr("saml2.config.logger.warning", r)
109+
110+
@pytest.fixture(scope="function")
111+
def config(self):
112+
return copy.deepcopy(CONFIG)
113+
114+
def test_singlelogout_declared(self, config, raise_error_on_warning):
115+
config["service"]["idp"]["endpoints"]["single_logout_service"] = \
116+
[("https://example.com", BINDING_HTTP_POST)]
117+
self.assert_validation_error(config)
118+
119+
def test_artifact_resolution_declared(self, config, raise_error_on_warning):
120+
config["service"]["idp"]["endpoints"]["artifact_resolution_service"] = \
121+
[("https://example.com", BINDING_HTTP_POST)]
122+
self.assert_validation_error(config)
123+
124+
def test_manage_nameid_service_declared(self, config, raise_error_on_warning):
125+
config["service"]["idp"]["endpoints"]["manage_name_id_service"] = \
126+
[("https://example.com", BINDING_HTTP_POST)]
127+
self.assert_validation_error(config)
128+
129+
def test_no_keydescriptor(self, config):
130+
del config["cert_file"]
131+
self.assert_validation_error(config)
132+
133+
def test_no_nodecountry(self, config):
134+
del config["service"]["idp"]["node_country"]
135+
self.assert_validation_error(config)
136+
137+
def test_nodecountry_wrong_format(self, config):
138+
config["service"]["idp"]["node_country"] = "gr"
139+
self.assert_validation_error(config)
140+
141+
def test_no_application_identifier_warning(self, config, raise_error_on_warning):
142+
del config["service"]["idp"]["application_identifier"]
143+
144+
self.assert_validation_error(config)
145+
146+
def test_empty_application_identifier_warning(self, config, raise_error_on_warning):
147+
config["service"]["idp"]["application_identifier"] = ""
148+
149+
self.assert_validation_error(config)
150+
151+
def test_application_identifier_wrong_format(self, config):
152+
config["service"]["idp"]["application_identifier"] = "TEST:Node.1"
153+
154+
self.assert_validation_error(config)
155+
156+
def test_application_identifier_ok_format(self, config, raise_error_on_warning):
157+
conf = eIDASIdPConfig()
158+
conf.load(config)
159+
conf.validate()
160+
161+
def test_no_protocol_version_warning(self, config, raise_error_on_warning):
162+
del config["service"]["idp"]["protocol_version"]
163+
164+
self.assert_validation_error(config)
165+
166+
def test_empty_protocol_version_warning(self, config, raise_error_on_warning):
167+
config["service"]["idp"]["protocol_version"] = ""
168+
169+
self.assert_validation_error(config)
170+
171+
def test_no_organization_info_warning(self, config, raise_error_on_warning):
172+
del config["organization"]
173+
174+
self.assert_validation_error(config)
175+
176+
def test_empty_organization_info_warning(self, config, raise_error_on_warning):
177+
config["organization"] = {}
178+
179+
self.assert_validation_error(config)
180+
181+
def test_no_technical_contact_person(self,
182+
config,
183+
technical_contacts,
184+
raise_error_on_warning):
185+
for contact in technical_contacts:
186+
contact["contact_type"] = "other"
187+
188+
self.assert_validation_error(config)
189+
190+
def test_technical_contact_person_no_email(self,
191+
config,
192+
technical_contacts,
193+
raise_error_on_warning):
194+
195+
for contact in technical_contacts:
196+
del contact["email_address"]
197+
198+
self.assert_validation_error(config)
199+
200+
def test_technical_contact_person_empty_email(self,
201+
config,
202+
technical_contacts,
203+
raise_error_on_warning):
204+
205+
for contact in technical_contacts:
206+
del contact["email_address"]
207+
208+
self.assert_validation_error(config)
209+
210+
def test_no_support_contact_person(self,
211+
config,
212+
support_contacts,
213+
raise_error_on_warning):
214+
for contact in support_contacts:
215+
contact["contact_type"] = "other"
216+
217+
self.assert_validation_error(config)
218+
219+
def test_support_contact_person_no_email(self,
220+
config,
221+
support_contacts,
222+
raise_error_on_warning):
223+
224+
for contact in support_contacts:
225+
del contact["email_address"]
226+
227+
self.assert_validation_error(config)
228+
229+
def test_support_contact_person_empty_email(self,
230+
config,
231+
support_contacts,
232+
raise_error_on_warning):
233+
234+
for contact in support_contacts:
235+
del contact["email_address"]
236+
237+
self.assert_validation_error(config)
238+
239+
def test_entityid_no_https(self, config):
240+
config["entityid"] = "urn:mace:example.com:saml:roland:idp"
241+
242+
self.assert_validation_error(config)

0 commit comments

Comments
 (0)