#773448: refactor session and 'no connection' handling, by introducing proper web session. We should now be able to see page even when no anon is configured, and be redirected to the login form as soon as one tries to do a query.
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 13 Apr 2010 12:19:24 +0200
changeset 5223 6abd6e3599f4
parent 5216 4f4369e63f5e
child 5224 34e669b6fd95
#773448: refactor session and 'no connection' handling, by introducing proper web session. We should now be able to see page even when no anon is configured, and be redirected to the login form as soon as one tries to do a query.
_exceptions.py
dbapi.py
devtools/testlib.py
etwist/server.py
hooks/test/unittest_bookmarks.py
selectors.py
test/unittest_dbapi.py
web/_exceptions.py
web/application.py
web/box.py
web/captcha.py
web/form.py
web/httpcache.py
web/request.py
web/test/unittest_application.py
web/test/unittest_views_basecontrollers.py
web/views/authentication.py
web/views/autoform.py
web/views/basecontrollers.py
web/views/basetemplates.py
web/views/editviews.py
web/views/sessions.py
wsgi/handler.py
--- a/_exceptions.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/_exceptions.py	Tue Apr 13 12:19:24 2010 +0200
@@ -52,9 +52,6 @@
     """raised when when an attempt to establish a connection failed do to wrong
     connection information (login / password or other authentication token)
     """
-    def __init__(self, *args, **kwargs):
-        super(AuthenticationError, self).__init__(*args)
-        self.__dict__.update(kwargs)
 
 class BadConnectionId(ConnectionError):
     """raised when a bad connection id is given"""
--- a/dbapi.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/dbapi.py	Tue Apr 13 12:19:24 2010 +0200
@@ -20,7 +20,8 @@
 from logilab.common.decorators import monkeypatch
 from logilab.common.deprecation import deprecated
 
-from cubicweb import ETYPE_NAME_MAP, ConnectionError, cwvreg, cwconfig
+from cubicweb import ETYPE_NAME_MAP, ConnectionError, AuthenticationError,\
+     cwvreg, cwconfig
 from cubicweb.req import RequestSessionBase
 
 
@@ -156,9 +157,25 @@
     return repo, cnx
 
 
+class DBAPISession(object):
+    def __init__(self, cnx, login=None, authinfo=None):
+        self.cnx = cnx
+        self.data = {}
+        self.login = login
+        self.authinfo = authinfo
+
+    @property
+    def anonymous_session(self):
+        return self.cnx is None or self.cnx.anonymous_connection
+
+    @property
+    def sessionid(self):
+        return self.cnx.sessionid
+
+
 class DBAPIRequest(RequestSessionBase):
 
-    def __init__(self, vreg, cnx=None):
+    def __init__(self, vreg, session=None):
         super(DBAPIRequest, self).__init__(vreg)
         try:
             # no vreg or config which doesn't handle translations
@@ -168,12 +185,12 @@
         self.set_default_language(vreg)
         # cache entities built during the request
         self._eid_cache = {}
-        # these args are initialized after a connection is
-        # established
-        self.cnx = None   # connection associated to the request
-        self._user = None # request's user, set at authentication
-        if cnx is not None:
-            self.set_connection(cnx)
+        if session is not None:
+            self.set_session(session)
+        else:
+            # these args are initialized after a connection is
+            # established
+            self.session = self.cnx = self._user = None
 
     def base_url(self):
         return self.vreg.config['base-url']
@@ -181,14 +198,22 @@
     def from_controller(self):
         return 'view'
 
-    def set_connection(self, cnx, user=None):
+    def set_session(self, session, user=None):
         """method called by the session handler when the user is authenticated
         or an anonymous connection is open
         """
-        self.cnx = cnx
-        self.cursor = cnx.cursor(self)
+        self.session = session
+        if session.cnx is not None:
+            self.cnx = session.cnx
+            self.execute = session.cnx.cursor(self).execute
         self.set_user(user)
 
+    def execute(self, *args, **kwargs):
+        """overriden when session is set. By default raise authentication error
+        so authentication is requested.
+        """
+        raise AuthenticationError()
+
     def set_default_language(self, vreg):
         try:
             self.lang = vreg.property_value('ui.language')
@@ -205,14 +230,6 @@
             self.pgettext = lambda x, y: y
         self.debug('request default language: %s', self.lang)
 
-    def describe(self, eid):
-        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
-        return self.cnx.describe(eid)
-
-    def source_defs(self):
-        """return the definition of sources used by the repository."""
-        return self.cnx.source_defs()
-
     # entities cache management ###############################################
 
     def entity_cache(self, eid):
@@ -232,26 +249,10 @@
 
     # low level session data management #######################################
 
-    def session_data(self):
-        """return a dictionnary containing session data"""
-        return self.cnx.session_data()
-
-    def get_session_data(self, key, default=None, pop=False):
-        """return value associated to `key` in session data"""
+    def get_shared_data(self, key, default=None, pop=False):
+        """return value associated to `key` in shared data"""
         if self.cnx is None:
             return default # before the connection has been established
-        return self.cnx.get_session_data(key, default, pop)
-
-    def set_session_data(self, key, value):
-        """set value associated to `key` in session data"""
-        return self.cnx.set_session_data(key, value)
-
-    def del_session_data(self, key):
-        """remove value associated to `key` in session data"""
-        return self.cnx.del_session_data(key)
-
-    def get_shared_data(self, key, default=None, pop=False):
-        """return value associated to `key` in shared data"""
         return self.cnx.get_shared_data(key, default, pop)
 
     def set_shared_data(self, key, value, querydata=False):
@@ -266,10 +267,18 @@
 
     # server session compat layer #############################################
 
+    def describe(self, eid):
+        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
+        return self.cnx.describe(eid)
+
+    def source_defs(self):
+        """return the definition of sources used by the repository."""
+        return self.cnx.source_defs()
+
     def hijack_user(self, user):
         """return a fake request/session using specified user"""
         req = DBAPIRequest(self.vreg)
-        req.set_connection(self.cnx, user)
+        req.set_session(self.session, user)
         return req
 
     @property
@@ -283,11 +292,25 @@
         if user:
             self.set_entity_cache(user)
 
-    def execute(self, rql, args=None, eid_key=None, build_descr=True):
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
-        return self.cursor.execute(rql, args, build_descr=build_descr)
+    @deprecated('[3.8] use direct access to req.session.data dictionary')
+    def session_data(self):
+        """return a dictionnary containing session data"""
+        return self.session.data
+
+    @deprecated('[3.8] use direct access to req.session.data dictionary')
+    def get_session_data(self, key, default=None, pop=False):
+        if pop:
+            return self.session.data.pop(key, default)
+        return self.session.data.get(key, default)
+
+    @deprecated('[3.8] use direct access to req.session.data dictionary')
+    def set_session_data(self, key, value):
+        self.session.data[key] = value
+
+    @deprecated('[3.8] use direct access to req.session.data dictionary')
+    def del_session_data(self, key):
+        self.session.data.pop(key, None)
+
 
 set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi'))
 
@@ -302,68 +325,104 @@
     etc.
     """
 
-# module level objects ########################################################
+
+# cursor / connection objects ##################################################
+
+class Cursor(object):
+    """These objects represent a database cursor, which is used to manage the
+    context of a fetch operation. Cursors created from the same connection are
+    not isolated, i.e., any changes done to the database by a cursor are
+    immediately visible by the other cursors. Cursors created from different
+    connections are isolated.
+    """
+
+    def __init__(self, connection, repo, req=None):
+        """This read-only attribute return a reference to the Connection
+        object on which the cursor was created.
+        """
+        self.connection = connection
+        """optionnal issuing request instance"""
+        self.req = req
+        self._repo = repo
+        self._sessid = connection.sessionid
+
+    def close(self):
+        """no effect"""
+        pass
+
+    def execute(self, rql, args=None, eid_key=None, build_descr=True):
+        """execute a rql query, return resulting rows and their description in
+        a :class:`~cubicweb.rset.ResultSet` object
+
+        * `rql` should be an Unicode string or a plain ASCII string, containing
+          the rql query
+
+        * `args` the optional args dictionary associated to the query, with key
+          matching named substitution in `rql`
+
+        * `build_descr` is a boolean flag indicating if the description should
+          be built on select queries (if false, the description will be en empty
+          list)
+
+        on INSERT queries, there will be one row for each inserted entity,
+        containing its eid
+
+        on SET queries, XXX describe
+
+        DELETE queries returns no result.
+
+        .. Note::
+          to maximize the rql parsing/analyzing cache performance, you should
+          always use substitute arguments in queries, i.e. avoid query such as::
+
+            execute('Any X WHERE X eid 123')
+
+          use::
+
+            execute('Any X WHERE X eid %(x)s', {'x': 123})
+        """
+        if eid_key is not None:
+            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
+                 DeprecationWarning, stacklevel=2)
+        rset = self._repo.execute(self._sessid, rql, args, build_descr)
+        rset.req = self.req
+        return rset
 
 
-apilevel = '2.0'
-
-"""Integer constant stating the level of thread safety the interface supports.
-Possible values are:
-
-                0     Threads may not share the module.
-                1     Threads may share the module, but not connections.
-                2     Threads may share the module and connections.
-                3     Threads may share the module, connections and
-                      cursors.
-
-Sharing in the above context means that two threads may use a resource without
-wrapping it using a mutex semaphore to implement resource locking. Note that
-you cannot always make external resources thread safe by managing access using
-a mutex: the resource may rely on global variables or other external sources
-that are beyond your control.
-"""
-threadsafety = 1
+class LogCursor(Cursor):
+    """override the standard cursor to log executed queries"""
 
-"""String constant stating the type of parameter marker formatting expected by
-the interface. Possible values are :
+    def execute(self, operation, parameters=None, eid_key=None, build_descr=True):
+        """override the standard cursor to log executed queries"""
+        if eid_key is not None:
+            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
+                 DeprecationWarning, stacklevel=2)
+        tstart, cstart = time(), clock()
+        rset = Cursor.execute(self, operation, parameters, build_descr=build_descr)
+        self.connection.executed_queries.append((operation, parameters,
+                                                 time() - tstart, clock() - cstart))
+        return rset
 
