def _parse_response(self, xmlstr, response_cls, service, binding, **kwargs): """ Deal with a Response :param xmlstr: The response as a xml string :param response_cls: What type of response it is :param binding: What type of binding this message came through. :param kwargs: Extra key word arguments :return: None if the reply doesn't contain a valid SAML Response, otherwise the response. """ response = None if self.config.accepted_time_diff: kwargs["timeslack"] = self.config.accepted_time_diff if "asynchop" not in kwargs: if binding in [BINDING_SOAP, BINDING_PAOS]: kwargs["asynchop"] = False else: kwargs["asynchop"] = True if xmlstr: if "return_addr" not in kwargs: if binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]: try: # expected return address kwargs["return_addr"] = self.config.endpoint( service, binding=binding)[0] except Exception: logger.info("Not supposed to handle this!") return None try: response = response_cls(self.sec, **kwargs) except Exception, exc: logger.info("%s" % exc) raise xmlstr = self.unravel(xmlstr, binding, response_cls.msgtype) if not xmlstr: # Not a valid reponse return None logger.debug("XMLSTR: %s" % xmlstr) response = response.loads(xmlstr, False) if response: response = response.verify() if not response: return None logger.debug(response)
class Entity(HTTPBase): def __init__(self, entity_type, config=None, config_file="", virtual_organization=""): self.entity_type = entity_type self.users = None if config: self.config = config elif config_file: self.config = config_factory(entity_type, config_file) else: raise SAMLError("Missing configuration") HTTPBase.__init__(self, self.config.verify_ssl_cert, self.config.ca_certs, self.config.key_file, self.config.cert_file) if self.config.vorg: for vo in self.config.vorg.values(): vo.sp = self self.metadata = self.config.metadata self.config.setup_logger() self.debug = self.config.debug self.seed = rndstr(32) self.sec = security_context(self.config) if virtual_organization: if isinstance(virtual_organization, basestring): self.vorg = self.config.vorg[virtual_organization] elif isinstance(virtual_organization, VirtualOrg): self.vorg = virtual_organization else: self.vorg = None self.artifact = {} if self.metadata: self.sourceid = self.metadata.construct_source_id() else: self.sourceid = {} def _issuer(self, entityid=None): """ Return an Issuer instance """ if entityid: if isinstance(entityid, Issuer): return entityid else: return Issuer(text=entityid, format=NAMEID_FORMAT_ENTITY) else: return Issuer(text=self.config.entityid, format=NAMEID_FORMAT_ENTITY) def apply_binding(self, binding, msg_str, destination="", relay_state="", response=False, sign=False): """ Construct the necessary HTTP arguments dependent on Binding :param binding: Which binding to use :param msg_str: The return message as a string (XML) if the message is to be signed it MUST contain the signature element. :param destination: Where to send the message :param relay_state: Relay_state if provided :param response: Which type of message this is :return: A dictionary """ # unless if BINDING_HTTP_ARTIFACT if response: typ = "SAMLResponse" else: typ = "SAMLRequest" if binding == BINDING_HTTP_POST: logger.info("HTTP POST") info = self.use_http_form_post(msg_str, destination, relay_state, typ) info["url"] = destination info["method"] = "GET" elif binding == BINDING_HTTP_REDIRECT: logger.info("HTTP REDIRECT") info = self.use_http_get(msg_str, destination, relay_state, typ) info["url"] = destination info["method"] = "GET" elif binding == BINDING_SOAP or binding == BINDING_PAOS: info = self.use_soap(msg_str, destination, sign=sign) elif binding == BINDING_URI: info = self.use_http_uri(msg_str, typ, destination) elif binding == BINDING_HTTP_ARTIFACT: if response: info = self.use_http_artifact(msg_str, destination, relay_state) info["method"] = "GET" info["status"] = 302 else: info = self.use_http_artifact(msg_str, destination, relay_state) else: raise SAMLError("Unknown binding type: %s" % binding) return info def pick_binding(self, service, bindings=None, descr_type="", request=None, entity_id=""): if request and not entity_id: entity_id = request.issuer.text.strip() sfunc = getattr(self.metadata, service) if bindings is None: bindings = self.config.preferred_binding[service] if not descr_type: if self.entity_type == "sp": descr_type = "idpsso" else: descr_type = "spsso" for binding in bindings: try: srvs = sfunc(entity_id, binding, descr_type) if srvs: return binding, destinations(srvs)[0] except UnsupportedBinding: pass logger.error("Failed to find consumer URL: %s, %s, %s" % (entity_id, bindings, descr_type)) #logger.error("Bindings: %s" % bindings) #logger.error("Entities: %s" % self.metadata) raise SAMLError("Unkown entity or unsupported bindings") def message_args(self, message_id=0): if not message_id: message_id = sid(self.seed) return {"id": message_id, "version": VERSION, "issue_instant": instant(), "issuer": self._issuer()} def response_args(self, message, bindings=None, descr_type=""): """ :param message: The message to which a reply is constructed :param bindings: Which bindings can be used. :param descr_type: Type of descriptor (spssp, idpsso, ) :return: Dictionary """ info = {"in_response_to": message.id} if isinstance(message, AuthnRequest): rsrv = "assertion_consumer_service" descr_type = "spsso" info["sp_entity_id"] = message.issuer.text info["name_id_policy"] = message.name_id_policy elif isinstance(message, LogoutRequest): rsrv = "single_logout_service" elif isinstance(message, AttributeQuery): info["sp_entity_id"] = message.issuer.text rsrv = "attribute_consuming_service" descr_type = "spsso" elif isinstance(message, ManageNameIDRequest): rsrv = "manage_name_id_service" # The once below are solely SOAP so no return destination needed elif isinstance(message, AssertionIDRequest): rsrv = "" elif isinstance(message, ArtifactResolve): rsrv = "" elif isinstance(message, AssertionIDRequest): rsrv = "" elif isinstance(message, NameIDMappingRequest): rsrv = "" else: raise SAMLError("No support for this type of query") if bindings == [BINDING_SOAP]: info["binding"] = BINDING_SOAP info["destination"] = "" return info if rsrv: if not descr_type: if self.entity_type == "sp": descr_type = "idpsso" else: descr_type = "spsso" binding, destination = self.pick_binding(rsrv, bindings, descr_type=descr_type, request=message) info["binding"] = binding info["destination"] = destination return info def unravel(self, txt, binding, msgtype="response"): #logger.debug("unravel '%s'" % txt) if binding not in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, BINDING_SOAP, BINDING_URI, None]: raise ValueError("Don't know how to handle '%s'" % binding) else: try: if binding == BINDING_HTTP_REDIRECT: xmlstr = decode_base64_and_inflate(txt) elif binding == BINDING_HTTP_POST: xmlstr = base64.b64decode(txt) elif binding == BINDING_SOAP: func = getattr(soap, "parse_soap_enveloped_saml_%s" % msgtype) xmlstr = func(txt) else: xmlstr = txt except Exception: raise UnravelError() return xmlstr def parse_soap_message(self, text): """ :param text: The SOAP message :return: A dictionary with two keys "body" and "header" """ return class_instances_from_soap_enveloped_saml_thingies(text, [paos, ecp, samlp]) def unpack_soap_message(self, text): """ Picks out the parts of the SOAP message, body and headers apart :param text: The SOAP message :return: A dictionary with two keys "body"/"header" """ return open_soap_envelope(text) # -------------------------------------------------------------------------- def sign(self, msg, mid=None, to_sign=None, sign_prepare=False): if msg.signature is None: msg.signature = pre_signature_part(msg.id, self.sec.my_cert, 1) if sign_prepare: return msg if mid is None: mid = msg.id try: to_sign.append([(class_name(msg), mid)]) except AttributeError: to_sign = [(class_name(msg), mid)] logger.info("REQUEST: %s" % msg) return signed_instance_factory(msg, self.sec, to_sign) def _message(self, request_cls, destination=None, message_id=0, consent=None, extensions=None, sign=False, sign_prepare=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 message_id: A message identifier :param consent: Whether the principal have given her consent :param extensions: Possible extensions :param sign: Whether the request should be signed or not. :param sign_prepare: Whether the signature should be prepared or not. :param kwargs: Key word arguments specific to one request type :return: An instance of the request_cls """ if not message_id: message_id = sid(self.seed) for key, val in self.message_args(message_id).items(): if key not in kwargs: kwargs[key] = val req = request_cls(**kwargs) if destination: req.destination = destination if consent: req.consent = consent if extensions: req.extensions = extensions if sign: return self.sign(req, sign_prepare=sign_prepare) else: logger.info("REQUEST: %s" % req) return req def _filter_args(self, instance, extensions=None, **kwargs): args = {} if extensions is None: extensions = [] allowed_attributes = instance.keys() for key, val in kwargs.items(): if key in allowed_attributes: args[key] = val elif isinstance(val, SamlBase): # extension elements allowed ? extensions.append(element_to_extension_element(val)) return args, extensions def _add_info(self, msg, **kwargs): """ Add information to a SAML message. If the attribute is not part of what's defined in the SAML standard add it as an extension. :param msg: :param kwargs: :return: """ args, extensions = self._filter_args(msg, **kwargs) for key, val in args.items(): setattr(msg, key, val) if extensions: if msg.extension_elements: msg.extension_elements.extend(extensions) else: msg.extension_elements = extensions def _response(self, in_response_to, consumer_url=None, status=None, issuer=None, sign=False, to_sign=None, **kwargs): """ Create a Response. :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: If there are other parts to sign :param kwargs: Extra key word arguments :return: A Response 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 self._add_info(response, **kwargs) if sign: self.sign(response, to_sign=to_sign) elif to_sign: return signed_instance_factory(response, self.sec, to_sign) else: return response 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() for key in ["destination", "binding"]: try: del kwargs[key] except KeyError: pass if not status: status = success_status_factory() response = response_class(issuer=issuer, id=mid, version=VERSION, issue_instant=instant(), status=status, **kwargs) if sign: return self.sign(response, mid) else: return response # ------------------------------------------------------------------------ def srv2typ(self, service): for typ in ["aa", "pdp", "aq"]: if service in ENDPOINTS[typ]: if typ == "aa": return "attribute_authority" elif typ == "aq": return "authn_authority" else: return typ def _parse_request(self, xmlstr, request_cls, service, binding): """Parse a Request :param xmlstr: The request in its transport format :param request_cls: The type of requests I expect :param service: :param binding: Which binding that was used to transport the message to this entity. :return: A request instance """ _log_info = logger.info _log_debug = logger.debug # The addresses I should receive messages like this on receiver_addresses = self.config.endpoint(service, binding, self.entity_type) if not receiver_addresses and self.entity_type == "idp": for typ in ["aa", "aq", "pdp"]: receiver_addresses = self.config.endpoint(service, binding, typ) if receiver_addresses: break _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 _request = request_cls(self.sec, receiver_addresses, self.config.attribute_converters, timeslack=timeslack) origdoc = xmlstr xmlstr = self.unravel(xmlstr, binding, request_cls.msgtype) _request = _request.loads(xmlstr, binding, origdoc=origdoc) _log_debug("Loaded request") if _request: _request = _request.verify() _log_debug("Verified request") if not _request: return None else: return _request # ------------------------------------------------------------------------ def create_error_response(self, in_response_to, destination, info, sign=False, issuer=None, **kwargs): """ Create a error response. :param in_response_to: The identifier of the message this is a response to. :param destination: The intended recipient of this message :param info: Either an Exception instance or a 2-tuple consisting of error code and descriptive text :param sign: Whether the response should be signed or not :param issuer: The issuer of the response :param kwargs: To capture key,value pairs I don't care about :return: A response instance """ status = error_status_factory(info) return self._response(in_response_to, destination, status, issuer, sign) # ------------------------------------------------------------------------ def create_logout_request(self, destination, issuer_entity_id, subject_id=None, name_id=None, reason=None, expire=None, message_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 message_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: if self.entity_type == "idp": name_id = NameID(text=self.users.get_entityid(subject_id, issuer_entity_id, False)) else: name_id = NameID(text=subject_id) if not name_id: raise SAMLError("Missing subject identification") return self._message(LogoutRequest, destination, message_id, consent, extensions, sign, name_id=name_id, reason=reason, not_on_or_after=expire) def create_logout_response(self, request, bindings=None, 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 If None the preferred bindings are gathered from the configuration :param status: The return status of the response operation If None the operation is regarded as a Success. :param issuer: The issuer of the message :return: HTTP args """ rinfo = self.response_args(request, bindings) response = self._status_response(samlp.LogoutResponse, issuer, status, sign, **rinfo) logger.info("Response: %s" % (response,)) return response def create_artifact_resolve(self, artifact, destination, sid, consent=None, extensions=None, sign=False): """ Create a ArtifactResolve request :param artifact: :param destination: :param sid: session id :param consent: :param extensions: :param sign: :return: The request message """ artifact = Artifact(text=artifact) return self._message(ArtifactResolve, destination, sid, consent, extensions, sign, artifact=artifact) def create_artifact_response(self, request, artifact, bindings=None, status=None, sign=False, issuer=None): """ Create an ArtifactResponse :return: """ rinfo = self.response_args(request, bindings) response = self._status_response(ArtifactResponse, issuer, status, sign=sign, **rinfo) msg = element_to_extension_element(self.artifact[artifact]) response.extension_elements = [msg] logger.info("Response: %s" % (response,)) return response def create_manage_name_id_request(self, destination, message_id=0, consent=None, extensions=None, sign=False, name_id=None, new_id=None, encrypted_id=None, new_encrypted_id=None, terminate=None): """ :param destination: :param message_id: :param consent: :param extensions: :param sign: :param name_id: :param new_id: :param encrypted_id: :param new_encrypted_id: :param terminate: :return: """ kwargs = self.message_args(message_id) if name_id: kwargs["name_id"] = name_id elif encrypted_id: kwargs["encrypted_id"] = encrypted_id else: raise AttributeError( "One of NameID or EncryptedNameID has to be provided") if new_id: kwargs["new_id"] = new_id elif new_encrypted_id: kwargs["new_encrypted_id"] = new_encrypted_id elif terminate: kwargs["terminate"] = terminate else: raise AttributeError( "One of NewID, NewEncryptedNameID or Terminate has to be provided") return self._message(ManageNameIDRequest, destination, consent=consent, extensions=extensions, sign=sign, **kwargs) def parse_manage_name_id_request(self, xmlstr, binding=BINDING_SOAP): """ Deal with a LogoutRequest :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. """ return self._parse_request(xmlstr, request.ManageNameIDRequest, "manage_name_id_service", binding) def create_manage_name_id_response(self, request, bindings=None, status=None, sign=False, issuer=None, **kwargs): rinfo = self.response_args(request, bindings) response = self._status_response(samlp.ManageNameIDResponse, issuer, status, sign, **rinfo) logger.info("Response: %s" % (response,)) return response def parse_manage_name_id_request_response(self, string, binding=BINDING_SOAP): return self._parse_response(string, response.ManageNameIDResponse, "manage_name_id_service", binding) # ------------------------------------------------------------------------ def _parse_response(self, xmlstr, response_cls, service, binding, **kwargs): """ Deal with a Response :param xmlstr: The response as a xml string :param response_cls: What type of response it is :param binding: What type of binding this message came through. :param kwargs: Extra key word arguments :return: None if the reply doesn't contain a valid SAML Response, otherwise the response. """ response = None if self.config.accepted_time_diff: kwargs["timeslack"] = self.config.accepted_time_diff if "asynchop" not in kwargs: if binding in [BINDING_SOAP, BINDING_PAOS]: kwargs["asynchop"] = False else: kwargs["asynchop"] = True if xmlstr: if "return_addr" not in kwargs: if binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]: try: # expected return address kwargs["return_addr"] = self.config.endpoint( service, binding=binding)[0] except Exception: logger.info("Not supposed to handle this!") return None try: response = response_cls(self.sec, **kwargs) except Exception, exc: logger.info("%s" % exc) raise xmlstr = self.unravel(xmlstr, binding, response_cls.msgtype) if not xmlstr: # Not a valid reponse return None logger.debug("XMLSTR: %s" % xmlstr) try: response = response.loads(xmlstr, False) except Exception, err: if "not well-formed" in "%s" % err: logger.error("Not well-formed XML") return None if response: response = response.verify() if not response: return None
def _parse_response(self, xmlstr, response_cls, service, binding, outstanding_certs=None, **kwargs): """ Deal with a Response :param xmlstr: The response as a xml string :param response_cls: What type of response it is :param binding: What type of binding this message came through. :param kwargs: Extra key word arguments :return: None if the reply doesn't contain a valid SAML Response, otherwise the response. """ response = None if self.config.accepted_time_diff: kwargs["timeslack"] = self.config.accepted_time_diff if "asynchop" not in kwargs: if binding in [BINDING_SOAP, BINDING_PAOS]: kwargs["asynchop"] = False else: kwargs["asynchop"] = True if xmlstr: if "return_addrs" not in kwargs: if binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]: try: # expected return address kwargs["return_addrs"] = self.config.endpoint( service, binding=binding) except Exception: logger.info("Not supposed to handle this!") return None try: response = response_cls(self.sec, **kwargs) except Exception as exc: logger.info("%s" % exc) raise xmlstr = self.unravel(xmlstr, binding, response_cls.msgtype) origxml = xmlstr if outstanding_certs is not None: _response = samlp.any_response_from_string(xmlstr) if len(_response.encrypted_assertion) > 0: _, cert_file = make_temp( "%s" % outstanding_certs[_response.in_response_to]["key"], decode=False) cbxs = CryptoBackendXmlSec1(self.config.xmlsec_binary) xmlstr = cbxs.decrypt(xmlstr, cert_file) if not xmlstr: # Not a valid reponse return None try: response = response.loads(xmlstr, False, origxml=origxml) except SigverError as err: logger.error("Signature Error: %s" % err) raise except UnsolicitedResponse: logger.error("Unsolicited response") raise except Exception as err: if "not well-formed" in "%s" % err: logger.error("Not well-formed XML") raise logger.debug("XMLSTR: %s" % xmlstr) if hasattr(response.response, 'encrypted_assertion'): for encrypted_assertion in response.response\ .encrypted_assertion: if encrypted_assertion.extension_elements is not None: assertion_list = extension_elements_to_elements( encrypted_assertion.extension_elements, [saml]) for assertion in assertion_list: _assertion = saml.assertion_from_string( str(assertion)) response.response.assertion.append(_assertion) if response: response = response.verify() if not response: return None #logger.debug(response) return response
if "not well-formed" in "%s" % err: logger.error("Not well-formed XML") raise logger.debug("XMLSTR: %s" % xmlstr) if hasattr(response.response, 'encrypted_assertion'): for encrypted_assertion in response.response.encrypted_assertion: if encrypted_assertion.extension_elements is not None: assertion_list = extension_elements_to_elements(encrypted_assertion.extension_elements, [saml]) for assertion in assertion_list: _assertion = saml.assertion_from_string(str(assertion)) response.response.assertion.append(_assertion) if response: response = response.verify() if not response: return None #logger.debug(response) return response # ------------------------------------------------------------------------ def parse_logout_request_response(self, xmlstr, binding=BINDING_SOAP): return self._parse_response(xmlstr, LogoutResponse, "single_logout_service", binding) # ------------------------------------------------------------------------
def _parse_response(self, xmlstr, response_cls, service, binding, outstanding_certs=None, **kwargs): """ Deal with a Response :param xmlstr: The response as a xml string :param response_cls: What type of response it is :param binding: What type of binding this message came through. :param kwargs: Extra key word arguments :return: None if the reply doesn't contain a valid SAML Response, otherwise the response. """ response = None if self.config.accepted_time_diff: kwargs["timeslack"] = self.config.accepted_time_diff if "asynchop" not in kwargs: if binding in [BINDING_SOAP, BINDING_PAOS]: kwargs["asynchop"] = False else: kwargs["asynchop"] = True if xmlstr: if "return_addrs" not in kwargs: if binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]: try: # expected return address kwargs["return_addrs"] = self.config.endpoint( service, binding=binding) except Exception: logger.info("Not supposed to handle this!") return None try: response = response_cls(self.sec, **kwargs) except Exception as exc: logger.info("%s" % exc) raise xmlstr = self.unravel(xmlstr, binding, response_cls.msgtype) origxml = xmlstr if outstanding_certs is not None: _response = samlp.any_response_from_string(xmlstr) if len(_response.encrypted_assertion) > 0: _, cert_file = make_temp( "%s" % outstanding_certs[_response.in_response_to][ "key"], decode=False) cbxs = CryptoBackendXmlSec1(self.config.xmlsec_binary) xmlstr = cbxs.decrypt(xmlstr, cert_file) if not xmlstr: # Not a valid reponse return None try: response = response.loads(xmlstr, False, origxml=origxml) except SigverError as err: logger.error("Signature Error: %s" % err) raise except UnsolicitedResponse: logger.error("Unsolicited response") raise except Exception as err: if "not well-formed" in "%s" % err: logger.error("Not well-formed XML") raise logger.debug("XMLSTR: %s" % xmlstr) if hasattr(response.response, 'encrypted_assertion'): for encrypted_assertion in response.response\ .encrypted_assertion: if encrypted_assertion.extension_elements is not None: assertion_list = extension_elements_to_elements( encrypted_assertion.extension_elements, [saml]) for assertion in assertion_list: _assertion = saml.assertion_from_string( str(assertion)) response.response.assertion.append(_assertion) if response: response = response.verify() if not response: return None #logger.debug(response) return response