# HG changeset patch # User Sylvain Thénault # Date 1271153964 -7200 # Node ID 6abd6e3599f46b869b92bbf42ca740b16a92fc4e # Parent 4f4369e63f5e2b29dfb5e7e7a37075a43b3cef8d #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. diff -r 4f4369e63f5e -r 6abd6e3599f4 _exceptions.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""" diff -r 4f4369e63f5e -r 6abd6e3599f4 dbapi.py --- 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 """ - 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 """ + 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 - diff -r 4f4369e63f5e -r 6abd6e3599f4 devtools/testlib.py --- 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') diff -r 4f4369e63f5e -r 6abd6e3599f4 etwist/server.py --- 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 diff -r 4f4369e63f5e -r 6abd6e3599f4 hooks/test/unittest_bookmarks.py --- 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() diff -r 4f4369e63f5e -r 6abd6e3599f4 selectors.py --- 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 diff -r 4f4369e63f5e -r 6abd6e3599f4 test/unittest_dbapi.py --- 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) diff -r 4f4369e63f5e -r 6abd6e3599f4 web/_exceptions.py --- 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 diff -r 4f4369e63f5e -r 6abd6e3599f4 web/application.py --- 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 + #
1: url = req.rebuild_url(breadcrumbs[-2], __message=req._('transaction undoed')) diff -r 4f4369e63f5e -r 6abd6e3599f4 web/views/basetemplates.py --- 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'') self.w(u'\n') self.w(u'\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) diff -r 4f4369e63f5e -r 6abd6e3599f4 web/views/editviews.py --- 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()) diff -r 4f4369e63f5e -r 6abd6e3599f4 web/views/sessions.py --- 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 diff -r 4f4369e63f5e -r 6abd6e3599f4 wsgi/handler.py --- 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