Rewrote to use NameID instances every where where I previously used just the text part of the instance.

This commit is contained in:
Roland Hedberg 2013-02-09 18:57:26 +01:00
parent 71246a3829
commit f295e06ab7
21 changed files with 514 additions and 689 deletions

View File

@ -31,7 +31,6 @@ from saml2.saml import AUTHN_PASSWORD
logger = logging.getLogger("saml2.idp") logger = logging.getLogger("saml2.idp")
def _expiration(timeout, tformat="%a, %d-%b-%Y %H:%M:%S GMT"): def _expiration(timeout, tformat="%a, %d-%b-%Y %H:%M:%S GMT"):
""" """
@ -143,7 +142,9 @@ class Service(object):
""" """
Single log out using HTTP_SOAP binding Single log out using HTTP_SOAP binding
""" """
logger.debug("- SOAP -")
_dict = self.unpack_soap() _dict = self.unpack_soap()
logger.debug("_dict: %s" % _dict)
return self.operation(_dict, BINDING_SOAP) return self.operation(_dict, BINDING_SOAP)
def uri(self): def uri(self):
@ -424,7 +425,9 @@ class SLO(Service):
def do(self, request, binding, relay_state=""): def do(self, request, binding, relay_state=""):
logger.info("--- Single Log Out Service ---") logger.info("--- Single Log Out Service ---")
try: try:
req_info = IDP.parse_logout_request(request, binding) _, body = request.split("\n")
logger.debug("req: '%s'" % body)
req_info = IDP.parse_logout_request(body, binding)
except Exception, exc: except Exception, exc:
logger.error("Bad request: %s" % exc) logger.error("Bad request: %s" % exc)
resp = BadRequest("%s" % exc) resp = BadRequest("%s" % exc)

View File

