Skip to content

Commit 63b2faa

Browse files
Merge pull request #888 from johanlundberg/lundberg_treat_requested_subject_id_as_attribute
Add support for subject-id requirements signalling in metadata
2 parents 2a8dd85 + 5bd9ec4 commit 63b2faa

File tree

5 files changed

+65
-4
lines changed

5 files changed

+65
-4
lines changed

src/saml2/assertion.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,11 +556,16 @@ def restrict(self, ava, sp_entity_id, metadata=None):
556556

557557
metadata_store = metadata or self.metadata_store
558558
spec = metadata_store.attribute_requirement(sp_entity_id) or {} if metadata_store else {}
559+
required_attributes = spec.get("required", [])
560+
optional_attributes = spec.get("optional", [])
561+
required_subject_id = metadata_store.subject_id_requirement(sp_entity_id) if metadata_store else None
562+
if required_subject_id and required_subject_id not in required_attributes:
563+
required_attributes.append(required_subject_id)
559564
return self.filter(
560565
ava,
561566
sp_entity_id,
562-
required=spec.get("required"),
563-
optional=spec.get("optional"),
567+
required=required_attributes or None,
568+
optional=optional_attributes or None,
564569
)
565570

566571
def conditions(self, sp_entity_id):

src/saml2/mdstore.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,17 @@ def attribute_requirement(self, entity_id, index=None):
418418
"""
419419
raise NotImplementedError
420420

421+
def subject_id_requirement(self, entity_id):
422+
"""
423+
Returns what subject identifier the SP requires if any
424+
425+
:param entity_id: The entity id of the SP
426+
:type entity_id: str
427+
:return: RequestedAttribute dict or None
428+
:rtype: Optional[dict]
429+
"""
430+
raise NotImplementedError
431+
421432
def dumps(self):
422433
return json.dumps(list(self.items()), indent=2)
423434

@@ -1290,6 +1301,32 @@ def attribute_requirement(self, entity_id, index=None):
12901301
if entity_id in _md:
12911302
return _md.attribute_requirement(entity_id, index)
12921303

1304+
def subject_id_requirement(self, entity_id):
1305+
try:
1306+
entity_attributes = self.entity_attributes(entity_id)
1307+
except KeyError:
1308+
return None
1309+
1310+
if "urn:oasis:names:tc:SAML:profiles:subject-id:req" in entity_attributes:
1311+
subject_id_req = entity_attributes["urn:oasis:names:tc:SAML:profiles:subject-id:req"][0]
1312+
if subject_id_req == "any" or subject_id_req == "pairwise-id":
1313+
return {
1314+
"__class__": "urn:oasis:names:tc:SAML:2.0:metadata&RequestedAttribute",
1315+
"name": "urn:oasis:names:tc:SAML:attribute:pairwise-id",
1316+
"name_format": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
1317+
"friendly_name": "pairwise-id",
1318+
"is_required": "true",
1319+
}
1320+
elif subject_id_req == "subject-id":
1321+
return {
1322+
"__class__": "urn:oasis:names:tc:SAML:2.0:metadata&RequestedAttribute",
1323+
"name": "urn:oasis:names:tc:SAML:attribute:subject-id",
1324+
"name_format": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
1325+
"friendly_name": "subject-id",
1326+
"is_required": "true",
1327+
}
1328+
return None
1329+
12931330
def keys(self):
12941331
res = []
12951332
for _md in self.metadata.values():

tests/entity_esi_and_coco_sp.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<saml:AttributeValue>https://myacademicid.org/entity-categories/esi</saml:AttributeValue>
88
<saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
99
</saml:Attribute>
10+
<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">
11+
<saml:AttributeValue>any</saml:AttributeValue>
12+
</saml:Attribute>
1013
</mdattr:EntityAttributes></ns0:Extensions>
1114
<ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
1215
<ns0:KeyDescriptor use="encryption">
@@ -65,7 +68,7 @@ wHyaxzYldWmVC5omkgZeAdCGpJ316GQF8Zwg/yDOUzm4cvGeIESf1Q6ZxBwI6zGE
6568
</ns0:KeyDescriptor>
6669
<ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://esi-coco.example.edu/saml2/ls/"/>
6770
<ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://esi-coco.example.edu/saml2/acs/" index="1"/>
68-
<!-- Require eduPersonTargetedID -->
71+
<!-- Require schacHomeOrganization and eduPersonScopedAffiliation -->
6972
<ns0:AttributeConsumingService index="0">
7073
<ns0:ServiceName xml:lang="en">esi-coco-SP</ns0:ServiceName>
7174
<ns0:ServiceDescription xml:lang="en">ESI and COCO SP</ns0:ServiceDescription>

tests/entity_personalized_sp.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ wHyaxzYldWmVC5omkgZeAdCGpJ316GQF8Zwg/yDOUzm4cvGeIESf1Q6ZxBwI6zGE
6464
</ns0:KeyDescriptor>
6565
<ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://personalized.example.edu/saml2/ls/"/>
6666
<ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://personalized.example.edu/saml2/acs/" index="1"/>
67-
<!-- Require eduPersonTargetedID -->
6867
<ns0:AttributeConsumingService index="0">
6968
<ns0:ServiceName xml:lang="en">personalized-SP</ns0:ServiceName>
7069
<ns0:ServiceDescription xml:lang="en">refeds personalized access SP</ns0:ServiceDescription>

tests/test_30_mdstore.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@
189189
"metadata": [(full_path("empty_metadata_file.xml"),)],
190190
}
191191
],
192+
"17": [
193+
{
194+
"class": "saml2.mdstore.MetaDataFile",
195+
"metadata": [(full_path("entity_esi_and_coco_sp.xml"),)],
196+
}
197+
],
192198
}
193199

194200

@@ -654,6 +660,17 @@ def test_registration_info_no_policy():
654660
assert registration_info["registration_policy"] == {}
655661

656662

663+
def test_subject_id_requirement():
664+
mds = MetadataStore(ATTRCONV, sec_config, disable_ssl_certificate_validation=True)
665+
mds.imp(METADATACONF["17"])
666+
required_subject_id = mds.subject_id_requirement(entity_id="https://esi-coco.example.edu/saml2/metadata/")
667+
assert required_subject_id["__class__"] == "urn:oasis:names:tc:SAML:2.0:metadata&RequestedAttribute"
668+
assert required_subject_id["name"] == "urn:oasis:names:tc:SAML:attribute:pairwise-id"
669+
assert required_subject_id["name_format"] == "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
670+
assert required_subject_id["friendly_name"] == "pairwise-id"
671+
assert required_subject_id["is_required"] == "true"
672+
673+
657674
def test_extension():
658675
mds = MetadataStore(ATTRCONV, None)
659676
# use ordered dict to force expected entity to be last

0 commit comments

Comments
 (0)