[web session] fix session handling so we get a chance to have for instance the 'forgotpwd' feature working on a site where anonymous are not allowed

fix several pbs: * we need a session id and a session cookie anyway, else subsequent http queries are unrelated * this imply some changes in the session attribution workflow for session without a cnx * some views/selectors must be fixed for cases where session has no cnx

On the way, avoid unnecessary Redirect on successful login.

closes #750543

authorSylvain Th?nault <sylvain.thenault@logilab.fr>
changeset5338d895b891
branchstable
phasepublic
hiddenno
parent revision#254bc099db1a [test] update test broken by previous commit
child revision#ef5165fa99e0 [ui messages] make application message component works when request has no cnx set and support for explicit message given through render argument, #20ef21926774 backport stable
files modified by this revision
dbapi.py
selectors.py
web/application.py
web/views/basecomponents.py
web/views/basetemplates.py
web/views/debug.py
web/views/sessions.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1306313923 -7200
# Wed May 25 10:58:43 2011 +0200
# Branch stable
# Node ID 5338d895b891d3c73996135a187db9b6a9e7023f
# Parent 254bc099db1a5397c1fab0e3ef2aaa2b99703714
[web session] fix session handling so we get a chance to have for instance the 'forgotpwd' feature working on a site where anonymous are not allowed

fix several pbs:
* we need a session id and a session cookie anyway, else subsequent http queries are unrelated
* this imply some changes in the session attribution workflow for session without a cnx
* some views/selectors must be fixed for cases where session has no cnx

On the way, avoid unnecessary Redirect on successful login.

closes #750543

