Even more refactoring

This commit is contained in:
Roland Hedberg
2012-12-30 17:28:33 +01:00
parent 956ff0a536
commit 7127aa55da
5 changed files with 77 additions and 429 deletions

View File

@@ -2,7 +2,7 @@
import logging
import re
from cgi import parse_qs
from urlparse import parse_qs
from saml2 import BINDING_HTTP_REDIRECT
logger = logging.getLogger("saml2.SP")
@@ -75,19 +75,22 @@ def not_authn(environ, start_response):
def slo(environ, start_response, user):
# so here I might get either a LogoutResponse or a LogoutRequest
client = environ['repoze.who.plugins']["saml2auth"]
sc = client.saml_client
sids = None
if "QUERY_STRING" in environ:
query = parse_qs(environ["QUERY_STRING"])
logger.info("query: %s" % query)
try:
(sids, code, head, message) = client.saml_client.logout_response(
query["SAMLResponse"][0],
binding=BINDING_HTTP_REDIRECT)
logger.info("LOGOUT reponse parsed OK")
response = sc.logout_request_response(query["SAMLResponse"][0],
binding=BINDING_HTTP_REDIRECT)
if response:
logger.info("LOGOUT response parsed OK")
except KeyError:
# return error reply
pass
if response is None:
request = sc.lo
if not sids:
start_response("302 Found", [("Location", "/done")])
return ["Successfull Logout"]

View File

