summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Kanakarakis <ivan.kanak@gmail.com>2022-12-11 19:44:42 +0200
committerGitHub <noreply@github.com>2022-12-11 19:44:42 +0200
commit63b2faa5a2365aa94a88a390d214234c814643d1 (patch)
tree674fb6dd2fb38e3bf7bb3a7ec2c15293453614a1
parent2a8dd85ea2cb2631391b3efa3113b9f3f6779028 (diff)
parent5bd9ec44e7fbfd7017ac9762b2e97d1e31db9368 (diff)
downloadpysaml2-63b2faa5a2365aa94a88a390d214234c814643d1.tar.gz
Merge pull request #888 from johanlundberg/lundberg_treat_requested_subject_id_as_attribute
Add support for subject-id requirements signalling in metadata
-rw-r--r--src/saml2/assertion.py9
-rw-r--r--src/saml2/mdstore.py37
-rw-r--r--tests/entity_esi_and_coco_sp.xml5
-rw-r--r--tests/entity_personalized_sp.xml1
-rw-r--r--tests/test_30_mdstore.py17
5 files changed, 65 insertions, 4 deletions
diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py
index 53f917be..46733f93 100644
--- a/src/saml2/assertion.py
+++ b/src/saml2/assertion.py
@@ -556,11 +556,16 @@ class Policy:
metadata_store = metadata or self.metadata_store
spec = metadata_store.attribute_requirement(sp_entity_id) or {} if metadata_store else {}
+ required_attributes = spec.get("required", [])
+ optional_attributes = spec.get("optional", [])
+ required_subject_id = metadata_store.subject_id_requirement(sp_entity_id) if metadata_store else None
+ if required_subject_id and required_subject_id not in required_attributes:
+ required_attributes.append(required_subject_id)
return self.filter(
ava,
sp_entity_id,
- required=spec.get("required"),
- optional=spec.get("optional"),
+ required=required_attributes or None,
+ optional=optional_attributes or None,
)
def conditions(self, sp_entity_id):
diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py
index b2bae0a7..7519a20e 100644
--- a/src/saml2/mdstore.py
+++ b/src/saml2/mdstore.py
@@ -418,6 +418,17 @@ class MetaData:
"""
raise NotImplementedError
+ def subject_id_requirement(self, entity_id):
+ """
+ Returns what subject identifier the SP requires if any
+
+ :param entity_id: The entity id of the SP
+ :type entity_id: str
+ :return: RequestedAttribute dict or None
+ :rtype: Optional[dict]
+ """
+ raise NotImplementedError
+
def dumps(self):
return json.dumps(list(self.items()), indent=2)
@@ -1290,6 +1301,32 @@ class MetadataStore(MetaData):
if entity_id in _md:
return _md.attribute_requirement(entity_id, index)
+ def subject_id_requirement(self, entity_id):
+ try:
+ entity_attributes = self.entity_attributes(entity_id)
+ except KeyError:
+ return None
+
+ if "urn:oasis:names:tc:SAML:profiles:subject-id:req" in entity_attributes:
+ subject_id_req = entity_attributes["urn:oasis:names:tc:SAML:profiles:subject-id:req"][0]
+ if subject_id_req == "any" or subject_id_req == "pairwise-id":
+ return {
+ "__class__": "urn:oasis:names:tc:SAML:2.0:metadata&RequestedAttribute",
+ "name": "urn:oasis:names:tc:SAML:attribute:pairwise-id",
+ "name_format": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ "friendly_name": "pairwise-id",
+ "is_required": "true",
+ }
+ elif subject_id_req == "subject-id":
+ return {
+ "__class__": "urn:oasis:names:tc:SAML:2.0:metadata&RequestedAttribute",
+ "name": "urn:oasis:names:tc:SAML:attribute:subject-id",
+ "name_format": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ "friendly_name": "subject-id",
+ "is_required": "true",
+ }
+ return None
+
def keys(self):
res = []
for _md in self.metadata.values():
diff --git a/tests/entity_esi_and_coco_sp.xml b/tests/entity_esi_and_coco_sp.xml
index a076535b..f4e0ccbb 100644
--- a/tests/entity_esi_and_coco_sp.xml
+++ b/tests/entity_esi_and_coco_sp.xml
@@ -7,6 +7,9 @@
<saml:AttributeValue>https://myacademicid.org/entity-categories/esi</saml:AttributeValue>
<saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
</saml:Attribute>
+ <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="urn:oasis:names:tc:SAML:profiles:subject-id:req" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>any</saml:AttributeValue>
+ </saml:Attribute>
</mdattr:EntityAttributes></ns0:Extensions>
<ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<ns0:KeyDescriptor use="encryption">
@@ -65,7 +68,7 @@ wHyaxzYldWmVC5omkgZeAdCGpJ316GQF8Zwg/yDOUzm4cvGeIESf1Q6ZxBwI6zGE
</ns0:KeyDescriptor>
<ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://esi-coco.example.edu/saml2/ls/"/>
<ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://esi-coco.example.edu/saml2/acs/" index="1"/>
- <!-- Require eduPersonTargetedID -->
+ <!-- Require schacHomeOrganization and eduPersonScopedAffiliation -->
<ns0:AttributeConsumingService index="0">
<ns0:ServiceName xml:lang="en">esi-coco-SP</ns0:ServiceName>
<ns0:ServiceDescription xml:lang="en">ESI and COCO SP</ns0:ServiceDescription>
diff --git a/tests/entity_personalized_sp.xml b/tests/entity_personalized_sp.xml
index a6bfb46b..aa48693a 100644
--- a/tests/entity_personalized_sp.xml
+++ b/tests/entity_personalized_sp.xml
@@ -64,7 +64,6 @@ wHyaxzYldWmVC5omkgZeAdCGpJ316GQF8Zwg/yDOUzm4cvGeIESf1Q6ZxBwI6zGE
</ns0:KeyDescriptor>
<ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://personalized.example.edu/saml2/ls/"/>
<ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://personalized.example.edu/saml2/acs/" index="1"/>
- <!-- Require eduPersonTargetedID -->
<ns0:AttributeConsumingService index="0">
<ns0:ServiceName xml:lang="en">personalized-SP</ns0:ServiceName>
<ns0:ServiceDescription xml:lang="en">refeds personalized access SP</ns0:ServiceDescription>
diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py
index 1c67a701..013a6062 100644
--- a/tests/test_30_mdstore.py
+++ b/tests/test_30_mdstore.py
@@ -189,6 +189,12 @@ METADATACONF = {
"metadata": [(full_path("empty_metadata_file.xml"),)],
}
],
+ "17": [
+ {
+ "class": "saml2.mdstore.MetaDataFile",
+ "metadata": [(full_path("entity_esi_and_coco_sp.xml"),)],
+ }
+ ],
}
@@ -654,6 +660,17 @@ def test_registration_info_no_policy():
assert registration_info["registration_policy"] == {}
+def test_subject_id_requirement():
+ mds = MetadataStore(ATTRCONV, sec_config, disable_ssl_certificate_validation=True)
+ mds.imp(METADATACONF["17"])
+ required_subject_id = mds.subject_id_requirement(entity_id="https://esi-coco.example.edu/saml2/metadata/")
+ assert required_subject_id["__class__"] == "urn:oasis:names:tc:SAML:2.0:metadata&RequestedAttribute"
+ assert required_subject_id["name"] == "urn:oasis:names:tc:SAML:attribute:pairwise-id"
+ assert required_subject_id["name_format"] == "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
+ assert required_subject_id["friendly_name"] == "pairwise-id"
+ assert required_subject_id["is_required"] == "true"
+
+
def test_extension():
mds = MetadataStore(ATTRCONV, None)
# use ordered dict to force expected entity to be last