[repo] enhanced security handling: deprecates unsafe_execute, in favor of explicit read/write security control using the `enabled_security` context manager. Also code executed on the repository side is now unsafe by default.
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 09 Mar 2010 08:59:43 +0100
changeset 4835 13b0b96d7982
parent 4834 b718626a0e60
child 4843 5f7363416765
[repo] enhanced security handling: deprecates unsafe_execute, in favor of explicit read/write security control using the `enabled_security` context manager. Also code executed on the repository side is now unsafe by default.
devtools/fake.py
devtools/repotest.py
devtools/testlib.py
entities/authobjs.py
entities/test/unittest_wfobjs.py
entities/wfobjs.py
entity.py
goa/appobjects/dbmgmt.py
goa/dbinit.py
hooks/email.py
hooks/integrity.py
hooks/metadata.py
hooks/notification.py
hooks/security.py
hooks/syncschema.py
hooks/workflow.py
mail.py
misc/migration/bootstrapmigration_repository.py
misc/migration/postcreate.py
req.py
schema.py
server/checkintegrity.py
server/hook.py
server/migractions.py
server/querier.py
server/repository.py
server/schemaserial.py
server/session.py
server/ssplanner.py
server/test/unittest_hook.py
server/test/unittest_repository.py
sobjects/notification.py
sobjects/supervising.py
test/unittest_entity.py
--- a/devtools/fake.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/devtools/fake.py	Tue Mar 09 08:59:43 2010 +0100
@@ -116,17 +116,6 @@
     def validate_cache(self):
         pass
 
-    # session compatibility (in some test are using this class to test server
-    # side views...)
-    def actual_session(self):
-        """return the original parent session if any, else self"""
-        return self
-
-    def unsafe_execute(self, *args, **kwargs):
-        """return the original parent session if any, else self"""
-        kwargs.pop('propagate', None)
-        return self.execute(*args, **kwargs)
-
 
 class FakeUser(object):
     login = 'toto'
@@ -136,18 +125,19 @@
 
 
 class FakeSession(RequestSessionBase):
+    read_security = write_security = True
+    set_read_security = set_write_security = lambda *args, **kwargs: None
+
     def __init__(self, repo=None, user=None):
         self.repo = repo
         self.vreg = getattr(self.repo, 'vreg', CubicWebVRegistry(FakeConfig(), initlog=False))
         self.pool = FakePool()
         self.user = user or FakeUser()
         self.is_internal_session = False
-        self.is_super_session = self.user.eid == -1
         self.transaction_data = {}
 
-    def execute(self, *args):
+    def execute(self, *args, **kwargs):
         pass
-    unsafe_execute = execute
 
     def commit(self, *args):
         self.transaction_data.clear()
--- a/devtools/repotest.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/devtools/repotest.py	Tue Mar 09 08:59:43 2010 +0100
@@ -178,9 +178,10 @@
         self._dumb_sessions = []
 
     def get_max_eid(self):
-        return self.session.unsafe_execute('Any MAX(X)')[0][0]
+        return self.session.execute('Any MAX(X)')[0][0]
     def cleanup(self):
-        self.session.unsafe_execute('DELETE Any X WHERE X eid > %s' % self.maxeid)
+        self.session.set_pool()
+        self.session.execute('DELETE Any X WHERE X eid > %s' % self.maxeid)
 
     def tearDown(self):
         undo_monkey_patch()
--- a/devtools/testlib.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/devtools/testlib.py	Tue Mar 09 08:59:43 2010 +0100
@@ -228,7 +228,9 @@
     @property
     def session(self):
         """return current server side session (using default manager account)"""
-        return self.repo._sessions[self.cnx.sessionid]
+        session = self.repo._sessions[self.cnx.sessionid]
+        session.set_pool()
+        return session
 
     @property
     def adminsession(self):
--- a/entities/authobjs.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/entities/authobjs.py	Tue Mar 09 08:59:43 2010 +0100
@@ -93,15 +93,10 @@
         return self.groups == frozenset(('guests', ))
 
     def owns(self, eid):
-        if hasattr(self._cw, 'unsafe_execute'):
-            # use unsafe_execute on the repository side, in case
-            # session's user doesn't have access to CWUser
-            execute = self._cw.unsafe_execute
-        else:
-            execute = self._cw.execute
         try:
-            return execute('Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s',
-                           {'x': eid, 'u': self.eid}, 'x')
+            return self._cw.execute(
+                'Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s',
+                {'x': eid, 'u': self.eid}, 'x')
         except Unauthorized:
             return False
     owns = cached(owns, keyarg=1)
--- a/entities/test/unittest_wfobjs.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/entities/test/unittest_wfobjs.py	Tue Mar 09 08:59:43 2010 +0100
@@ -1,5 +1,7 @@
+from __future__ import with_statement
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import ValidationError
+from cubicweb.server.session import security_enabled
 
 def add_wf(self, etype, name=None, default=False):
     if name is None:
@@ -126,10 +128,11 @@
         wf = add_wf(self, 'CWUser')
         s = wf.add_state(u'foo', initial=True)
         self.commit()
-        ex = self.assertRaises(ValidationError, self.session.unsafe_execute,
+        with security_enabled(self.session, write=False):
+            ex = self.assertRaises(ValidationError, self.session.execute,
                                'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
                                {'x': self.user().eid, 's': s.eid}, 'x')
-        self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. "
+            self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. "
                                       "You may want to set a custom workflow for this entity first."})
 
     def test_fire_transition(self):
@@ -505,7 +508,7 @@
                      {'wf': self.wf.eid})
         self.commit()
 
-    # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute
+    # XXX currently, we've to rely on hooks to set initial state, or to use execute
     # def test_initial_state(self):
     #     cnx = self.login('stduser')
     #     cu = cnx.cursor()
--- a/entities/wfobjs.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/entities/wfobjs.py	Tue Mar 09 08:59:43 2010 +0100
@@ -158,7 +158,7 @@
             todelstate = self.state_by_name(todelstate)
         if not hasattr(replacement, 'eid'):
             replacement = self.state_by_name(replacement)
