Skip to content

Commit da7a62e

Browse files
committed
Add eIDAS SP config class and validation
- Adds validate method in Config class to be used for configuration validation checks - Adds eIDASConfig as base eIDAS config class to host commonly (between IdP and SP) used validations and functions - Adds eIDASSPConfig class and override validate method with endpoint and keydescriptor validations based on eidas v1.2 specs - Adds utility->config module to host config helper classes and functions - Adds new ConfigValidationError for config error signaling - Adds RuleValidator class to be used for config elements validation rule crafting - Adds should_warning and must_error functions for signaling warnings and errors related to element rules using RFC2119 wording
1 parent c6ddfa8 commit da7a62e

File tree

4 files changed

+153
-8
lines changed

4 files changed

+153
-8
lines changed

src/saml2/config.py

+50
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import re
99
import sys
10+
from functools import partial
1011

1112
import six
1213

@@ -21,6 +22,7 @@
2122
from saml2.mdstore import MetadataStore
2223
from saml2.saml import NAME_FORMAT_URI
2324
from saml2.virtual_org import VirtualOrg
25+
from saml2.utility.config import RuleValidator, should_warning, must_error
2426

2527
logger = logging.getLogger(__name__)
2628

@@ -542,6 +544,9 @@ def service_per_endpoint(self, context=None):
542544
res[endp] = (service, binding)
543545
return res
544546

547+
def validate(self):
548+
pass
549+
545550

546551
class SPConfig(Config):
547552
def_context = "sp"
@@ -571,13 +576,58 @@ def ecp_endpoint(self, ipaddress):
571576
return None
572577

573578

579+
class eIDASConfig(Config):
580+
@classmethod
581+
def assert_not_declared(cls, error_signal):
582+
return (lambda x: x is None,
583+
partial(error_signal, message="not be declared"))
584+
585+
@classmethod
586+
def assert_declared(cls, error_signal):
587+
return (lambda x: x is not None,
588+
partial(error_signal, message="be declared"))
589+
590+
591+
class eIDASSPConfig(SPConfig, eIDASConfig):
592+
def validate(self):
593+
validators = [
594+
RuleValidator(
595+
"single_logout_service",
596+
self._sp_endpoints.get("single_logout_service"),
597+
*self.assert_not_declared(should_warning)
598+
),
599+
RuleValidator(
600+
"artifact_resolution_service",
601+
self._sp_endpoints.get("artifact_resolution_service"),
602+
*self.assert_not_declared(should_warning)
603+
),
604+
RuleValidator(
605+
"manage_name_id_service",
606+
self._sp_endpoints.get("manage_name_id_service"),
607+
*self.assert_not_declared(should_warning)
608+
),
609+
RuleValidator(
610+
"KeyDescriptor",
611+
self.cert_file or self.encryption_keypairs,
612+
*self.assert_declared(must_error)
613+
)
614+
]
615+
616+
for validator in validators:
617+
validator.validate()
618+
619+
574620
class IdPConfig(Config):
575621
def_context = "idp"
576622

577623
def __init__(self):
578624
Config.__init__(self)
579625

580626