-                'qmark'         Question mark style,
-                                e.g. '...WHERE name=?'
-                'numeric'       Numeric, positional style,
-                                e.g. '...WHERE name=:1'
-                'named'         Named style,
-                                e.g. '...WHERE name=:name'
-                'format'        ANSI C printf format codes,
-                                e.g. '...WHERE name=%s'
-                'pyformat'      Python extended format codes,
-                                e.g. '...WHERE name=%(name)s'
-"""
-paramstyle = 'pyformat'
-
-
-# connection object ###########################################################
 
 class Connection(object):
     """DB-API 2.0 compatible Connection object for CubicWeb
     """
     # make exceptions available through the connection object
     ProgrammingError = ProgrammingError
+    # attributes that may be overriden per connection instance
+    anonymous_connection = False
+    cursor_class = Cursor
+    vreg = None
+    _closed = None
 
     def __init__(self, repo, cnxid, cnxprops=None):
         self._repo = repo
         self.sessionid = cnxid
         self._close_on_del = getattr(cnxprops, 'close_on_del', True)
         self._cnxtype = getattr(cnxprops, 'cnxtype', 'pyro')
-        self._closed = None
         if cnxprops and cnxprops.log_queries:
             self.executed_queries = []
             self.cursor_class = LogCursor
-        else:
-            self.cursor_class = Cursor
-        self.anonymous_connection = False
-        self.vreg = None
-        # session's data
-        self.data = {}
 
     def __repr__(self):
         if self.anonymous_connection:
@@ -381,29 +440,7 @@
             return False #propagate the exception
 
     def request(self):
-        return DBAPIRequest(self.vreg, self)
-
-    def session_data(self):
-        """return a dictionnary containing session data"""
-        return self.data
-
-    def get_session_data(self, key, default=None, pop=False):
-        """return value associated to `key` in session data"""
-        if pop:
-            return self.data.pop(key, default)
-        else:
-            return self.data.get(key, default)
-
-    def set_session_data(self, key, value):
-        """set value associated to `key` in session data"""
-        self.data[key] = value
-
-    def del_session_data(self, key):
-        """remove value associated to `key` in session data"""
-        try:
-            del self.data[key]
-        except KeyError:
-            pass
+        return DBAPIRequest(self.vreg, DBAPISession(self))
 
     def check(self):
         """raise `BadConnectionId` if the connection is no more valid"""
@@ -477,8 +514,6 @@
             if self._repo.config.instance_hooks:
                 hm.register_hooks(config.load_hooks(self.vreg))
 
-    load_vobjects = deprecated()(load_appobjects)
-
     def use_web_compatible_requests(self, baseurl, sitetitle=None):
         """monkey patch DBAPIRequest to fake a cw.web.request, so you should
         able to call html views using rset from a simple dbapi connection.