-        execute = self._cw.unsafe_execute
+        execute = self._cw.execute
         execute('SET X in_state S WHERE S eid %(s)s', {'s': todelstate.eid}, 's')
         execute('SET X from_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s',
                 {'os': todelstate.eid, 'ns': replacement.eid}, 's')
--- a/entity.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/entity.py	Tue Mar 09 08:59:43 2010 +0100
@@ -20,6 +20,7 @@
 from cubicweb.rset import ResultSet
 from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
+from cubicweb.req import _check_cw_unsafe
 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
 from cubicweb.rqlrewrite import RQLRewriter
 
@@ -531,8 +532,8 @@
             # if some outer join are included to fetch inlined relations
             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
                                     ','.join(rql))
-            execute = getattr(self._cw, 'unsafe_execute', self._cw.execute)
-            rset = execute(rql, {'x': self.eid}, 'x', build_descr=False)[0]
+            rset = self._cw.execute(rql, {'x': self.eid}, 'x',
+                                    build_descr=False)[0]
             # handle attributes
             for i in xrange(1, lastattr):
                 self[str(selected[i-1][0])] = rset[i]
@@ -560,11 +561,8 @@
             if not self.is_saved():
                 return None
             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
-            # XXX should we really use unsafe_execute here? I think so (syt),
-            # see #344874
-            execute = getattr(self._cw, 'unsafe_execute', self._cw.execute)
             try:
-                rset = execute(rql, {'x': self.eid}, 'x')
+                rset = self._cw.execute(rql, {'x': self.eid}, 'x')
             except Unauthorized:
                 self[name] = value = None
             else:
@@ -595,10 +593,7 @@
             pass
         assert self.has_eid()
         rql = self.related_rql(rtype, role)
-        # XXX should we really use unsafe_execute here? I think so (syt),
-        # see #344874
-        execute = getattr(self._cw, 'unsafe_execute', self._cw.execute)
-        rset = execute(rql, {'x': self.eid}, 'x')
+        rset = self._cw.execute(rql, {'x': self.eid}, 'x')
         self.set_related_cache(rtype, role, rset)
         return self.related(rtype, role, limit, entities)
 
@@ -800,8 +795,9 @@
 
     # raw edition utilities ###################################################
 
-    def set_attributes(self, _cw_unsafe=False, **kwargs):
+    def set_attributes(self, **kwargs):
         assert kwargs
+        _check_cw_unsafe(kwargs)
         relations = []
         for key in kwargs:
             relations.append('X %s %%(%s)s' % (key, key))
@@ -809,25 +805,18 @@
         self.update(kwargs)
         # and now update the database
         kwargs['x'] = self.eid
-        if _cw_unsafe:
-            self._cw.unsafe_execute(
-                'SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x')
-        else:
-            self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
-                             kwargs, 'x')
+        self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
+                         kwargs, 'x')
 
-    def set_relations(self, _cw_unsafe=False, **kwargs):
+    def set_relations(self, **kwargs):
         """add relations to the given object. To set a relation where this entity
         is the object of the relation, use 'reverse_'<relation> as argument name.
 
         Values may be an entity, a list of entity, or None (meaning that all
         relations of the given type from or to this object should be deleted).
         """
-        if _cw_unsafe:
-            execute = self._cw.unsafe_execute
-        else:
-            execute = self._cw.execute
         # XXX update cache
+        _check_cw_unsafe(kwargs)
         for attr, values in kwargs.iteritems():
             if attr.startswith('reverse_'):
                 restr = 'Y %s X' % attr[len('reverse_'):]
@@ -839,14 +828,14 @@
                 continue
             if not isinstance(values, (tuple, list, set, frozenset)):
                 values = (values,)
-            execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
+            self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
                 restr, ','.join(str(r.eid) for r in values)),
-                    {'x': self.eid}, 'x')
+                             {'x': self.eid}, 'x')
 
-    def delete(self):
+    def delete(self, **kwargs):
         assert self.has_eid(), self.eid
         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
-                         {'x': self.eid})
+                         {'x': self.eid}, **kwargs)
 
     # server side utilities ###################################################
 
--- a/goa/appobjects/dbmgmt.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/goa/appobjects/dbmgmt.py	Tue Mar 09 08:59:43 2010 +0100
@@ -172,7 +172,7 @@
     skip_etypes = ('CWGroup', 'CWUser')
 
     def call(self):
-        # XXX should use unsafe_execute with all hooks deactivated
+        # XXX should use unsafe execute with all hooks deactivated
         # XXX step by catching datastore errors?
         for eschema in self.schema.entities():
             if eschema.final or eschema in self.skip_etypes:
--- a/goa/dbinit.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/goa/dbinit.py	Tue Mar 09 08:59:43 2010 +0100
@@ -84,7 +84,7 @@
             Put(gaeentity)
 
 def init_persistent_schema(ssession, schema):
-    execute = ssession.unsafe_execute
+    execute = ssession.execute
     rql = ('INSERT CWEType X: X name %(name)s, X description %(descr)s,'
            'X final FALSE')
     eschema = schema.eschema('CWEType')
@@ -96,7 +96,7 @@
                       'descr': unicode(eschema.description)})
 
 def insert_versions(ssession, config):
