cubicweb/server/session.py
changeset 11057 0b59724cb3f2
parent 10752 c69f109293f2
child 11195 5de859b95988
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/session.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1141 @@
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Repository users' and internal' sessions."""
+from __future__ import print_function
+
+__docformat__ = "restructuredtext en"
+
+import sys
+from time import time
+from uuid import uuid4
+from warnings import warn
+import functools
+from contextlib import contextmanager
+
+from six import text_type
+
+from logilab.common.deprecation import deprecated
+from logilab.common.textutils import unormalize
+from logilab.common.registry import objectify_predicate
+
+from cubicweb import QueryError, schema, server, ProgrammingError
+from cubicweb.req import RequestSessionBase
+from cubicweb.utils import make_uid
+from cubicweb.rqlrewrite import RQLRewriter
+from cubicweb.server.edition import EditedEntity
+
+
+NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy()
+NO_UNDO_TYPES.add('CWCache')
+# is / is_instance_of are usually added by sql hooks except when using
+# dataimport.NoHookRQLObjectStore, and we don't want to record them
+# anyway in the later case
+NO_UNDO_TYPES.add('is')
+NO_UNDO_TYPES.add('is_instance_of')
+NO_UNDO_TYPES.add('cw_source')
+# XXX rememberme,forgotpwd,apycot,vcsfile
+
+@objectify_predicate
+def is_user_session(cls, req, **kwargs):
+    """return 1 when session is not internal.
+
+    This predicate can only be used repository side only. """
+    return not req.is_internal_session
+
+@objectify_predicate
+def is_internal_session(cls, req, **kwargs):
+    """return 1 when session is not internal.
+
+    This predicate can only be used repository side only. """
+    return req.is_internal_session
+
+@objectify_predicate
+def repairing(cls, req, **kwargs):
+    """return 1 when repository is running in repair mode"""
+    return req.vreg.config.repairing
+
+
+@deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
+def hooks_control(obj, mode, *categories):
+    assert mode in  (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
+    if mode == HOOKS_ALLOW_ALL:
+        return obj.allow_all_hooks_but(*categories)
+    elif mode == HOOKS_DENY_ALL:
+        return obj.deny_all_hooks_but(*categories)
+
+
+class _hooks_control(object):
+    """context manager to control activated hooks categories.
+
+    If mode is `HOOKS_DENY_ALL`, given hooks categories will
+    be enabled.
+
+    If mode is `HOOKS_ALLOW_ALL`, given hooks categories will
+    be disabled.
+
+    .. sourcecode:: python
+
+       with _hooks_control(cnx, HOOKS_ALLOW_ALL, 'integrity'):
+           # ... do stuff with all but 'integrity' hooks activated
+
+       with _hooks_control(cnx, HOOKS_DENY_ALL, 'integrity'):
+           # ... do stuff with none but 'integrity' hooks activated
+
+    This is an internal API, you should rather use
+    :meth:`~cubicweb.server.session.Connection.deny_all_hooks_but` or
+    :meth:`~cubicweb.server.session.Connection.allow_all_hooks_but`
+    Connection methods.
+    """
+    def __init__(self, cnx, mode, *categories):
+        assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
+        self.cnx = cnx
+        self.mode = mode
+        self.categories = categories
+        self.oldmode = None
+        self.changes = ()
+
+    def __enter__(self):
+        self.oldmode = self.cnx.hooks_mode
+        self.cnx.hooks_mode = self.mode
+        if self.mode is HOOKS_DENY_ALL:
+            self.changes = self.cnx.enable_hook_categories(*self.categories)
+        else:
+            self.changes = self.cnx.disable_hook_categories(*self.categories)
+        self.cnx.ctx_count += 1
+
+    def __exit__(self, exctype, exc, traceback):
+        self.cnx.ctx_count -= 1
+        try:
+            if self.categories:
+                if self.mode is HOOKS_DENY_ALL:
+                    self.cnx.disable_hook_categories(*self.categories)
+                else:
+                    self.cnx.enable_hook_categories(*self.categories)
+        finally:
+            self.cnx.hooks_mode = self.oldmode
+
+
+@deprecated('[3.17] use <object>.security_enabled instead')
+def security_enabled(obj, *args, **kwargs):
+    return obj.security_enabled(*args, **kwargs)
+
+class _security_enabled(object):
+    """context manager to control security w/ session.execute,
+
+    By default security is disabled on queries executed on the repository
+    side.
+    """
+    def __init__(self, cnx, read=None, write=None):
+        self.cnx = cnx
+        self.read = read
+        self.write = write
+        self.oldread = None
+        self.oldwrite = None
+
+    def __enter__(self):
+        if self.read is None:
+            self.oldread = None
+        else:
+            self.oldread = self.cnx.read_security
+            self.cnx.read_security = self.read
+        if self.write is None:
+            self.oldwrite = None
+        else:
+            self.oldwrite = self.cnx.write_security
+            self.cnx.write_security = self.write
+        self.cnx.ctx_count += 1
+
+    def __exit__(self, exctype, exc, traceback):
+        self.cnx.ctx_count -= 1
+        if self.oldread is not None:
+            self.cnx.read_security = self.oldread
+        if self.oldwrite is not None:
+            self.cnx.write_security = self.oldwrite
+
+HOOKS_ALLOW_ALL = object()
+HOOKS_DENY_ALL = object()
+DEFAULT_SECURITY = object() # evaluated to true by design
+
+class SessionClosedError(RuntimeError):
+    pass
+
+
+def _open_only(func):
+    """decorator for Connection method that check it is open"""
+    @functools.wraps(func)
+    def check_open(cnx, *args, **kwargs):
+        if not cnx._open:
+            raise ProgrammingError('Closed Connection: %s'
+                                    % cnx.connectionid)
+        return func(cnx, *args, **kwargs)
+    return check_open
+
+
+class Connection(RequestSessionBase):
+    """Repository Connection
+
+    Holds all connection related data
+
+    Database connection resources:
+
+      :attr:`hooks_in_progress`, boolean flag telling if the executing
+      query is coming from a repoapi connection or is a query from
+      within the repository (e.g. started by hooks)
+
+      :attr:`cnxset`, the connections set to use to execute queries on sources.
+      If the transaction is read only, the connection set may be freed between
+      actual queries. This allows multiple connections with a reasonably low
+      connection set pool size.  Control mechanism is detailed below.
+
+    .. automethod:: cubicweb.server.session.Connection.set_cnxset
+    .. automethod:: cubicweb.server.session.Connection.free_cnxset
+
+      :attr:`mode`, string telling the connections set handling mode, may be one
+      of 'read' (connections set may be freed), 'write' (some write was done in
+      the connections set, it can't be freed before end of the transaction),
+      'transaction' (we want to keep the connections set during all the
+      transaction, with or without writing)
+
+    Shared data:
+
+      :attr:`data` is a dictionary bound to the underlying session,
+      who will be present for the life time of the session. This may
+      be useful for web clients that rely on the server for managing
+      bits of session-scoped data.
+
+      :attr:`transaction_data` is a dictionary cleared at the end of
+      the transaction. Hooks and operations may put arbitrary data in
+      there.
+
+    Internal state:
+
+      :attr:`pending_operations`, ordered list of operations to be processed on
+      commit/rollback
+
+      :attr:`commit_state`, describing the transaction commit state, may be one
+      of None (not yet committing), 'precommit' (calling precommit event on
+      operations), 'postcommit' (calling postcommit event on operations),
+      'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
+      has been raised during the transaction and so it must be rolled back).
+
+    Hooks controls:
+
+      :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
+
+      :attr:`enabled_hook_cats`, when :attr:`hooks_mode` is
+      `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
+
+      :attr:`disabled_hook_cats`, when :attr:`hooks_mode` is
+      `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
+
+    Security level Management:
+
+      :attr:`read_security` and :attr:`write_security`, boolean flags telling if
+      read/write security is currently activated.
+
+    """
+    is_request = False
+    hooks_in_progress = False
+    is_repo_in_memory = True # bw compat
+
+    def __init__(self, session):
+        # using super(Connection, self) confuse some test hack
+        RequestSessionBase.__init__(self, session.vreg)
+        #: connection unique id
+        self._open = None
+        self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex)
+        self.session = session
+        self.sessionid = session.sessionid
+        #: reentrance handling
+        self.ctx_count = 0
+
+        #: server.Repository object
+        self.repo = session.repo
+        self.vreg = self.repo.vreg
+        self._execute = self.repo.querier.execute
+
+        # other session utility
+        self._session_timestamp = session._timestamp
+
+        # internal (root) session
+        self.is_internal_session = isinstance(session.user, InternalManager)
+
+        #: dict containing arbitrary data cleared at the end of the transaction
+        self.transaction_data = {}
+        self._session_data = session.data
+        #: ordered list of operations to be processed on commit/rollback
+        self.pending_operations = []
+        #: (None, 'precommit', 'postcommit', 'uncommitable')
+        self.commit_state = None
+
+        ### hook control attribute
+        self.hooks_mode = HOOKS_ALLOW_ALL
+        self.disabled_hook_cats = set()
+        self.enabled_hook_cats = set()
+        self.pruned_hooks_cache = {}
+
+
+        ### security control attributes
+        self._read_security = DEFAULT_SECURITY # handled by a property
+        self.write_security = DEFAULT_SECURITY
+
+        # undo control
+        config = session.repo.config
+        if config.creating or config.repairing or self.is_internal_session:
+            self.undo_actions = False
+        else:
+            self.undo_actions = config['undo-enabled']
+
+        # RQLRewriter are not thread safe
+        self._rewriter = RQLRewriter(self)
+
+        # other session utility
+        if session.user.login == '__internal_manager__':
+            self.user = session.user
+            self.set_language(self.user.prefered_language())
+        else:
+            self._set_user(session.user)
+
+    @_open_only
+    def source_defs(self):
+        """Return the definition of sources used by the repository."""
+        return self.session.repo.source_defs()
+
+    @_open_only
+    def get_schema(self):
+        """Return the schema currently used by the repository."""
+        return self.session.repo.source_defs()
+
+    @_open_only
+    def get_option_value(self, option):
+        """Return the value for `option` in the configuration."""
+        return self.session.repo.get_option_value(option)
+
+    # transaction api
+
+    @_open_only
+    def undoable_transactions(self, ueid=None, **actionfilters):
+        """Return a list of undoable transaction objects by the connection's
+        user, ordered by descendant transaction time.
+
+        Managers may filter according to user (eid) who has done the transaction
+        using the `ueid` argument. Others will only see their own transactions.
+
+        Additional filtering capabilities is provided by using the following
+        named arguments:
+
+        * `etype` to get only transactions creating/updating/deleting entities
+          of the given type
+
+        * `eid` to get only transactions applied to entity of the given eid
+
+        * `action` to get only transactions doing the given action (action in
+          'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
+          'D'.
+
+        * `public`: when additional filtering is provided, they are by default
+          only searched in 'public' actions, unless a `public` argument is given
+          and set to false.
+        """
+        return self.repo.system_source.undoable_transactions(self, ueid,
+                                                             **actionfilters)
+
+    @_open_only
+    def transaction_info(self, txuuid):
+        """Return transaction object for the given uid.
+
+        raise `NoSuchTransaction` if not found or if session's user is
+        not allowed (eg not in managers group and the transaction
+        doesn't belong to him).
+        """
+        return self.repo.system_source.tx_info(self, txuuid)
+
+    @_open_only
+    def transaction_actions(self, txuuid, public=True):
+        """Return an ordered list of actions effectued during that transaction.
+
+        If public is true, return only 'public' actions, i.e. not ones
+        triggered under the cover by hooks, else return all actions.
+
+        raise `NoSuchTransaction` if the transaction is not found or
+        if the user is not allowed (eg not in managers group).
+        """
+        return self.repo.system_source.tx_actions(self, txuuid, public)
+
+    @_open_only
+    def undo_transaction(self, txuuid):
+        """Undo the given transaction. Return potential restoration errors.
+
+        raise `NoSuchTransaction` if not found or if user is not
+        allowed (eg not in managers group).
+        """
+        return self.repo.system_source.undo_transaction(self, txuuid)
+
+    # life cycle handling ####################################################
+
+    def __enter__(self):
+        assert self._open is None # first opening
+        self._open = True
+        self.cnxset = self.repo._get_cnxset()
+        return self
+
+    def __exit__(self, exctype=None, excvalue=None, tb=None):
+        assert self._open # actually already open
+        self.rollback()
+        self._open = False
+        self.cnxset.cnxset_freed()
+        self.repo._free_cnxset(self.cnxset)
+        self.cnxset = None
+
+    @contextmanager
+    def running_hooks_ops(self):
+        """this context manager should be called whenever hooks or operations
+        are about to be run (but after hook selection)
+
+        It will help the undo logic record pertinent metadata or some
+        hooks to run (or not) depending on who/what issued the query.
+        """
+        prevmode = self.hooks_in_progress
+        self.hooks_in_progress = True
+        yield
+        self.hooks_in_progress = prevmode
+
+    # shared data handling ###################################################
+
+    @property
+    def data(self):
+        return self._session_data
+
+    @property
+    def rql_rewriter(self):
+        return self._rewriter
+
+    @_open_only
+    @deprecated('[3.19] use session or transaction data', stacklevel=3)
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
+        """return value associated to `key` in session data"""
+        if txdata:
+            data = self.transaction_data
+        else:
+            data = self._session_data
+        if pop:
+            return data.pop(key, default)
+        else:
+            return data.get(key, default)
+
+    @_open_only
+    @deprecated('[3.19] use session or transaction data', stacklevel=3)
+    def set_shared_data(self, key, value, txdata=False):
+        """set value associated to `key` in session data"""
+        if txdata:
+            self.transaction_data[key] = value
+        else:
+            self._session_data[key] = value
+
+    def clear(self):
+        """reset internal data"""
+        self.transaction_data = {}
+        #: ordered list of operations to be processed on commit/rollback
+        self.pending_operations = []
+        #: (None, 'precommit', 'postcommit', 'uncommitable')
+        self.commit_state = None
+        self.pruned_hooks_cache = {}
+        self.local_perm_cache.clear()
+        self.rewriter = RQLRewriter(self)
+
+    @deprecated('[3.19] cnxset are automatically managed now.'
+                ' stop using explicit set and free.')
+    def set_cnxset(self):
+        pass
+
+    @deprecated('[3.19] cnxset are automatically managed now.'
+                ' stop using explicit set and free.')
+    def free_cnxset(self, ignoremode=False):
+        pass
+
+    @property
+    @contextmanager
+    @_open_only
+    @deprecated('[3.21] a cnxset is automatically set on __enter__ call now.'
+                ' stop using .ensure_cnx_set')
+    def ensure_cnx_set(self):
+        yield
+
+    @property
+    def anonymous_connection(self):
+        return self.session.anonymous_session
+
+    # Entity cache management #################################################
+    #
+    # The connection entity cache as held in cnx.transaction_data is removed at the
+    # end of the connection (commit and rollback)
+    #
+    # XXX connection level caching may be a pb with multiple repository
+    # instances, but 1. this is probably not the only one :$ and 2. it may be
+    # an acceptable risk. Anyway we could activate it or not according to a
+    # configuration option
+
+    def set_entity_cache(self, entity):
+        """Add `entity` to the connection entity cache"""
+        # XXX not using _open_only because before at creation time. _set_user
+        # call this function to cache the Connection user.
+        if entity.cw_etype != 'CWUser' and not self._open:
+            raise ProgrammingError('Closed Connection: %s'
+                                    % self.connectionid)
+        ecache = self.transaction_data.setdefault('ecache', {})
+        ecache.setdefault(entity.eid, entity)
+
+    @_open_only
+    def entity_cache(self, eid):
+        """get cache entity for `eid`"""
+        return self.transaction_data['ecache'][eid]
+
+    @_open_only
+    def cached_entities(self):
+        """return the whole entity cache"""
+        return self.transaction_data.get('ecache', {}).values()
+
+    @_open_only
+    def drop_entity_cache(self, eid=None):
+        """drop entity from the cache
+
+        If eid is None, the whole cache is dropped"""
+        if eid is None:
+            self.transaction_data.pop('ecache', None)
+        else:
+            del self.transaction_data['ecache'][eid]
+
+    # relations handling #######################################################
+
+    @_open_only
+    def add_relation(self, fromeid, rtype, toeid):
+        """provide direct access to the repository method to add a relation.
+
+        This is equivalent to the following rql query:
+
+          SET X rtype Y WHERE X eid  fromeid, T eid toeid
+
+        without read security check but also all the burden of rql execution.
+        You may use this in hooks when you know both eids of the relation you
+        want to add.
+        """
+        self.add_relations([(rtype, [(fromeid,  toeid)])])
+
+    @_open_only
+    def add_relations(self, relations):
+        '''set many relation using a shortcut similar to the one in add_relation
+
+        relations is a list of 2-uples, the first element of each
+        2-uple is the rtype, and the second is a list of (fromeid,
+        toeid) tuples
+        '''
+        edited_entities = {}
+        relations_dict = {}
+        with self.security_enabled(False, False):
+            for rtype, eids in relations:
+                if self.vreg.schema[rtype].inlined:
+                    for fromeid, toeid in eids:
+                        if fromeid not in edited_entities:
+                            entity = self.entity_from_eid(fromeid)
+                            edited = EditedEntity(entity)
+                            edited_entities[fromeid] = edited
+                        else:
+                            edited = edited_entities[fromeid]
+                        edited.edited_attribute(rtype, toeid)
+                else:
+                    relations_dict[rtype] = eids
+            self.repo.glob_add_relations(self, relations_dict)
+            for edited in edited_entities.values():
+                self.repo.glob_update_entity(self, edited)
+
+
+    @_open_only
+    def delete_relation(self, fromeid, rtype, toeid):
+        """provide direct access to the repository method to delete a relation.
+
+        This is equivalent to the following rql query:
+
+          DELETE X rtype Y WHERE X eid  fromeid, T eid toeid
+
+        without read security check but also all the burden of rql execution.
+        You may use this in hooks when you know both eids of the relation you
+        want to delete.
+        """
+        with self.security_enabled(False, False):
+            if self.vreg.schema[rtype].inlined:
+                entity = self.entity_from_eid(fromeid)
+                entity.cw_attr_cache[rtype] = None
+                self.repo.glob_update_entity(self, entity, set((rtype,)))
+            else:
+                self.repo.glob_delete_relation(self, fromeid, rtype, toeid)
+
+    # relations cache handling #################################################
+
+    @_open_only
+    def update_rel_cache_add(self, subject, rtype, object, symmetric=False):
+        self._update_entity_rel_cache_add(subject, rtype, 'subject', object)
+        if symmetric:
+            self._update_entity_rel_cache_add(object, rtype, 'subject', subject)
+        else:
+            self._update_entity_rel_cache_add(object, rtype, 'object', subject)
+
+    @_open_only
+    def update_rel_cache_del(self, subject, rtype, object, symmetric=False):
+        self._update_entity_rel_cache_del(subject, rtype, 'subject', object)
+        if symmetric:
+            self._update_entity_rel_cache_del(object, rtype, 'object', object)
+        else:
+            self._update_entity_rel_cache_del(object, rtype, 'object', subject)
+
+    @_open_only
+    def _update_entity_rel_cache_add(self, eid, rtype, role, targeteid):
+        try:
+            entity = self.entity_cache(eid)
+        except KeyError:
+            return
+        rcache = entity.cw_relation_cached(rtype, role)
+        if rcache is not None:
+            rset, entities = rcache
+            rset = rset.copy()
+            entities = list(entities)
+            rset.rows.append([targeteid])
+            if not isinstance(rset.description, list): # else description not set
+                rset.description = list(rset.description)
+            rset.description.append([self.entity_metas(targeteid)['type']])
+            targetentity = self.entity_from_eid(targeteid)
+            if targetentity.cw_rset is None:
+                targetentity.cw_rset = rset
+                targetentity.cw_row = rset.rowcount
+                targetentity.cw_col = 0
+            rset.rowcount += 1
+            entities.append(targetentity)
+            entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+                rset, tuple(entities))
+
+    @_open_only
+    def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid):
+        try:
+            entity = self.entity_cache(eid)
+        except KeyError:
+            return
+        rcache = entity.cw_relation_cached(rtype, role)
+        if rcache is not None:
+            rset, entities = rcache
+            for idx, row in enumerate(rset.rows):
+                if row[0] == targeteid:
+                    break
+            else:
+                # this may occurs if the cache has been filed by a hook
+                # after the database update
+                self.debug('cache inconsistency for %s %s %s %s', eid, rtype,
+                           role, targeteid)
+                return
+            rset = rset.copy()
+            entities = list(entities)
+            del rset.rows[idx]
+            if isinstance(rset.description, list): # else description not set
+                del rset.description[idx]
+            del entities[idx]
+            rset.rowcount -= 1
+            entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+                rset, tuple(entities))
+
+    # Tracking of entities added of removed in the transaction ##################
+
+    @_open_only
+    def deleted_in_transaction(self, eid):
+        """return True if the entity of the given eid is being deleted in the
+        current transaction
+        """
+        return eid in self.transaction_data.get('pendingeids', ())
+
+    @_open_only
+    def added_in_transaction(self, eid):
+        """return True if the entity of the given eid is being created in the
+        current transaction
+        """
+        return eid in self.transaction_data.get('neweids', ())
+
+    # Operation management ####################################################
+
+    @_open_only
+    def add_operation(self, operation, index=None):
+        """add an operation to be executed at the end of the transaction"""
+        if index is None:
+            self.pending_operations.append(operation)
+        else:
+            self.pending_operations.insert(index, operation)
+
+    # Hooks control ###########################################################
+
+    @_open_only
+    def allow_all_hooks_but(self, *categories):
+        return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
+
+    @_open_only
+    def deny_all_hooks_but(self, *categories):
+        return _hooks_control(self, HOOKS_DENY_ALL, *categories)
+
+    @_open_only
+    def disable_hook_categories(self, *categories):
+        """disable the given hook categories:
+
+        - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
+        - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
+        """
+        changes = set()
+        self.pruned_hooks_cache.clear()
+        categories = set(categories)
+        if self.hooks_mode is HOOKS_DENY_ALL:
+            enabledcats = self.enabled_hook_cats
+            changes = enabledcats & categories
+            enabledcats -= changes # changes is small hence faster
+        else:
+            disabledcats = self.disabled_hook_cats
+            changes = categories - disabledcats
+            disabledcats |= changes # changes is small hence faster
+        return tuple(changes)
+
+    @_open_only
+    def enable_hook_categories(self, *categories):
+        """enable the given hook categories:
+
+        - on HOOKS_DENY_ALL mode, ensure those categories are enabled
+        - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
+        """
+        changes = set()
+        self.pruned_hooks_cache.clear()
+        categories = set(categories)
+        if self.hooks_mode is HOOKS_DENY_ALL:
+            enabledcats = self.enabled_hook_cats
+            changes = categories - enabledcats
+            enabledcats |= changes # changes is small hence faster
+        else:
+            disabledcats = self.disabled_hook_cats
+            changes = disabledcats & categories
+            disabledcats -= changes # changes is small hence faster
+        return tuple(changes)
+
+    @_open_only
+    def is_hook_category_activated(self, category):
+        """return a boolean telling if the given category is currently activated
+        or not
+        """
+        if self.hooks_mode is HOOKS_DENY_ALL:
+            return category in self.enabled_hook_cats
+        return category not in self.disabled_hook_cats
+
+    @_open_only
+    def is_hook_activated(self, hook):
+        """return a boolean telling if the given hook class is currently
+        activated or not
+        """
+        return self.is_hook_category_activated(hook.category)
+
+    # Security management #####################################################
+
+    @_open_only
+    def security_enabled(self, read=None, write=None):
+        return _security_enabled(self, read=read, write=write)
+
+    @property
+    @_open_only
+    def read_security(self):
+        return self._read_security
+
+    @read_security.setter
+    @_open_only
+    def read_security(self, activated):
+        self._read_security = activated
+
+    # undo support ############################################################
+
+    @_open_only
+    def ertype_supports_undo(self, ertype):
+        return self.undo_actions and ertype not in NO_UNDO_TYPES
+
+    @_open_only
+    def transaction_uuid(self, set=True):
+        uuid = self.transaction_data.get('tx_uuid')
+        if set and uuid is None:
+            self.transaction_data['tx_uuid'] = uuid = text_type(uuid4().hex)
+            self.repo.system_source.start_undoable_transaction(self, uuid)
+        return uuid
+
+    @_open_only
+    def transaction_inc_action_counter(self):
+        num = self.transaction_data.setdefault('tx_action_count', 0) + 1
+        self.transaction_data['tx_action_count'] = num
+        return num
+
+    # db-api like interface ###################################################
+
+    @_open_only
+    def source_defs(self):
+        return self.repo.source_defs()
+
+    @deprecated('[3.19] use .entity_metas(eid) instead')
+    @_open_only
+    def describe(self, eid, asdict=False):
+        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
+        etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
+        metas = {'type': etype, 'source': source, 'extid': extid}
+        if asdict:
+            metas['asource'] = metas['source'] # XXX pre 3.19 client compat
+            return metas
+        return etype, source, extid
+
+    @_open_only
+    def entity_metas(self, eid):
+        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
+        etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
+        return {'type': etype, 'source': source, 'extid': extid}
+
+    # core method #############################################################
+
+    @_open_only
+    def execute(self, rql, kwargs=None, build_descr=True):
+        """db-api like method directly linked to the querier execute method.
+
+        See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
+        """
+        self._session_timestamp.touch()
+        rset = self._execute(self, rql, kwargs, build_descr)
+        rset.req = self
+        self._session_timestamp.touch()
+        return rset
+
+    @_open_only
+    def rollback(self, free_cnxset=None, reset_pool=None):
+        """rollback the current transaction"""
+        if free_cnxset is not None:
+            warn('[3.21] free_cnxset is now unneeded',
+                 DeprecationWarning, stacklevel=2)
+        if reset_pool is not None:
+            warn('[3.13] reset_pool is now unneeded',
+                 DeprecationWarning, stacklevel=2)
+        cnxset = self.cnxset
+        assert cnxset is not None
+        try:
+            # by default, operations are executed with security turned off
+            with self.security_enabled(False, False):
+                while self.pending_operations:
+                    try:
+                        operation = self.pending_operations.pop(0)
+                        operation.handle_event('rollback_event')
+                    except BaseException:
+                        self.critical('rollback error', exc_info=sys.exc_info())
+                        continue
+                cnxset.rollback()
+                self.debug('rollback for transaction %s done', self.connectionid)
+        finally:
+            self._session_timestamp.touch()
+            self.clear()
+
+    @_open_only
+    def commit(self, free_cnxset=None, reset_pool=None):
+        """commit the current session's transaction"""
+        if free_cnxset is not None:
+            warn('[3.21] free_cnxset is now unneeded',
+                 DeprecationWarning, stacklevel=2)
+        if reset_pool is not None:
+            warn('[3.13] reset_pool is now unneeded',
+                 DeprecationWarning, stacklevel=2)
+        assert self.cnxset is not None
+        cstate = self.commit_state
+        if cstate == 'uncommitable':
+            raise QueryError('transaction must be rolled back')
+        if cstate == 'precommit':
+            self.warn('calling commit in precommit makes no sense; ignoring commit')
+            return
+        if cstate == 'postcommit':
+            self.critical('postcommit phase is not allowed to write to the db; ignoring commit')
+            return
+        assert cstate is None
+        # 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
+        debug = server.DEBUG & server.DBG_OPS
+        try:
+            # by default, operations are executed with security turned off
+            with self.security_enabled(False, False):
+                processed = []
+                self.commit_state = 'precommit'
+                if debug:
+                    print(self.commit_state, '*' * 20)
+                try:
+                    with self.running_hooks_ops():
+                        while self.pending_operations:
+                            operation = self.pending_operations.pop(0)
+                            operation.processed = 'precommit'
+                            processed.append(operation)
+                            if debug:
+                                print(operation)
+                            operation.handle_event('precommit_event')
+                    self.pending_operations[:] = processed
+                    self.debug('precommit transaction %s done', self.connectionid)
+                except BaseException:
+                    # 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
+                    if debug:
+                        print(self.commit_state, '*' * 20)
+                    with self.running_hooks_ops():
+                        for operation in reversed(processed):
+                            if debug:
+                                print(operation)
+                            try:
+                                operation.handle_event('revertprecommit_event')
+                            except BaseException:
+                                self.critical('error while reverting precommit',
+                                              exc_info=True)
+                    # XXX use slice notation since self.pending_operations is a
+                    # read-only property.
+                    self.pending_operations[:] = processed + self.pending_operations
+                    self.rollback()
+                    raise
+                self.cnxset.commit()
+                self.commit_state = 'postcommit'
+                if debug:
+                    print(self.commit_state, '*' * 20)
+                with self.running_hooks_ops():
+                    while self.pending_operations:
+                        operation = self.pending_operations.pop(0)
+                        if debug:
+                            print(operation)
+                        operation.processed = 'postcommit'
+                        try:
+                            operation.handle_event('postcommit_event')
+                        except BaseException:
+                            self.critical('error while postcommit',
+                                          exc_info=sys.exc_info())
+                self.debug('postcommit transaction %s done', self.connectionid)
+                return self.transaction_uuid(set=False)
+        finally:
+            self._session_timestamp.touch()
+            self.clear()
+
+    # resource accessors ######################################################
+
+    @_open_only
+    def call_service(self, regid, **kwargs):
+        self.debug('calling service %s', regid)
+        service = self.vreg['services'].select(regid, self, **kwargs)
+        return service.call(**kwargs)
+
+    @_open_only
+    def system_sql(self, sql, args=None, rollback_on_failure=True):
+        """return a sql cursor on the system database"""
+        source = self.repo.system_source
+        try:
+            return source.doexec(self, sql, args, rollback=rollback_on_failure)
+        except (source.OperationalError, source.InterfaceError):
+            if not rollback_on_failure:
+                raise
+            source.warning("trying to reconnect")
+            self.cnxset.reconnect()
+            return source.doexec(self, sql, args, rollback=rollback_on_failure)
+
+    @_open_only
+    def rtype_eids_rdef(self, rtype, eidfrom, eidto):
+        # use type_and_source_from_eid instead of type_from_eid for optimization
+        # (avoid two extra methods call)
+        subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0]
+        objtype = self.repo.type_and_source_from_eid(eidto, self)[0]
+        return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
+
+
+def cnx_attr(attr_name, writable=False):
+    """return a property to forward attribute access to connection.
+
+    This is to be used by session"""
+    args = {}
+    @deprecated('[3.19] use a Connection object instead')
+    def attr_from_cnx(session):
+        return getattr(session._cnx, attr_name)
+    args['fget'] = attr_from_cnx
+    if writable:
+        @deprecated('[3.19] use a Connection object instead')
+        def write_attr(session, value):
+            return setattr(session._cnx, attr_name, value)
+        args['fset'] = write_attr
+    return property(**args)
+
+
+class Timestamp(object):
+
+    def __init__(self):
+        self.value = time()
+
+    def touch(self):
+        self.value = time()
+
+    def __float__(self):
+        return float(self.value)
+
+
+class Session(object):
+    """Repository user session
+
+    This ties all together:
+     * session id,
+     * user,
+     * other session data.
+    """
+
+    def __init__(self, user, repo, cnxprops=None, _id=None):
+        self.sessionid = _id or make_uid(unormalize(user.login))
+        self.user = user # XXX repoapi: deprecated and store only a login.
+        self.repo = repo
+        self.vreg = repo.vreg
+        self._timestamp = Timestamp()
+        self.data = {}
+        self.closed = False
+
+    def close(self):
+        self.closed = True
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        pass
+
+    def __unicode__(self):
+        return '<session %s (%s 0x%x)>' % (
+            unicode(self.user.login), self.sessionid, id(self))
+
+    @property
+    def timestamp(self):
+        return float(self._timestamp)
+
+    @property
+    @deprecated('[3.19] session.id is deprecated, use session.sessionid')
+    def id(self):
+        return self.sessionid
+
+    @property
+    def login(self):
+        return self.user.login
+
+    def new_cnx(self):
+        """Return a new Connection object linked to the session
+
+        The returned Connection will *not* be managed by the Session.
+        """
+        return Connection(self)
+
+    @deprecated('[3.19] use a Connection object instead')
+    def get_option_value(self, option, foreid=None):
+        if foreid is not None:
+            warn('[3.19] foreid argument is deprecated', DeprecationWarning,
+                 stacklevel=2)
+        return self.repo.get_option_value(option)
+
+    def _touch(self):
+        """update latest session usage timestamp and reset mode to read"""
+        self._timestamp.touch()
+
+    local_perm_cache = cnx_attr('local_perm_cache')
+    @local_perm_cache.setter
+    def local_perm_cache(self, value):
+        #base class assign an empty dict:-(
+        assert value == {}
+        pass
+
+    # deprecated ###############################################################
+
+    @property
+    def anonymous_session(self):
+        # XXX for now, anonymous_user only exists in webconfig (and testconfig).
+        # It will only be present inside all-in-one instance.
+        # there is plan to move it down to global config.
+        if not hasattr(self.repo.config, 'anonymous_user'):
+            # not a web or test config, no anonymous user
+            return False
+        return self.user.login == self.repo.config.anonymous_user()[0]
+
+    @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
+    def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
+        return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
+
+    # these are overridden by set_log_methods below
+    # only defining here to prevent pylint from complaining
+    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
+
+
+
+class InternalManager(object):
+    """a manager user with all access rights used internally for task such as
+    bootstrapping the repository or creating regular users according to
+    repository content
+    """
+
+    def __init__(self, lang='en'):
+        self.eid = -1
+        self.login = u'__internal_manager__'
+        self.properties = {}
+        self.groups = set(['managers'])
+        self.lang = lang
+
+    def matching_groups(self, groups):
+        return 1
+
+    def is_in_group(self, group):
+        return True
+
+    def owns(self, eid):
+        return True
+
+    def property_value(self, key):
+        if key == 'ui.language':
+            return self.lang
+        return None
+
+    def prefered_language(self, language=None):
+        # mock CWUser.prefered_language, mainly for testing purpose
+        return self.property_value('ui.language')
+
+    # CWUser compat for notification ###########################################
+
+    def name(self):
+        return 'cubicweb'
+
+    class _IEmailable:
+        @staticmethod
+        def get_email():
+            return ''
+
+    def cw_adapt_to(self, iface):
+        if iface == 'IEmailable':
+            return self._IEmailable
+        return None
+
+from logging import getLogger
+from cubicweb import set_log_methods
+set_log_methods(Session, getLogger('cubicweb.session'))
+set_log_methods(Connection, getLogger('cubicweb.session'))