@@ -662,220 +697,3 @@
         him).
         """
         return self._repo.undo_transaction(self.sessionid, txuuid)
-
-
-# cursor object ###############################################################
-
-class Cursor(object):
-    """These objects represent a database cursor, which is used to manage the
-    context of a fetch operation. Cursors created from the same connection are
-    not isolated, i.e., any changes done to the database by a cursor are
-    immediately visible by the other cursors. Cursors created from different
-    connections can or can not be isolated, depending on how the transaction
-    support is implemented (see also the connection's rollback() and commit()
-    methods.)
-    """
-
-    def __init__(self, connection, repo, req=None):
-        """This read-only attribute return a reference to the Connection
-        object on which the cursor was created.
-        """
-        self.connection = connection
-        """optionnal issuing request instance"""
-        self.req = req
-
-        """This read/write attribute specifies the number of rows to fetch at a
-        time with fetchmany(). It defaults to 1 meaning to fetch a single row
-        at a time.
-
-        Implementations must observe this value with respect to the fetchmany()
-        method, but are free to interact with the database a single row at a
-        time. It may also be used in the implementation of executemany().
-        """
-        self.arraysize = 1
-
-        self._repo = repo
-        self._sessid = connection.sessionid
-        self._res = None
-        self._closed = None
-        self._index = 0
-
-
-    def close(self):
-        """Close the cursor now (rather than whenever __del__ is called).  The
-        cursor will be unusable from this point forward; an Error (or subclass)
-        exception will be raised if any operation is attempted with the cursor.
-        """
-        self._closed = True
-
-
-    def execute(self, rql, args=None, eid_key=None, build_descr=True):
-        """execute a rql query, return resulting rows and their description in
-        a :class:`~cubicweb.rset.ResultSet` object
-
-        * `rql` should be an Unicode string or a plain ASCII string, containing
-          the rql query
-
-        * `args` the optional args dictionary associated to the query, with key
-          matching named substitution in `rql`
-
-        * `build_descr` is a boolean flag indicating if the description should
-          be built on select queries (if false, the description will be en empty
-          list)
-
-        on INSERT queries, there will be one row for each inserted entity,
-        containing its eid
-
-        on SET queries, XXX describe
-
-        DELETE queries returns no result.
-
-        .. Note::
-          to maximize the rql parsing/analyzing cache performance, you should
-          always use substitute arguments in queries, i.e. avoid query such as::
-
-            execute('Any X WHERE X eid 123')
-
-          use::
-
-            execute('Any X WHERE X eid %(x)s', {'x': 123})
-        """
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
-        self._res = rset = self._repo.execute(self._sessid, rql,
-                                              args, build_descr)
-        rset.req = self.req
-        self._index = 0
-        return rset
-
-
-    def executemany(self, operation, seq_of_parameters):
-        """Prepare a database operation (query or command) and then execute it
-        against all parameter sequences or mappings found in the sequence
-        seq_of_parameters.
-
-        Modules are free to implement this method using multiple calls to the
-        execute() method or by using array operations to have the database
-        process the sequence as a whole in one call.
-
-        Use of this method for an operation which produces one or more result
-        sets constitutes undefined behavior, and the implementation is
-        permitted (but not required) to raise an exception when it detects that
-        a result set has been created by an invocation of the operation.
-
-        The same comments as for execute() also apply accordingly to this
-        method.
-
-        Return values are not defined.
-        """
-        for parameters in seq_of_parameters:
-            self.execute(operation, parameters)
-            if self._res.rows is not None:
-                self._res = None
-                raise ProgrammingError('Operation returned a result set')
-
-
-    def fetchone(self):
-        """Fetch the next row of a query result set, returning a single
-        sequence, or None when no more data is available.
-
-        An Error (or subclass) exception is raised if the previous call to
-        execute*() did not produce any result set or no call was issued yet.
-        """
-        if self._res is None:
-            raise ProgrammingError('No result set')
-        row = self._res.rows[self._index]
-        self._index += 1
-        return row
-
-
-    def fetchmany(self, size=None):
-        """Fetch the next set of rows of a query result, returning a sequence
-        of sequences (e.g. a list of tuples). An empty sequence is returned
-        when no more rows are available.
-
-        The number of rows to fetch per call is specified by the parameter.  If
-        it is not given, the cursor's arraysize determines the number of rows
-        to be fetched. The method should try to fetch as many rows as indicated
-        by the size parameter. If this is not possible due to the specified
-        number of rows not being available, fewer rows may be returned.
-
-        An Error (or subclass) exception is raised if the previous call to
-        execute*() did not produce any result set or no call was issued yet.
-
-        Note there are performance considerations involved with the size
-        parameter.  For optimal performance, it is usually best to use the
-        arraysize attribute.  If the size parameter is used, then it is best
-        for it to retain the same value from one fetchmany() call to the next.
-        """
-        if self._res is None:
-            raise ProgrammingError('No result set')
-        if size is None:
-            size = self.arraysize
-        rows = self._res.rows[self._index:self._index + size]
-        self._index += size
-        return rows
-
-
-    def fetchall(self):
-        """Fetch all (remaining) rows of a query result, returning them as a
-        sequence of sequences (e.g. a list of tuples).  Note that the cursor's
-        arraysize attribute can affect the performance of this operation.
-
-        An Error (or subclass) exception is raised if the previous call to
-        execute*() did not produce any result set or no call was issued yet.
-        """
-        if self._res is None:
-            raise ProgrammingError('No result set')
-        if not self._res.rows:
-            return []
-        rows = self._res.rows[self._index:]
-        self._index = len(self._res)
-        return rows
-
-
-    def setinputsizes(self, sizes):
-        """This can be used before a call to execute*() to predefine memory
-        areas for the operation's parameters.
-
-        sizes is specified as a sequence -- one item for each input parameter.
-        The item should be a Type Object that corresponds to the input that
-        will be used, or it should be an integer specifying the maximum length
-        of a string parameter.  If the item is None, then no predefined memory
-        area will be reserved for that column (this is useful to avoid
-        predefined areas for large inputs).
-
-        This method would be used before the execute*() method is invoked.
-
-        Implementations are free to have this method do nothing and users are
-        free to not use it.
-        """
-        pass
-
-
-    def setoutputsize(self, size, column=None):
-        """Set a column buffer size for fetches of large columns (e.g. LONGs,
-        BLOBs, etc.).  The column is specified as an index into the result
-        sequence.  Not specifying the column will set the default size for all
-        large columns in the cursor.
-
-        This method would be used before the execute*() method is invoked.
-
-        Implementations are free to have this method do nothing and users are
-        free to not use it.
-        """
-        pass
-
-
-class LogCursor(Cursor):
-    """override the standard cursor to log executed queries"""
-
-    def execute(self, operation, parameters=None, eid_key=None, build_descr=True):
-        """override the standard cursor to log executed queries"""
-        tstart, cstart = time(), clock()
-        rset = Cursor.execute(self, operation, parameters, eid_key, build_descr)
-        self.connection.executed_queries.append((operation, parameters,
-                                                 time() - tstart, clock() - cstart))
-        return rset
-
--- a/devtools/testlib.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/devtools/testlib.py	Tue Apr 13 12:19:24 2010 +0200
@@ -30,7 +30,7 @@
 
 from cubicweb import ValidationError, NoSelectableObject, AuthenticationError
 from cubicweb import cwconfig, devtools, web, server
-from cubicweb.dbapi import repo_connect, ConnectionProperties, ProgrammingError
+from cubicweb.dbapi import ProgrammingError, DBAPISession, repo_connect
 from cubicweb.sobjects import notification
 from cubicweb.web import Redirect, application
 from cubicweb.server.session import security_enabled
@@ -214,11 +214,10 @@
         cls.init_config(cls.config)
         cls.repo.hm.call_hooks('server_startup', repo=cls.repo)
         cls.vreg = cls.repo.vreg
-        cls._orig_cnx = cls.cnx
+        cls.websession = DBAPISession(cls.cnx, cls.admlogin,
+                                      {'password': cls.admpassword})
+        cls._orig_cnx = (cls.cnx, cls.websession)
         cls.config.repository = lambda x=None: cls.repo
-        # necessary for authentication tests
-        cls.cnx.login = cls.admlogin
-        cls.cnx.authinfo = {'password': cls.admpassword}
 
     @classmethod
     def _refresh_repo(cls):
@@ -241,7 +240,7 @@
     @property
     def adminsession(self):
         """return current server side session (using default manager account)"""
-        return self.repo._sessions[self._orig_cnx.sessionid]
+        return self.repo._sessions[self._orig_cnx[0].sessionid]
 
     def set_option(self, optname, value):
         self.config.global_set_option(optname, value)
@@ -291,7 +290,7 @@
         if password is None:
             password = login.encode('utf8')
         if req is None:
-            req = self._orig_cnx.request()
+            req = self._orig_cnx[0].request()
         user = req.create_entity('CWUser', login=unicode(login),
                                  upassword=password, **kwargs)
         req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
@@ -309,22 +308,21 @@
         else:
             if not kwargs:
                 kwargs['password'] = str(login)
-            self.cnx = repo_connect(self.repo, unicode(login),
-                                    cnxprops=ConnectionProperties('inmemory'),
-                                    **kwargs)
+            self.cnx = repo_connect(self.repo, unicode(login), **kwargs)
+            self.websession = DBAPISession(self.cnx)
             self._cnxs.append(self.cnx)
         if login == self.vreg.config.anonymous_user()[0]:
             self.cnx.anonymous_connection = True
         return self.cnx
 
     def restore_connection(self):
-        if not self.cnx is self._orig_cnx:
+        if not self.cnx is self._orig_cnx[0]:
             try:
                 self.cnx.close()
                 self._cnxs.remove(self.cnx)
             except ProgrammingError:
                 pass # already closed
-        self.cnx = self._orig_cnx
+        self.cnx, self.websession = self._orig_cnx
 
     # db api ##################################################################
 
@@ -486,7 +484,7 @@
     def request(self, *args, **kwargs):
         """return a web ui request"""
         req = self.requestcls(self.vreg, form=kwargs)
-        req.set_connection(self.cnx)
+        req.set_session(self.websession)
         return req
 
     def remote_call(self, fname, *args):
@@ -542,27 +540,31 @@
         self.set_option('auth-mode', authmode)
         self.set_option('anonymous-user', anonuser)
         req = self.request()
-        origcnx = req.cnx
-        req.cnx = None
+        origsession = req.session
+        req.session = req.cnx = None
+        del req.execute # get back to class implementation
         sh = self.app.session_handler
         authm = sh.session_manager.authmanager
         authm.anoninfo = self.vreg.config.anonymous_user()
+        authm.anoninfo = authm.anoninfo[0], {'password': authm.anoninfo[1]}
         # not properly cleaned between tests
         self.open_sessions = sh.session_manager._sessions = {}
-        return req, origcnx
+        return req, origsession
 
-    def assertAuthSuccess(self, req, origcnx, nbsessions=1):
+    def assertAuthSuccess(self, req, origsession, nbsessions=1):
         sh = self.app.session_handler
         path, params = self.expect_redirect(lambda x: self.app.connect(x), req)
-        cnx = req.cnx
+        session = req.session
         self.assertEquals(len(self.open_sessions), nbsessions, self.open_sessions)
-        self.assertEquals(cnx.login, origcnx.login)
-        self.assertEquals(cnx.anonymous_connection, False)
+        self.assertEquals(session.login, origsession.login)
+        self.assertEquals(session.anonymous_session, False)
         self.assertEquals(path, 'view')
-        self.assertEquals(params, {'__message': 'welcome %s !' % cnx.user().login})
+        self.assertEquals(params, {'__message': 'welcome %s !' % req.user.login})
 
     def assertAuthFailure(self, req, nbsessions=0):
-        self.assertRaises(AuthenticationError, self.app.connect, req)
+        self.app.connect(req)
+        self.assertIsInstance(req.session, DBAPISession)
+        self.assertEquals(req.session.cnx, None)
         self.assertEquals(req.cnx, None)
         self.assertEquals(len(self.open_sessions), nbsessions)
         clear_cache(req, 'get_authorization')
--- a/etwist/server.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/etwist/server.py	Tue Apr 13 12:19:24 2010 +0200
@@ -25,10 +25,8 @@
 
 from logilab.common.decorators import monkeypatch
 
-from cubicweb import ConfigurationError, CW_EVENT_MANAGER
-from cubicweb.web import (AuthenticationError, NotFound, Redirect,
-                          RemoteCallFailed, DirectResponse, StatusResponse,
-                          ExplicitLogin)
+from cubicweb import AuthenticationError, ConfigurationError, CW_EVENT_MANAGER
+from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.web.http_headers import generateDateTime
 from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
@@ -221,11 +219,9 @@
             req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
         try:
             self.appli.connect(req)
-        except AuthenticationError:
-            return self.request_auth(request=req)
         except Redirect, ex:
             return self.redirect(request=req, location=ex.location)
-        if https and req.cnx.anonymous_connection:
+        if https and req.session.anonymous_session:
             # don't allow anonymous on https connection
             return self.request_auth(request=req)
         if self.url_rewriter is not None:
@@ -247,19 +243,10 @@
             return HTTPResponse(stream=ex.content, code=ex.status,
                                 twisted_request=req._twreq,
                                 headers=req.headers_out)
-        except RemoteCallFailed, ex:
-            req.set_header('content-type', 'application/json')
-            return HTTPResponse(twisted_request=req._twreq, code=http.INTERNAL_SERVER_ERROR,
-                                stream=ex.dumps(), headers=req.headers_out)
-        except NotFound:
-            result = self.appli.notfound_content(req)
-            return HTTPResponse(twisted_request=req._twreq, code=http.NOT_FOUND,
-                                stream=result, headers=req.headers_out)
-
-        except ExplicitLogin:  # must be before AuthenticationError
+        except AuthenticationError:
             return self.request_auth(request=req)
-        except AuthenticationError, ex:
-            if self.config['auth-mode'] == 'cookie' and getattr(ex, 'url', None):
+        except LogOut, ex:
+            if self.config['auth-mode'] == 'cookie' and ex.url:
                 return self.redirect(request=req, location=ex.url)
             # in http we have to request auth to flush current http auth
             # information
--- a/hooks/test/unittest_bookmarks.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/hooks/test/unittest_bookmarks.py	Tue Apr 13 12:19:24 2010 +0200
@@ -1,7 +1,7 @@
 """
 
 :organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