-    execute = ssession.unsafe_execute
+    execute = ssession.execute
     # insert versions
     execute('INSERT CWProperty X: X pkey %(pk)s, X value%(v)s',
             {'pk': u'system.version.cubicweb',
--- a/hooks/email.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/hooks/email.py	Tue Mar 09 08:59:43 2010 +0100
@@ -26,7 +26,7 @@
 
     def precommit_event(self):
         if self.condition():
-            self.session.unsafe_execute(
+            self.session.execute(
                 'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype,
                 {'x': self.entity.eid, 'y': self.email.eid}, 'x')
 
--- a/hooks/integrity.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/hooks/integrity.py	Tue Mar 09 08:59:43 2010 +0100
@@ -35,13 +35,12 @@
     RQLUniqueConstraint in two different transactions, as explained in
     http://intranet.logilab.fr/jpl/ticket/36564
     """
-    asession = session.actual_session()
-    if 'uniquecstrholder' in asession.transaction_data:
+    if 'uniquecstrholder' in session.transaction_data:
         return
     _UNIQUE_CONSTRAINTS_LOCK.acquire()
-    asession.transaction_data['uniquecstrholder'] = True
+    session.transaction_data['uniquecstrholder'] = True
     # register operation responsible to release the lock on commit/rollback
-    _ReleaseUniqueConstraintsOperation(asession)
+    _ReleaseUniqueConstraintsOperation(session)
 
 def _release_unique_cstr_lock(session):
     if 'uniquecstrholder' in session.transaction_data:
@@ -69,7 +68,7 @@
             return
         if self.rtype in self.session.transaction_data.get('pendingrtypes', ()):
             return
-        if self.session.unsafe_execute(*self._rql()).rowcount < 1:
+        if self.session.execute(*self._rql()).rowcount < 1:
             etype = self.session.describe(self.eid)[0]
             _ = self.session._
             msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
@@ -99,12 +98,8 @@
     __abstract__ = True
     category = 'integrity'
 
-class UserIntegrityHook(IntegrityHook):
-    __abstract__ = True
-    __select__ = IntegrityHook.__select__ & hook.regular_session()
 
-
-class CheckCardinalityHook(UserIntegrityHook):
+class CheckCardinalityHook(IntegrityHook):
     """check cardinalities are satisfied"""
     __regid__ = 'checkcard'
     events = ('after_add_entity', 'before_delete_relation')
@@ -176,7 +171,7 @@
         pass
 
 
-class CheckConstraintHook(UserIntegrityHook):
+class CheckConstraintHook(IntegrityHook):
     """check the relation satisfy its constraints
 
     this is delayed to a precommit time operation since other relation which
@@ -194,7 +189,7 @@
                                rdef=(self.eidfrom, self.rtype, self.eidto))
 
 
-class CheckAttributeConstraintHook(UserIntegrityHook):
+class CheckAttributeConstraintHook(IntegrityHook):
     """check the attribute relation satisfy its constraints
 
     this is delayed to a precommit time operation since other relation which
@@ -214,7 +209,7 @@
                                         rdef=(self.entity.eid, attr, None))
 
 
-class CheckUniqueHook(UserIntegrityHook):
+class CheckUniqueHook(IntegrityHook):
     __regid__ = 'checkunique'
     events = ('before_add_entity', 'before_update_entity')
 
@@ -227,7 +222,7 @@
                 if val is None:
                     continue
                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
-                rset = self._cw.unsafe_execute(rql, {'val': val})
+                rset = self._cw.execute(rql, {'val': val})
                 if rset and rset[0][0] != entity.eid:
                     msg = self._cw._('the value "%s" is already used, use another one')
                     raise ValidationError(entity.eid, {attr: msg % val})
@@ -244,9 +239,9 @@
         if not (session.deleted_in_transaction(self.eid) or
                 session.added_in_transaction(self.eid)):
             etype = session.describe(self.eid)[0]
-            session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
-                                   % (etype, self.relation),
-                                   {'x': self.eid}, 'x')
+            session.execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
+                            % (etype, self.relation),
+                            {'x': self.eid}, 'x')
 
 
 class DeleteCompositeOrphanHook(IntegrityHook):
@@ -290,7 +285,7 @@
             self.entity['name'] = newname
 
 
-class TidyHtmlFields(UserIntegrityHook):
+class TidyHtmlFields(IntegrityHook):
     """tidy HTML in rich text strings"""
     __regid__ = 'htmltidy'
     events = ('before_add_entity', 'before_update_entity')
--- a/hooks/metadata.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/hooks/metadata.py	Tue Mar 09 08:59:43 2010 +0100
@@ -19,7 +19,7 @@
     # eschema.eid is None if schema has been readen from the filesystem, not
     # from the database (eg during tests)
     if eschema.eid is None:
-        eschema.eid = session.unsafe_execute(
+        eschema.eid = session.execute(
             'Any X WHERE X is CWEType, X name %(name)s',
             {'name': str(eschema)})[0][0]
     return eschema.eid
@@ -103,18 +103,17 @@
     events = ('after_add_entity',)
 
     def __call__(self):
-        asession = self._cw.actual_session()
-        if not asession.is_internal_session:
-            self._cw.add_relation(self.entity.eid, 'owned_by', asession.user.eid)
-            _SetCreatorOp(asession, entity=self.entity)
+        if not self._cw.is_internal_session:
+            self._cw.add_relation(self.entity.eid, 'owned_by', self._cw.user.eid)
+            _SetCreatorOp(self._cw, entity=self.entity)
 
 
 class _SyncOwnersOp(hook.Operation):
     def precommit_event(self):
-        self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
-                                    'NOT EXISTS(X owned_by U, X eid %(x)s)',
-                                    {'c': self.compositeeid, 'x': self.composedeid},
-                                    ('c', 'x'))
+        self.session.execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
+                             'NOT EXISTS(X owned_by U, X eid %(x)s)',
+                             {'c': self.compositeeid, 'x': self.composedeid},
+                             ('c', 'x'))
 
 
 class SyncCompositeOwner(MetaDataHook):
--- a/hooks/notification.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/hooks/notification.py	Tue Mar 09 08:59:43 2010 +0100
@@ -103,7 +103,7 @@
 class EntityUpdateHook(NotificationHook):
     __regid__ = 'notifentityupdated'
     __abstract__ = True # do not register by default
-
+    __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
     events = ('before_update_entity',)
     skip_attrs = set()
 
@@ -111,8 +111,6 @@
         session = self._cw
         if self.entity.eid in session.transaction_data.get('neweids', ()):
             return # entity is being created
-        if session.is_super_session:
-            return # ignore changes triggered by hooks
         # then compute changes
         changes = session.transaction_data.setdefault('changes', {})
         thisentitychanges = changes.setdefault(self.entity.eid, set())
@@ -125,7 +123,7 @@
             rqlsel.append(var)
             rqlrestr.append('X %s %s' % (attr, var))
         rql = 'Any %s WHERE %s' % (','.join(rqlsel), ','.join(rqlrestr))
-        rset = session.unsafe_execute(rql, {'x': self.entity.eid}, 'x')
+        rset = session.execute(rql, {'x': self.entity.eid}, 'x')
         for i, attr in enumerate(attrs):
             oldvalue = rset[0][i]
             newvalue = self.entity[attr]
@@ -139,13 +137,11 @@
 
 class SomethingChangedHook(NotificationHook):
     __regid__ = 'supervising'
+    __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
     events = ('before_add_relation', 'before_delete_relation',
               'after_add_entity', 'before_update_entity')
 
     def __call__(self):
-        # XXX use proper selectors
-        if self._cw.is_super_session or self._cw.repo.config.repairing:
-            return # ignore changes triggered by hooks or maintainance shell
         dest = self._cw.vreg.config['supervising-addrs']
         if not dest: # no supervisors, don't do this for nothing...
             return
--- a/hooks/security.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/hooks/security.py	Tue Mar 09 08:59:43 2010 +0100
@@ -9,6 +9,7 @@
 __docformat__ = "restructuredtext en"
 
 from cubicweb import Unauthorized
+from cubicweb.selectors import objectify_selector, lltrace
 from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
 
 
@@ -53,10 +54,17 @@
         pass
 
 
+@objectify_selector
+@lltrace
+def write_security_enabled(cls, req, **kwargs):
+    if req is None or not req.write_security:
+        return 0
+    return 1
+
 class SecurityHook(hook.Hook):
     __abstract__ = True
     category = 'security'
-    __select__ = hook.Hook.__select__ & hook.regular_session()
+    __select__ = hook.Hook.__select__ & write_security_enabled()
 
 
 class AfterAddEntitySecurityHook(SecurityHook):
--- a/hooks/syncschema.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/hooks/syncschema.py	Tue Mar 09 08:59:43 2010 +0100
@@ -801,7 +801,7 @@
         if name in CORE_ETYPES:
             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
         # delete every entities of this type
-        self._cw.unsafe_execute('DELETE %s X' % name)
+        self._cw.execute('DELETE %s X' % name)
         DropTable(self._cw, table=SQL_PREFIX + name)
         MemSchemaCWETypeDel(self._cw, name)
 
@@ -986,7 +986,7 @@
             if not (subjschema.eid in pendings or objschema.eid in pendings):
                 session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
                                 % (rschema, subjschema, objschema))
-        execute = session.unsafe_execute
+        execute = session.execute
         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
                        'R eid %%(x)s' % rdeftype, {'x': self.eidto})
         lastrel = rset[0][0] == 0
--- a/hooks/workflow.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/hooks/workflow.py	Tue Mar 09 08:59:43 2010 +0100
@@ -19,8 +19,8 @@
     nocheck = session.transaction_data.setdefault('skip-security', set())
     nocheck.add((x, 'in_state', oldstate))
     nocheck.add((x, 'in_state', newstate))
-    # delete previous state first in case we're using a super session,
-    # unless in_state isn't stored in the system source
+    # delete previous state first unless in_state isn't stored in the system
+    # source
     fromsource = session.describe(x)[1]
     if fromsource == 'system' or \
            not session.repo.sources_by_uri[fromsource].support_relation('in_state'):
@@ -42,9 +42,7 @@
                and entity.current_workflow:
             state = entity.current_workflow.initial
             if state:
-                # use super session to by-pass security checks
-                session.super_session.add_relation(entity.eid, 'in_state',
-                                                   state.eid)
+                session.add_relation(entity.eid, 'in_state', state.eid)
 
 
 class _FireAutotransitionOp(hook.Operation):
@@ -122,14 +120,7 @@
             msg = session._('exiting from subworkflow %s')
             msg %= session._(forentity.current_workflow.name)
             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
-            # XXX iirk
-            req = forentity._cw
-            forentity._cw = session.super_session
-            try:
-                trinfo = forentity.change_state(tostate, msg, u'text/plain',
-                                                tr=wftr)
-            finally:
-                forentity._cw = req
+            forentity.change_state(tostate, msg, u'text/plain', tr=wftr)
 
 
 # hooks ########################################################################
@@ -195,7 +186,8 @@
             raise ValidationError(entity.eid, {None: msg})
         # True if we are coming back from subworkflow
         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
-        cowpowers = session.is_super_session or 'managers' in session.user.groups
+        cowpowers = ('managers' in session.user.groups
+                     or not session.write_security)
         # no investigate the requested state change...
         try:
             treid = entity['by_transition']
@@ -266,7 +258,7 @@
 
 
 class CheckInStateChangeAllowed(WorkflowHook):
-    """check state apply, in case of direct in_state change using unsafe_execute
+    """check state apply, in case of direct in_state change using unsafe execute
     """
     __regid__ = 'wfcheckinstate'
     __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
@@ -307,8 +299,7 @@
             return
         entity = self._cw.entity_from_eid(self.eidfrom)
         try:
-            entity.set_attributes(modification_date=datetime.now(),
-                                  _cw_unsafe=True)
+            entity.set_attributes(modification_date=datetime.now())
         except RepositoryError, ex:
             # usually occurs if entity is coming from a read-only source
             # (eg ldap user)
--- a/mail.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/mail.py	Tue Mar 09 08:59:43 2010 +0100
@@ -215,16 +215,9 @@
         """return a list of either 2-uple (email, language) or user entity to
         who this email should be sent
         """
-        # use super_session when available, we don't want to consider security
-        # when selecting recipients_finder
-        try:
-            req = self._cw.super_session
-        except AttributeError:
-            req = self._cw
-        finder = self._cw.vreg['components'].select('recipients_finder', req,
-                                                    rset=self.cw_rset,
-                                                    row=self.cw_row or 0,
-                                                    col=self.cw_col or 0)
+        finder = self._cw.vreg['components'].select(
+            'recipients_finder', self._cw, rset=self.cw_rset,
+            row=self.cw_row or 0, col=self.cw_col or 0)
         return finder.recipients()
 
     def send_now(self, recipients, msg):
--- a/misc/migration/bootstrapmigration_repository.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Tue Mar 09 08:59:43 2010 +0100
@@ -20,8 +20,7 @@
 if applcubicwebversion == (3, 6, 0) and cubicwebversion >= (3, 6, 0):
     _add_relation_definition_no_perms('CWAttribute', 'update_permission', 'CWGroup')
     _add_relation_definition_no_perms('CWAttribute', 'update_permission', 'RQLExpression')
-    session.set_pool()
-    session.unsafe_execute('SET X update_permission Y WHERE X is CWAttribute, X add_permission Y')
+    rql('SET X update_permission Y WHERE X is CWAttribute, X add_permission Y')
     drop_relation_definition('CWAttribute', 'add_permission', 'CWGroup')
     drop_relation_definition('CWAttribute', 'add_permission', 'RQLExpression')
     drop_relation_definition('CWAttribute', 'delete_permission', 'CWGroup')
@@ -29,7 +28,6 @@
 
 elif applcubicwebversion < (3, 6, 0) and cubicwebversion >= (3, 6, 0):
     session.set_pool()
-    session.execute = session.unsafe_execute
     permsdict = ss.deserialize_ertype_permissions(session)
 
     changes = session.disable_hooks_category.add('integrity')
@@ -81,13 +79,11 @@
     deactivate_verification_hooks()
     add_relation_type('cwuri')
     base_url = session.base_url()
-    # use an internal session since some entity might forbid modifications to admin
-    isession = repo.internal_session()
     for eid, in rql('Any X', ask_confirm=False):
         type, source, extid = session.describe(eid)
         if source == 'system':
-            isession.execute('SET X cwuri %(u)s WHERE X eid %(x)s',
-                             {'x': eid, 'u': base_url + u'eid/%s' % eid})
+            rql('SET X cwuri %(u)s WHERE X eid %(x)s',
+                {'x': eid, 'u': base_url + u'eid/%s' % eid})
     isession.commit()
     reactivate_verification_hooks()
     session.set_shared_data('do-not-insert-cwuri', False)
--- a/misc/migration/postcreate.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/misc/migration/postcreate.py	Tue Mar 09 08:59:43 2010 +0100
@@ -42,8 +42,8 @@
 
 # need this since we already have at least one user in the database (the default admin)
 for user in rql('Any X WHERE X is CWUser').entities():
-    session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                           {'x': user.eid, 's': activated.eid}, 'x')
+    rql('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
+        {'x': user.eid, 's': activated.eid}, 'x')
 
 # on interactive mode, ask for level 0 persistent options
 if interactive_mode:
--- a/req.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/req.py	Tue Mar 09 08:59:43 2010 +0100
@@ -22,6 +22,12 @@
 CACHE_REGISTRY = {}
 
 
+def _check_cw_unsafe(kwargs):
+    if kwargs.pop('_cw_unsafe', False):
+        warn('[3.7] _cw_unsafe argument is deprecated, now unsafe by '
+             'default, control it using cw_[read|write]_security.',
+             DeprecationWarning, stacklevel=3)
+
 class Cache(dict):
     def __init__(self):
         super(Cache, self).__init__()
@@ -110,19 +116,18 @@
     # XXX move to CWEntityManager or even better as factory method (unclear
     # where yet...)
 
-    def create_entity(self, etype, _cw_unsafe=False, **kwargs):
+    def create_entity(self, etype, **kwargs):
         """add a new entity of the given type
 
         Example (in a shell session):
 
-        c = create_entity('Company', name=u'Logilab')
-        create_entity('Person', works_for=c, firstname=u'John', lastname=u'Doe')
+        >>> c = create_entity('Company', name=u'Logilab')
+        >>> create_entity('Person', firstname=u'John', lastname=u'Doe',
+        ...               works_for=c)
 
         """
-        if _cw_unsafe:
-            execute = self.unsafe_execute
-        else:
-            execute = self.execute
+        _check_cw_unsafe(kwargs)
+        execute = self.execute
         rql = 'INSERT %s X' % etype
         relations = []
         restrictions = set()
@@ -162,7 +167,7 @@
                 restr = 'X %s Y' % attr
             execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
                 restr, ','.join(str(r.eid) for r in values)),
