#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.
--- 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