@@ -18,10 +18,10 @@
         self.commit()
         self.execute('DELETE X bookmarked_by U WHERE U login "admin"')
         self.commit()
-        self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': beid}, 'x'))
+        self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': beid}))
         self.execute('DELETE X bookmarked_by U WHERE U login "anon"')
         self.commit()
-        self.failIf(self.execute('Any X WHERE X eid %(x)s', {'x': beid}, 'x'))
+        self.failIf(self.execute('Any X WHERE X eid %(x)s', {'x': beid}))
 
 if __name__ == '__main__':
     unittest_main()
--- a/selectors.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/selectors.py	Tue Apr 13 12:19:24 2010 +0200
@@ -107,7 +107,7 @@
 	__regid__ = 'loggeduserlink'
 
 	def call(self):
-	    if self._cw.cnx.anonymous_connection:
+	    if self._cw.session.anonymous_session:
 		# display login link
 		...
 	    else:
@@ -1038,12 +1038,24 @@
 
 @objectify_selector
 @lltrace
+def no_cnx(cls, req, rset, *args, **kwargs):
+    """Return 1 if the web session has no connection set. This occurs when
+    anonymous access is not allowed and user isn't authenticated.
+
+    May only be used on the web side, not on the data repository side.
+    """
+    if req.cnx is None:
+        return 1
+    return 0
+
+@objectify_selector
+@lltrace
 def authenticated_user(cls, req, **kwargs):
     """Return 1 if the user is authenticated (e.g. not the anonymous user).
 
     May only be used on the web side, not on the data repository side.
     """
-    if req.cnx.anonymous_connection:
+    if req.session.anonymous_session:
         return 0
     return 1
 
--- a/test/unittest_dbapi.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/test/unittest_dbapi.py	Tue Apr 13 12:19:24 2010 +0200
@@ -40,21 +40,6 @@
         self.assertRaises(ProgrammingError, cnx.user, None)
         self.assertRaises(ProgrammingError, cnx.describe, 1)
 
-    def test_session_data_api(self):
-        cnx = self.login('anon')
-        self.assertEquals(cnx.get_session_data('data'), None)
-        self.assertEquals(cnx.session_data(), {})
-        cnx.set_session_data('data', 4)
-        self.assertEquals(cnx.get_session_data('data'), 4)
-        self.assertEquals(cnx.session_data(), {'data': 4})
-        cnx.del_session_data('data')
-        cnx.del_session_data('whatever')
-        self.assertEquals(cnx.get_session_data('data'), None)
-        self.assertEquals(cnx.session_data(), {})
-        cnx.session_data()['data'] = 4
-        self.assertEquals(cnx.get_session_data('data'), 4)
-        self.assertEquals(cnx.session_data(), {'data': 4})
-
     def test_shared_data_api(self):
         cnx = self.login('anon')
         self.assertEquals(cnx.get_shared_data('data'), None)
--- a/web/_exceptions.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/_exceptions.py	Tue Apr 13 12:19:24 2010 +0200
@@ -40,10 +40,6 @@
         self.status = int(status)
         self.content = content
 
-class ExplicitLogin(AuthenticationError):
-    """raised when a bad connection id is given or when an attempt to establish
-    a connection failed"""
-
 class InvalidSession(CubicWebException):
     """raised when a session id is found but associated session is not found or
     invalid
@@ -59,3 +55,9 @@
     def dumps(self):
         import simplejson
         return simplejson.dumps({'reason': self.reason})
+
+class LogOut(PublishException):
+    """raised to ask for deauthentication of a logged in user"""
+    def __init__(self, url):
+        super(LogOut, self).__init__()
+        self.url = url
--- a/web/application.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/application.py	Tue Apr 13 12:19:24 2010 +0200
@@ -5,6 +5,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -18,10 +20,11 @@
 from cubicweb import (
     ValidationError, Unauthorized, AuthenticationError, NoSelectableObject,
     RepositoryError, CW_EVENT_MANAGER)
+from cubicweb.dbapi import DBAPISession
 from cubicweb.web import LOGGER, component
 from cubicweb.web import (
-    StatusResponse, DirectResponse, Redirect, NotFound,
-    RemoteCallFailed, ExplicitLogin, InvalidSession, RequestError)
+    StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
+    RemoteCallFailed, InvalidSession, RequestError)
 
 # make session manager available through a global variable so the debug view can
 # print information about web session
@@ -52,7 +55,7 @@
         for session in self.current_sessions():
             no_use_time = (time() - session.last_usage_time)
             total += 1
-            if session.anonymous_connection:
+            if session.anonymous_session:
                 if no_use_time >= self.cleanup_anon_session_time:
                     self.close_session(session)
                     closed += 1
@@ -76,9 +79,11 @@
         raise NotImplementedError()
 
     def open_session(self, req):
-        """open and return a new session for the given request
+        """open and return a new session for the given request. The session is
+        also bound to the request.
 
-        :raise ExplicitLogin: if authentication is required
+        raise :exc:`cubicweb.AuthenticationError` if authentication failed
+        (no authentication info found or wrong user/password)
         """
         raise NotImplementedError()
 
@@ -97,11 +102,24 @@
     def __init__(self, vreg):
         self.vreg = vreg
 
-    def authenticate(self, req):
-        """authenticate user and return corresponding user object
+    def validate_session(self, req, session):
+        """check session validity, reconnecting it to the repository if the
+        associated connection expired in the repository side (hence the
+        necessity for this method).
 
-        :raise ExplicitLogin: if authentication is required (no authentication
-        info found or wrong user/password)
+        raise :exc:`InvalidSession` if session is corrupted for a reason or
+        another and should be closed
+        """
+        raise NotImplementedError()
+
+    def authenticate(self, req):
+        """authenticate user using connection information found in the request,
+        and return corresponding a :class:`~cubicweb.dbapi.Connection` instance,
+        as well as login and authentication information dictionary used to open
+        the connection.
+
+        raise :exc:`cubicweb.AuthenticationError` if authentication failed
+        (no authentication info found or wrong user/password)
         """
         raise NotImplementedError()
 
@@ -165,9 +183,11 @@
             try:
                 session = self.get_session(req, sessionid)
             except InvalidSession:
+                # try to open a new session, so we get an anonymous session if
+                # allowed
                 try:
                     session = self.open_session(req)
-                except ExplicitLogin:
+                except AuthenticationError:
                     req.remove_cookie(cookie, self.SESSION_VAR)
                     raise
         # remember last usage time for web session tracking
@@ -183,7 +203,7 @@
         req.set_cookie(cookie, self.SESSION_VAR, maxage=None)
         # remember last usage time for web session tracking
         session.last_usage_time = time()
-        if not session.anonymous_connection:
+        if not session.anonymous_session:
             self._postlogin(req)
         return session
 
@@ -227,7 +247,7 @@
         """
         self.session_manager.close_session(req.cnx)
         req.remove_cookie(req.get_cookie(), self.SESSION_VAR)
-        raise AuthenticationError(url=goto_url)
+        raise LogOut(url=goto_url)
 
 
 class CubicWebPublisher(object):