-                         {'x': created.eid}, 'x')
+                    {'x': created.eid}, 'x', build_descr=False)
         return created
 
     def ensure_ro_rql(self, rql):
@@ -281,7 +286,7 @@
             userinfo['name'] = "cubicweb"
             userinfo['email'] = ""
             return userinfo
-        user = self.actual_session().user
+        user = self.user
         userinfo['login'] = user.login
         userinfo['name'] = user.name()
         userinfo['email'] = user.get_email()
--- a/schema.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/schema.py	Tue Mar 09 08:59:43 2010 +0100
@@ -704,7 +704,7 @@
         rql = 'Any %s WHERE %s' % (self.mainvars,  restriction)
         if self.distinct_query:
             rql = 'DISTINCT ' + rql
-        return session.unsafe_execute(rql, args, ck, build_descr=False)
+        return session.execute(rql, args, ck, build_descr=False)
 
 
 class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint):
@@ -830,13 +830,10 @@
                 return True
             return False
         if keyarg is None:
-            # on the server side, use unsafe_execute, but this is not available
-            # on the client side (session is actually a request)
-            execute = getattr(session, 'unsafe_execute', session.execute)
             kwargs.setdefault('u', session.user.eid)
             cachekey = kwargs.keys()
             try:
-                rset = execute(rql, kwargs, cachekey, build_descr=True)
+                rset = session.execute(rql, kwargs, cachekey, build_descr=True)
             except NotImplementedError:
                 self.critical('cant check rql expression, unsupported rql %s', rql)
                 if self.eid is not None:
