diff -r b718626a0e60 -r 13b0b96d7982 server/session.py --- a/server/session.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/session.py Tue Mar 09 08:59:43 2010 +0100 @@ -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 @@ -71,6 +73,37 @@ self.session.enable_hooks_category(*self.changes) self.session.set_hooks_mode(self.oldmode) +INDENT = '' +class security_enabled(object): + """context manager to control security w/ session.execute, since by + default security is disabled on queries executed on the repository + side. + """ + def __init__(self, session, read=None, write=None): + self.session = session + self.read = read + self.write = write + + def __enter__(self): +# global INDENT + if self.read is not None: + self.oldread = self.session.set_read_security(self.read) +# print INDENT + 'read', self.read, self.oldread + if self.write is not None: + self.oldwrite = self.session.set_write_security(self.write) +# print INDENT + 'write', self.write, self.oldwrite +# INDENT += ' ' + + def __exit__(self, exctype, exc, traceback): +# global INDENT +# INDENT = INDENT[:-2] + if self.read is not None: + self.session.set_read_security(self.oldread) +# print INDENT + 'reset read to', self.oldread + if self.write is not None: + self.session.set_write_security(self.oldwrite) +# print INDENT + 'reset write to', self.oldwrite + class Session(RequestSessionBase): @@ -88,7 +121,6 @@ self.creation = time() self.timestamp = self.creation self.is_internal_session = False - self.is_super_session = False self.default_mode = 'read' # short cut to querier .execute method self._execute = repo.querier.execute @@ -109,19 +141,9 @@ def hijack_user(self, user): """return a fake request/session using specified user""" session = Session(user, self.repo) - session._threaddata = self.actual_session()._threaddata + session._threaddata.pool = pool return session - def _super_call(self, __cb, *args, **kwargs): - if self.is_super_session: - __cb(self, *args, **kwargs) - return - self.is_super_session = True - try: - __cb(self, *args, **kwargs) - finally: - self.is_super_session = False - def add_relation(self, fromeid, rtype, toeid): """provide direct access to the repository method to add a relation. @@ -133,14 +155,13 @@ You may use this in hooks when you know both eids of the relation you want to add. """ - if self.vreg.schema[rtype].inlined: - entity = self.entity_from_eid(fromeid) - entity[rtype] = toeid - self._super_call(self.repo.glob_update_entity, - entity, set((rtype,))) - else: - self._super_call(self.repo.glob_add_relation, - fromeid, rtype, toeid) + with security_enabled(self, False, False): + if self.vreg.schema[rtype].inlined: + entity = self.entity_from_eid(fromeid) + entity[rtype] = toeid + self.repo.glob_update_entity(self, entity, set((rtype,))) + else: + self.repo.glob_add_relation(self, fromeid, rtype, toeid) def delete_relation(self, fromeid, rtype, toeid): """provide direct access to the repository method to delete a relation. @@ -153,14 +174,13 @@ You may use this in hooks when you know both eids of the relation you want to delete. """ - if self.vreg.schema[rtype].inlined: - entity = self.entity_from_eid(fromeid) - entity[rtype] = None - self._super_call(self.repo.glob_update_entity, - entity, set((rtype,))) - else: - self._super_call(self.repo.glob_delete_relation, - fromeid, rtype, toeid) + with security_enabled(self, False, False): + if self.vreg.schema[rtype].inlined: + entity = self.entity_from_eid(fromeid) + entity[rtype] = None + self.repo.glob_update_entity(self, entity, set((rtype,))) + else: + self.repo.glob_delete_relation(self, fromeid, rtype, toeid) # relations cache handling ################################################# @@ -229,10 +249,6 @@ # resource accessors ###################################################### - def actual_session(self): - """return the original parent session if any, else self""" - return self - def system_sql(self, sql, args=None, rollback_on_failure=True): """return a sql cursor on the system database""" if not sql.split(None, 1)[0].upper() == 'SELECT': @@ -276,6 +292,74 @@ rdef = rschema.rdef(subjtype, objtype) return rdef.get(rprop) + # security control ######################################################### + + DEFAULT_SECURITY = object() # evaluated to true by design + + @property + def read_security(self): + """return a boolean telling if read security is activated or not""" + try: + return self._threaddata.read_security + except AttributeError: + self._threaddata.read_security = self.DEFAULT_SECURITY + return self._threaddata.read_security + + def set_read_security(self, activated): + """[de]activate read security, returning the previous value set for + later restoration. + + you should usually use the `security_enabled` context manager instead + of this to change security settings. + """ + oldmode = self.read_security + self._threaddata.read_security = activated + # dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not + # issued on the session). This is tricky since we the execution model of + # a (write) user query is: + # + # repository.execute (security enabled) + # \-> querier.execute + # \-> repo.glob_xxx (add/update/delete entity/relation) + # \-> deactivate security before calling hooks + # \-> WE WANT TO CHECK QUERY NATURE HERE + # \-> potentially, other calls to querier.execute + # + # so we can't rely on simply checking session.read_security, but + # recalling the first transition from DEFAULT_SECURITY to something + # else (False actually) is not perfect but should be enough + self._threaddata.dbapi_query = oldmode is self.DEFAULT_SECURITY + return oldmode + + @property + def write_security(self): + """return a boolean telling if write security is activated or not""" + try: + return self._threaddata.write_security + except: + self._threaddata.write_security = self.DEFAULT_SECURITY + return self._threaddata.write_security + + def set_write_security(self, activated): + """[de]activate write security, returning the previous value set for + later restoration. + + you should usually use the `security_enabled` context manager instead + of this to change security settings. + """ + oldmode = self.write_security + self._threaddata.write_security = activated + return oldmode + + @property + def running_dbapi_query(self): + """return a boolean telling if it's triggered by a db-api query or by + a session query. + + To be used in hooks, else may have a wrong value. + """ + return getattr(self._threaddata, 'dbapi_query', True) + # hooks activation control ################################################# # all hooks should be activated during normal execution @@ -505,46 +589,15 @@ """return the source where the entity with id is located""" return self.repo.source_from_eid(eid, self) - def decorate_rset(self, rset, propagate=False): + def decorate_rset(self, rset): rset.vreg = self.vreg - rset.req = propagate and self or self.actual_session() + rset.req = self return rset - @property - def super_session(self): - try: - csession = self.childsession - except AttributeError: - if isinstance(self, (ChildSession, InternalSession)): - csession = self - else: - csession = ChildSession(self) - self.childsession = csession - # need shared pool set - self.set_pool(checkclosed=False) - return csession - - def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True, - propagate=False): - """like .execute but with security checking disabled (this method is - internal to the server, it's not part of the db-api) - - if `propagate` is true, the super_session will be attached to the result - set instead of the parent session, hence further query done through - entities fetched from this result set will bypass security as well - """ - return self.super_session.execute(rql, kwargs, eid_key, build_descr, - propagate) - - def execute(self, rql, kwargs=None, eid_key=None, build_descr=True, - propagate=False): - """db-api like method directly linked to the querier execute method - - Becare that unlike actual cursor.execute, `build_descr` default to - false - """ + def execute(self, rql, kwargs=None, eid_key=None, build_descr=True): + """db-api like method directly linked to the querier execute method""" rset = self._execute(self, rql, kwargs, eid_key, build_descr) - return self.decorate_rset(rset, propagate) + return self.decorate_rset(rset) def _clear_thread_data(self): """remove everything from the thread local storage, except pool @@ -569,58 +622,60 @@ return if self.commit_state: return - # on rollback, an operation should have the following state - # information: - # - processed by the precommit/commit event or not - # - if processed, is it the failed operation - try: - for trstate in ('precommit', 'commit'): - processed = [] - self.commit_state = trstate - try: - while self.pending_operations: - operation = self.pending_operations.pop(0) - operation.processed = trstate - processed.append(operation) + # by default, operations are executed with security turned off + with security_enabled(self, False, False): + # on rollback, an operation should have the following state + # information: + # - processed by the precommit/commit event or not + # - if processed, is it the failed operation + try: + for trstate in ('precommit', 'commit'): + processed = [] + self.commit_state = trstate + try: + while self.pending_operations: + operation = self.pending_operations.pop(0) + operation.processed = trstate + processed.append(operation) + operation.handle_event('%s_event' % trstate) + self.pending_operations[:] = processed + self.debug('%s session %s done', trstate, self.id) + except: + self.exception('error while %sing', trstate) + # if error on [pre]commit: + # + # * set .failed = True on the operation causing the failure + # * call revert_event on processed operations + # * call rollback_event on *all* operations + # + # that seems more natural than not calling rollback_event + # for processed operations, and allow generic rollback + # instead of having to implements rollback, revertprecommit + # and revertcommit, that will be enough in mont case. + operation.failed = True + for operation in processed: + operation.handle_event('revert%s_event' % trstate) + # XXX use slice notation since self.pending_operations is a + # read-only property. + self.pending_operations[:] = processed + self.pending_operations + self.rollback(reset_pool) + raise + self.pool.commit() + self.commit_state = trstate = 'postcommit' + while self.pending_operations: + operation = self.pending_operations.pop(0) + operation.processed = trstate + try: operation.handle_event('%s_event' % trstate) - self.pending_operations[:] = processed - self.debug('%s session %s done', trstate, self.id) - except: - self.exception('error while %sing', trstate) - # if error on [pre]commit: - # - # * set .failed = True on the operation causing the failure - # * call revert_event on processed operations - # * call rollback_event on *all* operations - # - # that seems more natural than not calling rollback_event - # for processed operations, and allow generic rollback - # instead of having to implements rollback, revertprecommit - # and revertcommit, that will be enough in mont case. - operation.failed = True - for operation in processed: - operation.handle_event('revert%s_event' % trstate) - # XXX use slice notation since self.pending_operations is a - # read-only property. - self.pending_operations[:] = processed + self.pending_operations - self.rollback(reset_pool) - raise - self.pool.commit() - self.commit_state = trstate = 'postcommit' - while self.pending_operations: - operation = self.pending_operations.pop(0) - operation.processed = trstate - try: - operation.handle_event('%s_event' % trstate) - except: - self.critical('error while %sing', trstate, - exc_info=sys.exc_info()) - self.info('%s session %s done', trstate, self.id) - finally: - self._clear_thread_data() - self._touch() - if reset_pool: - self.reset_pool(ignoremode=True) + except: + self.critical('error while %sing', trstate, + exc_info=sys.exc_info()) + self.info('%s session %s done', trstate, self.id) + finally: + self._clear_thread_data() + self._touch() + if reset_pool: + self.reset_pool(ignoremode=True) def rollback(self, reset_pool=True): """rollback the current session's transaction""" @@ -630,21 +685,23 @@ self._touch() self.debug('rollback session %s done (no db activity)', self.id) return - try: - while self.pending_operations: - try: - operation = self.pending_operations.pop(0) - operation.handle_event('rollback_event') - except: - self.critical('rollback error', exc_info=sys.exc_info()) - continue - self.pool.rollback() - self.debug('rollback for session %s done', self.id) - finally: - self._clear_thread_data() - self._touch() - if reset_pool: - self.reset_pool(ignoremode=True) + # by default, operations are executed with security turned off + with security_enabled(self, False, False): + try: + while self.pending_operations: + try: + operation = self.pending_operations.pop(0) + operation.handle_event('rollback_event') + except: + self.critical('rollback error', exc_info=sys.exc_info()) + continue + self.pool.rollback() + self.debug('rollback for session %s done', self.id) + finally: + self._clear_thread_data() + self._touch() + if reset_pool: + self.reset_pool(ignoremode=True) def close(self): """do not close pool on session close, since they are shared now""" @@ -767,6 +824,25 @@ # deprecated ############################################################### + @deprecated("[3.7] control security with session.[read|write]_security") + def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True, + propagate=False): + """like .execute but with security checking disabled (this method is + internal to the server, it's not part of the db-api) + """ + return self.execute(rql, kwargs, eid_key, build_descr) + + @property + @deprecated("[3.7] is_super_session is deprecated, test " + "session.read_security and or session.write_security") + def is_super_session(self): + return not self.read_security or not self.write_security + + @deprecated("[3.7] session is actual session") + def actual_session(self): + """return the original parent session if any, else self""" + return self + @property @deprecated("[3.6] use session.vreg.schema") def schema(self): @@ -793,99 +869,6 @@ return self.entity_from_eid(eid) -class ChildSession(Session): - """child (or internal) session are used to hijack the security system - """ - cnxtype = 'inmemory' - - def __init__(self, parent_session): - self.id = None - self.is_internal_session = False - self.is_super_session = True - # session which has created this one - self.parent_session = parent_session - self.user = InternalManager() - self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone - self.repo = parent_session.repo - self.vreg = parent_session.vreg - self.data = parent_session.data - self.encoding = parent_session.encoding - self.lang = parent_session.lang - self._ = self.__ = parent_session._ - # short cut to querier .execute method - self._execute = self.repo.querier.execute - - @property - def super_session(self): - return self - - @property - def hooks_mode(self): - return self.parent_session.hooks_mode - def set_hooks_mode(self, mode): - return self.parent_session.set_hooks_mode(mode) - - @property - def disabled_hooks_categories(self): - return self.parent_session.disabled_hooks_categories - - @property - def enabled_hooks_categories(self): - return self.parent_session.enabled_hooks_categories - - - def get_mode(self): - return self.parent_session.mode - def set_mode(self, value): - self.parent_session.set_mode(value) - mode = property(get_mode, set_mode) - - def get_commit_state(self): - return self.parent_session.commit_state - def set_commit_state(self, value): - self.parent_session.set_commit_state(value) - commit_state = property(get_commit_state, set_commit_state) - - @property - def pool(self): - return self.parent_session.pool - @property - def pending_operations(self): - return self.parent_session.pending_operations - @property - def transaction_data(self): - return self.parent_session.transaction_data - - def set_pool(self): - """the session need a pool to execute some queries""" - self.parent_session.set_pool() - - def reset_pool(self): - """the session has no longer using its pool, at least for some time - """ - self.parent_session.reset_pool() - - def actual_session(self): - """return the original parent session if any, else self""" - return self.parent_session - - def commit(self, reset_pool=True): - """commit the current session's transaction""" - self.parent_session.commit(reset_pool) - - def rollback(self, reset_pool=True): - """rollback the current session's transaction""" - self.parent_session.rollback(reset_pool) - - def close(self): - """do not close pool on session close, since they are shared now""" - self.rollback() - - def user_data(self): - """returns a dictionnary with this user's information""" - return self.parent_session.user_data() - - class InternalSession(Session): """special session created internaly by the repository""" @@ -895,11 +878,7 @@ self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone self.cnxtype = 'inmemory' self.is_internal_session = True - self.is_super_session = True - - @property - def super_session(self): - return self + self.disable_hooks_category('integrity') class InternalManager(object):