627+
class eIDASIdPConfig(IdPConfig):
628+
pass
629+
630+
581631
def config_factory(_type, config):
582632
"""
583633

src/saml2/utility/__init__.py

Whitespace-only changes.

src/saml2/utility/config.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import logging
2+
3+
4+
logger = logging.getLogger(__name__)
5+
6+
7+
class ConfigValidationError(Exception):
8+
pass
9+
10+
11+
class RuleValidator(object):
12+
def __init__(self, element_name, element_value, validator, error_signal):
13+
"""
14+
:param element_name: the name of the element that will be
15+
validated
16+
:param element_value: function to be called
17+
with config as parameter to fetch an element value
18+
:param validator: function to be called
19+
with a config element value as a parameter
20+
:param error_signal: function to be called
21+
with an element name and value to signal an error (can be a log
22+
function, raise an error etc)
23+
"""
24+
self.element_name = element_name
25+
self.element_value = element_value
26+
self.validator = validator
27+
self.error_signal = error_signal
28+
29+
def validate(self):
30+
if not self.validator(self.element_value):
31+
self.error_signal(self.element_name)
32+
33+
34+
def should_warning(element_name, message):
35+
logger.warning("{element} SHOULD {message}".format(
36+
element=element_name, message=message))
37+
38+
39+
def must_error(element_name, message):
40+
error = "{element} MUST {message}".format(
41+
element=element_name, message=message)
42+
logger.error(error)
43+
raise ConfigValidationError(error)

tests/eidas/test_sp.py

+60-8
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1+
import pytest
2+
import copy
3+
from saml2 import BINDING_HTTP_POST
14
from saml2 import metadata
25
from saml2 import samlp
36
from saml2.client import Saml2Client
47
from saml2.server import Server
5-
from saml2.config import SPConfig
8+
from saml2.config import eIDASSPConfig
69
from eidas.sp_conf import CONFIG
10+
from saml2.utility.config import ConfigValidationError
711

812

913
class TestSP:
1014
def setup_class(self):
1115
self.server = Server("idp_conf")
1216

13-
self.conf = SPConfig()
17+
self.conf = eIDASSPConfig()
1418
self.conf.load_file("sp_conf")
1519

1620
self.client = Saml2Client(self.conf)
1721

1822
def teardown_class(self):
1923
self.server.close()
2024

25+
@pytest.fixture(scope="function")
26+
def config(self):
27+
return copy.deepcopy(CONFIG)
28+
2129
def test_authn_request_force_authn(self):
2230
req_str = "{0}".format(self.client.create_authn_request(
2331
"http://www.example.com/sso", message_id="id1")[-1])
@@ -35,10 +43,10 @@ def test_sp_type_only_in_request(self):
3543
assert not any(filter(lambda x: x.tag == "SPType",
3644
entd.extensions.extension_elements))
3745

38-
def test_sp_type_in_metadata(self):
39-
CONFIG["service"]["sp"]["sp_type_in_metadata"] = True
40-
sconf = SPConfig()
41-
sconf.load(CONFIG)
46+
def test_sp_type_in_metadata(self, config):
47+
config["service"]["sp"]["sp_type_in_metadata"] = True
48+
sconf = eIDASSPConfig()
49+
sconf.load(config)
4250
custom_client = Saml2Client(sconf)
4351

4452
req_str = "{0}".format(custom_client.create_authn_request(
@@ -58,5 +66,49 @@ def test_node_country_in_metadata(self):
5866
entd.extensions.extension_elements))
5967

6068

61-
if __name__ == '__main__':
62-
TestSP()
69+
class TestSPConfig:
70+
@pytest.fixture(scope="function")
71+
def raise_error_on_warning(self, monkeypatch):
72+
def r(*args, **kwargs):
73+
raise ConfigValidationError()
74+
monkeypatch.setattr("saml2.utility.config.logger.warning", r)
75+
76+
@pytest.fixture(scope="function")
77+
def config(self):
78+
return copy.deepcopy(CONFIG)
79+
80+
def test_singlelogout_declared(self, config, raise_error_on_warning):
81+
config["service"]["sp"]["endpoints"]["single_logout_service"] = \
82+
[("https://example.com", BINDING_HTTP_POST)]
83+
conf = eIDASSPConfig()
84+
conf.load(config)
85+
86+
with pytest.raises(ConfigValidationError):
87+
conf.validate()
88+
89+
def test_artifact_resolution_declared(self, config, raise_error_on_warning):
90+
config["service"]["sp"]["endpoints"]["artifact_resolution_service"] = \
91+
[("https://example.com", BINDING_HTTP_POST)]
92+
conf = eIDASSPConfig()
93+
conf.load(config)
94+
95+
with pytest.raises(ConfigValidationError):
96+
conf.validate()
97+
98+
def test_manage_nameid_service_declared(self, config, raise_error_on_warning):
99+
config["service"]["sp"]["endpoints"]["manage_name_id_service"] = \
100+
[("https://example.com", BINDING_HTTP_POST)]
101+
conf = eIDASSPConfig()
102+
conf.load(config)
103+
104+
with pytest.raises(ConfigValidationError):
105+
conf.validate()
106+
107+
def test_no_keydescriptor(self, config):
108+
del config["cert_file"]
109+
del config["encryption_keypairs"]
110+
conf = eIDASSPConfig()
111+
conf.load(config)
112+
113+
with pytest.raises(ConfigValidationError):
114+
conf.validate()

0 commit comments

Comments
 (0)