@@ -1084,9 +1081,9 @@
     elif form is not None:
         cw = form._cw
     if cw is not None:
-        if hasattr(cw, 'is_super_session'):
+        if hasattr(cw, 'write_security'): # test it's a session and not a request
             # cw is a server session
-            hasperm = cw.is_super_session or \
+            hasperm = not cw.write_security or \
                       not cw.is_hooks_category_activated('integrity') or \
                       cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT)
         else:
--- a/server/checkintegrity.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/checkintegrity.py	Tue Mar 09 08:59:43 2010 +0100
@@ -6,6 +6,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
@@ -15,6 +17,7 @@
 
 from cubicweb.schema import PURE_VIRTUAL_RTYPES
 from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.server.session import security_enabled
 
 def has_eid(sqlcursor, eid, eids):
     """return true if the eid is a valid eid"""
@@ -277,9 +280,10 @@
     # yo, launch checks
     if checks:
         eids_cache = {}
-        for check in checks:
-            check_func = globals()['check_%s' % check]
-            check_func(repo.schema, session, eids_cache, fix=fix)
+        with security_enabled(session, read=False): # ensure no read security
+            for check in checks:
+                check_func = globals()['check_%s' % check]
+                check_func(repo.schema, session, eids_cache, fix=fix)
         if fix:
             cnx.commit()
         else:
