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