From f5012004d3f09e3bb1cd098dba105d1ac0c98284 Mon Sep 17 00:00:00 2001 From: Patrick Brosi Date: Mon, 29 Sep 2014 17:40:58 +0200 Subject: [PATCH 1/2] correctly restoring environ["wsgi.input"] after reading POST content --- src/s2repoze/plugins/sp.py | 116 +++++++++++++++---------------------- 1 file changed, 47 insertions(+), 69 deletions(-) diff --git a/src/s2repoze/plugins/sp.py b/src/s2repoze/plugins/sp.py index 60a34e5..4b34119 100644 --- a/src/s2repoze/plugins/sp.py +++ b/src/s2repoze/plugins/sp.py @@ -1,6 +1,6 @@ # -""" -A plugin that allows you to use SAML2 SSO as authentication +""" +A plugin that allows you to use SAML2 SSO as authentication and SAML2 attribute aggregations as metadata collector in your WSGI application. @@ -49,20 +49,20 @@ PAOS_HEADER_INFO = 'ver="%s";"%s"' % (paos.NAMESPACE, ECP_SERVICE) def construct_came_from(environ): - """ The URL that the user used when the process where interupted + """ The URL that the user used when the process where interupted for single-sign-on processing. """ - - came_from = environ.get("PATH_INFO") + + came_from = environ.get("PATH_INFO") qstr = environ.get("QUERY_STRING", "") if qstr: came_from += '?' + qstr return came_from - + def cgi_field_storage_to_dict(field_storage): """Get a plain dictionary, rather than the '.value' system used by the cgi module.""" - + params = {} for key in field_storage.keys(): try: @@ -70,26 +70,9 @@ def cgi_field_storage_to_dict(field_storage): except AttributeError: if isinstance(field_storage[key], basestring): params[key] = field_storage[key] - + return params - -def get_body(environ): - length = int(environ["CONTENT_LENGTH"]) - try: - body = environ["wsgi.input"].read(length) - except Exception, excp: - logger.exception("Exception while reading post: %s" % (excp,)) - raise - - # restore what I might have upset - from StringIO import StringIO - environ['wsgi.input'] = StringIO(body) - environ['s2repoze.body'] = body - - return body - - def exception_trace(tag, exc, log): message = traceback.format_exception(*sys.exc_info()) log.error("[%s] ExcList: %s" % (tag, "".join(message),)) @@ -113,7 +96,7 @@ class ECP_response(object): class SAML2Plugin(object): implements(IChallenger, IIdentifier, IAuthenticator, IMetadataProvider) - + def __init__(self, rememberer_name, config, saml_client, wayf, cache, sid_store=None, discovery="", idp_query_param="", sid_store_cert=None,): @@ -158,27 +141,24 @@ class SAML2Plugin(object): def _get_post(self, environ): """ Get the posted information - + :param environ: A dictionary with environment variables """ - - post_env = environ.copy() - post_env['QUERY_STRING'] = '' - - _ = get_body(environ) - + + body= '' try: - post = cgi.FieldStorage( - fp=environ['wsgi.input'], - environ=post_env, - keep_blank_values=True - ) - except Exception, excp: - logger.debug("Exception (II): %s" % (excp,)) - raise - + length= int(environ.get('CONTENT_LENGTH', '0')) + except ValueError: + length= 0 + if length!=0: + body = environ['wsgi.input'].read(length) # get the POST variables + environ['s2repoze.body'] = body # store the request body for later use by pysaml2 + environ['wsgi.input'] = StringIO(body) # restore the request body as a stream so that everything seems untouched + + post = parse_qs(body) # parse the POST fields into a dict + logger.debug('identify post: %s' % (post,)) - + return post def _wayf_redirect(self, came_from): @@ -190,8 +170,8 @@ class SAML2Plugin(object): #noinspection PyUnusedLocal def _pick_idp(self, environ, came_from): - """ - If more than one idp and if none is selected, I have to do wayf or + """ + If more than one idp and if none is selected, I have to do wayf or disco """ @@ -230,7 +210,7 @@ class SAML2Plugin(object): detail='unknown ECP version') idps = self.metadata.with_descriptor("idpsso") - + logger.info("IdP URL: %s" % idps) idp_entity_id = query = None @@ -290,7 +270,7 @@ class SAML2Plugin(object): logger.info("Chosen IdP: '%s'" % idp_entity_id) return 0, idp_entity_id - + #### IChallenger #### #noinspection PyUnusedLocal def challenge(self, environ, _status, _app_headers, _forget_headers): @@ -320,7 +300,7 @@ class SAML2Plugin(object): came_from = construct_came_from(environ) environ["myapp.came_from"] = came_from logger.debug("[sp.challenge] RelayState >> '%s'" % came_from) - + # Am I part of a virtual organization or more than one ? try: vorg_name = environ["myapp.vo"] @@ -329,7 +309,7 @@ class SAML2Plugin(object): vorg_name = _cli.vorg._name except AttributeError: vorg_name = "" - + logger.info("[sp.challenge] VO: %s" % vorg_name) # If more than one idp and if none is selected, I have to do wayf @@ -373,7 +353,7 @@ class SAML2Plugin(object): req_id, msg_str = _cli.create_authn_request( dest, vorg=vorg_name, sign=_cli.authn_requests_signed, message_id=_sid, extensions=extensions) - _sid = req_id + _sid = req_id else: req_id, req = _cli.create_authn_request( dest, vorg=vorg_name, sign=False, extensions=extensions) @@ -423,7 +403,7 @@ class SAML2Plugin(object): logger.debug("Identity: %s" % identity) return identity - + def _eval_authn_response(self, environ, post, binding=BINDING_HTTP_POST): logger.info("Got AuthN response, checking..") logger.info("Outstanding: %s" % (self.outstanding_queries,)) @@ -432,18 +412,18 @@ class SAML2Plugin(object): # Evaluate the response, returns a AuthnResponse instance try: authresp = self.saml_client.parse_authn_request_response( - post["SAMLResponse"], binding, self.outstanding_queries, + post["SAMLResponse"][0], binding, self.outstanding_queries, self.outstanding_certs) except Exception, excp: logger.exception("Exception: %s" % (excp,)) raise - + session_info = authresp.session_info() except TypeError, excp: logger.exception("Exception: %s" % (excp,)) return None - + if session_info["came_from"]: logger.debug("came_from << %s" % session_info["came_from"]) try: @@ -478,13 +458,13 @@ class SAML2Plugin(object): "SAMLResponse" not in query and "SAMLRequest" not in query: logger.debug('[identify] get or empty post') return None - + # if logger: # logger.info("ENVIRON: %s" % environ) # logger.info("self: %s" % (self.__dict__,)) - + uri = environ.get('REQUEST_URI', construct_url(environ)) - + logger.debug('[sp.identify] uri: %s' % (uri,)) query = parse_dict_querystring(environ) @@ -495,15 +475,13 @@ class SAML2Plugin(object): binding = BINDING_HTTP_REDIRECT else: post = self._get_post(environ) - if post.list is None: - post.list = [] binding = BINDING_HTTP_POST try: logger.debug('[sp.identify] post keys: %s' % (post.keys(),)) except (TypeError, IndexError): pass - + try: path_info = environ['PATH_INFO'] logout = False @@ -514,7 +492,7 @@ class SAML2Plugin(object): print("logout request received") try: response = self.saml_client.handle_logout_request( - post["SAMLRequest"], + post["SAMLRequest"][0], self.saml_client.users.subjects()[0], binding) environ['samlsp.pending'] = self._handle_logout(response) return {} @@ -536,7 +514,7 @@ class SAML2Plugin(object): try: if logout: response = self.saml_client.parse_logout_request_response( - post["SAMLResponse"], binding) + post["SAMLResponse"][0], binding) if response: action = self.saml_client.handle_logout_response( response) @@ -572,8 +550,8 @@ class SAML2Plugin(object): exception_trace("sp.identity", exc, logger) environ["post.fieldstorage"] = post return {} - - if session_info: + + if session_info: environ["s2repoze.sessioninfo"] = session_info return self._construct_identity(session_info) else: @@ -596,12 +574,12 @@ class SAML2Plugin(object): logger.debug("Issuers: %s" % _cli.users.sources(name_id)) except KeyError: pass - + if "user" not in identity: identity["user"] = {} try: (ava, _) = _cli.users.get_identity(name_id) - #now = time.gmtime() + #now = time.gmtime() logger.debug("[add_metadata] adds: %s" % ava) identity["user"].update(ava) except KeyError: @@ -625,7 +603,7 @@ class SAML2Plugin(object): if not identity["user"]: # remove cookie and demand re-authentication pass - + # used 2 times : one to get the ticket, the other to validate it @staticmethod def _service_url(environ, qstr=None): @@ -635,7 +613,7 @@ class SAML2Plugin(object): url = construct_url(environ) return url - #### IAuthenticatorPlugin #### + #### IAuthenticatorPlugin #### #noinspection PyUnusedLocal def authenticate(self, environ, identity=None): if identity: @@ -672,7 +650,7 @@ def make_plugin(remember_name=None, # plugin for remember discovery="", idp_query_param="" ): - + if saml_conf is "": raise ValueError( 'must include saml_conf in configuration') From 8db1a3b362449b7f872bdaf238a5a6c400b93355 Mon Sep 17 00:00:00 2001 From: Patrick Brosi Date: Mon, 29 Sep 2014 17:59:11 +0200 Subject: [PATCH 2/2] remove unneeded method cgi_field_storage() --- src/s2repoze/plugins/sp.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/s2repoze/plugins/sp.py b/src/s2repoze/plugins/sp.py index 4b34119..46232ea 100644 --- a/src/s2repoze/plugins/sp.py +++ b/src/s2repoze/plugins/sp.py @@ -58,21 +58,6 @@ def construct_came_from(environ): came_from += '?' + qstr return came_from - -def cgi_field_storage_to_dict(field_storage): - """Get a plain dictionary, rather than the '.value' system used by the - cgi module.""" - - params = {} - for key in field_storage.keys(): - try: - params[key] = field_storage[key].value - except AttributeError: - if isinstance(field_storage[key], basestring): - params[key] = field_storage[key] - - return params - def exception_trace(tag, exc, log): message = traceback.format_exception(*sys.exc_info()) log.error("[%s] ExcList: %s" % (tag, "".join(message),)) @@ -530,7 +515,7 @@ class SAML2Plugin(object): return {} else: session_info = self._eval_authn_response( - environ, cgi_field_storage_to_dict(post), + environ, post, binding=binding) except Exception, err: environ["s2repoze.saml_error"] = err