--- a/server/hook.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/hook.py	Tue Mar 09 08:59:43 2010 +0100
@@ -33,6 +33,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"
 
 from warnings import warn
@@ -47,7 +49,7 @@
 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
                                 implements)
 from cubicweb.appobject import AppObject
-
+from cubicweb.server.session import security_enabled
 
 ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
                       'before_update_entity', 'after_update_entity',
@@ -76,13 +78,20 @@
                     event, obj.__module__, obj.__name__))
         super(HooksRegistry, self).register(obj, **kwargs)
 
-    def call_hooks(self, event, req=None, **kwargs):
+    def call_hooks(self, event, session=None, **kwargs):
         kwargs['event'] = event
-        for hook in sorted(self.possible_objects(req, **kwargs), key=lambda x: x.order):
-            if hook.enabled:
+        if session is None:
+            for hook in sorted(self.possible_objects(session, **kwargs),
+                               key=lambda x: x.order):
                 hook()
-            else:
-                warn('[3.6] %s: enabled is deprecated' % hook.__class__)
+        else:
+            # by default, hooks are executed with security turned off
+            with security_enabled(session, read=False):
+                hooks = sorted(self.possible_objects(session, **kwargs),
+                               key=lambda x: x.order)
+                with security_enabled(session, write=False):
+                    for hook in hooks:
+                        hook()
 
 VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
 
@@ -104,6 +113,14 @@
 
 @objectify_selector
 @lltrace
+def _bw_is_enabled(cls, req, **kwargs):
+    if cls.enabled:
+        return 1
+    warn('[3.6] %s: enabled is deprecated' % cls)
+    return 0
+
+@objectify_selector
+@lltrace
 def match_event(cls, req, **kwargs):
     if kwargs.get('event') in cls.events:
         return 1
@@ -118,11 +135,10 @@
 
 @objectify_selector
 @lltrace
-def regular_session(cls, req, **kwargs):
-    if req is None or req.is_super_session:
-        return 0
-    return 1
-
+def from_dbapi_query(cls, req, **kwargs):
+    if req.running_dbapi_query:
+        return 1
+    return 0
 
 class rechain(object):
     def __init__(self, *iterators):
@@ -175,7 +191,7 @@
 
 class Hook(AppObject):
     __registry__ = 'hooks'
-    __select__ = match_event() & enabled_category()
+    __select__ = match_event() & enabled_category() & _bw_is_enabled()
     # set this in derivated classes
     events = None
     category = None
@@ -260,7 +276,7 @@
         else:
             assert self.rtype in self.object_relations
             meid, seid = self.eidto, self.eidfrom
