Skip to content

Commit 3525c60

Browse files
nsklikasc00kiemon5ter
authored andcommitted
Add freshness period feature for MetaDataMDX
Add a configurable period for which the metadata is valid, this is only available when using MDX.
1 parent c569598 commit 3525c60

File tree

2 files changed

+63
-16
lines changed

2 files changed

+63
-16
lines changed

src/saml2/mdstore.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from saml2.s_utils import UnknownSystemEntity
3333
from saml2.sigver import split_len
3434
from saml2.validate import valid_instance
35-
from saml2.time_util import valid
35+
from saml2.time_util import valid, instant, add_duration, before, str_to_time
3636
from saml2.validate import NotValid
3737
from saml2.sigver import security_context
3838
from saml2.extension.mdattr import NAMESPACE as NS_MDATTR
@@ -821,7 +821,7 @@ def sha1_entity_transform(entity_id):
821821
hashlib.sha1(entity_id.encode("utf-8")).hexdigest())
822822

823823
def __init__(self, url=None, security=None, cert=None,
824-
entity_transform=None, **kwargs):
824+
entity_transform=None, freshness_period=None, **kwargs):
825825
"""
826826
:params url: mdx service url
827827
:params security: SecurityContext()
@@ -831,6 +831,8 @@ def __init__(self, url=None, security=None, cert=None,
831831
hash) the entity id. It is applied to the entity id before it is
832832
concatenated with the request URL sent to the MDX server. Defaults to
833833
sha1 transformation.
834+
:params freshness_period: a duration in the format described at
835+
https://www.w3.org/TR/xmlschema-2/#duration
834836
"""
835837
super(MetaDataMDX, self).__init__(None, **kwargs)
836838
if not url:
@@ -845,6 +847,9 @@ def __init__(self, url=None, security=None, cert=None,
845847

846848
self.cert = cert
847849
self.security = security
850+
self.freshness_period = freshness_period
851+
if freshness_period:
852+
self.expiration_date = {}
848853

849854
# We assume that the MDQ server will return a single entity
850855
# described by a single <EntityDescriptor> element. The protocol
@@ -859,21 +864,37 @@ def load(self, *args, **kwargs):
859864
# Do nothing
860865
pass
861866

867+
def fetch_metadata(self, item):
868+
mdx_url = "%s/entities/%s" % (self.url, self.entity_transform(item))
869+
response = requests.get(mdx_url, headers={
870+
'Accept': SAML_METADATA_CONTENT_TYPE})
871+
if response.status_code == 200:
872+
_txt = response.content
873+
if self.parse_and_check_signature(_txt):
874+
if self.freshness_period:
875+
curr_time = str_to_time(instant())
876+
self.expiration_date[item] = add_duration(
877+
curr_time, self.freshness_period)
878+
return self.entity[item]
879+
else:
880+
logger.info("Response status: %s", response.status_code)
881+
raise KeyError
882+
883+
def _is_fresh(self, item):
884+
return self.freshness_period and before(self.expiration_date[item])
885+
862886
def __getitem__(self, item):
863-
try:
864-
return self.entity[item]
865-
except KeyError:
866-
mdx_url = "%s/entities/%s" % (self.url, self.entity_transform(item))
867-
response = requests.get(mdx_url, headers={
868-
'Accept': SAML_METADATA_CONTENT_TYPE})
869-
if response.status_code == 200:
870-
_txt = response.content
871-
872-
if self.parse_and_check_signature(_txt):
873-
return self.entity[item]
887+
if item in self.entity:
888+
if self._is_fresh(item):
889+
entity = self.entity[item]
874890
else:
875-
logger.info("Response status: %s", response.status_code)
876-
raise KeyError
891+
logger.info("Metadata for {} have expired, refreshing "
892+
"metadata".format(item))
893+
self.entity.pop(item, None)
894+
entity = self.fetch_metadata(item)
895+
else:
896+
entity = self.fetch_metadata(item)
897+
return entity
877898

878899
def single_sign_on_service(self, entity_id, binding=None, typ="idpsso"):
879900
if binding is None:
@@ -960,9 +981,11 @@ def load(self, *args, **kwargs):
960981
key = kwargs['url']
961982
url = kwargs['url']
962983
cert = kwargs.get('cert')
984+
freshness_period = kwargs.get('freshness_period', None)
963985
security = self.security
964986
entity_transform = kwargs.get('entity_transform', None)
965-
_md = MetaDataMDX(url, security, cert, entity_transform)
987+
_md = MetaDataMDX(url, security, cert, entity_transform,
988+
freshness_period=freshness_period)
966989
else:
967990
key = args[1]
968991
url = args[1]

tests/test_30_mdstore.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from pathutils import full_path
2828

2929
import responses
30+
import mock
3031

3132

3233
TESTS_DIR = os.path.dirname(__file__)
@@ -334,6 +335,29 @@ def test_mdx_single_sign_on_service():
334335
assert sso_loc[0]["location"] == "http://xenosmilus.umdc.umu.se/simplesaml/saml2/idp/metadata.php"
335336

336337

338+
@responses.activate
339+
@mock.patch('saml2.mdstore.before')
340+
def test_mdx_metadata_freshness_period(mock_datetime):
341+
"""Ensure that metadata is refreshed only when they have expired."""
342+
entity_id = \
343+
"http://xenosmilus.umdc.umu.se/simplesaml/saml2/idp/metadata.php"
344+
345+
url = "http://mdx.example.com/entities/{}".format(
346+
parse.quote_plus(MetaDataMDX.sha1_entity_transform(entity_id)))
347+
responses.add(responses.GET, url, body=TEST_METADATA_STRING, status=200,
348+
content_type=SAML_METADATA_CONTENT_TYPE)
349+
350+
mock_datetime.return_value = True
351+
mdx = MetaDataMDX("http://mdx.example.com",
352+
freshness_period="P0Y0M0DT0H2M0S")
353+
mdx.single_sign_on_service(entity_id, BINDING_HTTP_REDIRECT)
354+
mdx.single_sign_on_service(entity_id, BINDING_HTTP_REDIRECT)
355+
assert len(responses.calls) == 1
356+
mock_datetime.return_value = False
357+
mdx.single_sign_on_service(entity_id, BINDING_HTTP_REDIRECT)
358+
assert len(responses.calls) == 2
359+
360+
337361
# pyff-test not available
338362
# def test_mdx_service():
339363
# sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"])

0 commit comments

Comments
 (0)