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 class SAMLSession(Log):
18 uuidval - Unique ID stored in the database
19 session_id - ID of the login session
20 provider_id - ID of the SP
21 user - the login name of the user that owns the session
22 login_session - the Login session object
23 logoutstate - an integer constant representing where in the
24 logout process this request is
25 relaystate - where the user will be redirected when logout is
27 request_id - the logout request ID if initiated from IdP. The
28 logout response will include an InResponseTo value
30 logout_request - the Logout request object
31 expiration_time - the time the login session expires
33 def __init__(self, uuidval, session_id, provider_id, user,
34 login_session, logoutstate=None, relaystate=None,
35 logout_request=None, request_id=None,
36 expiration_time=None):
38 self.uuidval = uuidval
39 self.session_id = session_id
40 self.provider_id = provider_id
42 self.login_session = login_session
43 self.logoutstate = logoutstate
44 self.relaystate = relaystate
45 self.request_id = request_id
46 self.logout_request = logout_request
47 self.expiration_time = expiration_time
49 def set_logoutstate(self, relaystate=None, request=None, request_id=None):
51 Update attributes needed to determine the state of the session for
54 The database is not updated when these are set. It is expected that
55 this is called prior to start_logout()
58 self.relaystate = relaystate
60 self.logout_request = request
62 self.request_id = request_id
65 self.debug('session_id %s' % self.session_id)
66 self.debug('provider_id %s' % self.provider_id)
67 self.debug('login session %s' % self.login_session)
68 self.debug('logoutstate %s' % self.logoutstate)
72 Convert this object into something suitable to store in the
76 data['session_id'] = self.session_id
77 data['provider_id'] = self.provider_id
78 data['user'] = self.user
79 data['login_session'] = self.login_session
80 data['logoutstate'] = self.logoutstate
81 data['relaystate'] = self.relaystate
82 data['logout_request'] = self.logout_request
83 data['request_id'] = self.request_id
84 data['expiration_time'] = self.expiration_time
86 return {self.uuidval: data}
89 class SAMLSessionFactory(Log):
91 Access SAML session information.
93 The sessions are stored via the data backend.
95 When a user logs in, add_session() is called and a new SAMLSession
96 created and added to the table.
98 When a user logs out, the next login session is found and moved to
99 sessions_logging_out. remove_session() will look in both when trying
102 Returns a SAMLSession object representing the new session.
104 def __init__(self, database_url):
105 self._ss = SAML2SessionStore(database_url=database_url)
108 def _data_to_samlsession(self, uuidval, data):
110 Convert data from the data backend to a SAMLSession object.
112 return SAMLSession(uuidval,
113 data.get('session_id'),
114 data.get('provider_id'),
116 data.get('login_session'),
117 data.get('logoutstate'),
118 data.get('relaystate'),
119 data.get('logout_request'),
120 data.get('request_id'),
121 data.get('expiration_time'))
123 def add_session(self, session_id, provider_id, user, login_session,
126 Add a new login session to the table.
130 timeout = cherrypy_config['tools.sessions.timeout']
131 t = datetime.timedelta(seconds=timeout * 60)
132 expiration_time = datetime.datetime.now() + t
134 data = {'session_id': session_id,
135 'provider_id': provider_id,
137 'login_session': login_session,
138 'logoutstate': LOGGED_IN,
139 'expiration_time': expiration_time}
141 data['request_id'] = request_id
143 uuidval = self._ss.new_session(data)
145 return SAMLSession(uuidval, session_id, provider_id, user,
146 login_session, LOGGED_IN,
147 request_id=request_id,
148 expiration_time=expiration_time)
150 def get_session_by_id(self, session_id):
152 Retrieve a session by session ID
154 uuidval, data = self._ss.get_session(session_id=session_id)
158 return self._data_to_samlsession(uuidval, data)
160 def get_session_id_by_provider_id(self, provider_id):
162 Return a tuple of logged-in session IDs by provider_id
164 candidates = self._ss.get_user_sessions(self.user)
169 if c[key].get('provider_id') == provider_id:
170 samlsession = self._data_to_samlsession(key, c[key])
171 session_ids.append(samlsession.session_id.encode('utf-8'))
173 return tuple(session_ids)
175 def get_session_by_request_id(self, request_id):
177 Retrieve a session by logout request ID
179 uuidval, data = self._ss.get_session(request_id=request_id)
183 return self._data_to_samlsession(uuidval, data)
185 def remove_session(self, samlsession):
186 return self._ss.remove_session(samlsession.uuidval)
188 def remove_session_by_session_id(self, session_id):
189 session = self.get_session_by_id(session_id)
190 return self._ss.remove_session(session.uuidval)
192 def start_logout(self, samlsession, relaystate=None, initial=True):
194 Move a session into the logging_out state
196 samlsession: the SAMLSession object to start logging out
197 relaystate: URL to redirect user to when logout is completed
198 initial: boolean to indicate if this session started logout.
199 Only the initial session's relaystate is used.
204 samlsession.logoutstate = INIT_LOGOUT
206 samlsession.logoutstate = LOGGING_OUT
208 samlsession.relaystate = relaystate
209 datum = samlsession.convert()
210 self._ss.update_session(datum)
212 def get_next_logout(self, peek=False):
214 Get the next session in the logged-in state and move
215 it to the logging_out state. Return the session that is
218 :param peek: for IdP-initiated logout we can't remove the
219 session otherwise when the request comes back
220 in the user won't be seen as being logged-on.
222 Return None if no more sessions in LOGGED_IN state.
224 candidates = self._ss.get_user_sessions(self.user)
228 if int(c[key].get('logoutstate', 0)) == LOGGED_IN:
229 samlsession = self._data_to_samlsession(key, c[key])
230 self.start_logout(samlsession, initial=False)
234 def get_initial_logout(self):
236 Get the initial logout request.
238 Return None if no sessions in INIT_LOGOUT state.
240 candidates = self._ss.get_user_sessions(self.user)
242 # FIXME: what does it mean if there are multiple in init? We
243 # just return the first one for now. How do we know
244 # it's the "right" one if multiple logouts are started
245 # at the same time from different SPs?
248 if int(c[key].get('logoutstate', 0)) == INIT_LOGOUT:
249 samlsession = self._data_to_samlsession(key, c[key])
258 Dump all sessions to debug log
260 candidates = self._ss.get_user_sessions(self.user)
265 samlsession = self._data_to_samlsession(key, c[key])
266 self.debug('session %d: %s' % (count, samlsession.convert()))
269 if __name__ == '__main__':
270 provider1 = "http://127.0.0.10/saml2"
271 provider2 = "http://127.0.0.11/saml2"
273 # temporary values to simulate cherrypy
274 cherrypy_config['tools.sessions.timeout'] = 60
276 factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')
279 sess1 = factory.add_session('_123456', provider1, "admin", "<Login/>")
280 sess2 = factory.add_session('_789012', provider2, "testuser", "<Login/>")
282 # Test finding sessions by provider
283 ids = factory.get_session_id_by_provider_id(provider2)
284 assert(len(ids) == 1)
286 sess3 = factory.add_session('_345678', provider2, "testuser", "<Login/>")
287 ids = factory.get_session_id_by_provider_id(provider2)
288 assert(len(ids) == 2)
290 # Test finding sessions by session ID
291 test1 = factory.get_session_by_id('_123456')
292 assert(test1.user == 'admin')
293 assert(test1.provider_id == provider1)
295 # Log out and remove the first session
296 test1.set_logoutstate('http://www.example.com/idp')
297 factory.start_logout(test1, initial=True)
298 test1 = factory.get_session_by_id('_123456')
299 assert(test1.relaystate == 'http://www.example.com/idp')
301 factory.remove_session_by_session_id('_123456')
303 # Make sure it is gone from the db
304 test1 = factory.get_session_by_id('_123456')
305 assert(test1 is None)
307 test2 = factory.get_session_by_id('_789012')
308 factory.start_logout(test2, initial=True)
310 test3 = factory.get_next_logout()
311 assert(test3.session_id == '_345678')
313 test4 = factory.get_initial_logout()
314 assert(test4.session_id == '_789012')
316 # Even though we've started logout, make sure we can still find
317 # all sessions for a provider.
318 ids = factory.get_session_id_by_provider_id(provider2)
319 assert(len(ids) == 2)