1 # copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """web session: by default the session is actually the db connection """ |
|
19 __docformat__ = "restructuredtext en" |
|
20 |
|
21 from time import time |
|
22 from logging import getLogger |
|
23 |
|
24 from logilab.common.registry import RegistrableObject, yes |
|
25 |
|
26 from cubicweb import RepositoryError, Unauthorized, set_log_methods |
|
27 from cubicweb.web import InvalidSession |
|
28 |
|
29 from cubicweb.web.views import authentication |
|
30 |
|
31 |
|
32 class AbstractSessionManager(RegistrableObject): |
|
33 """manage session data associated to a session identifier""" |
|
34 __abstract__ = True |
|
35 __select__ = yes() |
|
36 __registry__ = 'sessions' |
|
37 __regid__ = 'sessionmanager' |
|
38 |
|
39 def __init__(self, repo): |
|
40 vreg = repo.vreg |
|
41 self.session_time = vreg.config['http-session-time'] or None |
|
42 self.authmanager = authentication.RepositoryAuthenticationManager(repo) |
|
43 interval = (self.session_time or 0) / 2. |
|
44 if vreg.config.anonymous_user()[0] is not None: |
|
45 self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60 |
|
46 assert self.cleanup_anon_session_time > 0 |
|
47 if self.session_time is not None: |
|
48 self.cleanup_anon_session_time = min(self.session_time, |
|
49 self.cleanup_anon_session_time) |
|
50 interval = self.cleanup_anon_session_time / 2. |
|
51 # we don't want to check session more than once every 5 minutes |
|
52 self.clean_sessions_interval = max(5 * 60, interval) |
|
53 |
|
54 def clean_sessions(self): |
|
55 """cleanup sessions which has not been unused since a given amount of |
|
56 time. Return the number of sessions which have been closed. |
|
57 """ |
|
58 self.debug('cleaning http sessions') |
|
59 session_time = self.session_time |
|
60 closed, total = 0, 0 |
|
61 for session in self.current_sessions(): |
|
62 total += 1 |
|
63 last_usage_time = session.mtime |
|
64 no_use_time = (time() - last_usage_time) |
|
65 if session.anonymous_session: |
|
66 if no_use_time >= self.cleanup_anon_session_time: |
|
67 self.close_session(session) |
|
68 closed += 1 |
|
69 elif session_time is not None and no_use_time >= session_time: |
|
70 self.close_session(session) |
|
71 closed += 1 |
|
72 return closed, total - closed |
|
73 |
|
74 def current_sessions(self): |
|
75 """return currently open sessions""" |
|
76 raise NotImplementedError() |
|
77 |
|
78 def get_session(self, req, sessionid): |
|
79 """return existing session for the given session identifier""" |
|
80 raise NotImplementedError() |
|
81 |
|
82 def open_session(self, req): |
|
83 """open and return a new session for the given request. |
|
84 |
|
85 raise :exc:`cubicweb.AuthenticationError` if authentication failed |
|
86 (no authentication info found or wrong user/password) |
|
87 """ |
|
88 raise NotImplementedError() |
|
89 |
|
90 def close_session(self, session): |
|
91 """close session on logout or on invalid session detected (expired out, |
|
92 corrupted...) |
|
93 """ |
|
94 raise NotImplementedError() |
|
95 |
|
96 |
|
97 set_log_methods(AbstractSessionManager, getLogger('cubicweb.sessionmanager')) |
|
98 |
|
99 |
|
100 class InMemoryRepositorySessionManager(AbstractSessionManager): |
|
101 """manage session data associated to a session identifier""" |
|
102 |
|
103 def __init__(self, *args, **kwargs): |
|
104 super(InMemoryRepositorySessionManager, self).__init__(*args, **kwargs) |
|
105 # XXX require a RepositoryAuthenticationManager which violates |
|
106 # authenticate interface by returning a session instead of a user |
|
107 #assert isinstance(self.authmanager, RepositoryAuthenticationManager) |
|
108 self._sessions = {} |
|
109 |
|
110 # dump_data / restore_data to avoid loosing open sessions on registry |
|
111 # reloading |
|
112 def dump_data(self): |
|
113 return self._sessions |
|
114 def restore_data(self, data): |
|
115 self._sessions = data |
|
116 |
|
117 def current_sessions(self): |
|
118 return self._sessions.values() |
|
119 |
|
120 def get_session(self, req, sessionid): |
|
121 """return existing session for the given session identifier""" |
|
122 if sessionid not in self._sessions: |
|
123 raise InvalidSession() |
|
124 session = self._sessions[sessionid] |
|
125 try: |
|
126 user = self.authmanager.validate_session(req, session) |
|
127 except InvalidSession: |
|
128 self.close_session(session) |
|
129 raise |
|
130 if session.closed: |
|
131 self.close_session(session) |
|
132 raise InvalidSession() |
|
133 return session |
|
134 |
|
135 def open_session(self, req): |
|
136 """open and return a new session for the given request. The session is |
|
137 also bound to the request. |
|
138 |
|
139 raise :exc:`cubicweb.AuthenticationError` if authentication failed |
|
140 (no authentication info found or wrong user/password) |
|
141 """ |
|
142 session, login = self.authmanager.authenticate(req) |
|
143 self._sessions[session.sessionid] = session |
|
144 session.mtime = time() |
|
145 return session |
|
146 |
|
147 def postlogin(self, req, session): |
|
148 """postlogin: the user have been related to a session |
|
149 |
|
150 Both req and session are passed to this function because actually |
|
151 linking the request to the session is not yet done and not the |
|
152 responsability of this object. |
|
153 """ |
|
154 # Update last connection date |
|
155 # XXX: this should be in a post login hook in the repository, but there |
|
156 # we can't differentiate actual login of automatic session |
|
157 # reopening. Is it actually a problem? |
|
158 if 'last_login_time' in req.vreg.schema: |
|
159 self._update_last_login_time(session) |
|
160 req.set_message(req._('welcome %s!') % session.user.login) |
|
161 |
|
162 def _update_last_login_time(self, session): |
|
163 # XXX should properly detect missing permission / non writeable source |
|
164 # and avoid "except (RepositoryError, Unauthorized)" below |
|
165 try: |
|
166 with session.new_cnx() as cnx: |
|
167 cnx.execute('SET X last_login_time NOW WHERE X eid %(x)s', |
|
168 {'x' : session.user.eid}) |
|
169 cnx.commit() |
|
170 except (RepositoryError, Unauthorized): |
|
171 pass |
|
172 |
|
173 def close_session(self, session): |
|
174 """close session on logout or on invalid session detected (expired out, |
|
175 corrupted...) |
|
176 """ |
|
177 self.info('closing http session %s' % session.sessionid) |
|
178 self._sessions.pop(session.sessionid, None) |
|
179 if not session.closed: |
|
180 session.repo.close(session.sessionid) |
|