Fix incorrect raise exception syntax
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / logout.py
index bfb5d0d..75c7869 100644 (file)
@@ -1,27 +1,13 @@
-# Copyright (C) 2015  Rob Crittenden <rcritten@redhat.com>
-#
-# 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 <http://www.gnu.org/licenses/>.
+# 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):
@@ -66,16 +52,20 @@ class LogoutRequest(ProviderPageBase):
             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')
@@ -104,11 +94,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
 
@@ -135,21 +129,13 @@ 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,
@@ -197,11 +183,37 @@ class LogoutRequest(ProviderPageBase):
         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, 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
@@ -214,18 +226,14 @@ 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
-            return self._not_logged_in(logout, message)
-
-        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)
@@ -239,19 +247,27 @@ class LogoutRequest(ProviderPageBase):
         # 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()
@@ -260,37 +276,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:
-            return self._not_logged_in(logout, message)
+            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' %