X-Git-Url: http://git.cascardo.eti.br/?a=blobdiff_plain;f=ipsilon%2Fproviders%2Fsaml2%2Flogout.py;h=f706c727a8627a033e1f20b9d03fcdf3bf608c46;hb=ee4d965a5d2329e9691059ddf08ab3a0a8f77330;hp=da8edcf434f38a10e541615810aeb0270f89e024;hpb=c84eaa4d5f44524ea37f8c2444cbd53520d75a0c;p=cascardo%2Fipsilon.git diff --git a/ipsilon/providers/saml2/logout.py b/ipsilon/providers/saml2/logout.py index da8edcf..f706c72 100644 --- a/ipsilon/providers/saml2/logout.py +++ b/ipsilon/providers/saml2/logout.py @@ -1,27 +1,13 @@ -# Copyright (C) 2015 Rob Crittenden -# -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING from ipsilon.providers.common import ProviderPageBase from ipsilon.providers.common import InvalidRequest -from ipsilon.providers.saml2.sessions import SAMLSessionsContainer from ipsilon.providers.saml2.auth import UnknownProvider from ipsilon.util.user import UserSession +from ipsilon.util.constants import SOAP_MEDIA_TYPE import cherrypy import lasso +import requests class LogoutRequest(ProviderPageBase): @@ -42,8 +28,8 @@ class LogoutRequest(ProviderPageBase): deleted. """ - def __init__(self, *args, **kwargs): - super(LogoutRequest, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(LogoutRequest, self).__init__(site, provider) def _handle_logout_request(self, us, logout, saml_sessions, message): self.debug('Logout request') @@ -56,39 +42,47 @@ class LogoutRequest(ProviderPageBase): e, message) self.error(msg) raise UnknownProvider(msg) + except lasso.DsInvalidSigalgError as e: + msg = 'Invalid SAML Request: missing or invalid signature ' \ + 'algorithm' + self.error(msg) + raise InvalidRequest(msg) except (lasso.ProfileInvalidProtocolprofileError, - lasso.DsError), e: + lasso.DsError) as e: msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request, e, message) self.error(msg) raise InvalidRequest(msg) - except lasso.Error, e: + except lasso.Error as e: self.error('SLO unknown error: %s' % message) raise cherrypy.HTTPError(400, 'Invalid logout request') - # TODO: verify that the session index is in the request session_indexes = logout.request.sessionIndexes self.debug('SLO from %s with %s sessions' % (logout.remoteProviderId, session_indexes)) - session = saml_sessions.find_session_by_provider( - logout.remoteProviderId) + # Find the first session being asked to log out. Later we loop over + # all the session indexes and mark them as logging out but only one + # is needed to handle the request. + if len(session_indexes) < 1: + self.error('SLO empty session Indexes') + raise cherrypy.HTTPError(400, 'Invalid logout request') + session = saml_sessions.get_session_by_id(session_indexes[0]) if session: try: - logout.setSessionFromDump(session.session.dump()) + logout.setSessionFromDump(session.login_session) except lasso.ProfileBadSessionDumpError as e: self.error('loading session failed: %s' % e) raise cherrypy.HTTPError(400, 'Invalid logout session') else: - self.error('Logout attempt without being loggged in.') - raise cherrypy.HTTPError(400, 'Not logged in') + return self._not_logged_in(logout, message) try: logout.validateRequest() except lasso.ProfileSessionNotFoundError, e: self.error('Logout failed. No sessions for %s' % logout.remoteProviderId) - raise cherrypy.HTTPError(400, 'Not logged in') + return self._not_logged_in(logout, message) except lasso.LogoutUnsupportedProfileError: self.error('Logout failed. Unsupported profile %s' % logout.remoteProviderId) @@ -105,11 +99,15 @@ class LogoutRequest(ProviderPageBase): except lasso.Error, e: self.error('SLO failed to build logout response: %s' % e) - session.set_logoutstate(logout.msgUrl, logout.request.id, - message) - saml_sessions.start_logout(session) - - us.save_provider_data('saml2', saml_sessions) + for ind in session_indexes: + session = saml_sessions.get_session_by_id(ind) + if session: + session.set_logoutstate(relaystate=logout.msgUrl, + request=message) + saml_sessions.start_logout(session) + else: + self.error('SLO request to log out non-existent session: %s' % + ind) return @@ -136,36 +134,91 @@ class LogoutRequest(ProviderPageBase): self.debug('SLO response to request id %s' % logout.response.inResponseTo) - saml_sessions = us.get_provider_data('saml2') - if saml_sessions is None: - # TODO: return logged out instead - saml_sessions = SAMLSessionsContainer() - - # TODO: need to log out each SessionIndex? - session = saml_sessions.find_session_by_provider( - logout.remoteProviderId) + session = saml_sessions.get_session_by_request_id( + logout.response.inResponseTo) if session is not None: self.debug('Logout response session logout id is: %s' % session.session_id) - saml_sessions.remove_session_by_provider( - logout.remoteProviderId) - us.save_provider_data('saml2', saml_sessions) + saml_sessions.remove_session(session) user = us.get_user() self._audit('Logged out user: %s [%s] from %s' % (user.name, user.fullname, logout.remoteProviderId)) else: - self.error('Logout attempt without being loggged in.') - raise cherrypy.HTTPError(400, 'Not logged in') + return self._not_logged_in(logout, message) return + def _not_logged_in(self, logout, message): + """ + The user requested a logout but isn't logged in, or we can't + find a session for the user. Try to be nice and redirect them + back to the RelayState in the logout request. + + We are only nice in the case of a valid logout request. If the + request is invalid (not signed, unknown SP, etc) then an + exception is raised. + """ + self.error('Logout attempt without being logged in.') + + if logout.msgRelayState is not None: + raise cherrypy.HTTPRedirect(logout.msgRelayState) + + try: + logout.processRequestMsg(message) + except (lasso.ServerProviderNotFoundError, + lasso.ProfileUnknownProviderError) as e: + msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId, + e, message) + self.error(msg) + raise UnknownProvider(msg) + except (lasso.ProfileInvalidProtocolprofileError, + lasso.DsError), e: + msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request, + e, message) + self.error(msg) + raise InvalidRequest(msg) + except lasso.Error, e: + self.error('SLO unknown error: %s' % message) + raise cherrypy.HTTPError(400, 'Invalid logout request') + + if logout.msgRelayState: + raise cherrypy.HTTPRedirect(logout.msgRelayState) + else: + raise cherrypy.HTTPError(400, 'Not logged in') + + def _soap_logout(self, logout): + """ + Send a SOAP logout request over HTTP and return the result. + """ + headers = {'Content-Type': SOAP_MEDIA_TYPE} + try: + response = requests.post(logout.msgUrl, data=logout.msgBody, + headers=headers) + except Exception as e: # pylint: disable=broad-except + self.error('SOAP HTTP request failed: (%s) (on %s)' % + (e, logout.msgUrl)) + raise + + if response.status_code != 200: + self.error('SOAP error (%s) (on %s)' % + (response.status_code, logout.msgUrl)) + raise InvalidRequest('SOAP HTTP error code %s' % + response.status_code) + + if not response.text: + self.error('Empty SOAP response') + raise InvalidRequest('No content in SOAP response') + + return response.text + def logout(self, message, relaystate=None, samlresponse=None): """ - Handle HTTP Redirect logout. This is an asynchronous logout - request process that relies on the HTTP agent to forward - logout requests to any other SP's that are also logged in. + Handle HTTP logout. The supported logout methods are stored + in each session. First all the SOAP sessions are logged out + then the HTTP Redirect method is used for any remaining + sessions. The basic process is this: 1. A logout request is received. It is processed and the response @@ -178,45 +231,52 @@ class LogoutRequest(ProviderPageBase): Repeat steps 2-3 until only the initial logout request is left unhandled, at which time the pre-generated response is sent back to the SP that originated the logout request. + + The final logout response is always a redirect. """ logout = self.cfg.idp.get_logout_handler() us = UserSession() - saml_sessions = us.get_provider_data('saml2') - if saml_sessions is None: - # No sessions means nothing to log out - self.error('Logout attempt without being loggged in.') - raise cherrypy.HTTPError(400, 'Not logged in') - - self.debug('%d sessions loaded' % saml_sessions.count()) - saml_sessions.dump() + saml_sessions = self.cfg.idp.sessionfactory - if lasso.SAML2_FIELD_REQUEST in message: - self._handle_logout_request(us, logout, saml_sessions, message) - elif samlresponse: - self._handle_logout_response(us, logout, saml_sessions, message, - samlresponse) - else: - raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' + - 'request or response.') + try: + if lasso.SAML2_FIELD_REQUEST in message: + self._handle_logout_request(us, logout, saml_sessions, + message) + elif samlresponse: + self._handle_logout_response(us, logout, saml_sessions, + message, samlresponse) + else: + raise cherrypy.HTTPError(400, 'Bad Request. Not a ' + + 'logout request or response.') + except InvalidRequest as e: + raise cherrypy.HTTPError(400, 'Bad Request. %s' % e) # Fall through to handle any remaining sessions. # Find the next SP to logout and send a LogoutRequest - saml_sessions = us.get_provider_data('saml2') - session = saml_sessions.get_next_logout() - if session: + logout_order = [ + lasso.SAML2_METADATA_BINDING_SOAP, + lasso.SAML2_METADATA_BINDING_REDIRECT, + ] + (logout_mech, session) = saml_sessions.get_next_logout( + logout_mechs=logout_order) + while session: self.debug('Going to log out %s' % session.provider_id) try: - logout.setSessionFromDump(session.session.dump()) + logout.setSessionFromDump(session.login_session) except lasso.ProfileBadSessionDumpError as e: self.error('Failed to load session: %s' % e) raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e) - - logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT) + if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT: + logout.initRequest(session.provider_id, + lasso.HTTP_METHOD_REDIRECT) + else: + logout.initRequest(session.provider_id, + lasso.HTTP_METHOD_SOAP) try: logout.buildRequestMsg() @@ -225,38 +285,63 @@ class LogoutRequest(ProviderPageBase): raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e) - # Now set the full list of session indexes to log out + # Set the full list of session indexes for this provider to + # log out + self.debug('logging out provider id %s' % session.provider_id) + indexes = saml_sessions.get_session_id_by_provider_id( + session.provider_id + ) + self.debug('Requesting logout for sessions %s' % (indexes,)) req = logout.get_request() - req.setSessionIndexes(tuple(set(session.session_indexes))) + req.setSessionIndexes(indexes) - session.set_logoutstate(logout.msgUrl, logout.request.id, None) - us.save_provider_data('saml2', saml_sessions) + session.set_logoutstate(relaystate=logout.msgUrl, + request_id=logout.request.id) + saml_sessions.start_logout(session, initial=False) self.debug('Request logout ID %s for session ID %s' % (logout.request.id, session.session_id)) - self.debug('Redirecting to another SP to logout on %s at %s' % - (logout.remoteProviderId, logout.msgUrl)) - - raise cherrypy.HTTPRedirect(logout.msgUrl) - - # Otherwise we're done, respond to the original request using the - # response we cached earlier. - saml_sessions = us.get_provider_data('saml2') - if saml_sessions is None or saml_sessions.count() == 0: - self.error('Logout attempt without being loggged in.') - raise cherrypy.HTTPError(400, 'Not logged in') + if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT: + self.debug('Redirecting to another SP to logout on %s at %s' % + (logout.remoteProviderId, logout.msgUrl)) + raise cherrypy.HTTPRedirect(logout.msgUrl) + else: + self.debug('SOAP request to another SP to logout on %s at %s' % + (logout.remoteProviderId, logout.msgUrl)) + if logout.msgBody: + message = self._soap_logout(logout) + try: + self._handle_logout_response(us, + logout, + saml_sessions, + message, + samlresponse) + except Exception as e: # pylint: disable=broad-except + self.error('SOAP SLO failed %s' % e) + else: + self.error('Provider does not support SOAP') + + (logout_mech, session) = saml_sessions.get_next_logout( + logout_mechs=logout_order) + + # done while + + # All sessions should be logged out now. Respond to the + # original request using the response we cached earlier. try: - session = saml_sessions.get_last_session() + session = saml_sessions.get_initial_logout() except ValueError: self.debug('SLO get_last_session() unable to find last session') raise cherrypy.HTTPError(400, 'Unable to determine logout state') - redirect = session.logoutstate.get('relaystate') + redirect = session.relaystate if not redirect: redirect = self.basepath + saml_sessions.remove_session(session) + # Log out of cherrypy session user = us.get_user() self._audit('Logged out user: %s [%s] from %s' %