@@ -271,7 +291,10 @@
         sessions (i.e. a new connection may be created or an already existing
         one may be reused
         """
-        self.session_handler.set_session(req)
+        try:
+            self.session_handler.set_session(req)
+        except AuthenticationError:
+            req.set_session(DBAPISession(None))
 
     # publish methods #########################################################
 
@@ -283,19 +306,18 @@
             return self.main_publish(path, req)
         finally:
             cnx = req.cnx
-            self._logfile_lock.acquire()
-            try:
-                try:
-                    result = ['\n'+'*'*80]
-                    result.append(req.url())
-                    result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q for q in cnx.executed_queries]
-                    cnx.executed_queries = []
-                    self._query_log.write('\n'.join(result).encode(req.encoding))
-                    self._query_log.flush()
-                except Exception:
-                    self.exception('error while logging queries')
-            finally:
-                self._logfile_lock.release()
+            if cnx is not None:
+                with self._logfile_lock:
+                    try:
+                        result = ['\n'+'*'*80]
+                        result.append(req.url())
+                        result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q
+                                   for q in cnx.executed_queries]
+                        cnx.executed_queries = []
+                        self._query_log.write('\n'.join(result).encode(req.encoding))
+                        self._query_log.flush()
+                    except Exception:
+                        self.exception('error while logging queries')
 
     @deprecated("[3.4] use vreg['controllers'].select(...)")
     def select_controller(self, oid, req):
@@ -340,7 +362,10 @@
                     # displaying the cookie authentication form
                     req.cnx.commit()
             except (StatusResponse, DirectResponse):
-                req.cnx.commit()
+                if req.cnx is not None:
+                    req.cnx.commit()
+                raise
+            except (AuthenticationError, LogOut):
                 raise
             except Redirect:
                 # redirect is raised by edit controller when everything went fine,
@@ -362,10 +387,13 @@
                 else:
                     # delete validation errors which may have been previously set
                     if '__errorurl' in req.form:
-                        req.del_session_data(req.form['__errorurl'])
+                        req.session.data.pop(req.form['__errorurl'], None)
                     raise
-            except (AuthenticationError, NotFound, RemoteCallFailed):
-                raise
+            except RemoteCallFailed, ex:
+                req.set_header('content-type', 'application/json')
+                raise StatusResponse(500, ex.dumps())
+            except NotFound:
+                raise StatusResponse(404, self.notfound_content(req))
             except ValidationError, ex:
                 self.validation_error_handler(req, ex)
             except (Unauthorized, BadRQLQuery, RequestError), ex:
@@ -388,7 +416,7 @@
                         'values': req.form,
                         'eidmap': req.data.get('eidmap', {})
                         }
-            req.set_session_data(req.form['__errorurl'], forminfo)
+            req.session.data[req.form['__errorurl']] = forminfo
             # XXX form session key / __error_url should be differentiated:
             # session key is 'url + #<form dom id', though we usually don't want
             # the browser to move to the form since it hides the global
--- a/web/box.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/box.py	Tue Apr 13 12:19:24 2010 +0200
@@ -12,7 +12,7 @@
 
 from cubicweb import Unauthorized, role as get_role, target as get_target
 from cubicweb.schema import display_name
-from cubicweb.selectors import (one_line_rset,  primary_view,
+from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
                                 match_context_prop, partial_has_related_entities)
 from cubicweb.view import View, ReloadableMixIn
 
@@ -37,7 +37,7 @@
         box.render(self.w)
     """
     __registry__ = 'boxes'
-    __select__ = match_context_prop()
+    __select__ = ~no_cnx() & match_context_prop()
 
     categories_in_order = ()
     cw_property_defs = {
--- a/web/captcha.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/captcha.py	Tue Apr 13 12:19:24 2010 +0200
@@ -70,8 +70,7 @@
         return img + super(CaptchaWidget, self).render(form, field, renderer)
 
     def process_field_data(self, form, field):
-        captcha = form._cw.get_session_data(field.input_name(form), None,
-                                            pop=True)
+        captcha = form._cw.session.data.pop(field.input_name(form), None)
         val = super(CaptchaWidget, self).process_field_data(form, field)
         if val is None:
             return val # required will be checked by field
--- a/web/form.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/form.py	Tue Apr 13 12:19:24 2010 +0200
@@ -191,7 +191,7 @@
             warn('[3.6.1] restore_previous_post already called, remove this call',
                  DeprecationWarning, stacklevel=2)
             return
-        forminfo = self._cw.get_session_data(sessionkey, pop=True)
+        forminfo = self._cw.session.data.pop(sessionkey, None)
         if forminfo:
             self._form_previous_values = forminfo['values']
             self._form_valerror = forminfo['error']
--- a/web/httpcache.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/httpcache.py	Tue Apr 13 12:19:24 2010 +0200
@@ -43,6 +43,8 @@
     """
 
     def etag(self):
+        if self.req.cnx is None:
+            return self.view.__regid__
         return self.view.__regid__ + '/' + ','.join(sorted(self.req.user.groups))
 
     def max_age(self):
--- a/web/request.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/request.py	Tue Apr 13 12:19:24 2010 +0200
@@ -122,11 +122,11 @@
             self.set_page_data('rql_varmaker', varmaker)
         return varmaker
 
-    def set_connection(self, cnx, user=None):
+    def set_session(self, session, user=None):
         """method called by the session handler when the user is authenticated
         or an anonymous connection is open
         """
-        super(CubicWebRequestBase, self).set_connection(cnx, user)
+        super(CubicWebRequestBase, self).set_session(session, user)
         # set request language
         vreg = self.vreg
         if self.user:
@@ -151,8 +151,9 @@
         gettext, self.pgettext = self.translations[lang]
         self._ = self.__ = gettext
         self.lang = lang
-        self.cnx.set_session_props(lang=lang)
         self.debug('request language: %s', lang)
+        if self.cnx is not None:
+            self.cnx.set_session_props(lang=lang)
 
     # input form parameters management ########################################
 
@@ -236,7 +237,7 @@
     @property
     def message(self):
         try:
-            return self.get_session_data(self._msgid, default=u'', pop=True)
+            return self.session.data.pop(self._msgid, '')
         except AttributeError:
             try:
                 return self._msg
@@ -257,17 +258,17 @@
     def set_redirect_message(self, msg):
         assert isinstance(msg, unicode)
         msgid = self.redirect_message_id()
-        self.set_session_data(msgid, msg)
+        self.session.data[msgid] = msg
         return msgid
 
     def append_to_redirect_message(self, msg):
         msgid = self.redirect_message_id()
-        currentmsg = self.get_session_data(msgid)
+        currentmsg = self.session.data.get(msgid)
         if currentmsg is not None:
             currentmsg = '%s %s' % (currentmsg, msg)
         else:
             currentmsg = msg
-        self.set_session_data(msgid, currentmsg)
+        self.session.data[msgid] = currentmsg
         return msgid
 
     def reset_message(self):
@@ -280,7 +281,7 @@
         """update the current search state"""
         searchstate = self.form.get('__mode')
         if not searchstate and self.cnx is not None:
-            searchstate = self.get_session_data('search_state', 'normal')
+            searchstate = self.session.data.get('search_state', 'normal')
         self.set_search_state(searchstate)
 
     def set_search_state(self, searchstate):
@@ -291,7 +292,7 @@
             self.search_state = ('linksearch', searchstate.split(':'))
             assert len(self.search_state[-1]) == 4
         if self.cnx is not None:
-            self.set_session_data('search_state', searchstate)
+            self.session.data['search_state'] = searchstate
 
     def match_search_state(self, rset):
         """when searching an entity to create a relation, return True if entities in
@@ -308,12 +309,12 @@
 
     def update_breadcrumbs(self):
         """stores the last visisted page in session data"""
-        searchstate = self.get_session_data('search_state')
+        searchstate = self.session.data.get('search_state')
         if searchstate == 'normal':
-            breadcrumbs = self.get_session_data('breadcrumbs', None)
+            breadcrumbs = self.session.data.get('breadcrumbs')
             if breadcrumbs is None:
                 breadcrumbs = SizeConstrainedList(10)
-                self.set_session_data('breadcrumbs', breadcrumbs)
+                self.session.data['breadcrumbs'] = breadcrumbs
                 breadcrumbs.append(self.url())
             else:
                 url = self.url()
@@ -321,7 +322,7 @@
                     breadcrumbs.append(url)
 
     def last_visited_page(self):
-        breadcrumbs = self.get_session_data('breadcrumbs', None)
+        breadcrumbs = self.session.data.get('breadcrumbs')
         if breadcrumbs:
             return breadcrumbs.pop()
         return self.base_url()
@@ -368,11 +369,10 @@
         self.del_page_data(cbname)
 
     def clear_user_callbacks(self):
-        if self.cnx is not None:
-            sessdata = self.session_data()
-            callbacks = [key for key in sessdata if key.startswith('cb_')]
-            for callback in callbacks:
-                self.del_session_data(callback)
+        if self.session is not None: # XXX
+            for key in self.session.data.keys():
+                if key.startswith('cb_'):
+                    del self.session.data[key]
 
     # web edition helpers #####################################################
 
@@ -438,13 +438,13 @@
         This is needed when the edition is completed (whether it's validated
         or cancelled)
         """
-        self.del_session_data('pending_insert')
-        self.del_session_data('pending_delete')
+        self.session.data.pop('pending_insert', None)
+        self.session.data.pop('pending_delete', None)
 
     def cancel_edition(self, errorurl):
         """remove pending operations and `errorurl`'s specific stored data
         """
-        self.del_session_data(errorurl)
+        self.session.data.pop(errorurl, None)
         self.remove_pending_operations()
 
     # high level methods for HTTP headers management ##########################
@@ -745,26 +745,29 @@
 
     def get_page_data(self, key, default=None):
         """return value associated to `key` in curernt page data"""
-        page_data = self.cnx.get_session_data(self.pageid, {})
+        page_data = self.session.data.get(self.pageid)
+        if page_data is None:
+            return default
         return page_data.get(key, default)
 
     def set_page_data(self, key, value):
         """set value associated to `key` in current page data"""
         self.html_headers.add_unload_pagedata()
-        page_data = self.cnx.get_session_data(self.pageid, {})
+        page_data = self.session.data.setdefault(self.pageid, {})
         page_data[key] = value
-        return self.cnx.set_session_data(self.pageid, page_data)
+        self.session.data[self.pageid] = page_data
 
     def del_page_data(self, key=None):
         """remove value associated to `key` in current page data
         if `key` is None, all page data will be cleared
         """
         if key is None:
-            self.cnx.del_session_data(self.pageid)
+            self.session.data.pop(self.pageid, None)
         else:
-            page_data = self.cnx.get_session_data(self.pageid, {})
-            page_data.pop(key, None)
-            self.cnx.set_session_data(self.pageid, page_data)
+            try:
+                del self.session.data[self.pageid][key]
+            except KeyError:
+                pass
 
     # user-agent detection ####################################################
 
--- a/web/test/unittest_application.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/test/unittest_application.py	Tue Apr 13 12:19:24 2010 +0200
@@ -14,9 +14,10 @@
 from logilab.common.testlib import TestCase, unittest_main
 from logilab.common.decorators import clear_cache
 
+from cubicweb import AuthenticationError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.fake import FakeRequest
-from cubicweb.web import Redirect, AuthenticationError, ExplicitLogin, INTERNAL_FIELD_VALUE
+from cubicweb.web import LogOut, Redirect, INTERNAL_FIELD_VALUE
 from cubicweb.web.views.basecontrollers import ViewController
 
 class FakeMapping:
@@ -39,10 +40,12 @@
     def __init__(self, form=None):
         self._cw = FakeRequest()
         self._cw.form = form or {}
-        self._cursor = self._cw.cursor = MockCursor()
+        self._cursor = MockCursor()
+        self._cw.execute = self._cursor.execute
 
     def new_cursor(self):
-        self._cursor = self._cw.cursor = MockCursor()
+        self._cursor = MockCursor()
+        self._cw.execute = self._cursor.execute
 
     def set_form(self, form):
         self._cw.form = form
@@ -178,7 +181,7 @@
             '__errorurl': 'view?vid=edition...'
             }
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
-        forminfo = req.get_session_data('view?vid=edition...')
+        forminfo = req.session.data['view?vid=edition...']
         eidmap = forminfo['eidmap']
         self.assertEquals(eidmap, {})
         values = forminfo['values']
@@ -208,7 +211,7 @@
                     '__errorurl': 'view?vid=edition...',
                     }
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
-        forminfo = req.get_session_data('view?vid=edition...')
+        forminfo = req.session.data['view?vid=edition...']
         self.assertEquals(set(forminfo['eidmap']), set('XY'))
         self.assertEquals(forminfo['eidmap']['X'], None)
         self.assertIsInstance(forminfo['eidmap']['Y'], int)
@@ -237,7 +240,7 @@
                     '__errorurl': 'view?vid=edition...',
                     }
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
-        forminfo = req.get_session_data('view?vid=edition...')
+        forminfo = req.session.data['view?vid=edition...']
         self.assertEquals(set(forminfo['eidmap']), set('XY'))
         self.assertIsInstance(forminfo['eidmap']['X'], int)
         self.assertIsInstance(forminfo['eidmap']['Y'], int)
@@ -299,29 +302,29 @@
     # authentication tests ####################################################
 
     def test_http_auth_no_anon(self):
-        req, origcnx = self.init_authentication('http')
+        req, origsession = self.init_authentication('http')
         self.assertAuthFailure(req)
-        self.assertRaises(ExplicitLogin, self.app_publish, req, 'login')
+        self.assertRaises(AuthenticationError, self.app_publish, req, 'login')
         self.assertEquals(req.cnx, None)
-        authstr = base64.encodestring('%s:%s' % (origcnx.login, origcnx.authinfo['password']))
+        authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password']))
         req._headers['Authorization'] = 'basic %s' % authstr
-        self.assertAuthSuccess(req, origcnx)
-        self.assertEquals(req.cnx.authinfo, {'password': origcnx.authinfo['password']})
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'logout')
+        self.assertAuthSuccess(req, origsession)
+        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertRaises(LogOut, self.app_publish, req, 'logout')
         self.assertEquals(len(self.open_sessions), 0)
 
     def test_cookie_auth_no_anon(self):
-        req, origcnx = self.init_authentication('cookie')
+        req, origsession = self.init_authentication('cookie')
         self.assertAuthFailure(req)
         form = self.app_publish(req, 'login')
         self.failUnless('__login' in form)
         self.failUnless('__password' in form)
         self.assertEquals(req.cnx, None)
-        req.form['__login'] = origcnx.login
-        req.form['__password'] = origcnx.authinfo['password']
-        self.assertAuthSuccess(req, origcnx)
-        self.assertEquals(req.cnx.authinfo, {'password': origcnx.authinfo['password']})
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'logout')
+        req.form['__login'] = origsession.login
+        req.form['__password'] = origsession.authinfo['password']
+        self.assertAuthSuccess(req, origsession)
+        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertRaises(LogOut, self.app_publish, req, 'logout')
         self.assertEquals(len(self.open_sessions), 0)
 
     def test_login_by_email(self):
@@ -331,71 +334,72 @@
                      'WHERE U login %(login)s', {'address': address, 'login': login})
         self.commit()
         # option allow-email-login not set
-        req, origcnx = self.init_authentication('cookie')
+        req, origsession = self.init_authentication('cookie')
         req.form['__login'] = address
-        req.form['__password'] = origcnx.authinfo['password']
+        req.form['__password'] = origsession.authinfo['password']
         self.assertAuthFailure(req)
         # option allow-email-login set
-        origcnx.login = address
+        origsession.login = address
         self.set_option('allow-email-login', True)
         req.form['__login'] = address
-        req.form['__password'] = origcnx.authinfo['password']
-        self.assertAuthSuccess(req, origcnx)
-        self.assertEquals(req.cnx.authinfo, {'password': origcnx.authinfo['password']})
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'logout')
+        req.form['__password'] = origsession.authinfo['password']
+        self.assertAuthSuccess(req, origsession)
+        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertRaises(LogOut, self.app_publish, req, 'logout')
         self.assertEquals(len(self.open_sessions), 0)
 
     def _reset_cookie(self, req):
         # preparing the suite of the test
         # set session id in cookie
         cookie = Cookie.SimpleCookie()
-        cookie['__session'] = req.cnx.sessionid
+        cookie['__session'] = req.session.sessionid
         req._headers['Cookie'] = cookie['__session'].OutputString()
         clear_cache(req, 'get_authorization')
-        # reset cnx as if it was a new incoming request
-        req.cnx = None
+        # reset session as if it was a new incoming request
+        req.session = req.cnx = None
 
     def _test_auth_anon(self, req):
         self.app.connect(req)
-        acnx = req.cnx
+        asession = req.session
         self.assertEquals(len(self.open_sessions), 1)
-        self.assertEquals(acnx.login, 'anon')
-        self.assertEquals(acnx.authinfo['password'], 'anon')
-        self.failUnless(acnx.anonymous_connection)
+        self.assertEquals(asession.login, 'anon')
+        self.assertEquals(asession.authinfo['password'], 'anon')
+        self.failUnless(asession.anonymous_session)
         self._reset_cookie(req)
 
     def _test_anon_auth_fail(self, req):
         self.assertEquals(len(self.open_sessions), 1)
         self.app.connect(req)
         self.assertEquals(req.message, 'authentication failure')
-        self.assertEquals(req.cnx.anonymous_connection, True)
+        self.assertEquals(req.session.anonymous_session, True)
         self.assertEquals(len(self.open_sessions), 1)
         self._reset_cookie(req)
 
     def test_http_auth_anon_allowed(self):
-        req, origcnx = self.init_authentication('http', 'anon')
+        req, origsession = self.init_authentication('http', 'anon')
         self._test_auth_anon(req)
         authstr = base64.encodestring('toto:pouet')
         req._headers['Authorization'] = 'basic %s' % authstr
         self._test_anon_auth_fail(req)
-        authstr = base64.encodestring('%s:%s' % (origcnx.login, origcnx.authinfo['password']))
+        authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password']))
         req._headers['Authorization'] = 'basic %s' % authstr
-        self.assertAuthSuccess(req, origcnx)
-        self.assertEquals(req.cnx.authinfo, {'password': origcnx.authinfo['password']})
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'logout')
+        self.assertAuthSuccess(req, origsession)
+        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertRaises(LogOut, self.app_publish, req, 'logout')
         self.assertEquals(len(self.open_sessions), 0)
 
     def test_cookie_auth_anon_allowed(self):
-        req, origcnx = self.init_authentication('cookie', 'anon')
+        req, origsession = self.init_authentication('cookie', 'anon')
         self._test_auth_anon(req)
         req.form['__login'] = 'toto'
         req.form['__password'] = 'pouet'
         self._test_anon_auth_fail(req)
-        req.form['__login'] = origcnx.login
-        req.form['__password'] = origcnx.authinfo['password']
-        self.assertAuthSuccess(req, origcnx)
-        self.assertEquals(req.cnx.authinfo, {'password': origcnx.authinfo['password']})
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'logout')
+        req.form['__login'] = origsession.login
+        req.form['__password'] = origsession.authinfo['password']
+        self.assertAuthSuccess(req, origsession)
+        self.assertEquals(req.session.authinfo,
+                          {'password': origsession.authinfo['password']})
+        self.assertRaises(LogOut, self.app_publish, req, 'logout')
         self.assertEquals(len(self.open_sessions), 0)
 
     def test_non_regr_optional_first_var(self):
--- a/web/test/unittest_views_basecontrollers.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Tue Apr 13 12:19:24 2010 +0200
@@ -247,7 +247,7 @@
         tmpgroup = self.request().create_entity('CWGroup', name=u"test")
         user = self.user()
         req = self.request(**req_form(user))
-        req.set_session_data('pending_insert', set([(user.eid, 'in_group', tmpgroup.eid)]))
+        req.session.data['pending_insert'] = set([(user.eid, 'in_group', tmpgroup.eid)])
         path, params = self.expect_redirect_publish(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
@@ -266,7 +266,7 @@
         self.assertUnorderedIterableEquals(usergroups, ['managers', 'test'])
         # now try to delete the relation
         req = self.request(**req_form(user))
-        req.set_session_data('pending_delete', set([(user.eid, 'in_group', groupeid)]))
+        req.session.data['pending_delete'] = set([(user.eid, 'in_group', groupeid)])
         path, params = self.expect_redirect_publish(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
@@ -554,17 +554,21 @@
 
     def test_remote_add_existing_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['python'])
-        self.assertUnorderedIterableEquals([tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
-                             ['python', 'cubicweb'])
-        self.assertEquals(self.execute('Any N WHERE T tags P, P is CWUser, T name N').rows,
-                          [['python']])
+        self.assertUnorderedIterableEquals(
+            [tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
+            ['python', 'cubicweb'])
+        self.assertEquals(
+            self.execute('Any N WHERE T tags P, P is CWUser, T name N').rows,
+            [['python']])
 
     def test_remote_add_new_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['javascript'])
-        self.assertUnorderedIterableEquals([tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
-                             ['python', 'cubicweb', 'javascript'])
-        self.assertEquals(self.execute('Any N WHERE T tags P, P is CWUser, T name N').rows,
-                          [['javascript']])
+        self.assertUnorderedIterableEquals(
+            [tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
+            ['python', 'cubicweb', 'javascript'])
+        self.assertEquals(
+            self.execute('Any N WHERE T tags P, P is CWUser, T name N').rows,
+            [['javascript']])
 
     def test_pending_insertion(self):
         res, req = self.remote_call('add_pending_inserts', [['12', 'tags', '13']])
--- a/web/views/authentication.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/views/authentication.py	Tue Apr 13 12:19:24 2010 +0200
@@ -12,7 +12,7 @@
 from cubicweb import AuthenticationError, BadConnectionId
 from cubicweb.view import Component
 from cubicweb.dbapi import repo_connect, ConnectionProperties
-from cubicweb.web import ExplicitLogin, InvalidSession
+from cubicweb.web import InvalidSession
 from cubicweb.web.application import AbstractAuthenticationManager
 
 class NoAuthInfo(Exception): pass
@@ -28,9 +28,10 @@
         """
         raise NotImplementedError()
 
-    def authenticated(self, req, cnx, retreiver):
+    def authenticated(self, retreiver, req, cnx, login, authinfo):
         """callback when return authentication information have opened a
-        repository connection successfully
+        repository connection successfully. Take care req has no session
+        attached yet, hence req.execute isn't available.
         """
         pass
 
@@ -59,50 +60,51 @@
         self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg),
                                     key=lambda x: x.order)
         assert self.authinforetreivers
