From 7127aa55da497688916e95dc47bf587c9b8c2037 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Sun, 30 Dec 2012 17:28:33 +0100 Subject: [PATCH] Even more refactoring --- example/sp/sp.py | 15 ++- src/saml2/client_base.py | 179 ++----------------------- src/saml2/request.py | 28 ++-- src/saml2/server.py | 276 ++++++--------------------------------- tests/test_50_server.py | 8 +- 5 files changed, 77 insertions(+), 429 deletions(-) diff --git a/example/sp/sp.py b/example/sp/sp.py index df76357..204269e 100755 --- a/example/sp/sp.py +++ b/example/sp/sp.py @@ -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"] diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 11500b6..7ac437a 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -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 diff --git a/src/saml2/request.py b/src/saml2/request.py index 2627896..4063e0d 100644 --- a/src/saml2/request.py +++ b/src/saml2/request.py @@ -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): diff --git a/src/saml2/server.py b/src/saml2/server.py index 625f795..dedec91 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -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,15 +251,18 @@ 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 @@ -277,120 +275,35 @@ class Server(Entity): sp_entity_id - the entity id of the SP 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) + return self._parse_request(enc_request, AuthnRequest, + "single_sign_on_service", binding, + "authentication_request") - - 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) - 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) # ------------------------------------------------------------------------ - + def _authn_response(self, in_response_to, consumer_url, sp_entity_id, identity=None, name_id=None, status=None, authn=None, @@ -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 diff --git a/tests/test_50_server.py b/tests/test_50_server.py index 990dec3..de72295 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -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()