Update Copyright header point to COPYING file
[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.sessions import SAMLSessionsContainer
6 from ipsilon.providers.saml2.auth import UnknownProvider
7 from ipsilon.util.user import UserSession
8 import cherrypy
9 import lasso
10
11
12 class LogoutRequest(ProviderPageBase):
13     """
14     SP-initiated logout.
15
16     The sequence is:
17       - On each logout a new session is created to represent that
18         provider
19       - Initial logout request is verified and stored in the login
20         session
21       - If there are other sessions then one is chosen that is not
22         the current provider and a logoutRequest is sent
23       - When a logoutResponse is received the session is removed
24       - When all other sessions but the initial one have been
25         logged out then it a final logoutResponse is sent and the
26         session removed. At this point the cherrypy session is
27         deleted.
28     """
29
30     def __init__(self, *args, **kwargs):
31         super(LogoutRequest, self).__init__(*args, **kwargs)
32
33     def _handle_logout_request(self, us, logout, saml_sessions, message):
34         self.debug('Logout request')
35
36         try:
37             logout.processRequestMsg(message)
38         except (lasso.ServerProviderNotFoundError,
39                 lasso.ProfileUnknownProviderError) as e:
40             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
41                                                  e, message)
42             self.error(msg)
43             raise UnknownProvider(msg)
44         except (lasso.ProfileInvalidProtocolprofileError,
45                 lasso.DsError), e:
46             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
47                                                           e, message)
48             self.error(msg)
49             raise InvalidRequest(msg)
50         except lasso.Error, e:
51             self.error('SLO unknown error: %s' % message)
52             raise cherrypy.HTTPError(400, 'Invalid logout request')
53
54         # TODO: verify that the session index is in the request
55         session_indexes = logout.request.sessionIndexes
56         self.debug('SLO from %s with %s sessions' %
57                    (logout.remoteProviderId, session_indexes))
58
59         session = saml_sessions.find_session_by_provider(
60             logout.remoteProviderId)
61         if session:
62             try:
63                 logout.setSessionFromDump(session.session.dump())
64             except lasso.ProfileBadSessionDumpError as e:
65                 self.error('loading session failed: %s' % e)
66                 raise cherrypy.HTTPError(400, 'Invalid logout session')
67         else:
68             return self._not_logged_in(logout, message)
69
70         try:
71             logout.validateRequest()
72         except lasso.ProfileSessionNotFoundError, e:
73             self.error('Logout failed. No sessions for %s' %
74                        logout.remoteProviderId)
75             return self._not_logged_in(logout, message)
76         except lasso.LogoutUnsupportedProfileError:
77             self.error('Logout failed. Unsupported profile %s' %
78                        logout.remoteProviderId)
79             raise cherrypy.HTTPError(400, 'Profile does not support logout')
80         except lasso.Error, e:
81             self.error('SLO validation failed: %s' % e)
82             raise cherrypy.HTTPError(400, 'Failed to validate logout request')
83
84         try:
85             logout.buildResponseMsg()
86         except lasso.ProfileUnsupportedProfileError:
87             self.error('Unsupported profile for %s' % logout.remoteProviderId)
88             raise cherrypy.HTTPError(400, 'Profile does not support logout')
89         except lasso.Error, e:
90             self.error('SLO failed to build logout response: %s' % e)
91
92         session.set_logoutstate(logout.msgUrl, logout.request.id,
93                                 message)
94         saml_sessions.start_logout(session)
95
96         us.save_provider_data('saml2', saml_sessions)
97
98         return
99
100     def _handle_logout_response(self, us, logout, saml_sessions, message,
101                                 samlresponse):
102
103         self.debug('Logout response')
104
105         try:
106             logout.processResponseMsg(message)
107         except getattr(lasso, 'ProfileRequestDeniedError',
108                        lasso.LogoutRequestDeniedError):
109             self.error('Logout request denied by %s' %
110                        logout.remoteProviderId)
111             # Fall through to next provider
112         except (lasso.ProfileInvalidMsgError,
113                 lasso.LogoutPartialLogoutError) as e:
114             self.error('Logout request from %s failed: %s' %
115                        (logout.remoteProviderId, e))
116         else:
117             self.debug('Processing SLO Response from %s' %
118                        logout.remoteProviderId)
119
120             self.debug('SLO response to request id %s' %
121                        logout.response.inResponseTo)
122
123             saml_sessions = us.get_provider_data('saml2')
124             if saml_sessions is None:
125                 # TODO: return logged out instead
126                 saml_sessions = SAMLSessionsContainer()
127
128             # TODO: need to log out each SessionIndex?
129             session = saml_sessions.find_session_by_provider(
130                 logout.remoteProviderId)
131
132             if session is not None:
133                 self.debug('Logout response session logout id is: %s' %
134                            session.session_id)
135                 saml_sessions.remove_session_by_provider(
136                     logout.remoteProviderId)
137                 us.save_provider_data('saml2', saml_sessions)
138                 user = us.get_user()
139                 self._audit('Logged out user: %s [%s] from %s' %
140                             (user.name, user.fullname,
141                              logout.remoteProviderId))
142             else:
143                 return self._not_logged_in(logout, message)
144
145         return
146
147     def _not_logged_in(self, logout, message):
148         """
149         The user requested a logout but isn't logged in, or we can't
150         find a session for the user. Try to be nice and redirect them
151         back to the RelayState in the logout request.
152
153         We are only nice in the case of a valid logout request. If the
154         request is invalid (not signed, unknown SP, etc) then an
155         exception is raised.
156         """
157         self.error('Logout attempt without being logged in.')
158
159         if logout.msgRelayState is not None:
160             raise cherrypy.HTTPRedirect(logout.msgRelayState)
161
162         try:
163             logout.processRequestMsg(message)
164         except (lasso.ServerProviderNotFoundError,
165                 lasso.ProfileUnknownProviderError) as e:
166             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
167                                                  e, message)
168             self.error(msg)
169             raise UnknownProvider(msg)
170         except (lasso.ProfileInvalidProtocolprofileError,
171                 lasso.DsError), e:
172             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
173                                                           e, message)
174             self.error(msg)
175             raise InvalidRequest(msg)
176         except lasso.Error, e:
177             self.error('SLO unknown error: %s' % message)
178             raise cherrypy.HTTPError(400, 'Invalid logout request')
179
180         if logout.msgRelayState:
181             raise cherrypy.HTTPRedirect(logout.msgRelayState)
182         else:
183             raise cherrypy.HTTPError(400, 'Not logged in')
184
185     def logout(self, message, relaystate=None, samlresponse=None):
186         """
187         Handle HTTP Redirect logout. This is an asynchronous logout
188         request process that relies on the HTTP agent to forward
189         logout requests to any other SP's that are also logged in.
190
191         The basic process is this:
192          1. A logout request is received. It is processed and the response
193             cached.
194          2. If any other SP's have also logged in as this user then the
195             first such session is popped off and a logout request is
196             generated and forwarded to the SP.
197          3. If a logout response is received then the user is marked
198             as logged out from that SP.
199          Repeat steps 2-3 until only the initial logout request is
200          left unhandled, at which time the pre-generated response is sent
201          back to the SP that originated the logout request.
202         """
203         logout = self.cfg.idp.get_logout_handler()
204
205         us = UserSession()
206
207         saml_sessions = us.get_provider_data('saml2')
208         if saml_sessions is None:
209             # No sessions means nothing to log out
210             return self._not_logged_in(logout, message)
211
212         self.debug('%d sessions loaded' % saml_sessions.count())
213         saml_sessions.dump()
214
215         if lasso.SAML2_FIELD_REQUEST in message:
216             self._handle_logout_request(us, logout, saml_sessions, message)
217         elif samlresponse:
218             self._handle_logout_response(us, logout, saml_sessions, message,
219                                          samlresponse)
220         else:
221             raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
222                                         'request or response.')
223
224         # Fall through to handle any remaining sessions.
225
226         # Find the next SP to logout and send a LogoutRequest
227         saml_sessions = us.get_provider_data('saml2')
228         session = saml_sessions.get_next_logout()
229         if session:
230             self.debug('Going to log out %s' % session.provider_id)
231
232             try:
233                 logout.setSessionFromDump(session.session.dump())
234             except lasso.ProfileBadSessionDumpError as e:
235                 self.error('Failed to load session: %s' % e)
236                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
237                                             % e)
238
239             logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
240
241             try:
242                 logout.buildRequestMsg()
243             except lasso.Error, e:
244                 self.error('failure to build logout request msg: %s' % e)
245                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
246                                             % e)
247
248             # Now set the full list of session indexes to log out
249             req = logout.get_request()
250             req.setSessionIndexes(tuple(set(session.session_indexes)))
251
252             session.set_logoutstate(logout.msgUrl, logout.request.id, None)
253             us.save_provider_data('saml2', saml_sessions)
254
255             self.debug('Request logout ID %s for session ID %s' %
256                        (logout.request.id, session.session_id))
257             self.debug('Redirecting to another SP to logout on %s at %s' %
258                        (logout.remoteProviderId, logout.msgUrl))
259
260             raise cherrypy.HTTPRedirect(logout.msgUrl)
261
262         # Otherwise we're done, respond to the original request using the
263         # response we cached earlier.
264
265         saml_sessions = us.get_provider_data('saml2')
266         if saml_sessions is None or saml_sessions.count() == 0:
267             return self._not_logged_in(logout, message)
268
269         try:
270             session = saml_sessions.get_last_session()
271         except ValueError:
272             self.debug('SLO get_last_session() unable to find last session')
273             raise cherrypy.HTTPError(400, 'Unable to determine logout state')
274
275         redirect = session.logoutstate.get('relaystate')
276         if not redirect:
277             redirect = self.basepath
278
279         # Log out of cherrypy session
280         user = us.get_user()
281         self._audit('Logged out user: %s [%s] from %s' %
282                     (user.name, user.fullname,
283                      session.provider_id))
284         us.logout(user)
285
286         self.debug('SLO redirect to %s' % redirect)
287
288         raise cherrypy.HTTPRedirect(redirect)