+        # 2-uple login / password, login is None when no anonymous access
+        # configured
         self.anoninfo = vreg.config.anonymous_user()
+        if self.anoninfo[0]:
+            self.anoninfo = (self.anoninfo[0], {'password': self.anoninfo[1]})
 
     def validate_session(self, req, session):
-        """check session validity, and return eventually hijacked session
+        """check session validity, reconnecting it to the repository if the
+        associated connection expired in the repository side (hence the
+        necessity for this method). Return the connected user on success.
 
-        :raise InvalidSession:
-          if session is corrupted for a reason or another and should be closed
+        raise :exc:`InvalidSession` if session is corrupted for a reason or
+        another and should be closed
         """
         # with this authentication manager, session is actually a dbapi
         # connection
-        cnx = session
+        cnx = session.cnx
         login = req.get_authorization()[0]
+        # check cnx.login and not user.login, since in case of login by
+        # email, login and cnx.login are the email while user.login is the
+        # actual user login
+        if login and session.login != login:
+            raise InvalidSession('login mismatch')
         try:
             # calling cnx.user() check connection validity, raise
             # BadConnectionId on failure
             user = cnx.user(req)
-            # check cnx.login and not user.login, since in case of login by
-            # email, login and cnx.login are the email while user.login is the
-            # actual user login
-            if login and cnx.login != login:
-                cnx.close()
-                raise InvalidSession('login mismatch')
         except BadConnectionId:
             # check if a connection should be automatically restablished
-            if (login is None or login == cnx.login):
-                cnx = self._authenticate(req, cnx.login, cnx.authinfo)
+            if (login is None or login == session.login):
+                cnx = self._authenticate(session.login, session.authinfo)
                 user = cnx.user(req)
-                # backport session's data
-                cnx.data = session.data
+                session.cnx = cnx
             else:
                 raise InvalidSession('bad connection id')
-        # associate the connection to the current request
-        req.set_connection(cnx, user)
-        return cnx
+        return user
 
     def authenticate(self, req):
-        """authenticate user and return corresponding user object
+        """authenticate user using connection information found in the request,
+        and return corresponding a :class:`~cubicweb.dbapi.Connection` instance,
+        as well as login and authentication information dictionary used to open
+        the connection.
 
-        :raise ExplicitLogin: if authentication is required (no authentication
-        info found or wrong user/password)
-
-        Note: this method is violating AuthenticationManager interface by
-        returning a session instance instead of the user. This is expected by
-        the InMemoryRepositorySessionManager.
+        raise :exc:`cubicweb.AuthenticationError` if authentication failed
+        (no authentication info found or wrong user/password)
         """
         for retreiver in self.authinforetreivers:
             try:
