Fix incorrect raise exception syntax
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / logout.py
1 # Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
2
3 from ipsilon.providers.common import ProviderPageBase
4 from ipsilon.providers.common import InvalidRequest
5 from ipsilon.providers.saml2.auth import UnknownProvider
6 from ipsilon.util.user import UserSession
7 from ipsilon.util.constants import SOAP_MEDIA_TYPE
8 import cherrypy
9 import lasso
10 import requests
11
12
13 class LogoutRequest(ProviderPageBase):
14     """
15     SP-initiated logout.
16
17     The sequence is:
18       - On each logout a new session is created to represent that
19         provider
20       - Initial logout request is verified and stored in the login
21         session
22       - If there are other sessions then one is chosen that is not
23         the current provider and a logoutRequest is sent
24       - When a logoutResponse is received the session is removed
25       - When all other sessions but the initial one have been
26         logged out then it a final logoutResponse is sent and the
27         session removed. At this point the cherrypy session is
28         deleted.
29     """
30
31     def __init__(self, *args, **kwargs):
32         super(LogoutRequest, self).__init__(*args, **kwargs)
33
34     def _handle_logout_request(self, us, logout, saml_sessions, message):
35         self.debug('Logout request')
36
37         try:
38             logout.processRequestMsg(message)
39         except (lasso.ServerProviderNotFoundError,
40                 lasso.ProfileUnknownProviderError) as e:
41             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
42                                                  e, message)
43             self.error(msg)
44             raise UnknownProvider(msg)
45         except (lasso.ProfileInvalidProtocolprofileError,
46                 lasso.DsError), e:
47             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
48                                                           e, message)
49             self.error(msg)
50             raise InvalidRequest(msg)
51         except lasso.Error, e:
52             self.error('SLO unknown error: %s' % message)
53             raise cherrypy.HTTPError(400, 'Invalid logout request')
54
55         session_indexes = logout.request.sessionIndexes
56         self.debug('SLO from %s with %s sessions' %
57                    (logout.remoteProviderId, session_indexes))
58
59         # Find the first session being asked to log out. Later we loop over
60         # all the session indexes and mark them as logging out but only one
61         # is needed to handle the request.
62         if len(session_indexes) < 1:
63             self.error('SLO empty session Indexes')
64             raise cherrypy.HTTPError(400, 'Invalid logout request')
65         session = saml_sessions.get_session_by_id(session_indexes[0])
66         if session:
67             try:
68                 logout.setSessionFromDump(session.login_session)
69             except lasso.ProfileBadSessionDumpError as e:
70                 self.error('loading session failed: %s' % e)
71                 raise cherrypy.HTTPError(400, 'Invalid logout session')
72         else:
73             return self._not_logged_in(logout, message)
74
75         try:
76             logout.validateRequest()
77         except lasso.ProfileSessionNotFoundError, e:
78             self.error('Logout failed. No sessions for %s' %
79                        logout.remoteProviderId)
80             return self._not_logged_in(logout, message)
81         except lasso.LogoutUnsupportedProfileError:
82             self.error('Logout failed. Unsupported profile %s' %
83                        logout.remoteProviderId)
84             raise cherrypy.HTTPError(400, 'Profile does not support logout')
85         except lasso.Error, e:
86             self.error('SLO validation failed: %s' % e)
87             raise cherrypy.HTTPError(400, 'Failed to validate logout request')
88
89         try:
90             logout.buildResponseMsg()
91         except lasso.ProfileUnsupportedProfileError:
92             self.error('Unsupported profile for %s' % logout.remoteProviderId)
93             raise cherrypy.HTTPError(400, 'Profile does not support logout')
94         except lasso.Error, e:
95             self.error('SLO failed to build logout response: %s' % e)
96
97         for ind in session_indexes:
98             session = saml_sessions.get_session_by_id(ind)
99             if session:
100                 session.set_logoutstate(relaystate=logout.msgUrl,
101                                         request=message)
102                 saml_sessions.start_logout(session)
103             else:
104                 self.error('SLO request to log out non-existent session: %s' %
105                            ind)
106
107         return
108
109     def _handle_logout_response(self, us, logout, saml_sessions, message,
110                                 samlresponse):
111
112         self.debug('Logout response')
113
114         try:
115             logout.processResponseMsg(message)
116         except getattr(lasso, 'ProfileRequestDeniedError',
117                        lasso.LogoutRequestDeniedError):
118             self.error('Logout request denied by %s' %
119                        logout.remoteProviderId)
120             # Fall through to next provider
121         except (lasso.ProfileInvalidMsgError,
122                 lasso.LogoutPartialLogoutError) as e:
123             self.error('Logout request from %s failed: %s' %
124                        (logout.remoteProviderId, e))
125         else:
126             self.debug('Processing SLO Response from %s' %
127                        logout.remoteProviderId)
128
129             self.debug('SLO response to request id %s' %
130                        logout.response.inResponseTo)
131
132             session = saml_sessions.get_session_by_request_id(
133                 logout.response.inResponseTo)
134
135             if session is not None:
136                 self.debug('Logout response session logout id is: %s' %
137                            session.session_id)
138                 saml_sessions.remove_session(session)
139                 user = us.get_user()
140                 self._audit('Logged out user: %s [%s] from %s' %
141                             (user.name, user.fullname,
142                              logout.remoteProviderId))
143             else:
144                 return self._not_logged_in(logout, message)
145
146         return
147
148     def _not_logged_in(self, logout, message):
149         """
150         The user requested a logout but isn't logged in, or we can't
151         find a session for the user. Try to be nice and redirect them
152         back to the RelayState in the logout request.
153
154         We are only nice in the case of a valid logout request. If the
155         request is invalid (not signed, unknown SP, etc) then an
156         exception is raised.
157         """
158         self.error('Logout attempt without being logged in.')
159
160         if logout.msgRelayState is not None:
161             raise cherrypy.HTTPRedirect(logout.msgRelayState)
162
163         try:
164             logout.processRequestMsg(message)
165         except (lasso.ServerProviderNotFoundError,
166                 lasso.ProfileUnknownProviderError) as e:
167             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
168                                                  e, message)
169             self.error(msg)
170             raise UnknownProvider(msg)
171         except (lasso.ProfileInvalidProtocolprofileError,
172                 lasso.DsError), e:
173             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
174                                                           e, message)
175             self.error(msg)
176             raise InvalidRequest(msg)
177         except lasso.Error, e:
178             self.error('SLO unknown error: %s' % message)
179             raise cherrypy.HTTPError(400, 'Invalid logout request')
180
181         if logout.msgRelayState:
182             raise cherrypy.HTTPRedirect(logout.msgRelayState)
183         else:
184             raise cherrypy.HTTPError(400, 'Not logged in')
185
186     def _soap_logout(self, logout):
187         """
188         Send a SOAP logout request over HTTP and return the result.
189         """
190         headers = {'Content-Type': SOAP_MEDIA_TYPE}
191         try:
192             response = requests.post(logout.msgUrl, data=logout.msgBody,
193                                      headers=headers)
194         except Exception as e:  # pylint: disable=broad-except
195             self.error('SOAP HTTP request failed: (%s) (on %s)' %
196                        (e, logout.msgUrl))
197             raise
198
199         if response.status_code != 200:
200             self.error('SOAP error (%s) (on %s)' %
201                        (response.status, logout.msgUrl))
202             raise InvalidRequest('SOAP HTTP error code %s' %
203                                  response.status_code)
204
205         if not response.text:
206             self.error('Empty SOAP response')
207             raise InvalidRequest('No content in SOAP response')
208
209         return response.text
210
211     def logout(self, message, relaystate=None, samlresponse=None):
212         """
213         Handle HTTP logout. The supported logout methods are stored
214         in each session. First all the SOAP sessions are logged out
215         then the HTTP Redirect method is used for any remaining
216         sessions.
217
218         The basic process is this:
219          1. A logout request is received. It is processed and the response
220             cached.
221          2. If any other SP's have also logged in as this user then the
222             first such session is popped off and a logout request is
223             generated and forwarded to the SP.
224          3. If a logout response is received then the user is marked
225             as logged out from that SP.
226          Repeat steps 2-3 until only the initial logout request is
227          left unhandled, at which time the pre-generated response is sent
228          back to the SP that originated the logout request.
229
230         The final logout response is always a redirect.
231         """
232         logout = self.cfg.idp.get_logout_handler()
233
234         us = UserSession()
235
236         saml_sessions = self.cfg.idp.sessionfactory
237
238         if lasso.SAML2_FIELD_REQUEST in message:
239             self._handle_logout_request(us, logout, saml_sessions, message)
240         elif samlresponse:
241             self._handle_logout_response(us, logout, saml_sessions, message,
242                                          samlresponse)
243         else:
244             raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
245                                         'request or response.')
246
247         # Fall through to handle any remaining sessions.
248
249         # Find the next SP to logout and send a LogoutRequest
250         logout_order = [
251             lasso.SAML2_METADATA_BINDING_SOAP,
252             lasso.SAML2_METADATA_BINDING_REDIRECT,
253         ]
254         (logout_mech, session) = saml_sessions.get_next_logout(
255             logout_mechs=logout_order)
256         while session:
257             self.debug('Going to log out %s' % session.provider_id)
258
259             try:
260                 logout.setSessionFromDump(session.login_session)
261             except lasso.ProfileBadSessionDumpError as e:
262                 self.error('Failed to load session: %s' % e)
263                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
264                                             % e)
265             if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
266                 logout.initRequest(session.provider_id,
267                                    lasso.HTTP_METHOD_REDIRECT)
268             else:
269                 logout.initRequest(session.provider_id,
270                                    lasso.HTTP_METHOD_SOAP)
271
272             try:
273                 logout.buildRequestMsg()
274             except lasso.Error, e:
275                 self.error('failure to build logout request msg: %s' % e)
276                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
277                                             % e)
278
279             # Set the full list of session indexes for this provider to
280             # log out
281             self.debug('logging out provider id %s' % session.provider_id)
282             indexes = saml_sessions.get_session_id_by_provider_id(
283                 session.provider_id
284             )
285             self.debug('Requesting logout for sessions %s' % (indexes,))
286             req = logout.get_request()
287             req.setSessionIndexes(indexes)
288
289             session.set_logoutstate(relaystate=logout.msgUrl,
290                                     request_id=logout.request.id)
291             saml_sessions.start_logout(session, initial=False)
292
293             self.debug('Request logout ID %s for session ID %s' %
294                        (logout.request.id, session.session_id))
295
296             if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
297                 self.debug('Redirecting to another SP to logout on %s at %s' %
298                            (logout.remoteProviderId, logout.msgUrl))
299                 raise cherrypy.HTTPRedirect(logout.msgUrl)
300             else:
301                 self.debug('SOAP request to another SP to logout on %s at %s' %
302                            (logout.remoteProviderId, logout.msgUrl))
303                 if logout.msgBody:
304                     message = self._soap_logout(logout)
305                     try:
306                         self._handle_logout_response(us,
307                                                      logout,
308                                                      saml_sessions,
309                                                      message,
310                                                      samlresponse)
311                     except Exception as e:  # pylint: disable=broad-except
312                         self.error('SOAP SLO failed %s' % e)
313                 else:
314                     self.error('Provider does not support SOAP')
315
316             (logout_mech, session) = saml_sessions.get_next_logout(
317                 logout_mechs=logout_order)
318
319         # done while
320
321         # All sessions should be logged out now. Respond to the
322         # original request using the response we cached earlier.
323
324         try:
325             session = saml_sessions.get_initial_logout()
326         except ValueError:
327             self.debug('SLO get_last_session() unable to find last session')
328             raise cherrypy.HTTPError(400, 'Unable to determine logout state')
329
330         redirect = session.relaystate
331         if not redirect:
332             redirect = self.basepath
333
334         saml_sessions.remove_session(session)
335
336         # Log out of cherrypy session
337         user = us.get_user()
338         self._audit('Logged out user: %s [%s] from %s' %
339                     (user.name, user.fullname,
340                      session.provider_id))
341         us.logout(user)
342
343         self.debug('SLO redirect to %s' % redirect)
344
345         raise cherrypy.HTTPRedirect(redirect)