-        self._cw.unsafe_execute(
+        self._cw.execute(
             'SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'\
             % (self.main_rtype, self.main_rtype, self.main_rtype),
             {'x': meid, 'e': seid}, ('x', 'e'))
@@ -278,7 +294,7 @@
 
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
-        execute = self._cw.unsafe_execute
+        execute = self._cw.execute
         for rel in self.subject_relations:
             if rel in eschema.subjrels:
                 execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
@@ -303,7 +319,7 @@
 
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
-        execute = self._cw.unsafe_execute
+        execute = self._cw.execute
         for rel in self.subject_relations:
             if rel in eschema.subjrels:
                 execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
@@ -507,6 +523,6 @@
 
 class RQLPrecommitOperation(Operation):
     def precommit_event(self):
-        execute = self.session.unsafe_execute
+        execute = self.session.execute
         for rql in self.rqls:
             execute(*rql)
--- a/server/migractions.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/migractions.py	Tue Mar 09 08:59:43 2010 +0100
@@ -243,17 +243,26 @@
     @property
     def session(self):
         if self.config is not None:
-            return self.repo._get_session(self.cnx.sessionid)
+            session = self.repo._get_session(self.cnx.sessionid)
+            if session.pool is None:
+                session.set_read_security(False)
+                session.set_write_security(False)
+            session.set_pool()
+            return session
         # no access to session on remote instance
         return None
 
     def commit(self):
         if hasattr(self, '_cnx'):
             self._cnx.commit()
+        if self.session:
+            self.session.set_pool()
 
     def rollback(self):
         if hasattr(self, '_cnx'):
             self._cnx.rollback()
+        if self.session:
+            self.session.set_pool()
 
     def rqlexecall(self, rqliter, cachekey=None, ask_confirm=True):
         for rql, kwargs in rqliter:
@@ -313,7 +322,6 @@
                     self.cmd_reactivate_verification_hooks()
 
     def install_custom_sql_scripts(self, directory, driver):
-        self.session.set_pool() # ensure pool is set
         for fpath in glob(osp.join(directory, '*.sql.%s' % driver)):
             newname = osp.basename(fpath).replace('.sql.%s' % driver,
                                                   '.%s.sql' % driver)
@@ -698,10 +706,7 @@
         groupmap = self.group_mapping()
         cstrtypemap = self.cstrtype_mapping()
         # register the entity into CWEType
-        try:
-            execute = self._cw.unsafe_execute
-        except AttributeError:
-            execute = self._cw.execute
+        execute = self._cw.execute
         ss.execschemarql(execute, eschema, ss.eschema2rql(eschema, groupmap))
         # add specializes relation if needed
         self.rqlexecall(ss.eschemaspecialize2rql(eschema), ask_confirm=confirm)
@@ -842,10 +847,7 @@
         """
         reposchema = self.repo.schema
         rschema = self.fs_schema.rschema(rtype)
-        try:
-            execute = self._cw.unsafe_execute
-        except AttributeError:
-            execute = self._cw.execute
+        execute = self._cw.execute
         # register the relation into CWRType and insert necessary relation
         # definitions
         ss.execschemarql(execute, rschema, ss.rschema2rql(rschema, addrdef=False))
@@ -905,10 +907,7 @@
         rschema = self.fs_schema.rschema(rtype)
         if not rtype in self.repo.schema:
             self.cmd_add_relation_type(rtype, addrdef=False, commit=True)
-        try:
-            execute = self._cw.unsafe_execute
-        except AttributeError:
-            execute = self._cw.execute
+        execute = self._cw.execute
         rdef = self._get_rdef(rschema, subjtype, objtype)
         ss.execschemarql(execute, rdef,
                          ss.rdef2rql(rdef, self.cstrtype_mapping(),
@@ -1184,7 +1183,6 @@
         level actions
         """
         if not ask_confirm or self.confirm('Execute sql: %s ?' % sql):
-            self.session.set_pool() # ensure pool is set
             try:
                 cu = self.session.system_sql(sql, args)
             except:
@@ -1203,10 +1201,7 @@
         if not isinstance(rql, (tuple, list)):
             rql = ( (rql, kwargs), )
         res = None
-        try:
-            execute = self._cw.unsafe_execute
-        except AttributeError:
-            execute = self._cw.execute
+        execute = self._cw.execute
         for rql, kwargs in rql:
             if kwargs:
                 msg = '%s (%s)' % (rql, kwargs)
--- a/server/querier.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/querier.py	Tue Mar 09 08:59:43 2010 +0100
@@ -6,6 +6,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"
 
 from itertools import repeat
@@ -23,7 +25,7 @@
 from cubicweb.server.utils import cleanup_solutions
 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
 from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
-
+from cubicweb.server.session import security_enabled
 
 def empty_rset(rql, args, rqlst=None):
     """build an empty result set object"""
@@ -200,8 +202,11 @@
         return rqlst to actually execute
         """
         noinvariant = set()
-        if security and not self.session.is_super_session:
-            self._insert_security(union, noinvariant)
+        if security and self.session.read_security:
+            # ensure security is turned of when security is inserted,
+            # else we may loop for ever...
+            with security_enabled(self.session, read=False):
+                self._insert_security(union, noinvariant)
         self.rqlhelper.simplify(union)
         self.sqlannotate(union)
         set_qdata(self.schema.rschema, union, noinvariant)
@@ -299,7 +304,6 @@
 
         note: rqlst should not have been simplified at this point
         """
-        assert not self.session.is_super_session
         user = self.session.user
         schema = self.schema
         msgs = []
@@ -601,14 +605,14 @@
             self._rql_cache[cachekey] = rqlst
         orig_rqlst = rqlst
         if not rqlst.TYPE == 'select':
-            if not session.is_super_session:
+            if session.read_security:
                 check_no_password_selected(rqlst)
             # write query, ensure session's mode is 'write' so connections
             # won't be released until commit/rollback
             session.mode = 'write'
             cachekey = None
         else:
-            if not session.is_super_session:
+            if session.read_security:
                 for select in rqlst.children:
                     check_no_password_selected(select)
             # on select query, always copy the cached rqlst so we don't have to
--- a/server/repository.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/repository.py	Tue Mar 09 08:59:43 2010 +0100
@@ -15,6 +15,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
@@ -36,7 +38,7 @@
                       typed_eid)
 from cubicweb import cwvreg, schema, server
 from cubicweb.server import utils, hook, pool, querier, sources
-from cubicweb.server.session import Session, InternalSession
+from cubicweb.server.session import Session, InternalSession, security_enabled
 
 
 class CleanupEidTypeCacheOp(hook.SingleLastOperation):
