summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst3
-rw-r--r--src/saml2/assertion.py7
-rw-r--r--src/saml2/attributemaps/saml_uri.py8
-rw-r--r--src/saml2/client_base.py4
-rw-r--r--src/saml2/ident.py23
-rw-r--r--src/saml2/mdstore.py2
-rw-r--r--src/saml2/mongo_store.py23
-rw-r--r--tests/entity_cat_rs.xml84
-rw-r--r--tests/myentitycategory.py16
-rw-r--r--tests/test_37_entity_categories.py39
-rw-r--r--tests/test_50_server.py4
-rw-r--r--tests/test_51_client.py4
12 files changed, 204 insertions, 13 deletions
diff --git a/README.rst b/README.rst
index 51a5e096..d67d980f 100644
--- a/README.rst
+++ b/README.rst
@@ -21,6 +21,9 @@ provider. The distribution contains examples of both. Originally written to
work in a WSGI environment there are extensions that allow you to use it with
other frameworks.
+Install
+=======
+You can install with `pip install pysaml2`
Testing
=======
diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py
index 8984db59..ed043d9a 100644
--- a/src/saml2/assertion.py
+++ b/src/saml2/assertion.py
@@ -353,8 +353,11 @@ class Policy(object):
else:
ecs = []
for cat in items:
- _mod = importlib.import_module(
- "saml2.entity_category.%s" % cat)
+ try:
+ _mod = importlib.import_module(cat)
+ except ImportError:
+ _mod = importlib.import_module(
+ "saml2.entity_category.%s" % cat)
_ec = {}
for key, items in _mod.RELEASE.items():
alist = [k.lower() for k in items]
diff --git a/src/saml2/attributemaps/saml_uri.py b/src/saml2/attributemaps/saml_uri.py
index 40f7b778..608fcc28 100644
--- a/src/saml2/attributemaps/saml_uri.py
+++ b/src/saml2/attributemaps/saml_uri.py
@@ -23,6 +23,10 @@ OPENOSI_OID = 'urn:oid:1.3.6.1.4.1.27630.2.1.1.'
EIDAS_NATURALPERSON = 'http://eidas.europa.eu/attributes/naturalperson/'
EIDAS_LEGALPERSON = 'http://eidas.europa.eu/attributes/legalperson/'
+# SAML subject id specification
+# https://docs.oasis-open.org/security/saml-subject-id-attr/v1.0/cs01/saml-subject-id-attr-v1.0-cs01.html
+SAML_SUBJECT_ID = 'urn:oasis:names:tc:SAML:attribute:'
+
MAP = {
'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
'fro': {
@@ -109,6 +113,8 @@ MAP = {
OPENOSI_OID+'109': 'osiOtherHomePhone',
OPENOSI_OID+'120': 'osiWorkURL',
PKCS_9+'1': 'email',
+ SAML_SUBJECT_ID+'subject-id': 'subject-id',
+ SAML_SUBJECT_ID+'pairwise-id': 'pairwise-id',
SCHAC+'1': 'schacMotherTongue',
SCHAC+'2': 'schacGender',
SCHAC+'3': 'schacDateOfBirth',
@@ -280,6 +286,7 @@ MAP = {
'osiWorkURL': OPENOSI_OID+'120',
'ou': X500ATTR_OID+'11',
'owner': X500ATTR_OID+'32',
+ 'pairwise-id': SAML_SUBJECT_ID+'pairwise-id',
'physicalDeliveryOfficeName': X500ATTR_OID+'19',
'postOfficeBox': X500ATTR_OID+'18',
'postalAddress': X500ATTR_OID+'16',
@@ -337,6 +344,7 @@ MAP = {
'sn': X500ATTR_OID+'4',
'st': X500ATTR_OID+'8',
'street': X500ATTR_OID+'9',
+ 'subject-id': SAML_SUBJECT_ID+'subject-id',
'supportedAlgorithms': X500ATTR_OID+'52',
'supportedApplicationContext': X500ATTR_OID+'30',
'telephoneNumber': X500ATTR_OID+'20',
diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py
index 39a7d0ed..15e3b0ec 100644
--- a/src/saml2/client_base.py
+++ b/src/saml2/client_base.py
@@ -339,6 +339,10 @@ class Base(Entity):
# If no nameid_format has been set in the configuration
# or passed in then transient is the default.
if nameid_format is None:
+ # SAML 2.0 errata says AllowCreate MUST NOT be used for
+ # transient ids - to make a conservative change this is
+ # only applied for the default cause
+ allow_create = None
nameid_format = NAMEID_FORMAT_TRANSIENT
# If a list has been configured or passed in choose the
diff --git a/src/saml2/ident.py b/src/saml2/ident.py
index db8365bc..d6a6620a 100644
--- a/src/saml2/ident.py
+++ b/src/saml2/ident.py
@@ -155,6 +155,16 @@ class IdentDB(object):
pass
def get_nameid(self, userid, nformat, sp_name_qualifier, name_qualifier):
+ if nformat == NAMEID_FORMAT_PERSISTENT:
+ nameid = self.match_local_id(userid, sp_name_qualifier, name_qualifier)
+ if nameid:
+ logger.debug(
+ "Found existing persistent NameId {nid} for user {uid}".format(
+ nid=nameid, uid=userid
+ )
+ )
+ return nameid
+
_id = self.create_id(nformat, name_qualifier, sp_name_qualifier)
if nformat == NAMEID_FORMAT_EMAILADDRESS:
@@ -163,11 +173,12 @@ class IdentDB(object):
_id = "%s@%s" % (_id, self.domain)
- # if nformat == NAMEID_FORMAT_PERSISTENT:
- # _id = userid
-
- nameid = NameID(format=nformat, sp_name_qualifier=sp_name_qualifier,
- name_qualifier=name_qualifier, text=_id)
+ nameid = NameID(
+ format=nformat,
+ sp_name_qualifier=sp_name_qualifier,
+ name_qualifier=name_qualifier,
+ text=_id,
+ )
self.store(userid, nameid)
return nameid
@@ -236,7 +247,7 @@ class IdentDB(object):
def construct_nameid(self, userid, local_policy=None,
sp_name_qualifier=None, name_id_policy=None,
name_qualifier=""):
- """ Returns a name_id for the object. How the name_id is
+ """ Returns a name_id for the userid. How the name_id is
constructed depends on the context.
:param local_policy: The policy the server is configured to follow
diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py
index f5ffbb41..a92414c5 100644
--- a/src/saml2/mdstore.py
+++ b/src/saml2/mdstore.py
@@ -720,7 +720,7 @@ class MetaDataLoader(MetaDataFile):
class MetaDataExtern(InMemoryMetaData):
"""
Class that handles metadata store somewhere on the net.
- Accessible but HTTP GET.
+ Accessible by HTTP GET.
"""
def __init__(self, attrc, url=None, security=None, cert=None,
diff --git a/src/saml2/mongo_store.py b/src/saml2/mongo_store.py
index 6a8f9f45..903dd481 100644
--- a/src/saml2/mongo_store.py
+++ b/src/saml2/mongo_store.py
@@ -1,3 +1,4 @@
+import datetime
from hashlib import sha1
import logging
@@ -5,6 +6,8 @@ from pymongo import MongoClient
from pymongo.mongo_replica_set_client import MongoReplicaSetClient
import pymongo.uri_parser
import pymongo.errors
+from saml2.saml import NAMEID_FORMAT_PERSISTENT
+
from saml2.eptid import Eptid
from saml2.mdstore import InMemoryMetaData
from saml2.mdstore import metadata_modules
@@ -163,6 +166,23 @@ class IdentMDB(IdentDB):
return item[self.mdb.primary_key]
return None
+ def match_local_id(self, userid, sp_name_qualifier, name_qualifier):
+ """
+ Match a local persistent identifier.
+
+ Look for an existing persistent NameID matching userid,
+ sp_name_qualifier and name_qualifier.
+ """
+ filter = {
+ "name_id.sp_name_qualifier": sp_name_qualifier,
+ "name_id.name_qualifier": name_qualifier,
+ "name_id.format": NAMEID_FORMAT_PERSISTENT,
+ }
+ res = self.mdb.get(value=userid, **filter)
+ if not res:
+ return None
+ return from_dict(res[0]["name_id"], ONTS, True)
+
def remove_remote(self, name_id):
cnid = to_dict(name_id, MMODS, True)
self.mdb.remove(name_id=cnid)
@@ -192,6 +212,9 @@ class MDB(object):
else:
doc = {}
doc.update(kwargs)
+ # Add timestamp to all documents to allow external garbage collecting
+ if "created_at" not in doc:
+ doc["created_at"] = datetime.datetime.utcnow()
_ = self.db.insert(doc)
def get(self, value=None, **kwargs):
diff --git a/tests/entity_cat_rs.xml b/tests/entity_cat_rs.xml
new file mode 100644
index 00000000..5f3e00f8
--- /dev/null
+++ b/tests/entity_cat_rs.xml
@@ -0,0 +1,84 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:ns1="urn:oasis:names:tc:SAML:metadata:attribute"
+ xmlns:ns2="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:ns5="http://www.w3.org/2000/09/xmldsig#"
+ xmlns:ns4="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ entityID="urn:mace:example.com:saml:roland:sp">
+ <ns0:Extensions>
+ <ns1:EntityAttributes>
+ <ns2:Attribute Name="http://macedir.org/entity-category">
+ <ns2:AttributeValue xsi:type="xs:string">
+ http://refeds.org/category/research-and-scholarship
+ </ns2:AttributeValue>
+ </ns2:Attribute>
+ </ns1:EntityAttributes>
+ </ns0:Extensions>
+ <ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true"
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <ns0:Extensions>
+ <ns4:DiscoveryResponse
+ Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
+ Location="https://xenosmilus2.umdc.umu.se:8086/disco"
+ index="1"/>
+ </ns0:Extensions>
+ <ns0:KeyDescriptor use="encryption">
+ <ns5:KeyInfo>
+ <ns5:X509Data>
+ <ns5:X509Certificate>
+ MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV
+ BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx
+ EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz
+ MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l
+ YTEYMBYGA1UEChMPVW1lYSBVbml2ZXJzaXR5MRAwDgYDVQQLEwdJVCBVbml0MRAw
+ DgYDVQQDEwdUZXN0IFNQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkJWP7
+ bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr6/ROgW96ZeQ57fzVy2MC
+ FiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43qCfLx+clUlOvtnsoMiiR
+ mo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQABo4G/MIG8MB0GA1UdDgQW
+ BBQ7RgbMJFDGRBu9o3tDQDuSoBy7JjCBjAYDVR0jBIGEMIGBgBQ7RgbMJFDGRBu9
+ o3tDQDuSoBy7JqFepFwwWjELMAkGA1UEBhMCU0UxDTALBgNVBAcTBFVtZWExGDAW
+ BgNVBAoTD1VtZWEgVW5pdmVyc2l0eTEQMA4GA1UECxMHSVQgVW5pdDEQMA4GA1UE
+ AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
+ BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO
+ zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN
+ +vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI=
+ </ns5:X509Certificate>
+ </ns5:X509Data>
+ </ns5:KeyInfo>
+ </ns0:KeyDescriptor>
+ <ns0:KeyDescriptor use="signing">
+ <ns5:KeyInfo>
+ <ns5:X509Data>
+ <ns5:X509Certificate>
+ MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV
+ BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx
+ EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz
+ MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l
+ YTEYMBYGA1UEChMPVW1lYSBVbml2ZXJzaXR5MRAwDgYDVQQLEwdJVCBVbml0MRAw
+ DgYDVQQDEwdUZXN0IFNQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkJWP7
+ bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr6/ROgW96ZeQ57fzVy2MC
+ FiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43qCfLx+clUlOvtnsoMiiR
+ mo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQABo4G/MIG8MB0GA1UdDgQW
+ BBQ7RgbMJFDGRBu9o3tDQDuSoBy7JjCBjAYDVR0jBIGEMIGBgBQ7RgbMJFDGRBu9
+ o3tDQDuSoBy7JqFepFwwWjELMAkGA1UEBhMCU0UxDTALBgNVBAcTBFVtZWExGDAW
+ BgNVBAoTD1VtZWEgVW5pdmVyc2l0eTEQMA4GA1UECxMHSVQgVW5pdDEQMA4GA1UE
+ AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
+ BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO
+ zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN
+ +vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI=
+ </ns5:X509Certificate>
+ </ns5:X509Data>
+ </ns5:KeyInfo>
+ </ns0:KeyDescriptor>
+ <ns0:AssertionConsumerService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://xenosmilus2.umdc.umu.se:8086/acs/sfs/re_nren/redirect"
+ index="1"/>
+ <ns0:AssertionConsumerService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ Location="https://xenosmilus2.umdc.umu.se:8086/acs/sfs/re_nren/post"
+ index="2"/>
+ </ns0:SPSSODescriptor>
+</ns0:EntityDescriptor>
diff --git a/tests/myentitycategory.py b/tests/myentitycategory.py
new file mode 100644
index 00000000..9ec55bf9
--- /dev/null
+++ b/tests/myentitycategory.py
@@ -0,0 +1,16 @@
+CUSTOM_R_AND_S = ['eduPersonTargetedID',
+ 'eduPersonPrincipalName',
+ 'mail',
+ 'displayName',
+ 'givenName',
+ 'sn',
+ 'eduPersonScopedAffiliation',
+ 'eduPersonUniqueId'
+ ]
+
+RESEARCH_AND_SCHOLARSHIP = "http://refeds.org/category/research-and-scholarship"
+
+RELEASE = {
+ "": ["eduPersonTargetedID"],
+ RESEARCH_AND_SCHOLARSHIP: CUSTOM_R_AND_S,
+}
diff --git a/tests/test_37_entity_categories.py b/tests/test_37_entity_categories.py
index 625caaa1..839030fd 100644
--- a/tests/test_37_entity_categories.py
+++ b/tests/test_37_entity_categories.py
@@ -152,5 +152,44 @@ def test_idp_policy_filter():
"eduPersonTargetedID"] # because no entity category
+def test_entity_category_import_from_path():
+ # The entity category module myentitycategory.py is in the tests
+ # directory which is on the standard module search path.
+ # The module uses a custom interpretation of the REFEDs R&S entity category
+ # by adding eduPersonUniqueId.
+ policy = Policy({
+ "default": {
+ "lifetime": {"minutes": 15},
+ "entity_categories": ["myentitycategory"]
+ }
+ })
+
+ mds = MetadataStore(ATTRCONV, sec_config,
+ disable_ssl_certificate_validation=True)
+
+ # The file entity_cat_rs.xml contains the SAML metadata for an SP
+ # tagged with the REFEDs R&S entity category.
+ mds.imp([{"class": "saml2.mdstore.MetaDataFile",
+ "metadata": [(full_path("entity_cat_rs.xml"),)]}])
+
+ ava = {"givenName": ["Derek"], "sn": ["Jeter"],
+ "displayName": "Derek Jeter",
+ "mail": ["derek@nyy.mlb.com"], "c": ["USA"],
+ "eduPersonTargetedID": "foo!bar!xyz",
+ "eduPersonUniqueId": "R13ET7UD68K0HGR153KE@my.org",
+ "eduPersonScopedAffiliation": "member@my.org",
+ "eduPersonPrincipalName": "user01@my.org",
+ "norEduPersonNIN": "19800101134"}
+
+ ava = policy.filter(ava, "urn:mace:example.com:saml:roland:sp", mds)
+
+ # We expect c and norEduPersonNIN to be filtered out since they are not
+ # part of the custom entity category.
+ assert _eq(list(ava.keys()),
+ ["eduPersonTargetedID", "eduPersonPrincipalName",
+ "eduPersonUniqueId", "displayName", "givenName",
+ "eduPersonScopedAffiliation", "mail", "sn"])
+
+
if __name__ == "__main__":
test_filter_ava3()
diff --git a/tests/test_50_server.py b/tests/test_50_server.py
index dc6cbf42..ecef319e 100644
--- a/tests/test_50_server.py
+++ b/tests/test_50_server.py
@@ -267,7 +267,7 @@ class TestServer1():
assert resp_args["destination"] == "http://lingon.catalogix.se:8087/"
assert resp_args["in_response_to"] == "id1"
name_id_policy = resp_args["name_id_policy"]
- assert _eq(name_id_policy.keyswv(), ["format", "allow_create"])
+ assert _eq(name_id_policy.keyswv(), ["format"])
assert name_id_policy.format == saml.NAMEID_FORMAT_TRANSIENT
assert resp_args[
"sp_entity_id"] == "urn:mace:example.com:saml:roland:sp"
@@ -1341,7 +1341,7 @@ class TestServer1NonAsciiAva():
assert resp_args["destination"] == "http://lingon.catalogix.se:8087/"
assert resp_args["in_response_to"] == "id1"
name_id_policy = resp_args["name_id_policy"]
- assert _eq(name_id_policy.keyswv(), ["format", "allow_create"])
+ assert _eq(name_id_policy.keyswv(), ["format"])
assert name_id_policy.format == saml.NAMEID_FORMAT_TRANSIENT
assert resp_args[
"sp_entity_id"] == "urn:mace:example.com:saml:roland:sp"
diff --git a/tests/test_51_client.py b/tests/test_51_client.py
index 2e2b7f1c..75dd8f75 100644
--- a/tests/test_51_client.py
+++ b/tests/test_51_client.py
@@ -269,7 +269,7 @@ class TestClient:
assert ar.provider_name == "urn:mace:example.com:saml:roland:sp"
assert ar.issuer.text == "urn:mace:example.com:saml:roland:sp"
nid_policy = ar.name_id_policy
- assert nid_policy.allow_create == "false"
+ assert nid_policy.allow_create is None
assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT
node_requested_attributes = None
@@ -1757,7 +1757,7 @@ class TestClientNonAsciiAva:
assert ar.provider_name == "urn:mace:example.com:saml:roland:sp"
assert ar.issuer.text == "urn:mace:example.com:saml:roland:sp"
nid_policy = ar.name_id_policy
- assert nid_policy.allow_create == "false"
+ assert nid_policy.allow_create is None
assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT
node_requested_attributes = None