@@ -110,44 +112,28 @@
             except NoAuthInfo:
                 continue
             try:
-                cnx = self._authenticate(req, login, authinfo)
-            except ExplicitLogin:
+                cnx = self._authenticate(login, authinfo)
+            except AuthenticationError:
                 continue # the next one may succeed
             for retreiver_ in self.authinforetreivers:
-                retreiver_.authenticated(req, cnx, retreiver)
-            break
-        else:
-            # false if no authentication info found, eg this is not an
-            # authentication failure
-            if 'login' in locals():
-                req.set_message(req._('authentication failure'))
-            cnx = self._open_anonymous_connection(req)
-        return cnx
+                retreiver_.authenticated(retreiver, req, cnx, login, authinfo)
+            return cnx, login, authinfo
+        # false if no authentication info found, eg this is not an
+        # authentication failure
+        if 'login' in locals():
+            req.set_message(req._('authentication failure'))
+        login, authinfo = self.anoninfo
+        if login:
+            cnx = self._authenticate(login, authinfo)
+            cnx.anonymous_connection = True
+            return cnx, login, authinfo
+        raise AuthenticationError()
 
-    def _authenticate(self, req, login, authinfo):
+    def _authenticate(self, login, authinfo):
         cnxprops = ConnectionProperties(self.vreg.config.repo_method,
                                         close=False, log=self.log_queries)
-        try:
-            cnx = repo_connect(self.repo, login, cnxprops=cnxprops, **authinfo)
-        except AuthenticationError:
-            raise ExplicitLogin()
-        self._init_cnx(cnx, login, authinfo)
-        # associate the connection to the current request
-        req.set_connection(cnx)
+        cnx = repo_connect(self.repo, login, cnxprops=cnxprops, **authinfo)
+        # decorate connection
+        cnx.vreg = self.vreg
         return cnx
 
-    def _open_anonymous_connection(self, req):
-        # restore an anonymous connection if possible
-        login, password = self.anoninfo
-        if login:
-            cnx = self._authenticate(req, login, {'password': password})
-            cnx.anonymous_connection = True
-            return cnx
-        raise ExplicitLogin()
-
-    def _init_cnx(self, cnx, login, authinfo):
-        # decorate connection
-        cnx.vreg = self.vreg
-        cnx.login = login
-        cnx.authinfo = authinfo
-
--- a/web/views/autoform.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/views/autoform.py	Tue Apr 13 12:19:24 2010 +0200
@@ -256,7 +256,7 @@
     This is where are stored relations being added while editing
     an entity. This used to be stored in a temporary cookie.
     """
-    pending = req.get_session_data('pending_insert') or ()
+    pending = req.session.data.get('pending_insert', ())
     return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
             if eid is None or eid in (subj, obj)]
 
@@ -266,7 +266,7 @@
     This is where are stored relations being removed while editing
     an entity. This used to be stored in a temporary cookie.
     """
-    pending = req.get_session_data('pending_delete') or ()
+    pending = req.session.data.get('pending_delete', ())
     return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
             if eid is None or eid in (subj, obj)]
 
--- a/web/views/basecontrollers.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/views/basecontrollers.py	Tue Apr 13 12:19:24 2010 +0200
@@ -10,22 +10,19 @@
 """
 __docformat__ = "restructuredtext en"
 
-from smtplib import SMTP
-
 import simplejson
 
 from logilab.common.decorators import cached
 from logilab.common.date import strptime
 
-from cubicweb import (NoSelectableObject, ValidationError, ObjectNotFound,
-                      typed_eid)
+from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
+                      AuthenticationError, typed_eid)
 from cubicweb.utils import CubicWebJsonEncoder
 from cubicweb.selectors import authenticated_user, match_form_params
 from cubicweb.mail import format_mail
-from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed, DirectResponse, json_dumps
+from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, json_dumps
 from cubicweb.web.controller import Controller
-from cubicweb.web.views import vid_from_rset
-from cubicweb.web.views.formrenderers import FormRenderer
+from cubicweb.web.views import vid_from_rset, formrenderers
 
 try:
     from cubicweb.web.facet import (FilterRQLBuilder, get_facet,
@@ -59,7 +56,7 @@
     user's session data
     """
     def wrapper(self, *args, **kwargs):