@@ -120,7 +120,6 @@ class Base(Entity):
setattr(self, foo, False)
# extra randomness
self.seed = rndstr(32)
self.logout_requests_signed_default = True
self.allow_unsolicited = self.config.getattr("allow_unsolicited", "sp")
@@ -138,18 +137,6 @@ class Base(Entity):
vals.append(signature(self.config.secret, vals))
return "|".join(vals)
def _issuer(self, entityid=None):
""" Return an Issuer instance """
if entityid:
if isinstance(entityid, saml.Issuer):
return entityid
else:
return saml.Issuer(text=entityid,
format=saml.NAMEID_FORMAT_ENTITY)
else:
return saml.Issuer(text=self.config.entityid,
format=saml.NAMEID_FORMAT_ENTITY)
def _sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT):
if entityid:
# verify that it's in the metadata
@@ -213,45 +200,6 @@ class Base(Entity):
else:
return None
def _message(self, request_cls, destination=None, id=0,
consent=None, extensions=None, sign=False, **kwargs):
"""
Some parameters appear in all requests so simplify by doing
it in one place
:param request_cls: The specific request type
:param destination: The recipient
:param id: A message identifier
:param consent: Whether the principal have given her consent
:param extensions: Possible extensions
:param kwargs: Key word arguments specific to one request type
:return: An instance of the request_cls
"""
if not id:
id = sid(self.seed)
req = request_cls(id=id, version=VERSION, issue_instant=instant(),
issuer=self._issuer(), **kwargs)
if destination:
req.destination = destination
if consent:
req.consent = consent
if extensions:
req.extensions = extensions
if sign:
req.signature = pre_signature_part(req.id, self.sec.my_cert, 1)
to_sign = [(class_name(req), req.id)]
else:
to_sign = []
logger.info("REQUEST: %s" % req)
return signed_instance_factory(req, self.sec, to_sign)
def create_authn_request(self, destination, vorg="", scoping=None,
binding=saml2.BINDING_HTTP_POST,
nameid_format=NAMEID_FORMAT_TRANSIENT,
@@ -360,63 +308,6 @@ class Base(Entity):
attribute=attribute)
def create_logout_request(self, destination, issuer_entity_id,
subject_id=None, name_id=None,
reason=None, expire=None,
id=0, consent=None, extensions=None, sign=False):
""" Constructs a LogoutRequest
:param destination: Destination of the request
:param issuer_entity_id: The entity ID of the IdP the request is
target at.
:param subject_id: The identifier of the subject
:param name_id: A NameID instance identifying the subject
:param reason: An indication of the reason for the logout, in the
form of a URI reference.
:param expire: The time at which the request expires,
after which the recipient may discard the message.
:param id: Request identifier
:param consent: Whether the principal have given her consent
:param extensions: Possible extensions
:param sign: Whether the query should be signed or not.
:return: A LogoutRequest instance
"""
if subject_id:
name_id = saml.NameID(
text = self.users.get_entityid(subject_id, issuer_entity_id,
False))
if not name_id:
raise Exception("Missing subject identification")
return self._message(LogoutRequest, destination, id,
consent, extensions, sign, name_id=name_id,
reason=reason, not_on_or_after=expire)
def create_logout_response(self, idp_entity_id, request_id,
status_code,
binding=BINDING_HTTP_REDIRECT):
""" Constructs a LogoutResponse
:param idp_entity_id: The entityid of the IdP that want to do the
logout
:param request_id: The Id of the request we are replying to
:param status_code: The status code of the response
:param binding: The type of binding that will be used for the response
:return: A LogoutResponse instance
"""
srvs = self.metadata.single_logout_services(idp_entity_id, "idpsso",
binding=binding)
destination = destinations(srvs)[0]
status = samlp.Status(
status_code=samlp.StatusCode(value=status_code))
return destination, self._message(LogoutResponse, destination,
in_response_to=request_id,
status=status)
# MUST use SOAP for
# AssertionIDRequest, SubjectQuery,
# AuthnQuery, AttributeQuery, or AuthzDecisionQuery
@@ -597,10 +488,11 @@ class Base(Entity):
status=status,
extension_elements=[ee])
# ======== response handling ===========
# ======== response handling ===========
def _response(self, post, outstanding, decode=True, asynchop=True):
""" Deal with an AuthnResponse or LogoutResponse
def parse_authn_request_response(self, post, outstanding, decode=True,
asynchop=True):
""" Deal with an AuthnResponse
:param post: The reply as a dictionary
:param outstanding: A dictionary with session IDs as keys and
@@ -645,79 +537,26 @@ class Base(Entity):
saml2.class_name(resp),))
return resp
def authn_request_response(self, post, outstanding, decode=True,
asynchop=True):
return self._response(post, outstanding, decode, asynchop)
def logout_request_response(self, xmlstr, binding=BINDING_SOAP):
""" Deal with a LogoutResponse
:param xmlstr: The response as a xml string
:param binding: What type of binding this message came through.
:return: None if the reply doesn't contain a valid SAML LogoutResponse,
otherwise the reponse if the logout was successful and None if it
was not.
"""
response = None
if xmlstr:
if binding == BINDING_HTTP_REDIRECT:
try:
# expected return address
return_addr = self.config.endpoint("single_logout_service",
binding=binding)[0]
except Exception:
logger.info("Not supposed to handle this!")
return None
else:
return_addr = None
try:
response = LogoutResponse(self.sec, return_addr)
except Exception, exc:
logger.info("%s" % exc)
return None
if binding == BINDING_HTTP_REDIRECT:
xmlstr = decode_base64_and_inflate(xmlstr)
elif binding == BINDING_HTTP_POST:
xmlstr = base64.b64decode(xmlstr)
logger.debug("XMLSTR: %s" % xmlstr)
response = response.loads(xmlstr, False)
if response:
response = response.verify()
if not response:
return None
logger.debug(response)
return response
#noinspection PyUnusedLocal
def authz_decision_query_response(self, response):
def parse_authz_decision_query_response(self, response):
""" Verify that the response is OK
"""
resp = samlp.response_from_string(response)
return resp
def assertion_id_request_response(self, response):
def parse_assertion_id_request_response(self, response):
""" Verify that the response is OK
"""
resp = samlp.response_from_string(response)
return resp
def authn_query_response(self, response):
def parse_authn_query_response(self, response):
""" Verify that the response is OK
"""
resp = samlp.response_from_string(response)
return resp
def attribute_query_response(self, response, **kwargs):
def parse_attribute_query_response(self, response, **kwargs):
try:
# synchronous operation
aresp = attribute_response(self.config, self.config.entityid)
@@ -740,7 +579,7 @@ class Base(Entity):
logger.info("session: %s" % session_info)
return session_info
def artifact_resolve_response(self, txt, **kwargs):
def parse_artifact_resolve_response(self, txt, **kwargs):
"""
Always done over SOAP

View File

@@ -16,7 +16,8 @@ def _dummy(_arg):
return None
class Request(object):
def __init__(self, sec_context, receiver_addrs, timeslack=0):
def __init__(self, sec_context, receiver_addrs, attribute_converters=None,
timeslack=0):
self.sec = sec_context
self.receiver_addrs = receiver_addrs
self.timeslack = timeslack
@@ -24,6 +25,7 @@ class Request(object):
self.name_id = ""
self.message = None
self.not_on_or_after = 0
self.attribute_converters = attribute_converters
self.signature_check = _dummy # has to be set !!!
@@ -123,14 +125,18 @@ class Request(object):
return self.message.issuer.text()
class LogoutRequest(Request):
def __init__(self, sec_context, receiver_addrs, timeslack=0):
Request.__init__(self, sec_context, receiver_addrs, timeslack)
def __init__(self, sec_context, receiver_addrs, attribute_converters=None,
timeslack=0):
Request.__init__(self, sec_context, receiver_addrs,
attribute_converters, timeslack)
self.signature_check = self.sec.correctly_signed_logout_request
class AttributeQuery(Request):
def __init__(self, sec_context, receiver_addrs, timeslack=0):
Request.__init__(self, sec_context, receiver_addrs, timeslack)
def __init__(self, sec_context, receiver_addrs, attribute_converters=None,
timeslack=0):
Request.__init__(self, sec_context, receiver_addrs,
attribute_converters, timeslack)
self.signature_check = self.sec.correctly_signed_attribute_query
def attribute(self):
@@ -138,10 +144,10 @@ class AttributeQuery(Request):
return []
class AuthnRequest(Request):
def __init__(self, sec_context, attribute_converters, receiver_addrs,
def __init__(self, sec_context, receiver_addrs, attribute_converters,
timeslack=0):
Request.__init__(self, sec_context, receiver_addrs, timeslack)
self.attribute_converters = attribute_converters
Request.__init__(self, sec_context, receiver_addrs,
attribute_converters, timeslack)
self.signature_check = self.sec.correctly_signed_authn_request
@@ -150,8 +156,10 @@ class AuthnRequest(Request):
class AuthzRequest(Request):
def __init__(self, sec_context, receiver_addrs, timeslack=0):
Request.__init__(self, sec_context, receiver_addrs, timeslack)
def __init__(self, sec_context, receiver_addrs,
attribute_converters=None, timeslack=0):
Request.__init__(self, sec_context, receiver_addrs,
attribute_converters, timeslack)
self.signature_check = self.sec.correctly_signed_logout_request
def action(self):

View File

@@ -23,31 +23,26 @@ import logging
import shelve
import sys
import memcache
from saml2.samlp import AuthzDecisionQuery
from saml2.entity import Entity
from saml2.samlp import LogoutResponse
from saml2 import saml, VERSION
from saml2 import saml
from saml2 import class_name
from saml2 import soap
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_SOAP
from saml2.request import AuthnRequest
from saml2.request import AttributeQuery
from saml2.request import LogoutRequest
from saml2.s_utils import sid
from saml2.s_utils import MissingValue
from saml2.s_utils import success_status_factory
from saml2.s_utils import error_status_factory
from saml2.time_util import instant
from saml2.sigver import signed_instance_factory
from saml2.sigver import pre_signature_part
from saml2.sigver import response_factory
from saml2.assertion import Assertion, Policy, restriction_from_attribute_spec, filter_attribute_value_assertions
from saml2.assertion import Assertion
from saml2.assertion import Policy
from saml2.assertion import restriction_from_attribute_spec
from saml2.assertion import filter_attribute_value_assertions
logger = logging.getLogger(__name__)
@@ -256,14 +251,17 @@ class Server(Entity):
if self.ident:
self.ident.map.close()
def issuer(self, entityid=None):
""" Return an Issuer precursor """
if entityid:
return saml.Issuer(text=entityid,
format=saml.NAMEID_FORMAT_ENTITY)
else:
return saml.Issuer(text=self.config.entityid,
format=saml.NAMEID_FORMAT_ENTITY)
def wants(self, sp_entity_id, index=None):
""" Returns what attributes the SP requires and which are optional
if any such demands are registered in the Metadata.
:param sp_entity_id: The entity id of the SP
:param index: which of the attribute consumer services its all about
:return: 2-tuple, list of required and list of optional attributes
"""
return self.metadata.attribute_requirement(sp_entity_id, index)
# -------------------------------------------------------------------------
def parse_authn_request(self, enc_request, binding=BINDING_HTTP_REDIRECT):
"""Parse a Authentication Request
@@ -278,116 +276,31 @@ class Server(Entity):
request - The verified request
"""
response = {}
_log_info = logger.info
_log_debug = logger.debug
# The addresses I should receive messages like this on
receiver_addresses = self.config.endpoint("single_sign_on_service",
binding)
_log_info("receiver addresses: %s" % receiver_addresses)
_log_info("Binding: %s" % binding)
try:
timeslack = self.config.accepted_time_diff
if not timeslack:
timeslack = 0
except AttributeError:
timeslack = 0
authn_request = AuthnRequest(self.sec,
self.config.attribute_converters,
receiver_addresses, timeslack=timeslack)
authn_request = authn_request.loads(enc_request, binding)
_log_debug("Loaded authn_request")
if authn_request:
authn_request = authn_request.verify()
_log_debug("Verified authn_request")
if not authn_request:
return None
else:
return authn_request
def wants(self, sp_entity_id, index=None):
""" Returns what attributes the SP requires and which are optional
if any such demands are registered in the Metadata.
:param sp_entity_id: The entity id of the SP
:param index: which of the attribute consumer services its all about
:return: 2-tuple, list of required and list of optional attributes
"""
return self.metadata.attribute_requirement(sp_entity_id, index)
return self._parse_request(enc_request, AuthnRequest,
"single_sign_on_service", binding,
"authentication_request")
def parse_attribute_query(self, xml_string, binding):
""" Parse an attribute query
:param xml_string: The Attribute Query as an XML string
: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
query - the whole query
"""
receiver_addresses = self.config.endpoint("attribute_service")
attribute_query = AttributeQuery( self.sec, receiver_addresses)
attribute_query = attribute_query.loads(xml_string, binding)
attribute_query = attribute_query.verify()
logger.info("KEYS: %s" % attribute_query.message.keys())
# Subject is described in the a saml.Subject instance
subject = attribute_query.subject_id()
attribute = attribute_query.attribute()
return subject, attribute, attribute_query.message
# ------------------------------------------------------------------------
def _response(self, in_response_to, consumer_url=None, status=None,
issuer=None, sign=False, to_sign=None,
**kwargs):
""" Create a Response that adhers to the ??? profile.
:param in_response_to: The session identifier of the request
:param consumer_url: The URL which should receive the response
:param status: The status of the response
:param issuer: The issuer of the response
:param sign: Whether the response should be signed or not
:param to_sign: What other parts to sign
:param kwargs: Extra key word arguments
:return: A Response instance
:return: A query instance
"""
if not status:
status = success_status_factory()
_issuer = self.issuer(issuer)
response = response_factory(
issuer=_issuer,
in_response_to = in_response_to,
status = status,
)
if consumer_url:
response.destination = consumer_url
for key, val in kwargs.items():
setattr(response, key, val)
if sign:
try:
to_sign.append((class_name(response), response.id))
except AttributeError:
to_sign = [(class_name(response), response.id)]
return self._parse_request(xml_string, AttributeQuery,
"attribute_service", binding)
return signed_instance_factory(response, self.sec, to_sign)
def parse_authz_decision_query(self, xml_string, binding):
""" Parse an attribute query
:param xml_string: The Authz decision Query as an XML string
:return: Query instance
"""
return self._parse_request(xml_string, AuthzDecisionQuery,
"authz_service", binding)
# ------------------------------------------------------------------------
@@ -417,7 +330,7 @@ class Server(Entity):
to_sign = []
args = {}
if identity:
_issuer = self.issuer(issuer)
_issuer = self._issuer(issuer)
ast = Assertion(identity)
if policy is None:
policy = Policy()
@@ -486,6 +399,7 @@ class Server(Entity):
sign)
# ------------------------------------------------------------------------
#noinspection PyUnusedLocal
def create_aa_response(self, in_response_to, consumer_url, sp_entity_id,
identity=None, userid="", name_id=None, status=None,
@@ -517,7 +431,7 @@ class Server(Entity):
to_sign = []
args = {}
if identity:
_issuer = self.issuer(issuer)
_issuer = self._issuer(issuer)
ast = Assertion(identity)
policy = self.config.getattr("policy", "aa")
if policy:
@@ -607,119 +521,3 @@ class Server(Entity):
except MissingValue, exc:
return self.create_error_response(in_response_to, destination,
sp_entity_id, exc, name_id)
def parse_logout_request(self, text, binding=BINDING_SOAP):
"""Parse a Logout Request
:param text: The request in its transport format, if the binding is
HTTP-Redirect or HTTP-Post the text *must* be the value of the
SAMLRequest attribute.
:return: A validated LogoutRequest instance or None if validation
failed.
"""
try:
slo = self.config.endpoint("single_logout_service", binding, "idp")
except IndexError:
logger.info("enpoints: %s" % self.config.getattr("endpoints", "idp"))
logger.info("binding wanted: %s" % (binding,))
raise
if not slo:
raise Exception("No single_logout_server for that binding")
logger.info("Endpoint: %s" % slo)
req = LogoutRequest(self.sec, slo)
if binding == BINDING_SOAP:
lreq = soap.parse_soap_enveloped_saml_logout_request(text)
try:
req = req.loads(lreq, binding)
except Exception:
return None
else:
try:
req = req.loads(text, binding)
except Exception, exc:
logger.error("%s" % (exc,))
return None
req = req.verify()
if not req: # Not a valid request
# return a error message with status code element set to
# urn:oasis:names:tc:SAML:2.0:status:Requester
return None
else:
return req
def _status_response(self, response_class, issuer, status, sign=False,
**kwargs):
""" Create a StatusResponse.
:param response_class: Which subclass of StatusResponse that should be
used
:param issuer: The issuer of the response message
:param status: The return status of the response operation
:param sign: Whether the response should be signed or not
:param kwargs: Extra arguments to the response class
:return: Class instance or string representation of the instance
"""
mid = sid()
if not status:
status = success_status_factory()
response = response_class(issuer=issuer, id=mid, version=VERSION,
issue_instant=instant(),
status=status, **kwargs)
if sign:
response.signature = pre_signature_part(mid)
to_sign = [(class_name(response), mid)]
response = signed_instance_factory(response, self.sec, to_sign)
return response
def create_logout_response(self, request, bindings, status=None,
sign=False, issuer=None):
""" Create a LogoutResponse.
:param request: The request this is a response to
:param bindings: Which bindings that can be used for the response
:param status: The return status of the response operation
:param issuer: The issuer of the message
:return: HTTP args
"""
rinfo = self.response_args(request, bindings, descr_type="spsso")
response = self._status_response(LogoutResponse, issuer, status,
sign=False, **rinfo)
logger.info("Response: %s" % (response,))
return response
def parse_authz_decision_query(self, xml_string, binding):
""" Parse an attribute query
:param xml_string: The Authz decision Query as an XML string
:return: 3-Tuple containing:
subject - identifier of the subject
attribute - which attributes that the requestor wants back
query - the whole query
"""
receiver_addresses = self.config.endpoint("attribute_service", "idp")
attribute_query = AttributeQuery( self.sec, receiver_addresses)
attribute_query = attribute_query.loads(xml_string, binding)
attribute_query = attribute_query.verify()
# Subject name is a BaseID,NameID or EncryptedID instance
subject = attribute_query.subject_id()
attribute = attribute_query.attribute()
return subject, attribute, attribute_query.message

View File

@@ -72,7 +72,7 @@ class TestServer1():
self.server.close_shelve_db()
def test_issuer(self):
issuer = self.server.issuer()
issuer = self.server._issuer()
assert isinstance(issuer, saml.Issuer)
assert _eq(issuer.keyswv(), ["text","format"])
assert issuer.format == saml.NAMEID_FORMAT_ENTITY
@@ -88,7 +88,7 @@ class TestServer1():
("","","surName"): ("Jeter",""),
("","","givenName") :("Derek",""),
}),
issuer=self.server.issuer(),
issuer=self.server._issuer(),
)
assert _eq(assertion.keyswv(),['attribute_statement', 'issuer', 'id',
@@ -128,9 +128,9 @@ class TestServer1():
("","","surName"): ("Jeter",""),
("","","givenName") :("Derek",""),
}),
issuer=self.server.issuer(),
issuer=self.server._issuer(),
),
issuer=self.server.issuer(),
issuer=self.server._issuer(),
)
print response.keyswv()