@ -79,7 +79,7 @@ CONFIG={
"name_form": NAME_FORMAT_URI "name_form": NAME_FORMAT_URI
}, },
}, },
"subject_data": "./idp.subject.db", "subject_data": "./idp.subject",
"name_id_format": [NAMEID_FORMAT_TRANSIENT, "name_id_format": [NAMEID_FORMAT_TRANSIENT,
NAMEID_FORMAT_PERSISTENT] NAMEID_FORMAT_PERSISTENT]
}, },
@ -88,7 +88,7 @@ CONFIG={
"key_file" : "pki/mykey.pem", "key_file" : "pki/mykey.pem",
"cert_file" : "pki/mycert.pem", "cert_file" : "pki/mycert.pem",
"metadata" : { "metadata" : {
"local": ["../sp.xml"], "local": ["../sp/sp.xml"],
}, },
"organization": { "organization": {
"display_name": "Rolands Identiteter", "display_name": "Rolands Identiteter",

View File

@ -14,6 +14,8 @@ from saml2.httputil import Redirect
logger = logging.getLogger("saml2.SP") logger = logging.getLogger("saml2.SP")
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def dict_to_table(ava, lev=0, width=1): def dict_to_table(ava, lev=0, width=1):
txt = ['<table border=%s bordercolor="black">\n' % width] txt = ['<table border=%s bordercolor="black">\n' % width]
for prop, valarr in ava.items(): for prop, valarr in ava.items():
@ -54,6 +56,7 @@ def dict_to_table(ava, lev=0, width=1):
txt.append('</table>\n') txt.append('</table>\n')
return txt return txt
def _expiration(timeout, tformat=None): def _expiration(timeout, tformat=None):
if timeout == "now": if timeout == "now":
return time_util.instant(tformat) return time_util.instant(tformat)
@ -61,6 +64,7 @@ def _expiration(timeout, tformat=None):
# validity time should match lifetime of assertions # validity time should match lifetime of assertions
return time_util.in_a_while(minutes=timeout, format=tformat) return time_util.in_a_while(minutes=timeout, format=tformat)
def delete_cookie(environ, name): def delete_cookie(environ, name):
kaka = environ.get("HTTP_COOKIE", '') kaka = environ.get("HTTP_COOKIE", '')
if kaka: if kaka:
@ -68,13 +72,14 @@ def delete_cookie(environ, name):
morsel = cookie_obj.get(name, None) morsel = cookie_obj.get(name, None)
cookie = SimpleCookie() cookie = SimpleCookie()
cookie[name] = morsel cookie[name] = morsel
cookie[name]["expires"] =\ cookie[name]["expires"] = _expiration("now",
_expiration("now", "%a, %d-%b-%Y %H:%M:%S CET") "%a, %d-%b-%Y %H:%M:%S CET")
return tuple(cookie.output().split(": ", 1)) return tuple(cookie.output().split(": ", 1))
return None return None
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
#noinspection PyUnusedLocal #noinspection PyUnusedLocal
def whoami(environ, start_response, user): def whoami(environ, start_response, user):
identity = environ["repoze.who.identity"]["user"] identity = environ["repoze.who.identity"]["user"]
@ -86,17 +91,20 @@ def whoami(environ, start_response, user):
resp = Response(response) resp = Response(response)
return resp(environ, start_response) return resp(environ, start_response)
#noinspection PyUnusedLocal #noinspection PyUnusedLocal
def not_found(environ, start_response): def not_found(environ, start_response):
"""Called if no URL matches.""" """Called if no URL matches."""
resp = NotFound('Not Found') resp = NotFound('Not Found')
return resp(environ, start_response) return resp(environ, start_response)
#noinspection PyUnusedLocal #noinspection PyUnusedLocal
def not_authn(environ, start_response): def not_authn(environ, start_response):
resp = Unauthorized('Unknown user') resp = Unauthorized('Unknown user')
return resp(environ, start_response) return resp(environ, start_response)
#noinspection PyUnusedLocal #noinspection PyUnusedLocal
def slo(environ, start_response, user): def slo(environ, start_response, user):
# so here I might get either a LogoutResponse or a LogoutRequest # so here I might get either a LogoutResponse or a LogoutRequest
@ -107,8 +115,8 @@ def slo(environ, start_response, user):
query = parse_qs(environ["QUERY_STRING"]) query = parse_qs(environ["QUERY_STRING"])
logger.info("query: %s" % query) logger.info("query: %s" % query)
try: try:
response = sc.parse_logout_request_response(query["SAMLResponse"][0], response = sc.parse_logout_request_response(
binding=BINDING_HTTP_REDIRECT) query["SAMLResponse"][0], binding=BINDING_HTTP_REDIRECT)
if response: if response:
logger.info("LOGOUT response parsed OK") logger.info("LOGOUT response parsed OK")
except KeyError: except KeyError:
@ -125,6 +133,7 @@ def slo(environ, start_response, user):
resp = Redirect("Successful Logout", headers=headers) resp = Redirect("Successful Logout", headers=headers)
return resp(environ, start_response) return resp(environ, start_response)
#noinspection PyUnusedLocal #noinspection PyUnusedLocal
def logout(environ, start_response, user): def logout(environ, start_response, user):
# This is where it starts when a user wants to log out # This is where it starts when a user wants to log out
@ -150,6 +159,7 @@ def logout(environ, start_response, user):
# start_response("500 Internal Server Error") # start_response("500 Internal Server Error")
# return [] # return []
#noinspection PyUnusedLocal #noinspection PyUnusedLocal
def done(environ, start_response, user): def done(environ, start_response, user):
# remove cookie and stored info # remove cookie and stored info
@ -175,6 +185,7 @@ urls = [
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
def application(environ, start_response): def application(environ, start_response):
""" """
The main WSGI application. Dispatch the current request to The main WSGI application. Dispatch the current request to
@ -208,6 +219,7 @@ def application(environ, start_response):
return callback(environ, start_response, user) return callback(environ, start_response, user)
else: else:
return not_authn(environ, start_response) return not_authn(environ, start_response)
return not_found(environ, start_response) return not_found(environ, start_response)
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
@ -215,13 +227,14 @@ def application(environ, start_response):
from repoze.who.config import make_middleware_with_config from repoze.who.config import make_middleware_with_config
app_with_auth = make_middleware_with_config(application, {"here": "."}, app_with_auth = make_middleware_with_config(application, {"here": "."},
'./who.ini', log_file="repoze_who.log") './who.ini',
log_file="repoze_who.log")
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
PORT = 8087 PORT = 8087
if __name__ == '__main__': if __name__ == '__main__':
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
srv = make_server('localhost', PORT, app_with_auth) srv = make_server('', PORT, app_with_auth)
print "SP listening on port: %s" % PORT print "SP listening on port: %s" % PORT
srv.serve_forever() srv.serve_forever()

View File

@ -1,15 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<ns0:EntitiesDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata" <ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ns1="http://www.w3.org/2000/09/xmldsig#" entityID="http://localhost:8087/sp.xml"><ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><ns0:KeyDescriptor use="signing"><ns1:KeyInfo><ns1:X509Data><ns1:X509Certificate>MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV
xmlns:ns1="http://www.w3.org/2000/09/xmldsig#">
<ns0:EntityDescriptor entityID="urn:mace:umu.se:saml:roland:sp">
<ns0:SPSSODescriptor AuthnRequestsSigned="false"
WantAssertionsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<ns0:KeyDescriptor>
<ns1:KeyInfo>
<ns1:X509Data>
<ns1:X509Certificate>
MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV
BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx
EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz
MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l
@ -25,51 +15,4 @@
BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO
zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN
+vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI= +vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI=
</ns1:X509Certificate> </ns1:X509Certificate></ns1:X509Data></ns1:KeyInfo></ns0:KeyDescriptor><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8087/slo" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8087" index="1" /></ns0:SPSSODescriptor><ns0:Organization><ns0:OrganizationName xml:lang="en">Exempel AB</ns0:OrganizationName><ns0:OrganizationDisplayName xml:lang="se">Exempel AB</ns0:OrganizationDisplayName><ns0:OrganizationDisplayName xml:lang="en">Example Co.</ns0:OrganizationDisplayName><ns0:OrganizationURL xml:lang="en">http://www.example.com/roland</ns0:OrganizationURL></ns0:Organization><ns0:ContactPerson contactType="technical"><ns0:GivenName>John</ns0:GivenName><ns0:SurName>Smith</ns0:SurName><ns0:EmailAddress>john.smith@example.com</ns0:EmailAddress></ns0:ContactPerson></ns0:EntityDescriptor>
</ns1:X509Data>
</ns1:KeyInfo>
</ns0:KeyDescriptor>
<ns0:SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://localhost:8087/slo"/>
<ns0:AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8087/" index="1"/>
<ns0:AttributeConsumingService index="1">
<ns0:ServiceName xml:lang="en">Rolands SP</ns0:ServiceName>
<ns0:ServiceDescription xml:lang="en">My SP
</ns0:ServiceDescription>
<ns0:RequestedAttribute FriendlyName="surname"
Name="urn:oid:2.5.4.4"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
isRequired="true"/>
<ns0:RequestedAttribute FriendlyName="givenname"
Name="urn:oid:2.5.4.42"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
isRequired="true"/>
<ns0:RequestedAttribute Name="edupersonaffiliation"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
isRequired="true"/>
<ns0:RequestedAttribute FriendlyName="title"
Name="urn:oid:2.5.4.12"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
isRequired="false"/>
</ns0:AttributeConsumingService>
</ns0:SPSSODescriptor>
<ns0:Organization>
<ns0:OrganizationName xml:lang="en">Exempel AB
</ns0:OrganizationName>
<ns0:OrganizationDisplayName xml:lang="se">Exempel AB
</ns0:OrganizationDisplayName>
<ns0:OrganizationDisplayName xml:lang="en">Example Co.
</ns0:OrganizationDisplayName>
<ns0:OrganizationURL xml:lang="en">http://www.example.com/roland
</ns0:OrganizationURL>
</ns0:Organization>
<ns0:ContactPerson contactType="technical">
<ns0:GivenName>John</ns0:GivenName>
<ns0:SurName>Smith</ns0:SurName>
<ns0:EmailAddress>john.smith@example.com</ns0:EmailAddress>
</ns0:ContactPerson>
</ns0:EntityDescriptor>
</ns0:EntitiesDescriptor>

View File

@ -1,23 +1,23 @@
from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_REDIRECT
from saml2.saml import NAME_FORMAT_URI from saml2.saml import NAME_FORMAT_URI
BASE= "http://localhost:8087/" BASE= "http://localhost:8087"
#BASE= "http://lingon.catalogix.se:8087"
CONFIG = { CONFIG = {
"entityid" : "urn:mace:umu.se:saml:roland:sp", "entityid" : "%s/sp.xml" % BASE,
"description": "My SP", "description": "My SP",
"service": { "service": {
"sp":{ "sp":{
"name" : "Rolands SP", "name" : "Rolands SP",
"endpoints":{ "endpoints":{
"assertion_consumer_service": [BASE], "assertion_consumer_service": [BASE],
"single_logout_service" : [(BASE+"slo", "single_logout_service" : [(BASE+"/slo",
BINDING_HTTP_REDIRECT)], BINDING_HTTP_REDIRECT)],
}, },
"required_attributes": ["surname", "givenname", "required_attributes": ["surname", "givenname",
"edupersonaffiliation"], "edupersonaffiliation"],
"optional_attributes": ["title"], "optional_attributes": ["title"],
"idp": [ "urn:mace:umu.se:saml:roland:idp"],
} }
}, },
"debug" : 1, "debug" : 1,
@ -25,7 +25,7 @@ CONFIG = {
"cert_file" : "pki/mycert.pem", "cert_file" : "pki/mycert.pem",
"attribute_map_dir" : "./attributemaps", "attribute_map_dir" : "./attributemaps",
"metadata" : { "metadata" : {
"local": ["../idp/idp.xml"], "local": ["../idp2/idp.xml"],
}, },
# -- below used by make_metadata -- # -- below used by make_metadata --
"organization": { "organization": {

View File

@ -35,16 +35,13 @@ class AttributeResolver(object):
self.saml2client = saml2client self.saml2client = saml2client
self.metadata = saml2client.config.metadata self.metadata = saml2client.config.metadata
def extend(self, subject_id, issuer, vo_members, name_id_format=None, def extend(self, name_id, issuer, vo_members):
sp_name_qualifier=None, real_id=None):
""" """
:param subject_id: The identifier by which the subject is know :param name_id: The identifier by which the subject is know
among all the participents of the VO among all the participents of the VO
:param issuer: Who am I the poses the query :param issuer: Who am I the poses the query
:param vo_members: The entity IDs of the IdP who I'm going to ask :param vo_members: The entity IDs of the IdP who I'm going to ask
for extra attributes for extra attributes
:param name_id_format: Used to make the IdPs aware of what's going
on here
:return: A dictionary with all the collected information about the :return: A dictionary with all the collected information about the
subject subject
""" """
@ -58,12 +55,8 @@ class AttributeResolver(object):
continue continue
# attribute query assumes SOAP binding # attribute query assumes SOAP binding
session_info = self.saml2client.attribute_query( session_info = self.saml2client.attribute_query(
subject_id, name_id, attr_serv.location, issuer_id=issuer,
attr_serv.location, )
issuer_id=issuer,
sp_name_qualifier=sp_name_qualifier,
nameid_format=name_id_format,
real_id=real_id)
if session_info: if session_info:
result.append(session_info) result.append(session_info)
return result return result

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import shelve import shelve
from saml2.ident import code, decode
from saml2 import time_util from saml2 import time_util
import logging import logging
@ -10,12 +11,15 @@ logger = logging.getLogger(__name__)
# gathered from several different sources, all with their own # gathered from several different sources, all with their own
# timeout time. # timeout time.
class ToOld(Exception): class ToOld(Exception):
pass pass
class CacheError(Exception): class CacheError(Exception):
pass pass
class Cache(object): class Cache(object):
def __init__(self, filename=None): def __init__(self, filename=None):
if filename: if filename:
@ -25,18 +29,25 @@ class Cache(object):
self._db = {} self._db = {}
self._sync = False self._sync = False
def delete(self, subject_id): def delete(self, name_id):
del self._db[subject_id] """
:param name_id: The subject identifier, a NameID instance
"""
del self._db[code(name_id)]
if self._sync: if self._sync:
try:
self._db.sync() self._db.sync()
except AttributeError:
pass
def get_identity(self, subject_id, entities=None, def get_identity(self, name_id, entities=None,
check_not_on_or_after=True): check_not_on_or_after=True):
""" Get all the identity information that has been received and """ Get all the identity information that has been received and
are still valid about the subject. are still valid about the subject.
:param subject_id: The identifier of the subject :param name_id: The subject identifier, a NameID instance
:param entities: The identifiers of the entities whoes assertions are :param entities: The identifiers of the entities whoes assertions are
interesting. If the list is empty all entities are interesting. interesting. If the list is empty all entities are interesting.
:return: A 2-tuple consisting of the identity information (a :return: A 2-tuple consisting of the identity information (a
@ -45,7 +56,8 @@ class Cache(object):
""" """
if not entities: if not entities:
try: try:
entities = self._db[subject_id].keys() cni = code(name_id)
entities = self._db[cni].keys()
except KeyError: except KeyError:
return {}, [] return {}, []
@ -53,7 +65,7 @@ class Cache(object):
oldees = [] oldees = []
for entity_id in entities: for entity_id in entities:
try: try:
info = self.get(subject_id, entity_id, check_not_on_or_after) info = self.get(name_id, entity_id, check_not_on_or_after)
except ToOld: except ToOld:
oldees.append(entity_id) oldees.append(entity_id)
continue continue
@ -70,74 +82,81 @@ class Cache(object):
res[key] = vals res[key] = vals
return res, oldees return res, oldees
def get(self, subject_id, entity_id, check_not_on_or_after=True): def get(self, name_id, entity_id, check_not_on_or_after=True):
""" Get session information about a subject gotten from a """ Get session information about a subject gotten from a
specified IdP/AA. specified IdP/AA.
:param subject_id: The identifier of the subject :param name_id: The subject identifier, a NameID instance
:param entity_id: The identifier of the entity_id :param entity_id: The identifier of the entity_id
:param check_not_on_or_after: if True it will check if this :param check_not_on_or_after: if True it will check if this
subject is still valid or if it is too old. Otherwise it subject is still valid or if it is too old. Otherwise it
will not check this. True by default. will not check this. True by default.
:return: The session information :return: The session information
""" """
(timestamp, info) = self._db[subject_id][entity_id] cni = code(name_id)
(timestamp, info) = self._db[cni][entity_id]
if check_not_on_or_after and time_util.after(timestamp): if check_not_on_or_after and time_util.after(timestamp):
raise ToOld("past %s" % timestamp) raise ToOld("past %s" % timestamp)
return info or None return info or None
def set(self, subject_id, entity_id, info, not_on_or_after=0): def set(self, name_id, entity_id, info, not_on_or_after=0):
""" Stores session information in the cache. Assumes that the subject_id """ Stores session information in the cache. Assumes that the name_id
is unique within the context of the Service Provider. is unique within the context of the Service Provider.
:param subject_id: The subject identifier :param name_id: The subject identifier, a NameID instance
:param entity_id: The identifier of the entity_id/receiver of an :param entity_id: The identifier of the entity_id/receiver of an
assertion assertion
:param info: The session info, the assertion is part of this :param info: The session info, the assertion is part of this
:param not_on_or_after: A time after which the assertion is not valid. :param not_on_or_after: A time after which the assertion is not valid.
""" """
if subject_id not in self._db: cni = code(name_id)
self._db[subject_id] = {} if cni not in self._db:
self._db[cni] = {}
self._db[subject_id][entity_id] = (not_on_or_after, info) self._db[cni][entity_id] = (not_on_or_after, info)
if self._sync: if self._sync:
try:
self._db.sync() self._db.sync()
except AttributeError:
pass
def reset(self, subject_id, entity_id): def reset(self, name_id, entity_id):
""" Scrap the assertions received from a IdP or an AA about a special """ Scrap the assertions received from a IdP or an AA about a special
subject. subject.
:param subject_id: The subjects identifier :param name_id: The subject identifier, a NameID instance
:param entity_id: The identifier of the entity_id of the assertion :param entity_id: The identifier of the entity_id of the assertion
:return: :return:
""" """
self.set(subject_id, entity_id, {}, 0) self.set(name_id, entity_id, {}, 0)
def entities(self, subject_id): def entities(self, name_id):
""" Returns all the entities of assertions for a subject, disregarding """ Returns all the entities of assertions for a subject, disregarding
whether the assertion still is valid or not. whether the assertion still is valid or not.
:param subject_id: The identifier of the subject :param name_id: The subject identifier, a NameID instance
:return: A possibly empty list of entity identifiers :return: A possibly empty list of entity identifiers
""" """
return self._db[subject_id].keys() cni = code(name_id)
return self._db[cni].keys()
def receivers(self, subject_id): def receivers(self, name_id):
""" Another name for entities() just to make it more logic in the IdP """ Another name for entities() just to make it more logic in the IdP
scenario """ scenario """
return self.entities(subject_id) return self.entities(name_id)
def active(self, subject_id, entity_id): def active(self, name_id, entity_id):
""" Returns the status of assertions from a specific entity_id. """ Returns the status of assertions from a specific entity_id.
:param subject_id: The ID of the subject :param name_id: The ID of the subject
:param entity_id: The entity ID of the entity_id of the assertion :param entity_id: The entity ID of the entity_id of the assertion
:return: True or False depending on if the assertion is still :return: True or False depending on if the assertion is still
valid or not. valid or not.
""" """
try: try:
(timestamp, info) = self._db[subject_id][entity_id] cni = code(name_id)
(timestamp, info) = self._db[cni][entity_id]
except KeyError: except KeyError:
return False return False
@ -151,4 +170,4 @@ class Cache(object):
:return: list of subject identifiers :return: list of subject identifiers
""" """
return self._db.keys() return [decode(c) for c in self._db.keys()]

View File

@ -20,7 +20,6 @@ to conclude its tasks.
""" """
from saml2.httpbase import HTTPError from saml2.httpbase import HTTPError
from saml2.s_utils import sid from saml2.s_utils import sid
from saml2.samlp import logout_response_from_string
import saml2 import saml2
try: try:
@ -46,6 +45,7 @@ from saml2 import BINDING_SOAP
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Saml2Client(Base): class Saml2Client(Base):
""" The basic pySAML2 service provider class """ """ The basic pySAML2 service provider class """
@ -81,12 +81,12 @@ class Saml2Client(Base):
return req.id, info return req.id, info
def global_logout(self, subject_id, reason="", expire=None, sign=None): def global_logout(self, name_id, reason="", expire=None, sign=None):
""" More or less a layer of indirection :-/ """ More or less a layer of indirection :-/
Bootstrapping the whole thing by finding all the IdPs that should Bootstrapping the whole thing by finding all the IdPs that should
be notified. be notified.
:param subject_id: The identifier of the subject that wants to be :param name_id: The identifier of the subject that wants to be
logged out. logged out.
:param reason: Why the subject wants to log out :param reason: Why the subject wants to log out
:param expire: The latest the log out should happen. :param expire: The latest the log out should happen.
@ -99,17 +99,17 @@ class Saml2Client(Base):
conversation. conversation.
""" """
logger.info("logout request for: %s" % subject_id) logger.info("logout request for: %s" % name_id)
# find out which IdPs/AAs I should notify # find out which IdPs/AAs I should notify
entity_ids = self.users.issuers_of_info(subject_id) entity_ids = self.users.issuers_of_info(name_id)
return self.do_logout(subject_id, entity_ids, reason, expire, sign) return self.do_logout(name_id, entity_ids, reason, expire, sign)
def do_logout(self, subject_id, entity_ids, reason, expire, sign=None): def do_logout(self, name_id, entity_ids, reason, expire, sign=None):
""" """
:param subject_id: Identifier of the Subject :param name_id: Identifier of the Subject a NameID instance
:param entity_ids: List of entity ids for the IdPs that have provided :param entity_ids: List of entity ids for the IdPs that have provided
information concerning the subject information concerning the subject
:param reason: The reason for doing the logout :param reason: The reason for doing the logout
@ -120,32 +120,31 @@ class Saml2Client(Base):
# check time # check time
if not not_on_or_after(expire): # I've run out of time if not not_on_or_after(expire): # I've run out of time
# Do the local logout anyway # Do the local logout anyway
self.local_logout(subject_id) self.local_logout(name_id)
return 0, "504 Gateway Timeout", [], [] return 0, "504 Gateway Timeout", [], []
# for all where I can use the SOAP binding, do those first
not_done = entity_ids[:] not_done = entity_ids[:]
responses = {} responses = {}
for entity_id in entity_ids: for entity_id in entity_ids:
response = False logger.debug("Logout from '%s'" % entity_id)
# for all where I can use the SOAP binding, do those first
for binding in [BINDING_SOAP, for binding in [BINDING_SOAP, BINDING_HTTP_POST,
BINDING_HTTP_POST,
BINDING_HTTP_REDIRECT]: BINDING_HTTP_REDIRECT]:
srvs = self.metadata.single_logout_service(entity_id, binding, srvs = self.metadata.single_logout_service(entity_id, binding,
"idpsso") "idpsso")
if not srvs: if not srvs:
logger.debug("No SLO '%s' service" % binding)
continue continue
destination = destinations(srvs)[0] destination = destinations(srvs)[0]
logger.info("destination to provider: %s" % destination) logger.info("destination to provider: %s" % destination)
request = self.create_logout_request(destination, entity_id, request = self.create_logout_request(destination, entity_id,
subject_id, reason=reason, name_id=name_id,
reason=reason,
expire=expire) expire=expire)
to_sign = [] #to_sign = []
if binding.startswith("http://"): if binding.startswith("http://"):
sign = True sign = True
@ -163,14 +162,14 @@ class Saml2Client(Base):
relay_state) relay_state)
if binding == BINDING_SOAP: if binding == BINDING_SOAP:
if response:
logger.info("Verifying response")
response = self.send(**http_info) response = self.send(**http_info)
if response: if response and response.status_code == 200:
not_done.remove(entity_id) not_done.remove(entity_id)
logger.info("OK response from %s" % destination) response = response.text
responses[entity_id] = logout_response_from_string(response) logger.info("Response: %s" % response)
res = self.parse_logout_request_response(response)
responses[entity_id] = res
else: else:
logger.info("NOT OK response from %s" % destination) logger.info("NOT OK response from %s" % destination)
@ -178,7 +177,7 @@ class Saml2Client(Base):
self.state[request.id] = {"entity_id": entity_id, self.state[request.id] = {"entity_id": entity_id,
"operation": "SLO", "operation": "SLO",
"entity_ids": entity_ids, "entity_ids": entity_ids,
"subject_id": subject_id, "name_id": name_id,
"reason": reason, "reason": reason,
"not_on_of_after": expire, "not_on_of_after": expire,
"sign": sign} "sign": sign}
@ -277,8 +276,7 @@ class Saml2Client(Base):
consent=None, extensions=None, sign=False): consent=None, extensions=None, sign=False):
subject = saml.Subject( subject = saml.Subject(
name_id = saml.NameID(text=subject_id, name_id=saml.NameID(text=subject_id, format=nameid_format,
format=nameid_format,
sp_name_qualifier=sp_name_qualifier, sp_name_qualifier=sp_name_qualifier,
name_qualifier=name_qualifier)) name_qualifier=name_qualifier))
@ -321,9 +319,8 @@ class Saml2Client(Base):
srvs = self.metadata.authn_request_service(entity_id, BINDING_SOAP) srvs = self.metadata.authn_request_service(entity_id, BINDING_SOAP)
for destination in destinations(srvs): for destination in destinations(srvs):
resp = self._use_soap(destination, "authn_query", resp = self._use_soap(destination, "authn_query", consent=consent,
consent=consent, extensions=extensions, extensions=extensions, sign=sign)
sign=sign)
if resp: if resp:
return resp return resp
@ -339,7 +336,8 @@ class Saml2Client(Base):
:param entityid: To whom the query should be sent :param entityid: To whom the query should be sent
:param subject_id: The identifier of the subject :param subject_id: The identifier of the subject
:param attribute: A dictionary of attributes and values that is asked for :param attribute: A dictionary of attributes and values that is
asked for
:param sp_name_qualifier: The unique identifier of the :param sp_name_qualifier: The unique identifier of the
service provider or affiliation of providers for whom the service provider or affiliation of providers for whom the
identifier was generated. identifier was generated.
@ -353,7 +351,6 @@ class Saml2Client(Base):
HTTP args if BINDING_HTT_POST was used. HTTP args if BINDING_HTT_POST was used.
""" """
if real_id: if real_id:
response_args = {"real_id": real_id} response_args = {"real_id": real_id}
else: else:

View File

@ -78,18 +78,23 @@ ECP_SERVICE = "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
ACTOR = "http://schemas.xmlsoap.org/soap/actor/next" ACTOR = "http://schemas.xmlsoap.org/soap/actor/next"
MIME_PAOS = "application/vnd.paos+xml" MIME_PAOS = "application/vnd.paos+xml"
class IdpUnspecified(Exception): class IdpUnspecified(Exception):
pass pass
class VerifyError(Exception): class VerifyError(Exception):
pass pass
class LogoutError(Exception): class LogoutError(Exception):
pass pass
class NoServiceDefined(Exception): class NoServiceDefined(Exception):
pass pass
class Base(Entity): class Base(Entity):
""" The basic pySAML2 service provider class """ """ The basic pySAML2 service provider class """
@ -166,25 +171,25 @@ class Base(Entity):
# Public API # Public API
# #
def add_vo_information_about_user(self, subject_id): def add_vo_information_about_user(self, name_id):
""" Add information to the knowledge I have about the user. This is """ Add information to the knowledge I have about the user. This is
for Virtual organizations. for Virtual organizations.
:param subject_id: The subject identifier :param name_id: The subject identifier
:return: A possibly extended knowledge. :return: A possibly extended knowledge.
""" """
ava = {} ava = {}
try: try:
(ava, _) = self.users.get_identity(subject_id) (ava, _) = self.users.get_identity(name_id)
except KeyError: except KeyError:
pass pass
# is this a Virtual Organization situation # is this a Virtual Organization situation
if self.vorg: if self.vorg:
if self.vorg.do_aggregation(subject_id): if self.vorg.do_aggregation(name_id):
# Get the extended identity # Get the extended identity
ava = self.users.get_identity(subject_id)[0] ava = self.users.get_identity(name_id)[0]
return ava return ava
#noinspection PyUnusedLocal #noinspection PyUnusedLocal
@ -228,7 +233,8 @@ class Base(Entity):
args = {} args = {}
try: try:
args["assertion_consumer_service_url"] = kwargs["assertion_consumer_service_url"] args["assertion_consumer_service_url"] = kwargs[
"assertion_consumer_service_url"]
except KeyError: except KeyError:
if service_url_binding is None: if service_url_binding is None:
service_url = self.service_url(binding) service_url = self.service_url(binding)
@ -247,16 +253,17 @@ class Base(Entity):
try: try:
args["name_id_policy"] = kwargs["name_id_policy"] args["name_id_policy"] = kwargs["name_id_policy"]
del kwargs["name_id_policy"] del kwargs["name_id_policy"]
except: except KeyError:
if allow_create: if allow_create:
allow_create = "true" allow_create = "true"
else: else:
allow_create = "false" allow_create = "false"
# Profile stuff, should be configurable # Profile stuff, should be configurable
if nameid_format is None or nameid_format == NAMEID_FORMAT_TRANSIENT: if nameid_format is None or \
name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, nameid_format == NAMEID_FORMAT_TRANSIENT:
format=NAMEID_FORMAT_TRANSIENT) name_id_policy = samlp.NameIDPolicy(
allow_create=allow_create, format=NAMEID_FORMAT_TRANSIENT)
else: else:
name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, name_id_policy = samlp.NameIDPolicy(allow_create=allow_create,
format=nameid_format) format=nameid_format)
@ -280,20 +287,23 @@ class Base(Entity):
extensions.append(saml2.element_to_extension_element(val)) extensions.append(saml2.element_to_extension_element(val))
else: else:
args[key] = val args[key] = val
try:
del args["id"]
except KeyError:
pass
return self._message(AuthnRequest, destination, sid, consent, return self._message(AuthnRequest, destination, sid, consent,
extensions, sign, extensions, sign,
protocol_binding=binding, protocol_binding=binding,
scoping=scoping, **args) scoping=scoping, **args)
def create_attribute_query(self, destination, name_id=None, def create_attribute_query(self, destination, name_id=None,
attribute=None, sid=0, consent=None, attribute=None, sid=0, consent=None,
extensions=None, sign=False, **kwargs): extensions=None, sign=False, **kwargs):
""" Constructs an AttributeQuery """ Constructs an AttributeQuery
:param destination: To whom the query should be sent :param destination: To whom the query should be sent
:param subject_id: The identifier of the subject :param name_id: The identifier of the subject
:param attribute: A dictionary of attributes and values that is :param attribute: A dictionary of attributes and values that is
asked for. The key are one of 4 variants: asked for. The key are one of 4 variants:
3-tuple of name_format,name and friendly_name, 3-tuple of name_format,name and friendly_name,
@ -342,11 +352,9 @@ class Base(Entity):
extensions, sign, subject=subject, extensions, sign, subject=subject,
attribute=attribute) attribute=attribute)
# MUST use SOAP for # MUST use SOAP for
# AssertionIDRequest, SubjectQuery, # AssertionIDRequest, SubjectQuery,
# AuthnQuery, AttributeQuery, or AuthzDecisionQuery # AuthnQuery, AttributeQuery, or AuthzDecisionQuery
def create_authz_decision_query(self, destination, action, def create_authz_decision_query(self, destination, action,
evidence=None, resource=None, subject=None, evidence=None, resource=None, subject=None,
sid=0, consent=None, extensions=None, sid=0, consent=None, extensions=None,
@ -369,8 +377,9 @@ class Base(Entity):
extensions, sign, action=action, evidence=evidence, extensions, sign, action=action, evidence=evidence,
resource=resource, subject=subject) resource=resource, subject=subject)
def create_authz_decision_query_using_assertion(self, destination, assertion, def create_authz_decision_query_using_assertion(self, destination,
action=None, resource=None, assertion, action=None,
resource=None,
subject=None, sid=0, subject=None, sid=0,
consent=None, consent=None,
extensions=None, extensions=None,
@ -397,13 +406,9 @@ class Base(Entity):
else: else:
_action = None _action = None
return self.create_authz_decision_query(destination, return self.create_authz_decision_query(
_action, destination, _action, saml.Evidence(assertion=assertion),
saml.Evidence(assertion=assertion), resource, subject, sid=sid, consent=consent, extensions=extensions,
resource, subject,
sid=sid,
consent=consent,
extensions=extensions,
sign=sign) sign=sign)
def create_assertion_id_request(self, assertion_id_refs, **kwargs): def create_assertion_id_request(self, assertion_id_refs, **kwargs):
@ -464,16 +469,17 @@ class Base(Entity):
assert name_id or base_id or encrypted_id assert name_id or base_id or encrypted_id
if name_id: if name_id:
return self._message(NameIDMappingRequest, destination, sid, consent, return self._message(NameIDMappingRequest, destination, sid,
extensions, sign, name_id_policy=name_id_policy, consent, extensions, sign,
name_id=name_id) name_id_policy=name_id_policy, name_id=name_id)
elif base_id: elif base_id:
return self._message(NameIDMappingRequest, destination, sid, consent, return self._message(NameIDMappingRequest, destination, sid,
extensions, sign, name_id_policy=name_id_policy, consent, extensions, sign,
base_id=base_id) name_id_policy=name_id_policy, base_id=base_id)
else: else:
return self._message(NameIDMappingRequest, destination, sid, consent, return self._message(NameIDMappingRequest, destination, sid,
extensions, sign, name_id_policy=name_id_policy, consent, extensions, sign,
name_id_policy=name_id_policy,
encrypted_id=encrypted_id) encrypted_id=encrypted_id)
# ======== response handling =========== # ======== response handling ===========
@ -622,15 +628,15 @@ class Base(Entity):
# SingleSignOnService # SingleSignOnService
_, location = self.pick_binding("single_sign_on_service", _, location = self.pick_binding("single_sign_on_service",
[_binding], entity_id=entityid) [_binding], entity_id=entityid)
authn_req = self.create_authn_request(location, authn_req = self.create_authn_request(
service_url_binding=BINDING_PAOS, location, service_url_binding=BINDING_PAOS, **kwargs)
**kwargs)
# ---------------------------------------- # ----------------------------------------
# The SOAP envelope # The SOAP envelope
# ---------------------------------------- # ----------------------------------------
soap_envelope = make_soap_enveloped_saml_thingy(authn_req,[paos_request, soap_envelope = make_soap_enveloped_saml_thingy(authn_req,
[paos_request,
relay_state]) relay_state])
return authn_req.id, "%s" % soap_envelope return authn_req.id, "%s" % soap_envelope

View File

@ -140,9 +140,9 @@ class HTTPBase(object):
if morsel["max-age"]: if morsel["max-age"]:
std_attr["expires"] = _since_epoch(morsel["max-age"]) std_attr["expires"] = _since_epoch(morsel["max-age"])
for att, set in PAIRS.items(): for att, item in PAIRS.items():
if std_attr[att]: if std_attr[att]:
std_attr[set] = True std_attr[item] = True
if std_attr["domain"] and std_attr["domain"].startswith("."): if std_attr["domain"] and std_attr["domain"].startswith("."):
std_attr["domain_initial_dot"] = True std_attr["domain_initial_dot"] = True

View File

@ -19,9 +19,11 @@ logger = logging.getLogger(__name__)
ATTR = ["name_qualifier", "sp_name_qualifier", "format", "sp_provided_id", ATTR = ["name_qualifier", "sp_name_qualifier", "format", "sp_provided_id",
"text"] "text"]
class Unknown(Exception): class Unknown(Exception):
pass pass
def code(item): def code(item):
_res = [] _res = []
i = 0 i = 0
@ -32,13 +34,15 @@ def code(item):
i += 1 i += 1
return ",".join(_res) return ",".join(_res)
def decode(str):
def decode(txt):
_nid = NameID() _nid = NameID()
for part in str.split(","): for part in txt.split(","):
i, val = part.split("=") i, val = part.split("=")
setattr(_nid, ATTR[int(i)], unquote(val)) setattr(_nid, ATTR[int(i)], unquote(val))
return _nid return _nid
class IdentDB(object): class IdentDB(object):
""" A class that handles identifiers of entities """ A class that handles identifiers of entities
Keeps a list of all nameIDs returned per SP Keeps a list of all nameIDs returned per SP
@ -51,19 +55,19 @@ class IdentDB(object):
self.domain = domain self.domain = domain
self.name_qualifier = name_qualifier self.name_qualifier = name_qualifier
def _create_id(self, format, name_qualifier="", sp_name_qualifier=""): def _create_id(self, nformat, name_qualifier="", sp_name_qualifier=""):
_id = sha256(rndstr(32)) _id = sha256(rndstr(32))
_id.update(format) _id.update(nformat)
if name_qualifier: if name_qualifier:
_id.update(name_qualifier) _id.update(name_qualifier)
if sp_name_qualifier: if sp_name_qualifier:
_id.update(sp_name_qualifier) _id.update(sp_name_qualifier)
return _id.hexdigest() return _id.hexdigest()
def create_id(self, format, name_qualifier="", sp_name_qualifier=""): def create_id(self, nformat, name_qualifier="", sp_name_qualifier=""):
_id = self._create_id(format, name_qualifier, sp_name_qualifier) _id = self._create_id(nformat, name_qualifier, sp_name_qualifier)
while _id in self.db: while _id in self.db:
_id = self._create_id(format, name_qualifier, sp_name_qualifier) _id = self._create_id(nformat, name_qualifier, sp_name_qualifier)
return _id return _id
def store(self, ident, name_id): def store(self, ident, name_id):
@ -92,30 +96,30 @@ class IdentDB(object):
del self.db[_cn] del self.db[_cn]
def remove_local(self, id): def remove_local(self, sid):
if isinstance(id, unicode): if isinstance(sid, unicode):
id = id.encode("utf-8") sid = sid.encode("utf-8")
try: try:
for val in self.db[id].split(" "): for val in self.db[sid].split(" "):
try: try:
del self.db[val] del self.db[val]
except KeyError: except KeyError:
pass pass
del self.db[id] del self.db[sid]
except KeyError: except KeyError:
pass pass
def get_nameid(self, userid, format, sp_name_qualifier, name_qualifier): def get_nameid(self, userid, nformat, sp_name_qualifier, name_qualifier):
_id = self.create_id(format, name_qualifier, sp_name_qualifier) _id = self.create_id(nformat, name_qualifier, sp_name_qualifier)
if format == NAMEID_FORMAT_EMAILADDRESS: if nformat == NAMEID_FORMAT_EMAILADDRESS:
if not self.domain: if not self.domain:
raise Exception("Can't issue email nameids, unknown domain") raise Exception("Can't issue email nameids, unknown domain")
_id = "%s@%s" % (_id, self.domain) _id = "%s@%s" % (_id, self.domain)
nameid = NameID(format=format, sp_name_qualifier=sp_name_qualifier, nameid = NameID(format=nformat, sp_name_qualifier=sp_name_qualifier,
name_qualifier=name_qualifier, text=_id) name_qualifier=name_qualifier, text=_id)
self.store(userid, nameid) self.store(userid, nameid)
@ -150,7 +154,8 @@ class IdentDB(object):
if not name_qualifier: if not name_qualifier:
name_qualifier = self.name_qualifier name_qualifier = self.name_qualifier
return {"format":nameid_format, "sp_name_qualifier": sp_name_qualifier, return {"nformat": nameid_format,
"sp_name_qualifier": sp_name_qualifier,
"name_qualifier": name_qualifier} "name_qualifier": name_qualifier}
def construct_nameid(self, userid, local_policy=None, def construct_nameid(self, userid, local_policy=None,
@ -175,7 +180,8 @@ class IdentDB(object):
return self.get_nameid(userid, NAMEID_FORMAT_TRANSIENT, return self.get_nameid(userid, NAMEID_FORMAT_TRANSIENT,
sp_name_qualifier, name_qualifier) sp_name_qualifier, name_qualifier)
def persistent_nameid(self, userid, sp_name_qualifier="", name_qualifier=""): def persistent_nameid(self, userid, sp_name_qualifier="",
name_qualifier=""):
nameid = self.match_local_id(userid, sp_name_qualifier, name_qualifier) nameid = self.match_local_id(userid, sp_name_qualifier, name_qualifier)
if nameid: if nameid:
return nameid return nameid
@ -194,6 +200,8 @@ class IdentDB(object):
try: try:
return self.db[code(name_id)] return self.db[code(name_id)]
except KeyError: except KeyError:
logger.debug("name: %s" % code(name_id))
logger.debug("id keys: %s" % self.db.keys())
return None return None
def match_local_id(self, userid, sp_name_qualifier, name_qualifier): def match_local_id(self, userid, sp_name_qualifier, name_qualifier):

View File

@ -154,9 +154,11 @@ def make_soap_enveloped_saml_thingy(thingy, header_parts=None):
if isinstance(thingy, basestring): if isinstance(thingy, basestring):
# remove the first XML version/encoding line # remove the first XML version/encoding line
logger.debug("thingy0: %s" % thingy)
_part = thingy.split("\n") _part = thingy.split("\n")
thingy = _part[1] thingy = "".join(_part[1:])
thingy = thingy.replace(PREFIX, "") thingy = thingy.replace(PREFIX, "")
logger.debug("thingy: %s" % thingy)
_child = ElementTree.Element('') _child = ElementTree.Element('')
_child.tag = '{%s}FuddleMuddle' % DUMMY_NAMESPACE _child.tag = '{%s}FuddleMuddle' % DUMMY_NAMESPACE
body.append(_child) body.append(_child)

View File

@ -3,6 +3,7 @@ from saml2.cache import Cache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Population(object): class Population(object):
def __init__(self, cache=None): def __init__(self, cache=None):
if cache: if cache:
@ -17,45 +18,48 @@ class Population(object):
"""If there already are information from this source in the cache """If there already are information from this source in the cache
this function will overwrite that information""" this function will overwrite that information"""
subject_id = session_info["name_id"] name_id = session_info["name_id"]
issuer = session_info["issuer"] issuer = session_info["issuer"]
del session_info["issuer"] del session_info["issuer"]
self.cache.set(subject_id, issuer, session_info, self.cache.set(name_id, issuer, session_info,
session_info["not_on_or_after"]) session_info["not_on_or_after"])
return subject_id return name_id
def stale_sources_for_person(self, subject_id, sources=None): def stale_sources_for_person(self, name_id, sources=None):
"""
:param name_id: Identifier of the subject, a NameID instance
:param sources: Sources for information about the subject
:return:
"""
if not sources: # assume that all the members has be asked if not sources: # assume that all the members has be asked
# once before, hence they are represented in the cache # once before, hence they are represented in the cache
sources = self.cache.entities(subject_id) sources = self.cache.entities(name_id)
sources = [m for m in sources \ sources = [m for m in sources if not self.cache.active(name_id, m)]
if not self.cache.active(subject_id, m)]
return sources return sources
def issuers_of_info(self, subject_id): def issuers_of_info(self, name_id):
return self.cache.entities(subject_id) return self.cache.entities(name_id)
def get_identity(self, subject_id, entities=None, def get_identity(self, name_id, entities=None, check_not_on_or_after=True):
check_not_on_or_after=True): return self.cache.get_identity(name_id, entities, check_not_on_or_after)
return self.cache.get_identity(subject_id, entities,
check_not_on_or_after)
def get_info_from(self, subject_id, entity_id): def get_info_from(self, name_id, entity_id):
return self.cache.get(subject_id, entity_id) return self.cache.get(name_id, entity_id)
def subjects(self): def subjects(self):
"""Returns the name id's for all the persons in the cache""" """Returns the name id's for all the persons in the cache"""
return self.cache.subjects() return self.cache.subjects()
def remove_person(self, subject_id): def remove_person(self, name_id):
self.cache.delete(subject_id) self.cache.delete(name_id)
def get_entityid(self, subject_id, source_id, check_not_on_or_after=True): def get_entityid(self, name_id, source_id, check_not_on_or_after=True):
try: try:
return self.cache.get(subject_id, source_id, return self.cache.get(name_id, source_id, check_not_on_or_after)[
check_not_on_or_after)["name_id"] "name_id"]
except (KeyError, ValueError): except (KeyError, ValueError):
return "" return ""
def sources(self, subject_id): def sources(self, name_id):
return self.cache.entities(subject_id) return self.cache.entities(name_id)

View File

@ -49,17 +49,21 @@ logger = logging.getLogger(__name__)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class IncorrectlySigned(Exception): class IncorrectlySigned(Exception):
pass pass
class VerificationError(Exception): class VerificationError(Exception):
pass pass
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _dummy(_): def _dummy(_):
return None return None
def for_me(condition, myself): def for_me(condition, myself):
# Am I among the intended audiences # Am I among the intended audiences
for restriction in condition.audience_restriction: for restriction in condition.audience_restriction:
@ -72,6 +76,7 @@ def for_me(condition, myself):
return False return False
def authn_response(conf, return_addr, outstanding_queries=None, timeslack=0, def authn_response(conf, return_addr, outstanding_queries=None, timeslack=0,
asynchop=True, allow_unsolicited=False): asynchop=True, allow_unsolicited=False):
sec = security_context(conf) sec = security_context(conf)
@ -85,6 +90,7 @@ def authn_response(conf, return_addr, outstanding_queries=None, timeslack=0,
return_addr, outstanding_queries, timeslack, return_addr, outstanding_queries, timeslack,
asynchop=asynchop, allow_unsolicited=allow_unsolicited) asynchop=asynchop, allow_unsolicited=allow_unsolicited)
# comes in over SOAP so synchronous # comes in over SOAP so synchronous
def attribute_response(conf, return_addr, timeslack=0, asynchop=False, def attribute_response(conf, return_addr, timeslack=0, asynchop=False,
test=False): test=False):
@ -99,6 +105,7 @@ def attribute_response(conf, return_addr, timeslack=0, asynchop=False,
return_addr, timeslack, asynchop=asynchop, return_addr, timeslack, asynchop=asynchop,
test=test) test=test)
class StatusResponse(object): class StatusResponse(object):
msgtype = "status_response" msgtype = "status_response"
@ -111,7 +118,7 @@ class StatusResponse(object):
self.request_id = request_id self.request_id = request_id
self.xmlstr = "" self.xmlstr = ""
self.name_id = "" self.name_id = None
self.response = None self.response = None
self.not_on_or_after = 0 self.not_on_or_after = 0
self.in_response_to = None self.in_response_to = None
@ -121,7 +128,7 @@ class StatusResponse(object):
def _clear(self): def _clear(self):
self.xmlstr = "" self.xmlstr = ""
self.name_id = "" self.name_id = None
self.response = None self.response = None
self.not_on_or_after = 0 self.not_on_or_after = 0
@ -149,9 +156,10 @@ class StatusResponse(object):
# This will check signature on Assertion which is the default # This will check signature on Assertion which is the default
try: try:
self.response = self.sec.check_signature(instance) self.response = self.sec.check_signature(instance)
except SignatureError: # The response as a whole might be signed or not except SignatureError:
self.response = self.sec.check_signature(instance, # The response as a whole might be signed or not
samlp.NAMESPACE+":Response") self.response = self.sec.check_signature(
instance, samlp.NAMESPACE + ":Response")
else: else:
self.not_signed = True self.not_signed = True
self.response = instance self.response = instance
@ -202,8 +210,7 @@ class StatusResponse(object):
if self.request_id and self.in_response_to and \ if self.request_id and self.in_response_to and \
self.in_response_to != self.request_id: self.in_response_to != self.request_id:
logger.error("Not the id I expected: %s != %s" % ( logger.error("Not the id I expected: %s != %s" % (
self.in_response_to, self.in_response_to, self.request_id))
self.request_id))
return None return None
try: try:
@ -244,14 +251,17 @@ class StatusResponse(object):
def issuer(self): def issuer(self):
return self.response.issuer.text.strip() return self.response.issuer.text.strip()
class LogoutResponse(StatusResponse): class LogoutResponse(StatusResponse):
msgtype = "logout_response" msgtype = "logout_response"
def __init__(self, sec_context, return_addr=None, timeslack=0, def __init__(self, sec_context, return_addr=None, timeslack=0,
asynchop=True): asynchop=True):
StatusResponse.__init__(self, sec_context, return_addr, timeslack, StatusResponse.__init__(self, sec_context, return_addr, timeslack,
asynchop=asynchop) asynchop=asynchop)
self.signature_check = self.sec.correctly_signed_logout_response self.signature_check = self.sec.correctly_signed_logout_response
class NameIDMappingResponse(StatusResponse): class NameIDMappingResponse(StatusResponse):
msgtype = "name_id_mapping_response" msgtype = "name_id_mapping_response"
@ -261,6 +271,7 @@ class NameIDMappingResponse(StatusResponse):
request_id, asynchop) request_id, asynchop)
self.signature_check = self.sec.correctly_signed_name_id_mapping_response self.signature_check = self.sec.correctly_signed_name_id_mapping_response
class ManageNameIDResponse(StatusResponse): class ManageNameIDResponse(StatusResponse):
msgtype = "manage_name_id_response" msgtype = "manage_name_id_response"
@ -273,6 +284,7 @@ class ManageNameIDResponse(StatusResponse):
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
class AuthnResponse(StatusResponse): class AuthnResponse(StatusResponse):
""" This is where all the profile compliance is checked. """ This is where all the profile compliance is checked.
This one does saml2int compliance. """ This one does saml2int compliance. """
@ -335,7 +347,8 @@ class AuthnResponse(StatusResponse):
if validate_on_or_after(authn_statement.session_not_on_or_after, if validate_on_or_after(authn_statement.session_not_on_or_after,
self.timeslack): self.timeslack):
self.session_not_on_or_after = calendar.timegm( self.session_not_on_or_after = calendar.timegm(
time_util.str_to_time(authn_statement.session_not_on_or_after)) time_util.str_to_time(
authn_statement.session_not_on_or_after))
else: else:
return False return False
return True return True
@ -364,8 +377,7 @@ class AuthnResponse(StatusResponse):
try: try:
if condition.not_on_or_after: if condition.not_on_or_after:
self.not_on_or_after = validate_on_or_after( self.not_on_or_after = validate_on_or_after(
condition.not_on_or_after, condition.not_on_or_after, self.timeslack)
self.timeslack)
if condition.not_before: if condition.not_before:
validate_before(condition.not_before, self.timeslack) validate_before(condition.not_before, self.timeslack)
except Exception, excp: except Exception, excp:
@ -375,7 +387,6 @@ class AuthnResponse(StatusResponse):
else: else:
self.not_on_or_after = 0 self.not_on_or_after = 0
if not for_me(condition, self.entity_id): if not for_me(condition, self.entity_id):
if not lax: if not lax:
#print condition #print condition
@ -501,7 +512,7 @@ class AuthnResponse(StatusResponse):
# The subject must contain a name_id # The subject must contain a name_id
assert subject.name_id assert subject.name_id
self.name_id = subject.name_id.text.strip() self.name_id = subject.name_id
return self.name_id return self.name_id
def _assertion(self, assertion): def _assertion(self, assertion):
@ -573,7 +584,6 @@ class AuthnResponse(StatusResponse):
return self._encrypted_assertion( return self._encrypted_assertion(
self.response.encrypted_assertion[0]) self.response.encrypted_assertion[0])
def verify(self): def verify(self):
""" Verify that the assertion is syntactically correct and """ Verify that the assertion is syntactically correct and
the signature is correct if present.""" the signature is correct if present."""
@ -632,19 +642,18 @@ class AuthnResponse(StatusResponse):
nooa = self.not_on_or_after nooa = self.not_on_or_after
if self.context == "AuthzQuery": if self.context == "AuthzQuery":
return {"name_id": self.name_id, return {"name_id": self.name_id, "came_from": self.came_from,
"came_from": self.came_from, "issuer": self.issuer(), "issuer": self.issuer(), "not_on_or_after": nooa,
"not_on_or_after": nooa,
"authz_decision_info": self.authz_decision_info() } "authz_decision_info": self.authz_decision_info() }
else: else:
return {"ava": self.ava, "name_id": self.name_id, return {"ava": self.ava, "name_id": self.name_id,
"came_from": self.came_from, "issuer": self.issuer(), "came_from": self.came_from, "issuer": self.issuer(),
"not_on_or_after": nooa, "not_on_or_after": nooa, "authn_info": self.authn_info()}
"authn_info": self.authn_info() }
def __str__(self): def __str__(self):
return "%s" % self.xmlstr return "%s" % self.xmlstr
class AuthnQueryResponse(AuthnResponse): class AuthnQueryResponse(AuthnResponse):
msgtype = "authn_query_response" msgtype = "authn_query_response"
@ -662,6 +671,7 @@ class AuthnQueryResponse(AuthnResponse):
def condition_ok(self, lax=False): # Should I care about conditions ? def condition_ok(self, lax=False): # Should I care about conditions ?
return True return True
class AttributeResponse(AuthnResponse): class AttributeResponse(AuthnResponse):
msgtype = "attribute_response" msgtype = "attribute_response"
@ -676,22 +686,26 @@ class AttributeResponse(AuthnResponse):
self.assertion = None self.assertion = None
self.context = "AttrQuery" self.context = "AttrQuery"
class AuthzResponse(AuthnResponse): class AuthzResponse(AuthnResponse):
""" A successful response will be in the form of assertions containing """ A successful response will be in the form of assertions containing
authorization decision statements.""" authorization decision statements."""
msgtype = "authz_decision_response" msgtype = "authz_decision_response"
def __init__(self, sec_context, attribute_converters, entity_id, def __init__(self, sec_context, attribute_converters, entity_id,
return_addr=None, timeslack=0, asynchop=False): return_addr=None, timeslack=0, asynchop=False):
AuthnResponse.__init__(self, sec_context, attribute_converters, AuthnResponse.__init__(self, sec_context, attribute_converters,
entity_id, return_addr, entity_id, return_addr, timeslack=timeslack,
timeslack=timeslack, asynchop=asynchop) asynchop=asynchop)
self.entity_id = entity_id self.entity_id = entity_id
self.attribute_converters = attribute_converters self.attribute_converters = attribute_converters
self.assertion = None self.assertion = None
self.context = "AuthzQuery" self.context = "AuthzQuery"
class ArtifactResponse(AuthnResponse): class ArtifactResponse(AuthnResponse):
msgtype = "artifact_response" msgtype = "artifact_response"
def __init__(self, sec_context, attribute_converters, entity_id, def __init__(self, sec_context, attribute_converters, entity_id,
return_addr=None, timeslack=0, asynchop=False, test=False): return_addr=None, timeslack=0, asynchop=False, test=False):
@ -704,10 +718,9 @@ class ArtifactResponse(AuthnResponse):
self.context = "ArtifactResolve" self.context = "ArtifactResolve"
def response_factory(xmlstr, conf, return_addr=None, def response_factory(xmlstr, conf, return_addr=None, outstanding_queries=None,
outstanding_queries=None, timeslack=0, decode=True, request_id=0, origxml=None,
timeslack=0, decode=True, request_id=0, asynchop=True, allow_unsolicited=False):
origxml=None, asynchop=True, allow_unsolicited=False):
sec_context = security_context(conf) sec_context = security_context(conf)
if not timeslack: if not timeslack:
try: try:
@ -724,8 +737,9 @@ def response_factory(xmlstr, conf, return_addr=None,
response.loads(xmlstr, decode, origxml) response.loads(xmlstr, decode, origxml)
if response.response.assertion or response.response.encrypted_assertion: if response.response.assertion or response.response.encrypted_assertion:
authnresp = AuthnResponse(sec_context, attribute_converters, authnresp = AuthnResponse(sec_context, attribute_converters,
entity_id, return_addr, outstanding_queries, entity_id, return_addr,
timeslack, asynchop, allow_unsolicited) outstanding_queries, timeslack, asynchop,
allow_unsolicited)
authnresp.update(response) authnresp.update(response)
return authnresp return authnresp
except TypeError: except TypeError:
@ -741,6 +755,7 @@ def response_factory(xmlstr, conf, return_addr=None,
# =========================================================================== # ===========================================================================
# A class of it's own # A class of it's own
class AssertionIDResponse(object): class AssertionIDResponse(object):
msgtype = "assertion_id_response" msgtype = "assertion_id_response"

View File

@ -4,6 +4,7 @@ from saml2.saml import NAMEID_FORMAT_PERSISTENT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class VirtualOrg(object): class VirtualOrg(object):
def __init__(self, sp, vorg, cnf): def __init__(self, sp, vorg, cnf):
self.sp = sp # The parent SP client instance self.sp = sp # The parent SP client instance
@ -28,7 +29,7 @@ class VirtualOrg(object):
""" """
return self.sp.config.metadata.vo_members(self._name) return self.sp.config.metadata.vo_members(self._name)
def members_to_ask(self, subject_id): def members_to_ask(self, name_id):
"""Find the member of the Virtual Organization that I haven't already """Find the member of the Virtual Organization that I haven't already
spoken too spoken too
""" """
@ -40,12 +41,12 @@ class VirtualOrg(object):
# Remove the ones I have cached data from about this subject # Remove the ones I have cached data from about this subject
vo_members = [m for m in vo_members if not self.sp.users.cache.active( vo_members = [m for m in vo_members if not self.sp.users.cache.active(
subject_id, m)] name_id, m)]
logger.info("VO members (not cached): %s" % vo_members) logger.info("VO members (not cached): %s" % vo_members)
return vo_members return vo_members
def get_common_identifier(self, subject_id): def get_common_identifier(self, name_id):
(ava, _) = self.sp.users.get_identity(subject_id) (ava, _) = self.sp.users.get_identity(name_id)
if ava == {}: if ava == {}:
return None return None
@ -56,36 +57,23 @@ class VirtualOrg(object):
except KeyError: except KeyError:
return None return None
def do_aggregation(self, subject_id): def do_aggregation(self, name_id):
logger.info("** Do VO aggregation **\nSubjectID: %s, VO:%s" % ( logger.info("** Do VO aggregation **\nSubjectID: %s, VO:%s" % (
subject_id, self._name)) name_id, self._name))
to_ask = self.members_to_ask(subject_id) to_ask = self.members_to_ask(name_id)
if to_ask: if to_ask:
# Find the NameIDFormat and the SPNameQualifier com_identifier = self.get_common_identifier(name_id)
if self.nameid_format:
name_id_format = self.nameid_format
sp_name_qualifier = ""
else:
sp_name_qualifier = self._name
name_id_format = ""
com_identifier = self.get_common_identifier(subject_id)
resolver = AttributeResolver(self.sp) resolver = AttributeResolver(self.sp)
# extends returns a list of session_infos # extends returns a list of session_infos
for session_info in resolver.extend(com_identifier, for session_info in resolver.extend(
self.sp.config.entityid, com_identifier, self.sp.config.entityid, to_ask):
to_ask,
name_id_format=name_id_format,
sp_name_qualifier=sp_name_qualifier,
real_id=subject_id):
_ = self._cache_session(session_info) _ = self._cache_session(session_info)
logger.info(">Issuers: %s" % self.sp.users.issuers_of_info( logger.info(">Issuers: %s" % self.sp.users.issuers_of_info(name_id))
subject_id)) logger.info("AVA: %s" % (self.sp.users.get_identity(name_id),))
logger.info("AVA: %s" % (self.sp.users.get_identity(subject_id),))
return True return True
else: else:

View File

@ -163,7 +163,7 @@ class FakeIDP(Server):
req = logout_request_from_string(_str) req = logout_request_from_string(_str)
_resp = self.create_logout_response(req, binding) _resp = self.create_logout_response(req, [binding])
if binding == BINDING_SOAP: if binding == BINDING_SOAP:
# SOAP packing # SOAP packing

View File

@ -2,8 +2,10 @@
import time import time
import py import py
from saml2.saml import NameID, NAMEID_FORMAT_TRANSIENT
from saml2.cache import Cache from saml2.cache import Cache
from saml2.time_util import in_a_while, str_to_time from saml2.time_util import in_a_while, str_to_time
from saml2.ident import code
SESSION_INFO_PATTERN = {"ava":{}, "came from":"", "not_on_or_after":0, SESSION_INFO_PATTERN = {"ava":{}, "came from":"", "not_on_or_after":0,
"issuer":"", "session_id":-1} "issuer":"", "session_id":-1}
@ -12,6 +14,13 @@ SESSION_INFO_PATTERN = {"ava":{}, "came from":"", "not_on_or_after":0,
def _eq(l1,l2): def _eq(l1,l2):
return set(l1) == set(l2) return set(l1) == set(l2)
def nid_eq(l1, l2):
return _eq([code(c) for c in l1], [code(c) for c in l2])
nid = [
NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT, text="1234"),
NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT, text="9876"),
NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT, text="1000")]
class TestClass: class TestClass:
def setup_class(self): def setup_class(self):
@ -22,10 +31,9 @@ class TestClass:
not_on_or_after = str_to_time(in_a_while(days=1)) not_on_or_after = str_to_time(in_a_while(days=1))
session_info = SESSION_INFO_PATTERN.copy() session_info = SESSION_INFO_PATTERN.copy()
session_info["ava"] = {"givenName":["Derek"]} session_info["ava"] = {"givenName":["Derek"]}
self.cache.set("1234", "abcd", session_info, self.cache.set(nid[0], "abcd", session_info, not_on_or_after)
not_on_or_after)
(ava, inactive) = self.cache.get_identity("1234") (ava, inactive) = self.cache.get_identity(nid[0])
assert inactive == [] assert inactive == []
assert ava.keys() == ["givenName"] assert ava.keys() == ["givenName"]
assert ava["givenName"] == ["Derek"] assert ava["givenName"] == ["Derek"]
@ -34,84 +42,83 @@ class TestClass:
not_on_or_after = str_to_time(in_a_while(days=1)) not_on_or_after = str_to_time(in_a_while(days=1))
session_info = SESSION_INFO_PATTERN.copy() session_info = SESSION_INFO_PATTERN.copy()
session_info["ava"] = {"surName":["Jeter"]} session_info["ava"] = {"surName":["Jeter"]}
self.cache.set("1234", "bcde", session_info, self.cache.set(nid[0], "bcde", session_info, not_on_or_after)
not_on_or_after)
(ava, inactive) = self.cache.get_identity("1234") (ava, inactive) = self.cache.get_identity(nid[0])
assert inactive == [] assert inactive == []
assert _eq(ava.keys(), ["givenName","surName"]) assert _eq(ava.keys(), ["givenName","surName"])
assert ava["givenName"] == ["Derek"] assert ava["givenName"] == ["Derek"]
assert ava["surName"] == ["Jeter"] assert ava["surName"] == ["Jeter"]
def test_from_one_target_source(self): def test_from_one_target_source(self):
session_info = self.cache.get("1234","bcde") session_info = self.cache.get(nid[0], "bcde")
ava = session_info["ava"] ava = session_info["ava"]
assert _eq(ava.keys(), ["surName"]) assert _eq(ava.keys(), ["surName"])
assert ava["surName"] == ["Jeter"] assert ava["surName"] == ["Jeter"]
session_info = self.cache.get("1234","abcd") session_info = self.cache.get(nid[0], "abcd")
ava = session_info["ava"] ava = session_info["ava"]
assert _eq(ava.keys(), ["givenName"]) assert _eq(ava.keys(), ["givenName"])
assert ava["givenName"] == ["Derek"] assert ava["givenName"] == ["Derek"]
def test_entities(self): def test_entities(self):
assert _eq(self.cache.entities("1234"), ["abcd", "bcde"]) assert _eq(self.cache.entities(nid[0]), ["abcd", "bcde"])
py.test.raises(Exception, "self.cache.entities('6666')") py.test.raises(Exception, "self.cache.entities('6666')")
def test_remove_info(self): def test_remove_info(self):
self.cache.reset("1234", "bcde") self.cache.reset(nid[0], "bcde")
assert self.cache.active("1234", "bcde") == False assert self.cache.active(nid[0], "bcde") == False
assert self.cache.active("1234", "abcd") assert self.cache.active(nid[0], "abcd")
(ava, inactive) = self.cache.get_identity("1234") (ava, inactive) = self.cache.get_identity(nid[0])
assert inactive == ['bcde'] assert inactive == ['bcde']
assert _eq(ava.keys(), ["givenName"]) assert _eq(ava.keys(), ["givenName"])
assert ava["givenName"] == ["Derek"] assert ava["givenName"] == ["Derek"]
def test_active(self): def test_active(self):
assert self.cache.active("1234", "bcde") == False assert self.cache.active(nid[0], "bcde") == False
assert self.cache.active("1234", "abcd") assert self.cache.active(nid[0], "abcd")
def test_subjects(self): def test_subjects(self):
assert self.cache.subjects() == ["1234"] assert nid_eq(self.cache.subjects(), [nid[0]])
def test_second_subject(self): def test_second_subject(self):
not_on_or_after = str_to_time(in_a_while(days=1)) not_on_or_after = str_to_time(in_a_while(days=1))
session_info = SESSION_INFO_PATTERN.copy() session_info = SESSION_INFO_PATTERN.copy()
session_info["ava"] = {"givenName":["Ichiro"], session_info["ava"] = {"givenName":["Ichiro"],
"surName":["Suzuki"]} "surName":["Suzuki"]}
self.cache.set("9876", "abcd", session_info, self.cache.set(nid[1], "abcd", session_info,
not_on_or_after) not_on_or_after)
(ava, inactive) = self.cache.get_identity("9876") (ava, inactive) = self.cache.get_identity(nid[1])
assert inactive == [] assert inactive == []
assert _eq(ava.keys(), ["givenName","surName"]) assert _eq(ava.keys(), ["givenName","surName"])
assert ava["givenName"] == ["Ichiro"] assert ava["givenName"] == ["Ichiro"]
assert ava["surName"] == ["Suzuki"] assert ava["surName"] == ["Suzuki"]
assert _eq(self.cache.subjects(), ["1234","9876"]) assert nid_eq(self.cache.subjects(), [nid[0], nid[1]])
def test_receivers(self): def test_receivers(self):
assert _eq(self.cache.receivers("9876"), ["abcd"]) assert _eq(self.cache.receivers(nid[1]), ["abcd"])
not_on_or_after = str_to_time(in_a_while(days=1)) not_on_or_after = str_to_time(in_a_while(days=1))
session_info = SESSION_INFO_PATTERN.copy() session_info = SESSION_INFO_PATTERN.copy()
session_info["ava"] = {"givenName":["Ichiro"], session_info["ava"] = {"givenName":["Ichiro"],
"surName":["Suzuki"]} "surName":["Suzuki"]}
self.cache.set("9876", "bcde", session_info, self.cache.set(nid[1], "bcde", session_info,
not_on_or_after) not_on_or_after)
assert _eq(self.cache.receivers("9876"), ["abcd", "bcde"]) assert _eq(self.cache.receivers(nid[1]), ["abcd", "bcde"])
assert _eq(self.cache.subjects(), ["1234","9876"]) assert nid_eq(self.cache.subjects(), nid[0:2])
def test_timeout(self): def test_timeout(self):
not_on_or_after = str_to_time(in_a_while(seconds=1)) not_on_or_after = str_to_time(in_a_while(seconds=1))
session_info = SESSION_INFO_PATTERN.copy() session_info = SESSION_INFO_PATTERN.copy()
session_info["ava"] = {"givenName":["Alex"], session_info["ava"] = {"givenName":["Alex"],
"surName":["Rodriguez"]} "surName":["Rodriguez"]}
self.cache.set("1000", "bcde", session_info, self.cache.set(nid[2], "bcde", session_info,
not_on_or_after) not_on_or_after)
time.sleep(2) time.sleep(2)
(ava, inactive) = self.cache.get_identity("1000") (ava, inactive) = self.cache.get_identity(nid[2])
assert inactive == ["bcde"] assert inactive == ["bcde"]
assert ava == {} assert ava == {}

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from saml2.ident import code
from saml2.saml import NAMEID_FORMAT_TRANSIENT, NameID
from saml2.population import Population from saml2.population import Population
from saml2.time_util import in_a_while from saml2.time_util import in_a_while
@ -6,6 +8,14 @@ from saml2.time_util import in_a_while
IDP_ONE = "urn:mace:example.com:saml:one:idp" IDP_ONE = "urn:mace:example.com:saml:one:idp"
IDP_OTHER = "urn:mace:example.com:saml:other:idp" IDP_OTHER = "urn:mace:example.com:saml:other:idp"
nid = NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT,
text="123456")
nida = NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT,
text="abcdef")
cnid = code(nid)
cnida = code(nida)
def _eq(l1, l2): def _eq(l1, l2):
return set(l1) == set(l2) return set(l1) == set(l2)
@ -15,7 +25,7 @@ class TestPopulationMemoryBased():
def test_add_person(self): def test_add_person(self):
session_info = { session_info = {
"name_id": "123456", "name_id": nid,
"issuer": IDP_ONE, "issuer": IDP_ONE,
"not_on_or_after": in_a_while(minutes=15), "not_on_or_after": in_a_while(minutes=15),
"ava": { "ava": {
@ -26,34 +36,34 @@ class TestPopulationMemoryBased():
} }
self.population.add_information_about_person(session_info) self.population.add_information_about_person(session_info)
issuers = self.population.issuers_of_info("123456") issuers = self.population.issuers_of_info(nid)
assert issuers == [IDP_ONE] assert issuers == [IDP_ONE]
subjects = self.population.subjects() subjects = [code(c) for c in self.population.subjects()]
assert subjects == ["123456"] assert subjects == [cnid]
# Are any of the sources gone stale # Are any of the sources gone stale
stales = self.population.stale_sources_for_person("123456") stales = self.population.stale_sources_for_person(nid)
assert stales == [] assert stales == []
# are any of the possible sources not used or gone stale # are any of the possible sources not used or gone stale
possible = [IDP_ONE, IDP_OTHER] possible = [IDP_ONE, IDP_OTHER]
stales = self.population.stale_sources_for_person("123456", possible) stales = self.population.stale_sources_for_person(nid, possible)
assert stales == [IDP_OTHER] assert stales == [IDP_OTHER]
(identity, stale) = self.population.get_identity("123456") (identity, stale) = self.population.get_identity(nid)
assert stale == [] assert stale == []
assert identity == {'mail': 'anders.andersson@example.com', assert identity == {'mail': 'anders.andersson@example.com',
'givenName': 'Anders', 'givenName': 'Anders',
'surName': 'Andersson'} 'surName': 'Andersson'}
info = self.population.get_info_from("123456", IDP_ONE) info = self.population.get_info_from(nid, IDP_ONE)
assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info.keys() == ["not_on_or_after", "name_id", "ava"]
assert info["name_id"] == '123456' assert info["name_id"] == nid
assert info["ava"] == {'mail': 'anders.andersson@example.com', assert info["ava"] == {'mail': 'anders.andersson@example.com',
'givenName': 'Anders', 'givenName': 'Anders',
'surName': 'Andersson'} 'surName': 'Andersson'}
def test_extend_person(self): def test_extend_person(self):
session_info = { session_info = {
"name_id": "123456", "name_id": nid,
"issuer": IDP_OTHER, "issuer": IDP_OTHER,
"not_on_or_after": in_a_while(minutes=15), "not_on_or_after": in_a_while(minutes=15),
"ava": { "ava": {
@ -63,33 +73,33 @@ class TestPopulationMemoryBased():
self.population.add_information_about_person(session_info) self.population.add_information_about_person(session_info)
issuers = self.population.issuers_of_info("123456") issuers = self.population.issuers_of_info(nid)
assert _eq(issuers, [IDP_ONE, IDP_OTHER]) assert _eq(issuers, [IDP_ONE, IDP_OTHER])
subjects = self.population.subjects() subjects = [code(c) for c in self.population.subjects()]
assert subjects == ["123456"] assert subjects == [cnid]
# Are any of the sources gone stale # Are any of the sources gone stale
stales = self.population.stale_sources_for_person("123456") stales = self.population.stale_sources_for_person(nid)
assert stales == [] assert stales == []
# are any of the possible sources not used or gone stale # are any of the possible sources not used or gone stale
possible = [IDP_ONE, IDP_OTHER] possible = [IDP_ONE, IDP_OTHER]
stales = self.population.stale_sources_for_person("123456", possible) stales = self.population.stale_sources_for_person(nid, possible)
assert stales == [] assert stales == []
(identity, stale) = self.population.get_identity("123456") (identity, stale) = self.population.get_identity(nid)
assert stale == [] assert stale == []
assert identity == {'mail': 'anders.andersson@example.com', assert identity == {'mail': 'anders.andersson@example.com',
'givenName': 'Anders', 'givenName': 'Anders',
'surName': 'Andersson', 'surName': 'Andersson',
"eduPersonEntitlement": "Anka"} "eduPersonEntitlement": "Anka"}
info = self.population.get_info_from("123456", IDP_OTHER) info = self.population.get_info_from(nid, IDP_OTHER)
assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info.keys() == ["not_on_or_after", "name_id", "ava"]
assert info["name_id"] == '123456' assert info["name_id"] == nid
assert info["ava"] == {"eduPersonEntitlement": "Anka"} assert info["ava"] == {"eduPersonEntitlement": "Anka"}
def test_add_another_person(self): def test_add_another_person(self):
session_info = { session_info = {
"name_id": "abcdef", "name_id": nida,
"issuer": IDP_ONE, "issuer": IDP_ONE,
"not_on_or_after": in_a_while(minutes=15), "not_on_or_after": in_a_while(minutes=15),
"ava": { "ava": {
@ -100,28 +110,28 @@ class TestPopulationMemoryBased():
} }
self.population.add_information_about_person(session_info) self.population.add_information_about_person(session_info)
issuers = self.population.issuers_of_info("abcdef") issuers = self.population.issuers_of_info(nida)
assert issuers == [IDP_ONE] assert issuers == [IDP_ONE]
subjects = self.population.subjects() subjects = [code(c) for c in self.population.subjects()]
assert _eq(subjects, ["123456", "abcdef"]) assert _eq(subjects, [cnid, cnida])
stales = self.population.stale_sources_for_person("abcdef") stales = self.population.stale_sources_for_person(nida)
assert stales == [] assert stales == []
# are any of the possible sources not used or gone stale # are any of the possible sources not used or gone stale
possible = [IDP_ONE, IDP_OTHER] possible = [IDP_ONE, IDP_OTHER]
stales = self.population.stale_sources_for_person("abcdef", possible) stales = self.population.stale_sources_for_person(nida, possible)
assert stales == [IDP_OTHER] assert stales == [IDP_OTHER]
(identity, stale) = self.population.get_identity("abcdef") (identity, stale) = self.population.get_identity(nida)
assert stale == [] assert stale == []
assert identity == {"givenName": "Bertil", assert identity == {"givenName": "Bertil",
"surName": "Bertilsson", "surName": "Bertilsson",
"mail": "bertil.bertilsson@example.com" "mail": "bertil.bertilsson@example.com"
} }
info = self.population.get_info_from("abcdef", IDP_ONE) info = self.population.get_info_from(nida, IDP_ONE)
assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info.keys() == ["not_on_or_after", "name_id", "ava"]
assert info["name_id"] == 'abcdef' assert info["name_id"] == nida
assert info["ava"] == {"givenName": "Bertil", assert info["ava"] == {"givenName": "Bertil",
"surName": "Bertilsson", "surName": "Bertilsson",
"mail": "bertil.bertilsson@example.com" "mail": "bertil.bertilsson@example.com"
@ -129,7 +139,7 @@ class TestPopulationMemoryBased():
def test_modify_person(self): def test_modify_person(self):
session_info = { session_info = {
"name_id": "123456", "name_id": nid,
"issuer": IDP_ONE, "issuer": IDP_ONE,
"not_on_or_after": in_a_while(minutes=15), "not_on_or_after": in_a_while(minutes=15),
"ava": { "ava": {
@ -140,26 +150,26 @@ class TestPopulationMemoryBased():
} }
self.population.add_information_about_person(session_info) self.population.add_information_about_person(session_info)
issuers = self.population.issuers_of_info("123456") issuers = self.population.issuers_of_info(nid)
assert _eq(issuers, [IDP_ONE, IDP_OTHER]) assert _eq(issuers, [IDP_ONE, IDP_OTHER])
subjects = self.population.subjects() subjects = [code(c) for c in self.population.subjects()]
assert _eq(subjects, ["123456", "abcdef"]) assert _eq(subjects, [cnid, cnida])
# Are any of the sources gone stale # Are any of the sources gone stale
stales = self.population.stale_sources_for_person("123456") stales = self.population.stale_sources_for_person(nid)
assert stales == [] assert stales == []
# are any of the possible sources not used or gone stale # are any of the possible sources not used or gone stale
possible = [IDP_ONE, IDP_OTHER] possible = [IDP_ONE, IDP_OTHER]
stales = self.population.stale_sources_for_person("123456", possible) stales = self.population.stale_sources_for_person(nid, possible)
assert stales == [] assert stales == []
(identity, stale) = self.population.get_identity("123456") (identity, stale) = self.population.get_identity(nid)
assert stale == [] assert stale == []
assert identity == {'mail': 'arne.andersson@example.com', assert identity == {'mail': 'arne.andersson@example.com',
'givenName': 'Arne', 'givenName': 'Arne',
'surName': 'Andersson', 'surName': 'Andersson',
"eduPersonEntitlement": "Anka"} "eduPersonEntitlement": "Anka"}
info = self.population.get_info_from("123456", IDP_OTHER) info = self.population.get_info_from(nid, IDP_OTHER)
assert info.keys() == ["not_on_or_after", "name_id", "ava"] assert info.keys() == ["not_on_or_after", "name_id", "ava"]
assert info["name_id"] == '123456' assert info["name_id"] == nid
assert info["ava"] == {"eduPersonEntitlement": "Anka"} assert info["ava"] == {"eduPersonEntitlement": "Anka"}

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import base64 import base64
from urlparse import parse_qs from urlparse import parse_qs
from saml2.saml import AUTHN_PASSWORD from saml2.saml import AUTHN_PASSWORD, NameID, NAMEID_FORMAT_TRANSIENT
from saml2.samlp import response_from_string from saml2.samlp import response_from_string
from saml2.server import Server from saml2.server import Server
@ -18,6 +18,9 @@ from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
from py.test import raises from py.test import raises
import os import os
nid = NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT,
text="123456")
def _eq(l1,l2): def _eq(l1,l2):
return set(l1) == set(l2) return set(l1) == set(l2)
@ -150,8 +153,7 @@ class TestServer1():
def test_parse_ok_request(self): def test_parse_ok_request(self):
authn_request = self.client.create_authn_request( authn_request = self.client.create_authn_request(
id = "id1", sid="id1", destination="http://localhost:8088/sso")
destination = "http://localhost:8088/sso")
print authn_request print authn_request
binding = BINDING_HTTP_REDIRECT binding = BINDING_HTTP_REDIRECT
@ -335,7 +337,7 @@ class TestServer1():
def test_slo_http_post(self): def test_slo_http_post(self):
soon = time_util.in_a_while(days=1) soon = time_util.in_a_while(days=1)
sinfo = { sinfo = {
"name_id": "foba0001", "name_id": nid,
"issuer": "urn:mace:example.com:saml:roland:idp", "issuer": "urn:mace:example.com:saml:roland:idp",
"not_on_or_after" : soon, "not_on_or_after" : soon,
"user": { "user": {
@ -346,8 +348,7 @@ class TestServer1():
self.client.users.add_information_about_person(sinfo) self.client.users.add_information_about_person(sinfo)
logout_request = self.client.create_logout_request( logout_request = self.client.create_logout_request(
destination = "http://localhost:8088/slop", destination="http://localhost:8088/slop", name_id=nid,
subject_id="foba0001",
issuer_entity_id="urn:mace:example.com:saml:roland:idp", issuer_entity_id="urn:mace:example.com:saml:roland:idp",
reason="I'm tired of this") reason="I'm tired of this")
@ -360,7 +361,7 @@ class TestServer1():
def test_slo_soap(self): def test_slo_soap(self):
soon = time_util.in_a_while(days=1) soon = time_util.in_a_while(days=1)
sinfo = { sinfo = {
"name_id": "foba0001", "name_id": nid,
"issuer": "urn:mace:example.com:saml:roland:idp", "issuer": "urn:mace:example.com:saml:roland:idp",
"not_on_or_after": soon, "not_on_or_after": soon,
"user": { "user": {
@ -373,8 +374,7 @@ class TestServer1():
sp.users.add_information_about_person(sinfo) sp.users.add_information_about_person(sinfo)
logout_request = sp.create_logout_request( logout_request = sp.create_logout_request(
subject_id = "foba0001", name_id=nid, destination="http://localhost:8088/slo",
destination = "http://localhost:8088/slo",
issuer_entity_id="urn:mace:example.com:saml:roland:idp", issuer_entity_id="urn:mace:example.com:saml:roland:idp",
reason="I'm tired of this") reason="I'm tired of this")
@ -429,7 +429,7 @@ def _logout_request(conf_file):
soon = time_util.in_a_while(days=1) soon = time_util.in_a_while(days=1)
sinfo = { sinfo = {
"name_id": "foba0001", "name_id": nid,
"issuer": "urn:mace:example.com:saml:roland:idp", "issuer": "urn:mace:example.com:saml:roland:idp",
"not_on_or_after" : soon, "not_on_or_after" : soon,
"user": { "user": {
@ -440,7 +440,7 @@ def _logout_request(conf_file):
sp.users.add_information_about_person(sinfo) sp.users.add_information_about_person(sinfo)
return sp.create_logout_request( return sp.create_logout_request(
subject_id = "foba0001", name_id = nid,
destination = "http://localhost:8088/slo", destination = "http://localhost:8088/slo",
issuer_entity_id = "urn:mace:example.com:saml:roland:idp", issuer_entity_id = "urn:mace:example.com:saml:roland:idp",
reason = "I'm tired of this") reason = "I'm tired of this")

View File

@ -3,14 +3,17 @@
import base64 import base64
import urllib import urllib
from saml2.response import LogoutResponse
from saml2.samlp import logout_request_from_string from saml2.samlp import logout_request_from_string
from saml2.client import Saml2Client from saml2.client import Saml2Client
from saml2 import samlp, BINDING_HTTP_POST from saml2 import samlp, BINDING_HTTP_POST
from saml2 import saml, config, class_name from saml2 import saml, config, class_name
from saml2.config import SPConfig from saml2.config import SPConfig
from saml2.saml import NAMEID_FORMAT_PERSISTENT, NAMEID_FORMAT_TRANSIENT, \ from saml2.saml import NAMEID_FORMAT_PERSISTENT
AUTHN_PASSWORD from saml2.saml import NAMEID_FORMAT_TRANSIENT
from saml2.saml import AUTHN_PASSWORD
from saml2.saml import NameID
from saml2.server import Server from saml2.server import Server
from saml2.time_util import in_a_while from saml2.time_util import in_a_while
@ -55,6 +58,9 @@ REQ1 = { "1.2.14": """<?xml version='1.0' encoding='UTF-8'?>
AUTHN = (AUTHN_PASSWORD, "http://www.example.com/login") AUTHN = (AUTHN_PASSWORD, "http://www.example.com/login")
nid = NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT,
text="123456")
class TestClient: class TestClient:
def setup_class(self): def setup_class(self):
self.server = Server("idp_conf") self.server = Server("idp_conf")
@ -68,7 +74,7 @@ class TestClient:
"https://idp.example.com/idp/", "https://idp.example.com/idp/",
"E8042FB4-4D5B-48C3-8E14-8EDD852790DD", "E8042FB4-4D5B-48C3-8E14-8EDD852790DD",
format=saml.NAMEID_FORMAT_PERSISTENT, format=saml.NAMEID_FORMAT_PERSISTENT,
id="id1") sid="id1")
reqstr = "%s" % req.to_string() reqstr = "%s" % req.to_string()
assert req.destination == "https://idp.example.com/idp/" assert req.destination == "https://idp.example.com/idp/"
@ -110,7 +116,7 @@ class TestClient:
"urn:oasis:names:tc:SAML:2.0:attrname-format:uri"):None, "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"):None,
}, },
format=saml.NAMEID_FORMAT_PERSISTENT, format=saml.NAMEID_FORMAT_PERSISTENT,
id="id1") sid="id1")
print req.to_string() print req.to_string()
assert req.destination == "https://idp.example.com/idp/" assert req.destination == "https://idp.example.com/idp/"
@ -144,7 +150,7 @@ class TestClient:
"https://aai-demo-idp.switch.ch/idp/shibboleth", "https://aai-demo-idp.switch.ch/idp/shibboleth",
"_e7b68a04488f715cda642fbdd90099f5", "_e7b68a04488f715cda642fbdd90099f5",
format=saml.NAMEID_FORMAT_TRANSIENT, format=saml.NAMEID_FORMAT_TRANSIENT,
id="id1") sid="id1")
assert isinstance(req, samlp.AttributeQuery) assert isinstance(req, samlp.AttributeQuery)
assert req.destination == "https://aai-demo-idp.switch.ch/idp/shibboleth" assert req.destination == "https://aai-demo-idp.switch.ch/idp/shibboleth"
@ -178,7 +184,7 @@ class TestClient:
def test_create_auth_request_0(self): def test_create_auth_request_0(self):
ar_str = "%s" % self.client.create_authn_request( ar_str = "%s" % self.client.create_authn_request(
"http://www.example.com/sso", "http://www.example.com/sso",
id="id1") sid="id1")
ar = samlp.authn_request_from_string(ar_str) ar = samlp.authn_request_from_string(ar_str)
print ar print ar
assert ar.assertion_consumer_service_url == "http://lingon.catalogix.se:8087/" assert ar.assertion_consumer_service_url == "http://lingon.catalogix.se:8087/"
@ -199,7 +205,7 @@ class TestClient:
"http://www.example.com/sso", "http://www.example.com/sso",
"urn:mace:example.com:it:tek", # vo "urn:mace:example.com:it:tek", # vo
nameid_format=NAMEID_FORMAT_PERSISTENT, nameid_format=NAMEID_FORMAT_PERSISTENT,
id="666") sid="666")
ar = samlp.authn_request_from_string(ar_str) ar = samlp.authn_request_from_string(ar_str)
print ar print ar
@ -221,7 +227,7 @@ class TestClient:
ar_str = "%s" % self.client.create_authn_request( ar_str = "%s" % self.client.create_authn_request(
"http://www.example.com/sso", "http://www.example.com/sso",
sign=True, sign=True,
id="id1") sid="id1")
ar = samlp.authn_request_from_string(ar_str) ar = samlp.authn_request_from_string(ar_str)
@ -343,10 +349,10 @@ class TestClientWithDummy():
self.client.send = self.server.receive self.client.send = self.server.receive
def test_do_authn(self): def test_do_authn(self):
id, http_args = self.client.prepare_for_authenticate(IDP, sid, http_args = self.client.prepare_for_authenticate(IDP,
"http://www.example.com/relay_state") "http://www.example.com/relay_state")
assert isinstance(id, basestring) assert isinstance(sid, basestring)
assert len(http_args) == 4 assert len(http_args) == 4
assert http_args["headers"][0][0] == "Location" assert http_args["headers"][0][0] == "Location"
assert http_args["data"] == [] assert http_args["data"] == []
@ -363,7 +369,7 @@ class TestClientWithDummy():
# information about the user from an IdP # information about the user from an IdP
session_info = { session_info = {
"name_id": "123456", "name_id": nid,
"issuer": "urn:mace:example.com:saml:roland:idp", "issuer": "urn:mace:example.com:saml:roland:idp",
"not_on_or_after": in_a_while(minutes=15), "not_on_or_after": in_a_while(minutes=15),
"ava": { "ava": {
@ -373,24 +379,18 @@ class TestClientWithDummy():
} }
} }
self.client.users.add_information_about_person(session_info) self.client.users.add_information_about_person(session_info)
entity_ids = self.client.users.issuers_of_info("123456") entity_ids = self.client.users.issuers_of_info(nid)
assert entity_ids == ["urn:mace:example.com:saml:roland:idp"] assert entity_ids == ["urn:mace:example.com:saml:roland:idp"]
resp = self.client.global_logout("123456", "Tired", in_a_while(minutes=5)) resp = self.client.global_logout(nid, "Tired", in_a_while(minutes=5))
print resp print resp
assert resp assert resp
assert len(resp) == 1 assert len(resp) == 1
assert resp.keys() == entity_ids assert resp.keys() == entity_ids
http_args = resp[entity_ids[0]] response = resp[entity_ids[0]]
assert isinstance(http_args, dict) assert isinstance(response, LogoutResponse)
assert http_args["headers"] == [('Content-type', 'text/html')]
info = unpack_form(http_args["data"][3])
xml_str = base64.b64decode(info["SAMLRequest"])
req = logout_request_from_string(xml_str)
print req
assert req.reason == "Tired"
def test_post_sso(self): def test_post_sso(self):
id, http_args = self.client.prepare_for_authenticate( sid, http_args = self.client.prepare_for_authenticate(
"urn:mace:example.com:saml:roland:idp", "urn:mace:example.com:saml:roland:idp",
relay_state="really", relay_state="really",
binding=BINDING_HTTP_POST) binding=BINDING_HTTP_POST)
@ -403,210 +403,17 @@ class TestClientWithDummy():
http_args["data"] = urllib.urlencode(_dic) http_args["data"] = urllib.urlencode(_dic)
http_args["method"] = "POST" http_args["method"] = "POST"
http_args["dummy"] = _dic["SAMLRequest"] http_args["dummy"] = _dic["SAMLRequest"]
http_args["headers"] = [('Content-type','application/x-www-form-urlencoded')] http_args["headers"] = [('Content-type',
'application/x-www-form-urlencoded')]
response = self.client.send(**http_args) response = self.client.send(**http_args)
print response.text print response.text
_dic = unpack_form(response.text[3], "SAMLResponse") _dic = unpack_form(response.text[3], "SAMLResponse")
resp = self.client.parse_authn_request_response(_dic["SAMLResponse"], resp = self.client.parse_authn_request_response(_dic["SAMLResponse"],
BINDING_HTTP_POST, BINDING_HTTP_POST,
{id: "/"}) {sid: "/"})
ac = resp.assertion.authn_statement[0].authn_context ac = resp.assertion.authn_statement[0].authn_context
assert ac.authenticating_authority[0].text == 'http://www.example.com/login' assert ac.authenticating_authority[0].text == \
'http://www.example.com/login'
assert ac.authn_context_class_ref.text == AUTHN_PASSWORD assert ac.authn_context_class_ref.text == AUTHN_PASSWORD
# def test_logout_2(self):
# """ one IdP/AA with BINDING_SOAP, can't actually send something"""
#
# conf = config.SPConfig()
# conf.load_file("server2_conf")
# client = Saml2Client(conf)
#
# # information about the user from an IdP
# session_info = {
# "name_id": "123456",
# "issuer": "urn:mace:example.com:saml:roland:idp",
# "not_on_or_after": in_a_while(minutes=15),
# "ava": {
# "givenName": "Anders",
# "surName": "Andersson",
# "mail": "anders.andersson@example.com"
# }
# }
# client.users.add_information_about_person(session_info)
# entity_ids = self.client.users.issuers_of_info("123456")
# assert entity_ids == ["urn:mace:example.com:saml:roland:idp"]
# destinations = client.config.single_logout_services(entity_ids[0],
# BINDING_SOAP)
# print destinations
# assert destinations == ['http://localhost:8088/slo']
#
# # Will raise an error since there is noone at the other end.
# raises(LogoutError, 'client.global_logout("123456", "Tired", in_a_while(minutes=5))')
#
# def test_logout_3(self):
# """ two or more IdP/AA with BINDING_HTTP_REDIRECT"""
#
# conf = config.SPConfig()
# conf.load_file("server3_conf")
# client = Saml2Client(conf)
#
# # information about the user from an IdP
# session_info_authn = {
# "name_id": "123456",
# "issuer": "urn:mace:example.com:saml:roland:idp",
# "not_on_or_after": in_a_while(minutes=15),
# "ava": {
# "givenName": "Anders",
# "surName": "Andersson",
# "mail": "anders.andersson@example.com"
# }
# }
# client.users.add_information_about_person(session_info_authn)
# session_info_aa = {
# "name_id": "123456",
# "issuer": "urn:mace:example.com:saml:roland:aa",
# "not_on_or_after": in_a_while(minutes=15),
# "ava": {
# "eduPersonEntitlement": "Foobar",
# }
# }
# client.users.add_information_about_person(session_info_aa)
# entity_ids = client.users.issuers_of_info("123456")
# assert _leq(entity_ids, ["urn:mace:example.com:saml:roland:idp",
# "urn:mace:example.com:saml:roland:aa"])
# resp = client.global_logout("123456", "Tired", in_a_while(minutes=5))
# print resp
# assert resp
# assert resp[0] # a session_id
# assert resp[1] == '200 OK'
# # HTTP POST
# assert resp[2] == [('Content-type', 'text/html')]
# assert resp[3][0] == '<head>'
# assert resp[3][1] == '<title>SAML 2.0 POST</title>'
#
# state_info = client.state[resp[0]]
# print state_info
# assert state_info["entity_id"] == entity_ids[0]
# assert state_info["subject_id"] == "123456"
# assert state_info["reason"] == "Tired"
# assert state_info["operation"] == "SLO"
# assert state_info["entity_ids"] == entity_ids
# assert state_info["sign"] == True
#
# def test_authz_decision_query(self):
# conf = config.SPConfig()
# conf.load_file("server3_conf")
# client = Saml2Client(conf)
#
# AVA = {'mail': u'roland.hedberg@adm.umu.se',
# 'eduPersonTargetedID': '95e9ae91dbe62d35198fbbd5e1fb0976',
# 'displayName': u'Roland Hedberg',
# 'uid': 'http://roland.hedberg.myopenid.com/'}
#
# sp_entity_id = "sp_entity_id"
# in_response_to = "1234"
# consumer_url = "http://example.com/consumer"
# name_id = saml.NameID(saml.NAMEID_FORMAT_TRANSIENT, text="name_id")
# policy = Policy()
# ava = Assertion(AVA)
# assertion = ava.construct(sp_entity_id, in_response_to,
# consumer_url, name_id,
# conf.attribute_converters,
# policy, issuer=client._issuer())
#
# adq = client.create_authz_decision_query_using_assertion("entity_id",
# assertion,
# "read",
# "http://example.com/text")
#
# assert adq
# print adq
# assert adq.keyswv() != []
# assert adq.destination == "entity_id"
# assert adq.resource == "http://example.com/text"
# assert adq.action[0].text == "read"
#
# def test_request_to_discovery_service(self):
# disc_url = "http://example.com/saml2/idp/disc"
# url = discovery_service_request_url("urn:mace:example.com:saml:roland:sp",
# disc_url)
# print url
# assert url == "http://example.com/saml2/idp/disc?entityID=urn%3Amace%3Aexample.com%3Asaml%3Aroland%3Asp"
#
# url = discovery_service_request_url(
# self.client.config.entityid,
# disc_url,
# return_url= "http://example.org/saml2/sp/ds")
#
# print url
# assert url == "http://example.com/saml2/idp/disc?entityID=urn%3Amace%3Aexample.com%3Asaml%3Aroland%3Asp&return=http%3A%2F%2Fexample.org%2Fsaml2%2Fsp%2Fds"
#
# def test_get_idp_from_discovery_service(self):
# pdir = {"entityID": "http://example.org/saml2/idp/sso"}
# params = urllib.urlencode(pdir)
# redirect_url = "http://example.com/saml2/sp/disc?%s" % params
#
# entity_id = discovery_service_response(url=redirect_url)
# assert entity_id == "http://example.org/saml2/idp/sso"
#
# pdir = {"idpID": "http://example.org/saml2/idp/sso"}
# params = urllib.urlencode(pdir)
# redirect_url = "http://example.com/saml2/sp/disc?%s" % params
#
# entity_id = discovery_service_response(url=redirect_url,
# returnIDParam="idpID")
#
# assert entity_id == "http://example.org/saml2/idp/sso"
# self.server.close_shelve_db()
#
# def test_unsolicited_response(self):
# """
#
# """
# self.server = Server("idp_conf")
#
# conf = config.SPConfig()
# conf.load_file("server_conf")
# self.client = Saml2Client(conf)
#
# for subject in self.client.users.subjects():
# self.client.users.remove_person(subject)
#
# IDP = "urn:mace:example.com:saml:roland:idp"
#
# ava = { "givenName": ["Derek"], "surName": ["Jeter"],
# "mail": ["derek@nyy.mlb.com"], "title": ["The man"]}
#
# resp_str = "%s" % self.server.create_authn_response(
# identity=ava,
# in_response_to="id1",
# destination="http://lingon.catalogix.se:8087/",
# sp_entity_id="urn:mace:example.com:saml:roland:sp",
# name_id_policy=samlp.NameIDPolicy(
# format=saml.NAMEID_FORMAT_PERSISTENT),
# userid="foba0001@example.com")
#
# resp_str = base64.encodestring(resp_str)
#
# self.client.allow_unsolicited = True
# authn_response = self.client.authn_request_response(
# {"SAMLResponse":resp_str}, ())
#
# assert authn_response is not None
# assert authn_response.issuer() == IDP
# assert authn_response.response.assertion[0].issuer.text == IDP
# session_info = authn_response.session_info()
#
# print session_info
# assert session_info["ava"] == {'mail': ['derek@nyy.mlb.com'],
# 'givenName': ['Derek'],
# 'surName': ['Jeter']}
# assert session_info["issuer"] == IDP
# assert session_info["came_from"] == ""
# response = samlp.response_from_string(authn_response.xmlstr)
# assert response.destination == "http://lingon.catalogix.se:8087/"
#
# # One person in the cache
# assert len(self.client.users.subjects()) == 1
# self.server.close_shelve_db()

View File

@ -1,3 +1,5 @@
from saml2.saml import NameID, NAMEID_FORMAT_TRANSIENT
__author__ = 'rolandh' __author__ = 'rolandh'
from saml2 import config from saml2 import config
@ -7,16 +9,23 @@ from saml2.time_util import str_to_time, in_a_while
SESSION_INFO_PATTERN = {"ava": {}, "came from": "", "not_on_or_after": 0, SESSION_INFO_PATTERN = {"ava": {}, "came from": "", "not_on_or_after": 0,
"issuer": "", "session_id": -1} "issuer": "", "session_id": -1}
nid = NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT,
text="abcdefgh")
nid0 = NameID(name_qualifier="foo", format=NAMEID_FORMAT_TRANSIENT,
text="01234567")
def add_derek_info(sp): def add_derek_info(sp):
not_on_or_after = str_to_time(in_a_while(days=1)) not_on_or_after = str_to_time(in_a_while(days=1))
session_info = SESSION_INFO_PATTERN.copy() session_info = SESSION_INFO_PATTERN.copy()
session_info["ava"] = {"givenName": ["Derek"], "umuselin": ["deje0001"]} session_info["ava"] = {"givenName": ["Derek"], "umuselin": ["deje0001"]}
session_info["issuer"] = "urn:mace:example.com:saml:idp" session_info["issuer"] = "urn:mace:example.com:saml:idp"
session_info["name_id"] = "abcdefgh" session_info["name_id"] = nid
session_info["not_on_or_after"] = not_on_or_after session_info["not_on_or_after"] = not_on_or_after
# subject_id, entity_id, info, timestamp # subject_id, entity_id, info, timestamp
sp.users.add_information_about_person(session_info) sp.users.add_information_about_person(session_info)
class TestVirtualOrg(): class TestVirtualOrg():
def setup_class(self): def setup_class(self):
conf = config.SPConfig() conf = config.SPConfig()
@ -28,24 +37,25 @@ class TestVirtualOrg():
add_derek_info(self.sp) add_derek_info(self.sp)
def test_mta(self): def test_mta(self):
aas = self.vo.members_to_ask("abcdefgh") aas = self.vo.members_to_ask(nid)
print aas print aas
assert len(aas) == 1 assert len(aas) == 1
assert 'urn:mace:example.com:saml:aa' in aas assert 'urn:mace:example.com:saml:aa' in aas
def test_unknown_subject(self): def test_unknown_subject(self):
aas = self.vo.members_to_ask("01234567") aas = self.vo.members_to_ask(nid0)
print aas print aas
assert len(aas) == 2 assert len(aas) == 2
def test_id(self): def test_id(self):
id = self.vo.get_common_identifier("abcdefgh") cid = self.vo.get_common_identifier(nid)
print id print cid
assert id == "deje0001" assert cid == "deje0001"
def test_id_unknown(self): def test_id_unknown(self):
id = self.vo.get_common_identifier("01234567") cid = self.vo.get_common_identifier(nid0)
assert id is None assert cid is None
class TestVirtualOrg_2(): class TestVirtualOrg_2():
def setup_class(self): def setup_class(self):
@ -56,21 +66,21 @@ class TestVirtualOrg_2():
add_derek_info(self.sp) add_derek_info(self.sp)
def test_mta(self): def test_mta(self):
aas = self.sp.vorg.members_to_ask("abcdefgh") aas = self.sp.vorg.members_to_ask(nid)
print aas print aas
assert len(aas) == 1 assert len(aas) == 1
assert 'urn:mace:example.com:saml:aa' in aas assert 'urn:mace:example.com:saml:aa' in aas
def test_unknown_subject(self): def test_unknown_subject(self):
aas = self.sp.vorg.members_to_ask("01234567") aas = self.sp.vorg.members_to_ask(nid0)
print aas print aas
assert len(aas) == 2 assert len(aas) == 2
def test_id(self): def test_id(self):
id = self.sp.vorg.get_common_identifier("abcdefgh") cid = self.sp.vorg.get_common_identifier(nid)
print id print cid
assert id == "deje0001" assert cid == "deje0001"
def test_id_unknown(self): def test_id_unknown(self):
id = self.sp.vorg.get_common_identifier("01234567") cid = self.sp.vorg.get_common_identifier(nid0)
assert id is None assert cid is None