-        data = self._cw.get_session_data(self._cw.pageid)
+        data = self._cw.session.data.get(self._cw.pageid)
         if data is None:
             raise RemoteCallFailed(self._cw._('pageid-not-found'))
         return func(self, *args, **kwargs)
@@ -73,7 +70,7 @@
         """log in the instance"""
         if self._cw.vreg.config['auth-mode'] == 'http':
             # HTTP authentication
-            raise ExplicitLogin()
+            raise AuthenticationError()
         else:
             # Cookie authentication
             return self.appli.need_login_content(self._cw)
@@ -119,7 +116,10 @@
         req = self._cw
         if rset is None and not hasattr(req, '_rql_processed'):
             req._rql_processed = True
-            rset = self.process_rql(req.form.get('rql'))
+            if req.cnx is None:
+                rset = None
+            else:
+                rset = self.process_rql(req.form.get('rql'))
         if rset and rset.rowcount == 1 and '__method' in req.form:
             entity = rset.get_entity(0, 0)
             try:
@@ -187,7 +187,7 @@
     req.cnx.rollback()
     # XXX necessary to remove existant validation error?
     # imo (syt), it's not necessary
-    req.get_session_data(req.form.get('__errorurl'), pop=True)
+    req.session.data.pop(req.form.get('__errorurl'), None)
     foreid = ex.entity
     eidmap = req.data.get('eidmap', {})
     for var, eid in eidmap.items():
@@ -380,7 +380,7 @@
         form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
         form.build_context()
         vfield = form.field_by_name('value')
-        renderer = FormRenderer(self._cw)
+        renderer = formrenderers.FormRenderer(self._cw)
         return vfield.render(form, renderer, tabindex=tabindex) \
                + renderer.render_help(form, vfield)
 
@@ -474,7 +474,7 @@
     @check_pageid
     @jsonize
     def js_user_callback(self, cbname):
-        page_data = self._cw.get_session_data(self._cw.pageid, {})
+        page_data = self._cw.session.data.get(self._cw.pageid, {})
         try:
             cb = page_data[cbname]
         except KeyError:
@@ -503,7 +503,7 @@
         self._cw.unregister_callback(self._cw.pageid, cbname)
 
     def js_unload_page_data(self):
-        self._cw.del_session_data(self._cw.pageid)
+        self._cw.session.data.pop(self._cw.pageid, None)
 
     def js_cancel_edition(self, errorurl):
         """cancelling edition from javascript
@@ -548,15 +548,13 @@
 
     def _add_pending(self, eidfrom, rel, eidto, kind):
         key = 'pending_%s' % kind
-        pendings = self._cw.get_session_data(key, set())
+        pendings = self._cw.session.data.setdefault(key, set())
         pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-        self._cw.set_session_data(key, pendings)
 
     def _remove_pending(self, eidfrom, rel, eidto, kind):
         key = 'pending_%s' % kind
-        pendings = self._cw.get_session_data(key)
+        pendings = self._cw.session.data[key]
         pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-        self._cw.set_session_data(key, pendings)
 
     def js_remove_pending_insert(self, (eidfrom, rel, eidto)):
         self._remove_pending(eidfrom, rel, eidto, 'insert')
@@ -613,7 +611,7 @@
         for recipient in self.recipients():
             text = body % recipient.as_email_context()
             self.sendmail(recipient.get_email(), subject, text)
-        # breadcrumbs = self._cw.get_session_data('breadcrumbs', None)
+        #breadcrumbs = self._cw.session.data.get('breadcrumbs', None)
         url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
         raise Redirect(url)
 
@@ -644,7 +642,7 @@
 
     def redirect(self):
         req = self._cw
-        breadcrumbs = req.get_session_data('breadcrumbs', None)
+        breadcrumbs = req.session.data.get('breadcrumbs', None)
         if breadcrumbs is not None and len(breadcrumbs) > 1:
             url = req.rebuild_url(breadcrumbs[-2],
                                   __message=req._('transaction undoed'))
--- a/web/views/basetemplates.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/views/basetemplates.py	Tue Apr 13 12:19:24 2010 +0200
@@ -12,7 +12,7 @@
 from logilab.common.deprecation import class_renamed
 
 from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import match_kwargs
+from cubicweb.selectors import match_kwargs, no_cnx
 from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW
 from cubicweb.utils import UStringIO
 from cubicweb.schema import display_name
@@ -78,7 +78,6 @@
         return 0
     return view.templatable
 
-
 class NonTemplatableViewTemplate(MainTemplate):
     """main template for any non templatable views (xml, binaries, etc.)"""
     __regid__ = 'main-template'
@@ -192,9 +191,9 @@
 
 
 class ErrorTemplate(TheMainTemplate):
-    """fallback template if an internal error occured during displaying the
-    main template. This template may be called for authentication error,
-    which means that req.cnx and req.user may not be set.
+    """fallback template if an internal error occured during displaying the main
+    template. This template may be called for authentication error, which means
+    that req.cnx and req.user may not be set.
     """
     __regid__ = 'error-template'
 
@@ -350,7 +349,7 @@
         self.w(u'<td id="lastcolumn">')
         self.w(u'</td>\n')
         self.w(u'</tr></table>\n')
-        if self._cw.cnx.anonymous_connection:
+        if self._cw.session.anonymous_session:
             self.wview('logform', rset=self.cw_rset, id='popupLoginBox',
                        klass='hidden', title=False, showmessage=False)
 
--- a/web/views/editviews.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/views/editviews.py	Tue Apr 13 12:19:24 2010 +0200
@@ -113,5 +113,5 @@
             text, data = captcha.captcha(self._cw.vreg.config['captcha-font-file'],
                                          self._cw.vreg.config['captcha-font-size'])
             key = self._cw.form.get('captchakey', 'captcha')
-            self._cw.set_session_data(key, text)
+            self._cw.session.data[key] = text
             self.w(data.read())
--- a/web/views/sessions.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/web/views/sessions.py	Tue Apr 13 12:19:24 2010 +0200
@@ -10,6 +10,7 @@
 
 from cubicweb.web import InvalidSession
 from cubicweb.web.application import AbstractSessionManager
+from cubicweb.dbapi import DBAPISession
 
 
 class InMemoryRepositorySessionManager(AbstractSessionManager):
@@ -40,26 +41,28 @@
         if self.has_expired(session):
             self.close_session(session)
             raise InvalidSession()
-        # give an opportunity to auth manager to hijack the session (necessary
-        # with the RepositoryAuthenticationManager in case the connection to the
-        # repository has expired)
         try:
-            session = self.authmanager.validate_session(req, session)
-            # necessary in case session has been hijacked
-            self._sessions[session.sessionid] = session
+            user = self.authmanager.validate_session(req, session)
         except InvalidSession:
             # invalid session
-            del self._sessions[sessionid]
+            self.close_session(session)
             raise
+        # associate the connection to the current request
+        req.set_session(session, user)
         return session
 
     def open_session(self, req):
-        """open and return a new session for the given request
+        """open and return a new session for the given request. The session is
+        also bound to the request.
 
-        :raise ExplicitLogin: if authentication is required
+        raise :exc:`cubicweb.AuthenticationError` if authentication failed
+        (no authentication info found or wrong user/password)
         """
-        session = self.authmanager.authenticate(req)
+        cnx, login, authinfo = self.authmanager.authenticate(req)
+        session = DBAPISession(cnx, login, authinfo)
         self._sessions[session.sessionid] = session
+        # associate the connection to the current request
+        req.set_session(session)
         return session
 
     def close_session(self, session):
@@ -69,8 +72,9 @@
         self.info('closing http session %s' % session)
         del self._sessions[session.sessionid]
         try:
-            session.close()
+            session.cnx.close()
         except:
             # already closed, may occurs if the repository session expired but
             # not the web session
             pass
+        session.cnx = None
--- a/wsgi/handler.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/wsgi/handler.py	Tue Apr 13 12:19:24 2010 +0200
@@ -9,8 +9,7 @@
 __docformat__ = "restructuredtext en"
 
 from cubicweb import AuthenticationError
-from cubicweb.web import (NotFound, Redirect, DirectResponse, StatusResponse,
-                          ExplicitLogin)
+from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.wsgi.request import CubicWebWsgiRequest
 
@@ -113,8 +112,6 @@
             req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
         try:
             self.appli.connect(req)
-        except AuthenticationError:
-            return self.request_auth(req)
         except Redirect, ex:
             return self.redirect(req, ex.location)
         path = req.path
@@ -126,12 +123,9 @@
             return WSGIResponse(200, req, ex.response)
         except StatusResponse, ex:
             return WSGIResponse(ex.status, req, ex.content)
-        except NotFound:
-            result = self.appli.notfound_content(req)
-            return WSGIResponse(404, req, result)
-        except ExplicitLogin:  # must be before AuthenticationError
+        except AuthenticationError:  # must be before AuthenticationError
             return self.request_auth(req)
-        except AuthenticationError:
+        except LogOut:
             if self.config['auth-mode'] == 'cookie':
                 # in cookie mode redirecting to the index view is enough :
                 # either anonymous connection is allowed and the page will