diff --git a/dbapi.py b/dbapi.py
@@ -28,10 +28,11 @@
1  from logging import getLogger
2  from time import time, clock
3  from itertools import count
4  from warnings import warn
5  from os.path import join
6 +from uuid import uuid4
7 
8  from logilab.common.logging_ext import set_log_methods
9  from logilab.common.decorators import monkeypatch
10  from logilab.common.deprecation import deprecated
11 
@@ -244,11 +245,11 @@
12          # identifier, but may later differ in case of auto-reconnection as done
13          # by the web authentication manager (in cw.web.views.authentication)
14          if cnx is not None:
15              self.sessionid = cnx.sessionid
16          else:
17 -            self.sessionid = None
18 +            self.sessionid = uuid4().hex
19 
20      @property
21      def anonymous_session(self):
22          return not self.cnx or self.cnx.anonymous_connection
23 
diff --git a/selectors.py b/selectors.py
@@ -1340,10 +1340,12 @@
24      * else check all entities in `col` (default to 0) are owned by the user
25      """
26 
27      @lltrace
28      def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
29 +        if not req.cnx:
30 +            return 0
31          user = req.user
32          if user is None:
33              return int('guests' in self.expected)
34          score = user.matching_groups(self.expected)
35          if not score and 'owners' in self.expected and rset:
diff --git a/web/application.py b/web/application.py
@@ -1,6 +1,6 @@
36 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
37 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
38  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
39  #
40  # This file is part of CubicWeb.
41  #
42  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -202,21 +202,38 @@
43              try:
44                  session = self.get_session(req, sessionid)
45              except InvalidSession:
46                  # try to open a new session, so we get an anonymous session if
47                  # allowed
48 -                try:
49 -                    session = self.open_session(req)
50 -                except AuthenticationError:
51 -                    req.remove_cookie(cookie, sessioncookie)
52 -                    raise
53 +                session = self.open_session(req)
54 +            else:
55 +                if not session.cnx:
56 +                    # session exists but is not bound to a connection. We should
57 +                    # try to authenticate
58 +                    loginsucceed = False
59 +                    try:
60 +                        if self.open_session(req, allow_no_cnx=False):
61 +                            loginsucceed = True
62 +                    except Redirect:
63 +                        # may be raised in open_session (by postlogin mechanism)
64 +                        # on successful connection
65 +                        loginsucceed = True
66 +                        raise
67 +                    except AuthenticationError:
68 +                        # authentication failed, continue to use this session
69 +                        req.set_session(session)
70 +                    finally:
71 +                        if loginsucceed:
72 +                            # session should be replaced by new session created
73 +                            # in open_session
74 +                            self.session_manager.close_session(session)
75 
76      def get_session(self, req, sessionid):
77          return self.session_manager.get_session(req, sessionid)
78 
79 -    def open_session(self, req):
80 -        session = self.session_manager.open_session(req)
81 +    def open_session(self, req, allow_no_cnx=True):
82 +        session = self.session_manager.open_session(req, allow_no_cnx=allow_no_cnx)
83          cookie = req.get_cookie()
84          sessioncookie = self.session_cookie(req)
85          cookie[sessioncookie] = session.sessionid
86          if req.https and req.base_url().startswith('https://'):
87              cookie[sessioncookie]['secure'] = True
@@ -277,14 +294,11 @@
88      def connect(self, req):
89          """return a connection for a logged user object according to existing
90          sessions (i.e. a new connection may be created or an already existing
91          one may be reused
92          """
93 -        try:
94 -            self.session_handler.set_session(req)
95 -        except AuthenticationError:
96 -            req.set_session(DBAPISession(None))
97 +        self.session_handler.set_session(req)
98 
99      # publish methods #########################################################
100 
101      def log_publish(self, path, req):
102          """wrapper around _publish to log all queries executed for a given
@@ -363,15 +377,16 @@
103                  raise
104              except Redirect:
105                  # redirect is raised by edit controller when everything went fine,
106                  # so try to commit
107                  try:
108 -                    txuuid = req.cnx.commit()
109 -                    if txuuid is not None:
110 -                        msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
111 -                            req.build_url('undo', txuuid=txuuid), req._('undo'))
112 -                        req.append_to_redirect_message(msg)
113 +                    if req.cnx:
114 +                        txuuid = req.cnx.commit()
115 +                        if txuuid is not None:
116 +                            msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
117 +                                req.build_url('undo', txuuid=txuuid), req._('undo'))
118 +                            req.append_to_redirect_message(msg)
119                  except ValidationError, ex:
120                      self.validation_error_handler(req, ex)
121                  except Unauthorized, ex:
122                      req.data['errmsg'] = req._('You\'re not authorized to access this page. '
123                                                 'If you think you should, please contact the site administrator.')
diff --git a/web/views/basecomponents.py b/web/views/basecomponents.py
@@ -27,12 +27,12 @@
124 
125  from logilab.mtconverter import xml_escape
126  from logilab.common.deprecation import class_renamed
127  from rql import parse
128 
129 -from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params,
130 -                                match_context, configuration_values,
131 +from cubicweb.selectors import (yes, no_cnx, match_form_params, match_context,
132 +                                multi_etypes_rset, configuration_values,
133                                  anonymous_user, authenticated_user)
134  from cubicweb.schema import display_name
135  from cubicweb.utils import wrap_on_write
136  from cubicweb.uilib import toggle_action
137  from cubicweb.web import component, uicfg
@@ -86,10 +86,11 @@
138 
139 
140  class ApplLogo(HeaderComponent):
141      """build the instance logo, usually displayed in the header"""
142      __regid__ = 'logo'
143 +    __select__ = yes() # no need for a cnx
144      order = -1
145 
146      def render(self, w):
147          w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
148            % (self._cw.base_url(), self._cw.uiprops['LOGO']))
@@ -148,20 +149,20 @@
149  AnonUserLink.__select__ &= yes(1)
150 
151 
152  class AnonUserStatusLink(HeaderComponent):
153      __regid__ = 'userstatus'
154 -    __select__ = HeaderComponent.__select__ & anonymous_user()
155 +    __select__ = anonymous_user()
156      context = _('header-right')
157      order = HeaderComponent.order - 10
158 
159      def render(self, w):
160          w(u'<span class="caption">%s</span>' % self._cw._('anonymous'))
161 
162 
163  class AuthenticatedUserStatus(AnonUserStatusLink):
164 -    __select__ = HeaderComponent.__select__ & authenticated_user()
165 +    __select__ = authenticated_user()
166 
167      def render(self, w):
168          # display useractions and siteactions
169          actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset)
170          box = MenuWidget('', 'userActionsBox', _class='', islist=False)
@@ -178,11 +179,11 @@
171 
172  class ApplicationMessage(component.Component):
173      """display messages given using the __message parameter into a special div
174      section
175      """
176 -    __select__ = yes()
177 +    __select__ = ~no_cnx()
178      __regid__ = 'applmessages'
179      # don't want user to hide this component using an cwproperty
180      cw_property_defs = {}
181 
182      def call(self):
diff --git a/web/views/basetemplates.py b/web/views/basetemplates.py
@@ -1,6 +1,6 @@
183 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
184 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
185  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
186  #
187  # This file is part of CubicWeb.
188  #
189  # CubicWeb is free software: you can redistribute it and/or modify it under the
diff --git a/web/views/debug.py b/web/views/debug.py
@@ -1,6 +1,6 @@
190 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
191 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
192  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
193  #
194  # This file is part of CubicWeb.
195  #
196  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -131,10 +131,13 @@
197              sessions = SESSION_MANAGER.current_sessions()
198              w(u'<h3>%s</h3>' % _('opened web sessions'))
199              if sessions:
200                  w(u'<ul>')
201                  for session in sessions:
202 +                    if not session.cnx:
203 +                        w(u'<li>%s (NO CNX)</li>' % session.sessionid)
204 +                        continue
205                      try:
206                          last_usage_time = session.cnx.check()
207                      except BadConnectionId:
208                          w(u'<li>%s (INVALID)</li>' % session.sessionid)
209                          continue
diff --git a/web/views/sessions.py b/web/views/sessions.py
@@ -1,6 +1,6 @@
210 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
211 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
212  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
213  #
214  # This file is part of CubicWeb.
215  #
216  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -19,11 +19,11 @@
217  object :/
218  """
219 
220  __docformat__ = "restructuredtext en"
221 
222 -from cubicweb import RepositoryError, Unauthorized
223 +from cubicweb import RepositoryError, Unauthorized, AuthenticationError
224  from cubicweb.web import InvalidSession, Redirect
225  from cubicweb.web.application import AbstractSessionManager
226  from cubicweb.dbapi import DBAPISession
227 
228 
@@ -47,32 +47,40 @@
229      def current_sessions(self):
230          return self._sessions.values()
231 
232      def get_session(self, req, sessionid):
233          """return existing session for the given session identifier"""
234 -        if not sessionid in self._sessions:
235 +        if sessionid not in self._sessions:
236              raise InvalidSession()
237          session = self._sessions[sessionid]
238 -        try:
239 -            user = self.authmanager.validate_session(req, session)
240 -        except InvalidSession:
241 -            # invalid session
242 -            self.close_session(session)
243 -            raise
244 -        # associate the connection to the current request
245 -        req.set_session(session, user)
246 +        if session.cnx:
247 +            try:
248 +                user = self.authmanager.validate_session(req, session)
249 +            except InvalidSession:
250 +                # invalid session
251 +                self.close_session(session)
252 +                raise
253 +            # associate the connection to the current request
254 +            req.set_session(session, user)
255          return session
256 
257 -    def open_session(self, req):
258 +    def open_session(self, req, allow_no_cnx=True):
259          """open and return a new session for the given request. The session is
260          also bound to the request.
261 
262          raise :exc:`cubicweb.AuthenticationError` if authentication failed
263          (no authentication info found or wrong user/password)
264          """
265 -        cnx, login = self.authmanager.authenticate(req)
266 -        session = DBAPISession(cnx, login)
267 +        try:
268 +            cnx, login = self.authmanager.authenticate(req)
269 +        except AuthenticationError:
270 +            if allow_no_cnx:
271 +                session = DBAPISession(None)
272 +            else:
273 +                raise
274 +        else:
275 +            session = DBAPISession(cnx, login)
276          self._sessions[session.sessionid] = session
277          # associate the connection to the current request
278          req.set_session(session)
279          return session
280 
@@ -87,19 +95,20 @@
281          if 'last_login_time' in req.vreg.schema:
282              self._update_last_login_time(req)
283          args = req.form
284          for forminternal_key in ('__form_id', '__domid', '__errorurl'):
285              args.pop(forminternal_key, None)
286 -        args['__message'] = req._('welcome %s !') % req.user.login
287 -        if 'vid' in req.form:
288 -            args['vid'] = req.form['vid']
289 -        if 'rql' in req.form:
290 -            args['rql'] = req.form['rql']
291          path = req.relative_path(False)
292          if path == 'login':
293              path = 'view'
294 -        raise Redirect(req.build_url(path, **args))
295 +            args['__message'] = req._('welcome %s !') % req.user.login
296 +            if 'vid' in req.form:
297 +                args['vid'] = req.form['vid']
298 +            if 'rql' in req.form:
299 +                args['rql'] = req.form['rql']
300 +            raise Redirect(req.build_url(path, **args))
301 +        req.set_message(req._('welcome %s !') % req.user.login)
302 
303      def _update_last_login_time(self, req):
304          # XXX should properly detect missing permission / non writeable source
305          # and avoid "except (RepositoryError, Unauthorized)" below
306          if req.user.cw_metainformation()['source']['type'] == 'ldapuser':
@@ -118,12 +127,13 @@
307          """close session on logout or on invalid session detected (expired out,
308          corrupted...)
309          """
310          self.info('closing http session %s' % session.sessionid)
311          del self._sessions[session.sessionid]
312 -        try:
313 -            session.cnx.close()
314 -        except:
315 -            # already closed, may occurs if the repository session expired but
316 -            # not the web session
317 -            pass
318 -        session.cnx = None
319 +        if session.cnx:
320 +            try:
321 +                session.cnx.close()
322 +            except:
323 +                # already closed, may occur if the repository session expired
324 +                # but not the web session
325 +                pass
326 +            session.cnx = None