1 # Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
3 from cherrypy import config as cherrypy_config
4 from ipsilon.util.log import Log
5 from ipsilon.util.data import SAML2SessionStore
14 def expire_sessions():
16 Find all expired sessions and remove them. This is executed as a
17 background cherrypy task.
19 ss = SAML2SessionStore()
21 now = datetime.datetime.now()
24 exp = r.get('expiration_time', None)
26 exp = datetime.datetime.strptime(exp, '%Y-%m-%d %H:%M:%S.%f')
28 ss.remove_session(idval)
31 class SAMLSession(Log):
35 uuidval - Unique ID stored in the database
36 session_id - ID of the login session
37 provider_id - ID of the SP
38 user - the login name of the user that owns the session
39 login_session - the Login session object
40 logoutstate - an integer constant representing where in the
41 logout process this request is
42 relaystate - where the user will be redirected when logout is
44 request_id - the logout request ID if initiated from IdP. The
45 logout response will include an InResponseTo value
47 logout_request - the Logout request object
48 expiration_time - the time the login session expires
50 def __init__(self, uuidval, session_id, provider_id, user,
51 login_session, logoutstate=None, relaystate=None,
52 logout_request=None, request_id=None,
53 expiration_time=None):
55 self.uuidval = uuidval
56 self.session_id = session_id
57 self.provider_id = provider_id
59 self.login_session = login_session
60 self.logoutstate = logoutstate
61 self.relaystate = relaystate
62 self.request_id = request_id
63 self.logout_request = logout_request
64 self.expiration_time = expiration_time
66 def set_logoutstate(self, relaystate=None, request=None, request_id=None):
68 Update attributes needed to determine the state of the session for
71 The database is not updated when these are set. It is expected that
72 this is called prior to start_logout()
75 self.relaystate = relaystate
77 self.logout_request = request
79 self.request_id = request_id
82 self.debug('session_id %s' % self.session_id)
83 self.debug('provider_id %s' % self.provider_id)
84 self.debug('login session %s' % self.login_session)
85 self.debug('logoutstate %s' % self.logoutstate)
89 Convert this object into something suitable to store in the
93 data['session_id'] = self.session_id
94 data['provider_id'] = self.provider_id
95 data['user'] = self.user
96 data['login_session'] = self.login_session
97 data['logoutstate'] = self.logoutstate
98 data['relaystate'] = self.relaystate
99 data['logout_request'] = self.logout_request
100 data['request_id'] = self.request_id
101 data['expiration_time'] = self.expiration_time
103 return {self.uuidval: data}
106 class SAMLSessionFactory(Log):
108 Access SAML session information.
110 The sessions are stored via the data backend.
112 When a user logs in, add_session() is called and a new SAMLSession
113 created and added to the table.
115 When a user logs out, the next login session is found and moved to
116 sessions_logging_out. remove_session() will look in both when trying
119 Returns a SAMLSession object representing the new session.
122 self._ss = SAML2SessionStore()
125 def _data_to_samlsession(self, uuidval, data):
127 Convert data from the data backend to a SAMLSession object.
129 return SAMLSession(uuidval,
130 data.get('session_id'),
131 data.get('provider_id'),
133 data.get('login_session'),
134 data.get('logoutstate'),
135 data.get('relaystate'),
136 data.get('logout_request'),
137 data.get('request_id'),
138 data.get('expiration_time'))
140 def add_session(self, session_id, provider_id, user, login_session,
143 Add a new login session to the table.
147 timeout = cherrypy_config['tools.sessions.timeout']
148 t = datetime.timedelta(seconds=timeout * 60)
149 expiration_time = datetime.datetime.now() + t
151 data = {'session_id': session_id,
152 'provider_id': provider_id,
154 'login_session': login_session,
155 'logoutstate': LOGGED_IN,
156 'expiration_time': expiration_time}
158 data['request_id'] = request_id
160 uuidval = self._ss.new_session(data)
162 return SAMLSession(uuidval, session_id, provider_id, user,
163 login_session, LOGGED_IN,
164 request_id=request_id,
165 expiration_time=expiration_time)
167 def get_session_by_id(self, session_id):
169 Retrieve a session by session ID
171 uuidval, data = self._ss.get_session(session_id=session_id)
175 return self._data_to_samlsession(uuidval, data)
177 def get_session_id_by_provider_id(self, provider_id):
179 Return a tuple of logged-in session IDs by provider_id
181 candidates = self._ss.get_user_sessions(self.user)
186 if c[key].get('provider_id') == provider_id:
187 samlsession = self._data_to_samlsession(key, c[key])
188 session_ids.append(samlsession.session_id.encode('utf-8'))
190 return tuple(session_ids)
192 def get_session_by_request_id(self, request_id):
194 Retrieve a session by logout request ID
196 uuidval, data = self._ss.get_session(request_id=request_id)
200 return self._data_to_samlsession(uuidval, data)
202 def remove_session(self, samlsession):
203 return self._ss.remove_session(samlsession.uuidval)
205 def remove_session_by_session_id(self, session_id):
206 session = self.get_session_by_id(session_id)
207 return self._ss.remove_session(session.uuidval)
209 def start_logout(self, samlsession, relaystate=None, initial=True):
211 Move a session into the logging_out state
213 samlsession: the SAMLSession object to start logging out
214 relaystate: URL to redirect user to when logout is completed
215 initial: boolean to indicate if this session started logout.
216 Only the initial session's relaystate is used.
221 samlsession.logoutstate = INIT_LOGOUT
223 samlsession.logoutstate = LOGGING_OUT
225 samlsession.relaystate = relaystate
226 datum = samlsession.convert()
227 self._ss.update_session(datum)
229 def get_next_logout(self, peek=False):
231 Get the next session in the logged-in state and move
232 it to the logging_out state. Return the session that is
235 :param peek: for IdP-initiated logout we can't remove the
236 session otherwise when the request comes back
237 in the user won't be seen as being logged-on.
239 Return None if no more sessions in LOGGED_IN state.
241 candidates = self._ss.get_user_sessions(self.user)
245 if int(c[key].get('logoutstate', 0)) == LOGGED_IN:
246 samlsession = self._data_to_samlsession(key, c[key])
247 self.start_logout(samlsession, initial=False)
251 def get_initial_logout(self):
253 Get the initial logout request.
255 Return None if no sessions in INIT_LOGOUT state.
257 candidates = self._ss.get_user_sessions(self.user)
259 # FIXME: what does it mean if there are multiple in init? We
260 # just return the first one for now. How do we know
261 # it's the "right" one if multiple logouts are started
262 # at the same time from different SPs?
265 if int(c[key].get('logoutstate', 0)) == INIT_LOGOUT:
266 samlsession = self._data_to_samlsession(key, c[key])
275 Dump all sessions to debug log
277 candidates = self._ss.get_user_sessions(self.user)
282 samlsession = self._data_to_samlsession(key, c[key])
283 self.debug('session %d: %s' % (count, samlsession.convert()))
286 if __name__ == '__main__':
287 provider1 = "http://127.0.0.10/saml2"
288 provider2 = "http://127.0.0.11/saml2"
290 # temporary values to simulate cherrypy
291 cherrypy_config['saml2.sessions.db'] = '/tmp/saml2sessions.sqlite'
292 cherrypy_config['tools.sessions.timeout'] = 60
294 factory = SAMLSessionFactory()
297 sess1 = factory.add_session('_123456', provider1, "admin", "<Login/>")
298 sess2 = factory.add_session('_789012', provider2, "testuser", "<Login/>")
300 # Test finding sessions by provider
301 ids = factory.get_session_id_by_provider_id(provider2)
302 assert(len(ids) == 1)
304 sess3 = factory.add_session('_345678', provider2, "testuser", "<Login/>")
305 ids = factory.get_session_id_by_provider_id(provider2)
306 assert(len(ids) == 2)
308 # Test finding sessions by session ID
309 test1 = factory.get_session_by_id('_123456')
310 assert(test1.user == 'admin')
311 assert(test1.provider_id == provider1)
313 # Log out and remove the first session
314 test1.set_logoutstate('http://www.example.com/idp')
315 factory.start_logout(test1, initial=True)
316 test1 = factory.get_session_by_id('_123456')
317 assert(test1.relaystate == 'http://www.example.com/idp')
319 factory.remove_session_by_session_id('_123456')
321 # Make sure it is gone from the db
322 test1 = factory.get_session_by_id('_123456')
323 assert(test1 is None)
325 test2 = factory.get_session_by_id('_789012')
326 factory.start_logout(test2, initial=True)
328 test3 = factory.get_next_logout()
329 assert(test3.session_id == '_345678')
331 test4 = factory.get_initial_logout()
332 assert(test4.session_id == '_789012')
334 # Even though we've started logout, make sure we can still find
335 # all sessions for a provider.
336 ids = factory.get_session_id_by_provider_id(provider2)
337 assert(len(ids) == 2)