Request preparation refactoring

This commit is contained in:
Roland Hedberg 2012-12-29 17:44:03 +01:00
parent a26772f637
commit 4dcd3cf270
13 changed files with 39766 additions and 32776 deletions

View File

@ -470,6 +470,8 @@ class SamlBase(ExtensionContainer):
#c_attribute_required = {}
c_child_order = []
c_cardinality = {}
c_any = None
c_any_attribute = None
def _get_all_c_children_with_order(self):
if len(self.c_child_order) > 0:
@ -534,7 +536,8 @@ class SamlBase(ExtensionContainer):
member = getattr(self, member_name)
if member is not None:
tree.attrib[xml_attribute] = member
# Lastly, call the ExtensionContainers's _add_members_to_element_tree
# Lastly, call the ExtensionContainers's _add_members_to_element_tree
# to convert any extension attributes.
ExtensionContainer._add_members_to_element_tree(self, tree)

View File

@ -50,11 +50,34 @@ logger = logging.getLogger(__name__)
class Saml2Client(Base):
""" The basic pySAML2 service provider class """
def do_authenticate(self, entityid=None, relay_state="",
binding=saml2.BINDING_HTTP_REDIRECT, vorg="",
nameid_format=NAMEID_FORMAT_PERSISTENT,
scoping=None, consent=None, extensions=None, sign=None):
""" Makes an authentication request.
def _request_info(self, binding, req_str, destination, relay_state):
if binding == saml2.BINDING_HTTP_POST:
logger.info("HTTP POST")
info = self.use_http_form_post(req_str, destination,
relay_state)
info["url"] = destination
info["method"] = "GET"
elif binding == saml2.BINDING_HTTP_REDIRECT:
logger.info("HTTP REDIRECT")
info = self.use_http_get(req_str, destination,
relay_state)
info["url"] = destination
info["method"] = "GET"
elif binding == BINDING_SOAP:
info = self.use_soap(req_str, destination)
else:
raise Exception("Unknown binding type: %s" % binding)
return info
def prepare_for_authenticate(self, entityid=None, relay_state="",
binding=saml2.BINDING_HTTP_REDIRECT, vorg="",
nameid_format=NAMEID_FORMAT_PERSISTENT,
scoping=None, consent=None, extensions=None,
sign=None):
""" Makes all necessary preparations for an authentication request.
:param entityid: The entity ID of the IdP to send the request to
:param relay_state: To where the user should be returned after
@ -65,30 +88,21 @@ class Saml2Client(Base):
:param consent: Whether the principal have given her consent
:param extensions: Possible extensions
:param sign: Whether the request should be signed or not.
:return: AuthnRequest response
:return: session id and AuthnRequest info
"""
location = self._sso_location(entityid, binding)
destination = self._sso_location(entityid, binding)
req = self.create_authn_request(location, vorg, scoping, binding,
req = self.create_authn_request(destination, vorg, scoping, binding,
nameid_format, consent, extensions,
sign)
_req_str = "%s" % req
logger.info("AuthNReq: %s" % _req_str)
if binding == saml2.BINDING_HTTP_POST:
logger.info("HTTP POST")
(header, body) = self.use_http_form_post(_req_str, location,
relay_state)
elif binding == saml2.BINDING_HTTP_REDIRECT:
logger.info("HTTP REDIRECT")
(header, body) = self.use_http_get(_req_str, location,
relay_state)
else:
raise Exception("Unknown binding type: %s" % binding)
info = self._request_info(binding, _req_str, destination, relay_state)
return req.id, header, body
return req.id, info
def global_logout(self, subject_id, reason="", expire=None, sign=None):
""" More or less a layer of indirection :-/
@ -169,12 +183,15 @@ class Saml2Client(Base):
logger.info("REQUEST: %s" % request)
srequest = signed_instance_factory(request, self.sec, to_sign)
relay_state = self._relay_state(request.id)
http_info = self._request_info(binding, srequest,
destination, relay_state)
if binding == BINDING_SOAP:
response = self.send_using_soap(srequest, destination)
if response:
logger.info("Verifying response")
response = self.logout_request_response(response)
response = self.send(**http_info)
if response:
not_done.remove(entity_id)
@ -184,27 +201,15 @@ class Saml2Client(Base):
logger.info("NOT OK response from %s" % destination)
else:
session_id = request.id
rstate = self._relay_state(session_id)
self.state[request.id] = {"entity_id": entity_id,
"operation": "SLO",
"entity_ids": entity_ids,
"subject_id": subject_id,
"reason": reason,
"not_on_of_after": expire,
"sign": sign}
self.state[session_id] = {"entity_id": entity_id,
"operation": "SLO",
"entity_ids": entity_ids,
"subject_id": subject_id,
"reason": reason,
"not_on_of_after": expire,
"sign": sign}
if binding == BINDING_HTTP_POST:
response = self.use_http_form_post(srequest,
destination,
rstate)
else:
response = self.use_http_get(srequest, destination,
rstate)
responses[entity_id] = response
responses[entity_id] = http_info
not_done.remove(entity_id)
# only try one binding
@ -247,11 +252,13 @@ class Saml2Client(Base):
status["reason"], status["not_on_or_after"],
status["sign"])
# ========================================================================
# MUST use SOAP for
# AssertionIDRequest, SubjectQuery,
# AuthnQuery, AttributeQuery, or AuthzDecisionQuery
# AssertionIDRequest, SubjectQuery, AuthnQuery, AttributeQuery or
# AuthzDecisionQuery
# ========================================================================
def use_soap(self, destination, query_type, **kwargs):
def _use_soap(self, destination, query_type, **kwargs):
_create_func = getattr(self, "create_%s" % query_type)
_response_func = getattr(self, "%s_response" % query_type)
try:
@ -296,7 +303,7 @@ class Saml2Client(Base):
srvs = self.metadata.authz_service(entity_id, BINDING_SOAP)
for dest in destinations(srvs):
resp = self.use_soap(dest, "authz_decision_query",
resp = self._use_soap(dest, "authz_decision_query",
action=action, evidence=evidence,
resource=resource, subject=subject)
if resp:
@ -319,7 +326,7 @@ class Saml2Client(Base):
_id_refs = [AssertionIDRef(_id) for _id in assertion_ids]
for destination in destinations(srvs):
res = self.use_soap(destination, "assertion_id_request",
res = self._use_soap(destination, "assertion_id_request",
assertion_id_refs=_id_refs, consent=consent,
extensions=extensions, sign=sign)
if res:
@ -333,7 +340,7 @@ class Saml2Client(Base):
srvs = self.metadata.authn_request_service(entity_id, BINDING_SOAP)
for destination in destinations(srvs):
resp = self.use_soap(destination, "authn_query",
resp = self._use_soap(destination, "authn_query",
consent=consent, extensions=extensions,
sign=sign)
if resp:
@ -376,20 +383,22 @@ class Saml2Client(Base):
response_args = {}
if binding == BINDING_SOAP:
return self.use_soap(destination, "attribute_query", consent=consent,
extensions=extensions, sign=sign,
subject_id=subject_id, attribute=attribute,
sp_name_qualifier=sp_name_qualifier,
name_qualifier=name_qualifier,
nameid_format=nameid_format,
response_args=response_args)
return self._use_soap(destination, "attribute_query",
consent=consent, extensions=extensions,
sign=sign, subject_id=subject_id,
attribute=attribute,
sp_name_qualifier=sp_name_qualifier,
name_qualifier=name_qualifier,
nameid_format=nameid_format,
response_args=response_args)
elif binding == BINDING_HTTP_POST:
return self.use_soap(destination, "attribute_query", consent=consent,
extensions=extensions, sign=sign,
subject_id=subject_id, attribute=attribute,
sp_name_qualifier=sp_name_qualifier,
name_qualifier=name_qualifier,
nameid_format=nameid_format,
response_args=response_args)
return self._use_soap(destination, "attribute_query",
consent=consent, extensions=extensions,
sign=sign, subject_id=subject_id,
attribute=attribute,
sp_name_qualifier=sp_name_qualifier,
name_qualifier=name_qualifier,
nameid_format=nameid_format,
response_args=response_args)
else:
raise Exception("Unsupported binding")

View File

@ -22,7 +22,7 @@ to conclude its tasks.
from saml2.httpbase import HTTPBase
from saml2.mdstore import destinations
from saml2.saml import AssertionIDRef, NAMEID_FORMAT_TRANSIENT
from saml2.samlp import AuthnQuery
from saml2.samlp import AuthnQuery, ArtifactResponse, StatusCode, Status
from saml2.samlp import ArtifactResolve
from saml2.samlp import artifact_resolve_from_string
from saml2.samlp import LogoutRequest
@ -64,7 +64,7 @@ from saml2.response import AuthnResponse
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_SOAP
from saml2 import BINDING_HTTP_POST
from saml2 import BINDING_PAOS
from saml2 import BINDING_PAOS, element_to_extension_element
import logging
logger = logging.getLogger(__name__)
@ -157,6 +157,8 @@ class Base(HTTPBase):
self.logout_requests_signed_default = True
self.allow_unsolicited = self.config.getattr("allow_unsolicited", "sp")
self.artifact2response = {}
#
# Private methods
#
@ -604,8 +606,29 @@ class Base(HTTPBase):
return self._message(ArtifactResolve, destination, id, consent,
extensions, sign, artifact=artifact, issuer=issuer)
def create_artifact_response(self):
pass
def create_artifact_response(self, artifact, status_code, in_response_to,
id=0, consent=None, extensions=None,
sign=False):
"""
:param artifact:
:param status_code:
:param in_response_to:
:param id:
:param consent:
:param extensions:
:param sign:
:return:
"""
ee = element_to_extension_element(self.artifact2response[artifact])
status = Status(status_code=StatusCode(value=status_code))
return self._message(ArtifactResponse, "", id, consent,
extensions, sign, in_response_to=in_response_to,
status=status,
extension_elements=[ee])
# ======== response handling ===========

View File

@ -159,7 +159,6 @@ class HTTPBase(object):
return http_form_post_message(message, destination, relay_state)
def use_http_get(self, message, destination, relay_state):
"""
Send a message using GET, this is the HTTP-Redirect case so
@ -175,9 +174,9 @@ class HTTPBase(object):
return http_redirect_message(message, destination, relay_state)
def send_using_soap(self, request, destination, headers=None, sign=False):
def use_soap(self, request, destination, headers=None, sign=False):
"""
Send a message using SOAP+POST
Construct the necessary information for using SOAP+POST
:param request:
:param destination:
@ -198,10 +197,24 @@ class HTTPBase(object):
nodeid=request.id)
soap_message = _signed
return {"url": destination, "method": "POST",
"data":soap_message, "headers":headers}
def send_using_soap(self, request, destination, headers=None, sign=False):
"""
Send a message using SOAP+POST
:param request:
:param destination:
:param headers:
:param sign:
:return:
"""
#_response = self.server.post(soap_message, headers, path=path)
try:
response = self.send(destination, "POST", data=soap_message,
headers=headers)
response = self.send(self.use_soap(request, destination, headers,
sign))
except Exception, exc:
logger.info("HTTPClient exception: %s" % (exc,))
return None

View File

@ -67,7 +67,7 @@ def http_form_post_message(message, location, relay_state="", typ="SAMLRequest")
if not isinstance(message, basestring):
message = "%s" % (message,)
if typ == "SAMLRequest":
if typ == "SAMLRequest" or typ == "SAMLResponse":
_msg = base64.b64encode(message)
else:
_msg = message
@ -79,19 +79,19 @@ def http_form_post_message(message, location, relay_state="", typ="SAMLRequest")
response.append("""</script>""")
response.append("</body>")
return [("Content-type", "text/html")], response
return {"headers": [("Content-type", "text/html")], "data": response}
#noinspection PyUnresolvedReferences
def http_post_message(message, location, relay_state="", typ="SAMLRequest"):
"""
:param message:
:param location:
:param relay_state:
:param typ:
:return:
"""
return [("Content-type", "text/xml")], message
##noinspection PyUnresolvedReferences
#def http_post_message(message, location, relay_state="", typ="SAMLRequest"):
# """
#
# :param message:
# :param location:
# :param relay_state:
# :param typ:
# :return:
# """
# return {"headers": [("Content-type", "text/xml")], "data": message}
def http_redirect_message(message, location, relay_state="", typ="SAMLRequest"):
"""The HTTP Redirect binding defines a mechanism by which SAML protocol
@ -120,7 +120,7 @@ def http_redirect_message(message, location, relay_state="", typ="SAMLRequest"):
headers = [('Location', login_url)]
body = [""]
return headers, body
return {"headers":headers, "data":body}
DUMMY_NAMESPACE = "http://example.org/"
PREFIX = '<?xml version="1.0" encoding="UTF-8"?>'
@ -164,12 +164,12 @@ def make_soap_enveloped_saml_thingy(thingy, header_parts=None):
return ElementTree.tostring(envelope, encoding="UTF-8")
def http_soap_message(message):
return ([("Content-type", "application/soap+xml")],
make_soap_enveloped_saml_thingy(message))
return {"headers": [("Content-type", "application/soap+xml")],
"data": make_soap_enveloped_saml_thingy(message)}
def http_paos(message, extra=None):
return ([("Content-type", "application/soap+xml")],
make_soap_enveloped_saml_thingy(message, extra))
return {"headers":[("Content-type", "application/soap+xml")],
"data": make_soap_enveloped_saml_thingy(message, extra)}
def parse_soap_enveloped_saml(text, body_class, header_class=None):
"""Parses a SOAP enveloped SAML thing and returns header parts and body
@ -219,3 +219,6 @@ def packager( identifier ):
return PACKING[identifier]
except KeyError:
raise Exception("Unkown binding type: %s" % identifier)
def factory(binding, message, location, relay_state="", typ="SAMLRequest"):
return PACKING[binding](message, location, relay_state, typ)

View File

@ -1,7 +1,8 @@
import base64
import logging
from attribute_converter import to_local
from saml2 import time_util
from saml2 import time_util, BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
from saml2 import s_utils
from saml2.s_utils import OtherError
@ -32,10 +33,12 @@ class Request(object):
self.message = None
self.not_on_or_after = 0
def _loads(self, xmldata, decode=True):
if decode:
def _loads(self, xmldata, binding):
if binding == BINDING_HTTP_REDIRECT:
logger.debug("Expected to decode and inflate xml data")
decoded_xml = s_utils.decode_base64_and_inflate(xmldata)
elif binding == BINDING_HTTP_POST:
decoded_xml = base64.b64decode(xmldata)
else:
decoded_xml = xmldata
@ -86,8 +89,8 @@ class Request(object):
assert self.issue_instant_ok()
return self
def loads(self, xmldata, decode=True):
return self._loads(xmldata, decode)
def loads(self, xmldata, binding):
return self._loads(xmldata, binding)
def verify(self):
try:

View File

@ -324,6 +324,9 @@ class SubjectConfirmationDataType_(SamlBase):
c_attributes['Recipient'] = ('recipient', 'anyURI', False)
c_attributes['InResponseTo'] = ('in_response_to', 'NCName', False)
c_attributes['Address'] = ('address', 'string', False)
c_any = {"namespace":"##any", "processContents":"lax", "minOccurs":"0",
"maxOccurs":"unbounded"}
c_any_attribute = {"namespace":"##other", "processContents":"lax"}
def __init__(self,
not_before=None,
@ -874,6 +877,7 @@ class AttributeType_(SamlBase):
c_attributes['NameFormat'] = ('name_format', 'anyURI', False)
c_attributes['FriendlyName'] = ('friendly_name', 'string', False)
c_child_order.extend(['attribute_value'])
c_any_attribute = {"namespace":"##other", "processContents":"lax"}
def __init__(self,
attribute_value=None,
@ -1481,6 +1485,7 @@ class AdviceType_(SamlBase):
c_children['{urn:oasis:names:tc:SAML:2.0:assertion}EncryptedAssertion'] = ('encrypted_assertion', [EncryptedAssertion])
c_cardinality['encrypted_assertion'] = {"min":0}
c_child_order.extend(['assertion_id_ref', 'assertion_uri_ref', 'assertion', 'encrypted_assertion'])
c_any = {"namespace":"##other", "processContents":"lax"}
def __init__(self,
assertion_id_ref=None,

View File

@ -88,6 +88,8 @@ class StatusDetailType_(SamlBase):
c_attributes = SamlBase.c_attributes.copy()
c_child_order = SamlBase.c_child_order[:]
c_cardinality = SamlBase.c_cardinality.copy()
c_any = {"namespace":"##any", "processContents":"lax", "minOccurs":"0",
"maxOccurs":"unbounded"}
def status_detail_type__from_string(xml_string):
return saml2.create_class_from_xml_string(StatusDetailType_, xml_string)
@ -1433,6 +1435,7 @@ class ArtifactResponseType_(StatusResponseType_):
c_attributes = StatusResponseType_.c_attributes.copy()
c_child_order = StatusResponseType_.c_child_order[:]
c_cardinality = StatusResponseType_.c_cardinality.copy()
c_any = {"namespace":"##any", "processContents":"lax", "minOccurs":"0"}
def artifact_response_type__from_string(xml_string):
return saml2.create_class_from_xml_string(ArtifactResponseType_, xml_string)

View File

@ -23,9 +23,6 @@ import logging
import shelve
import sys
import memcache
from saml2.pack import http_soap_message
from saml2.pack import http_redirect_message
from saml2.pack import http_post_message
from saml2.httpbase import HTTPBase
from saml2.mdstore import destinations
@ -338,10 +335,9 @@ class Server(HTTPBase):
if binding == BINDING_SOAP or binding == BINDING_PAOS:
# not base64 decoding and unzipping
authn_request.debug=True
_log_info("Don't decode")
authn_request = authn_request.loads(enc_request, decode=False)
authn_request = authn_request.loads(enc_request, binding)
else:
authn_request = authn_request.loads(enc_request)
authn_request = authn_request.loads(enc_request, binding)
_log_debug("Loaded authn_request")
@ -379,6 +375,7 @@ class Server(HTTPBase):
raise UnsupportedBinding(sp_entity_id)
response["sp_entity_id"] = sp_entity_id
response["binding"] = _binding
if authn_request.message.assertion_consumer_service_url:
return_destination = \
@ -404,11 +401,11 @@ class Server(HTTPBase):
"""
return self.metadata.attribute_requirement(sp_entity_id, index)
def parse_attribute_query(self, xml_string, decode=True):
def parse_attribute_query(self, xml_string, binding):
""" Parse an attribute query
:param xml_string: The Attribute Query as an XML string
:param decode: Whether the xmlstring is base64encoded and zipped
:param binding: Which binding that was used for the request
:return: 3-Tuple containing:
subject - identifier of the subject
attribute - which attributes that the requestor wants back
@ -417,7 +414,7 @@ class Server(HTTPBase):
receiver_addresses = self.conf.endpoint("attribute_service")
attribute_query = AttributeQuery( self.sec, receiver_addresses)
attribute_query = attribute_query.loads(xml_string, decode=decode)
attribute_query = attribute_query.loads(xml_string, binding)
attribute_query = attribute_query.verify()
logger.info("KEYS: %s" % attribute_query.message.keys())
@ -716,12 +713,12 @@ class Server(HTTPBase):
if binding == BINDING_SOAP:
lreq = soap.parse_soap_enveloped_saml_logout_request(text)
try:
req = req.loads(lreq, False) # Got it over SOAP so no base64+zip
req = req.loads(lreq, binding)
except Exception:
return None
else:
try:
req = req.loads(text)
req = req.loads(text, binding)
except Exception, exc:
logger.error("%s" % (exc,))
return None
@ -782,7 +779,7 @@ class Server(HTTPBase):
return response
def parse_authz_decision_query(self, xml_string):
def parse_authz_decision_query(self, xml_string, binding):
""" Parse an attribute query
:param xml_string: The Authz decision Query as an XML string
@ -794,7 +791,7 @@ class Server(HTTPBase):
receiver_addresses = self.conf.endpoint("attribute_service", "idp")
attribute_query = AttributeQuery( self.sec, receiver_addresses)
attribute_query = attribute_query.loads(xml_string)
attribute_query = attribute_query.loads(xml_string, binding)
attribute_query = attribute_query.verify()
# Subject name is a BaseID,NameID or EncryptedID instance

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,8 @@ CONFIG = {
"idp": {
"endpoints" : {
"single_sign_on_service" : [
("%s/sso" % BASE, BINDING_HTTP_REDIRECT)],
("%s/sso" % BASE, BINDING_HTTP_REDIRECT),
("%s/ssop" % BASE, BINDING_HTTP_POST)],
"single_logout_service": [
("%s/slo" % BASE, BINDING_SOAP),
("%s/slop" % BASE, BINDING_HTTP_POST)],

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import base64
from saml2.saml import AUTHN_PASSWORD
from saml2.samlp import response_from_string
@ -372,7 +373,7 @@ class TestServer1():
issuer_entity_id = "urn:mace:example.com:saml:roland:idp",
reason = "I'm tired of this")
intermed = s_utils.deflate_and_base64_encode("%s" % (logout_request,))
intermed = base64.b64encode("%s" % logout_request)
#saml_soap = make_soap_enveloped_saml_thingy(logout_request)
request = self.server.parse_logout_request(intermed, BINDING_HTTP_POST)
@ -399,7 +400,7 @@ class TestServer1():
issuer_entity_id = "urn:mace:example.com:saml:roland:idp",
reason = "I'm tired of this")
_ = s_utils.deflate_and_base64_encode("%s" % (logout_request,))
#_ = s_utils.deflate_and_base64_encode("%s" % (logout_request,))
saml_soap = make_soap_enveloped_saml_thingy(logout_request)
self.server.close_shelve_db()
@ -475,8 +476,8 @@ class TestServerLogout():
print request
binding = BINDING_HTTP_REDIRECT
response = server.create_logout_response(request, binding)
headers, message = server.use_http_get(response, response.destination,
http_args = server.use_http_get(response, response.destination,
"/relay_state")
assert len(headers) == 1
assert headers[0][0] == "Location"
assert message == ['']
assert len(http_args) == 2
assert http_args["headers"][0][0] == "Location"
assert http_args["data"] == ['']

View File

@ -14,7 +14,7 @@ from saml2.server import Server
from saml2.time_util import in_a_while
from py.test import raises
from fakeIDP import FakeIDP
from fakeIDP import FakeIDP, unpack_form
def for_me(condition, me ):
for restriction in condition.audience_restriction:
@ -341,13 +341,13 @@ class TestClientWithDummy():
self.client.send = self.server.receive
def test_do_authn(self):
id, header, body = self.client.do_authenticate(IDP,
id, http_args = self.client.prepare_for_authenticate(IDP,
"http://www.example.com/relay_state")
assert isinstance(id, basestring)
assert len(header) == 1
assert header[0][0] == "Location"
assert body == [""]
assert len(http_args) == 4
assert http_args["headers"][0][0] == "Location"
assert http_args["data"] == [""]
def test_do_attribute_query(self):
response = self.client.do_attribute_query(IDP,
@ -378,17 +378,11 @@ class TestClientWithDummy():
assert resp
assert len(resp) == 1
assert resp.keys() == entity_ids
item = resp[entity_ids[0]]
assert isinstance(item, tuple)
assert item[0] == [('Content-type', 'text/html')]
lead = "name=\"SAMLRequest\" value=\""
body = item[1][3]
i = body.find(lead)
i += len(lead)
j = i + body[i:].find('"')
info = body[i:j]
xml_str = base64.b64decode(info)
#xml_str = decode_base64_and_inflate(info)
http_args = resp[entity_ids[0]]
assert isinstance(http_args, dict)
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"