@@ -80,10 +82,10 @@
     this kind of behaviour has to be done in the repository so we don't have
     hooks order hazardness
     """
-    # XXX now that rql in migraction default to unsafe_execute we don't want to
-    #     skip that for super session (though we can still skip it for internal
-    #     sessions). Also we should imo rely on the orm to first fetch existing
-    #     entity if any then delete it.
+    # skip that for internal session or if integrity explicitly disabled
+    #
+    # XXX we should imo rely on the orm to first fetch existing entity if any
+    # then delete it.
     if session.is_internal_session \
            or not session.is_hooks_category_activated('integrity'):
         return
@@ -100,23 +102,15 @@
     rschema = session.repo.schema.rschema(rtype)
     if card[0] in '1?':
         if not rschema.inlined: # inlined relations will be implicitly deleted
-            rset = session.unsafe_execute('Any X,Y WHERE X %s Y, X eid %%(x)s, '
-                                          'NOT Y eid %%(y)s' % rtype,
-                                          {'x': eidfrom, 'y': eidto}, 'x')
-            if rset:
-                safe_delete_relation(session, rschema, *rset[0])
+            with security_enabled(session, read=False):
+                session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
+                                'NOT Y eid %%(y)s' % rtype,
+                                {'x': eidfrom, 'y': eidto}, 'x')
     if card[1] in '1?':
-        rset = session.unsafe_execute('Any X,Y WHERE X %s Y, Y eid %%(y)s, '
-                                      'NOT X eid %%(x)s' % rtype,
-                                      {'x': eidfrom, 'y': eidto}, 'y')
-        if rset:
-            safe_delete_relation(session, rschema, *rset[0])
-
-
-def safe_delete_relation(session, rschema, subject, object):
-    if not rschema.has_perm(session, 'delete', fromeid=subject, toeid=object):
-        raise Unauthorized()
-    session.repo.glob_delete_relation(session, subject, rschema.type, object)
+        with security_enabled(session, read=False):
+            session.execute('DELETE X %sY WHERE Y eid %%(y)s, '
+                            'NOT X eid %%(x)s' % rtype,
+                            {'x': eidfrom, 'y': eidto}, 'y')
 
 
 class Repository(object):
@@ -918,21 +912,22 @@
         rql = []
         eschema = self.schema.eschema(etype)
         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
-        for rschema, targetschemas, x in eschema.relation_definitions():
-            rtype = rschema.type
-            if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
-                continue
-            var = '%s%s' % (rtype.upper(), x.upper())
-            if x == 'subject':
-                # don't skip inlined relation so they are regularly
-                # deleted and so hooks are correctly called
-                selection = 'X %s %s' % (rtype, var)
-            else:
-                selection = '%s %s X' % (var, rtype)
-            rql = 'DELETE %s WHERE X eid %%(x)s' % selection
-            # unsafe_execute since we suppose that if user can delete the entity,
-            # he can delete all its relations without security checking
-            session.unsafe_execute(rql, {'x': eid}, 'x', build_descr=False)
+        with security_enabled(session, read=False, write=False):
+            for rschema, targetschemas, x in eschema.relation_definitions():
+                rtype = rschema.type
+                if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
+                    continue
+                var = '%s%s' % (rtype.upper(), x.upper())
+                if x == 'subject':
+                    # don't skip inlined relation so they are regularly
+                    # deleted and so hooks are correctly called
+                    selection = 'X %s %s' % (rtype, var)
+                else:
+                    selection = '%s %s X' % (var, rtype)
+                rql = 'DELETE %s WHERE X eid %%(x)s' % selection
+                # if user can delete the entity, he can delete all its relations
+                # without security checking
+                session.execute(rql, {'x': eid}, 'x', build_descr=False)
 
     def locate_relation_source(self, session, subject, rtype, object):
         subjsource = self.source_from_eid(subject, session)
--- a/server/schemaserial.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/schemaserial.py	Tue Mar 09 08:59:43 2010 +0100
@@ -218,7 +218,7 @@
     if not quiet:
         _title = '-> storing the schema in the database '
         print _title,
-    execute = cursor.unsafe_execute
+    execute = cursor.execute
     eschemas = schema.entities()
     if not quiet:
         pb_size = (len(eschemas + schema.relations())
--- 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 <eid> 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>_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>_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):
--- a/server/ssplanner.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/ssplanner.py	Tue Mar 09 08:59:43 2010 +0100
@@ -63,6 +63,7 @@
         return {}
     eidconsts = {}
     neweids = session.transaction_data.get('neweids', ())
+    checkread = session.read_security
     for rel in rqlst.where.get_nodes(Relation):
         if rel.r_type == 'eid' and not rel.neged(strict=True):
             lhs, rhs = rel.get_variable_parts()
@@ -71,7 +72,7 @@
                 # check read permission here since it may not be done by
                 # the generated select substep if not emited (eg nothing
                 # to be selected)
-                if eid not in neweids:
+                if checkread and eid not in neweids:
                     eschema(session.describe(eid)[0]).check_perm(session, 'read')
                 eidconsts[lhs.variable] = eid
     return eidconsts
--- a/server/test/unittest_hook.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/test/unittest_hook.py	Tue Mar 09 08:59:43 2010 +0100
@@ -110,6 +110,8 @@
         self.o.register(AddAnyHook)
         dis = set()
         cw = mock_object(vreg=self.vreg,
+                         set_read_security=lambda *a,**k: None,
+                         set_write_security=lambda *a,**k: None,
                          is_hook_activated=lambda x, cls: cls.category not in dis)
         self.assertRaises(HookCalled,
                           self.o.call_hooks, 'before_add_entity', cw)
--- a/server/test/unittest_repository.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/server/test/unittest_repository.py	Tue Mar 09 08:59:43 2010 +0100
@@ -184,7 +184,9 @@
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
         # rollback state change which trigger TrInfo insertion
-        user = repo._get_session(cnxid).user
+        session = repo._get_session(cnxid)
+        session.set_pool()
+        user = session.user
         user.fire_transition('deactivate')
         rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
         self.assertEquals(len(rset), 1)
--- a/sobjects/notification.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/sobjects/notification.py	Tue Mar 09 08:59:43 2010 +0100
@@ -33,11 +33,9 @@
     def recipients(self):
         mode = self._cw.vreg.config['default-recipients-mode']
         if mode == 'users':
-            # use unsafe execute else we may don't have the right to see users
-            # to notify...
-            execute = self._cw.unsafe_execute
+            execute = self._cw.execute
             dests = [(u.get_email(), u.property_value('ui.language'))
-                     for u in execute(self.user_rql, build_descr=True, propagate=True).entities()]
+                     for u in execute(self.user_rql, build_descr=True).entities()]
         elif mode == 'default-dest-addrs':
             lang = self._cw.vreg.property_value('ui.language')
             dests = zip(self._cw.vreg.config['default-dest-addrs'], repeat(lang))
@@ -158,7 +156,8 @@
                 if not rdef.has_perm(self._cw, 'read', eid=self.cw_rset[0][0]):
                     continue
             # XXX suppose it's a subject relation...
-            elif not rschema.has_perm(self._cw, 'read', fromeid=self.cw_rset[0][0]): # XXX toeid
+            elif not rschema.has_perm(self._cw, 'read',
+                                      fromeid=self.cw_rset[0][0]):
                 continue
             if attr in self.no_detailed_change_attrs:
                 msg = _('%s updated') % _(attr)
--- a/sobjects/supervising.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/sobjects/supervising.py	Tue Mar 09 08:59:43 2010 +0100
@@ -92,7 +92,7 @@
         return self._cw._('[%s supervision] changes summary') % self._cw.vreg.config.appid
 
     def call(self, changes):
-        user = self._cw.actual_session().user
+        user = self._cw.user
         self.w(self._cw._('user %s has made the following change(s):\n\n')
                % user.login)
         for event, changedescr in filter_changes(changes):
@@ -129,8 +129,8 @@
         self.w(u'  %s' % entity.absolute_url())
 
     def _relation_context(self, changedescr):
-        _ = self._cw._
-        session = self._cw.actual_session()
+        session = self._cw
+        _ = session._cw._
         def describe(eid):
             try:
                 return _(session.describe(eid)[0]).lower()
--- a/test/unittest_entity.py	Mon Mar 08 19:02:35 2010 +0100
+++ b/test/unittest_entity.py	Tue Mar 09 08:59:43 2010 +0100
@@ -436,7 +436,7 @@
 
     def test_complete_relation(self):
         session = self.session
-        eid = session.unsafe_execute(
+        eid = session.execute(
             'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 '
             'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0]
         trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x')