cubicweb/server/session.py
changeset 11348 70337ad23145
parent 11206 6454ee8f2137
child 11374 0e50215016f3
equal deleted inserted replaced
11347:b4dcfd734686 11348:70337ad23145
     1 # copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    18 """Repository users' and internal' sessions."""
    18 """Repository users' and internal' sessions."""
    19 from __future__ import print_function
    19 from __future__ import print_function
    20 
    20 
    21 __docformat__ = "restructuredtext en"
    21 __docformat__ = "restructuredtext en"
    22 
    22 
       
    23 import functools
    23 import sys
    24 import sys
    24 from time import time
    25 from time import time
    25 from uuid import uuid4
    26 from uuid import uuid4
    26 from warnings import warn
    27 from warnings import warn
    27 import functools
       
    28 from contextlib import contextmanager
    28 from contextlib import contextmanager
       
    29 from logging import getLogger
    29 
    30 
    30 from six import text_type
    31 from six import text_type
    31 
    32 
    32 from logilab.common.deprecation import deprecated
    33 from logilab.common.deprecation import deprecated
    33 from logilab.common.textutils import unormalize
    34 from logilab.common.textutils import unormalize
    34 from logilab.common.registry import objectify_predicate
    35 from logilab.common.registry import objectify_predicate
    35 
    36 
    36 from cubicweb import QueryError, schema, server, ProgrammingError
    37 from cubicweb import QueryError, ProgrammingError, schema, server
       
    38 from cubicweb import set_log_methods
    37 from cubicweb.req import RequestSessionBase
    39 from cubicweb.req import RequestSessionBase
    38 from cubicweb.utils import make_uid
    40 from cubicweb.utils import make_uid
    39 from cubicweb.rqlrewrite import RQLRewriter
    41 from cubicweb.rqlrewrite import RQLRewriter
    40 from cubicweb.server.edition import EditedEntity
    42 from cubicweb.server.edition import EditedEntity
    41 
    43 
    48 NO_UNDO_TYPES.add('is')
    50 NO_UNDO_TYPES.add('is')
    49 NO_UNDO_TYPES.add('is_instance_of')
    51 NO_UNDO_TYPES.add('is_instance_of')
    50 NO_UNDO_TYPES.add('cw_source')
    52 NO_UNDO_TYPES.add('cw_source')
    51 # XXX rememberme,forgotpwd,apycot,vcsfile
    53 # XXX rememberme,forgotpwd,apycot,vcsfile
    52 
    54 
       
    55 
    53 @objectify_predicate
    56 @objectify_predicate
    54 def is_user_session(cls, req, **kwargs):
    57 def is_user_session(cls, req, **kwargs):
    55     """return 1 when session is not internal.
    58     """return 1 when session is not internal.
    56 
    59 
    57     This predicate can only be used repository side only. """
    60     This predicate can only be used repository side only. """
    58     return not req.is_internal_session
    61     return not req.is_internal_session
    59 
    62 
       
    63 
    60 @objectify_predicate
    64 @objectify_predicate
    61 def is_internal_session(cls, req, **kwargs):
    65 def is_internal_session(cls, req, **kwargs):
    62     """return 1 when session is not internal.
    66     """return 1 when session is not internal.
    63 
    67 
    64     This predicate can only be used repository side only. """
    68     This predicate can only be used repository side only. """
    65     return req.is_internal_session
    69     return req.is_internal_session
       
    70 
    66 
    71 
    67 @objectify_predicate
    72 @objectify_predicate
    68 def repairing(cls, req, **kwargs):
    73 def repairing(cls, req, **kwargs):
    69     """return 1 when repository is running in repair mode"""
    74     """return 1 when repository is running in repair mode"""
    70     return req.vreg.config.repairing
    75     return req.vreg.config.repairing
    71 
    76 
    72 
    77 
    73 @deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
    78 @deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
    74 def hooks_control(obj, mode, *categories):
    79 def hooks_control(obj, mode, *categories):
    75     assert mode in  (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
    80     assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
    76     if mode == HOOKS_ALLOW_ALL:
    81     if mode == HOOKS_ALLOW_ALL:
    77         return obj.allow_all_hooks_but(*categories)
    82         return obj.allow_all_hooks_but(*categories)
    78     elif mode == HOOKS_DENY_ALL:
    83     elif mode == HOOKS_DENY_ALL:
    79         return obj.deny_all_hooks_but(*categories)
    84         return obj.deny_all_hooks_but(*categories)
    80 
    85 
   130 
   135 
   131 @deprecated('[3.17] use <object>.security_enabled instead')
   136 @deprecated('[3.17] use <object>.security_enabled instead')
   132 def security_enabled(obj, *args, **kwargs):
   137 def security_enabled(obj, *args, **kwargs):
   133     return obj.security_enabled(*args, **kwargs)
   138     return obj.security_enabled(*args, **kwargs)
   134 
   139 
       
   140 
   135 class _security_enabled(object):
   141 class _security_enabled(object):
   136     """context manager to control security w/ session.execute,
   142     """context manager to control security w/ session.execute,
   137 
   143 
   138     By default security is disabled on queries executed on the repository
   144     By default security is disabled on queries executed on the repository
   139     side.
   145     side.
   163         if self.oldwrite is not None:
   169         if self.oldwrite is not None:
   164             self.cnx.write_security = self.oldwrite
   170             self.cnx.write_security = self.oldwrite
   165 
   171 
   166 HOOKS_ALLOW_ALL = object()
   172 HOOKS_ALLOW_ALL = object()
   167 HOOKS_DENY_ALL = object()
   173 HOOKS_DENY_ALL = object()
   168 DEFAULT_SECURITY = object() # evaluated to true by design
   174 DEFAULT_SECURITY = object()  # evaluated to true by design
       
   175 
   169 
   176 
   170 class SessionClosedError(RuntimeError):
   177 class SessionClosedError(RuntimeError):
   171     pass
   178     pass
   172 
   179 
   173 
   180 
   175     """decorator for Connection method that check it is open"""
   182     """decorator for Connection method that check it is open"""
   176     @functools.wraps(func)
   183     @functools.wraps(func)
   177     def check_open(cnx, *args, **kwargs):
   184     def check_open(cnx, *args, **kwargs):
   178         if not cnx._open:
   185         if not cnx._open:
   179             raise ProgrammingError('Closed Connection: %s'
   186             raise ProgrammingError('Closed Connection: %s'
   180                                     % cnx.connectionid)
   187                                    % cnx.connectionid)
   181         return func(cnx, *args, **kwargs)
   188         return func(cnx, *args, **kwargs)
   182     return check_open
   189     return check_open
   183 
   190 
   184 
   191 
   185 class Connection(RequestSessionBase):
   192 class Connection(RequestSessionBase):
   273         #: ordered list of operations to be processed on commit/rollback
   280         #: ordered list of operations to be processed on commit/rollback
   274         self.pending_operations = []
   281         self.pending_operations = []
   275         #: (None, 'precommit', 'postcommit', 'uncommitable')
   282         #: (None, 'precommit', 'postcommit', 'uncommitable')
   276         self.commit_state = None
   283         self.commit_state = None
   277 
   284 
   278         ### hook control attribute
   285         # hook control attribute
   279         self.hooks_mode = HOOKS_ALLOW_ALL
   286         self.hooks_mode = HOOKS_ALLOW_ALL
   280         self.disabled_hook_cats = set()
   287         self.disabled_hook_cats = set()
   281         self.enabled_hook_cats = set()
   288         self.enabled_hook_cats = set()
   282         self.pruned_hooks_cache = {}
   289         self.pruned_hooks_cache = {}
   283 
   290 
   284 
   291         # security control attributes
   285         ### security control attributes
   292         self._read_security = DEFAULT_SECURITY  # handled by a property
   286         self._read_security = DEFAULT_SECURITY # handled by a property
       
   287         self.write_security = DEFAULT_SECURITY
   293         self.write_security = DEFAULT_SECURITY
   288 
   294 
   289         # undo control
   295         # undo control
   290         config = session.repo.config
   296         config = session.repo.config
   291         if config.creating or config.repairing or self.is_internal_session:
   297         if config.creating or config.repairing or self.is_internal_session:
   302             self.set_language(self.user.prefered_language())
   308             self.set_language(self.user.prefered_language())
   303         else:
   309         else:
   304             self._set_user(session.user)
   310             self._set_user(session.user)
   305 
   311 
   306     @_open_only
   312     @_open_only
   307     def source_defs(self):
       
   308         """Return the definition of sources used by the repository."""
       
   309         return self.session.repo.source_defs()
       
   310 
       
   311     @_open_only
       
   312     def get_schema(self):
   313     def get_schema(self):
   313         """Return the schema currently used by the repository."""
   314         """Return the schema currently used by the repository."""
   314         return self.session.repo.source_defs()
   315         return self.session.repo.source_defs()
   315 
   316 
   316     @_open_only
   317     @_open_only
   379         return self.repo.system_source.undo_transaction(self, txuuid)
   380         return self.repo.system_source.undo_transaction(self, txuuid)
   380 
   381 
   381     # life cycle handling ####################################################
   382     # life cycle handling ####################################################
   382 
   383 
   383     def __enter__(self):
   384     def __enter__(self):
   384         assert self._open is None # first opening
   385         assert self._open is None  # first opening
   385         self._open = True
   386         self._open = True
   386         self.cnxset = self.repo._get_cnxset()
   387         self.cnxset = self.repo._get_cnxset()
   387         return self
   388         return self
   388 
   389 
   389     def __exit__(self, exctype=None, excvalue=None, tb=None):
   390     def __exit__(self, exctype=None, excvalue=None, tb=None):
   390         assert self._open # actually already open
   391         assert self._open  # actually already open
   391         self.rollback()
   392         self.rollback()
   392         self._open = False
   393         self._open = False
   393         self.cnxset.cnxset_freed()
   394         self.cnxset.cnxset_freed()
   394         self.repo._free_cnxset(self.cnxset)
   395         self.repo._free_cnxset(self.cnxset)
   395         self.cnxset = None
   396         self.cnxset = None
   485     def set_entity_cache(self, entity):
   486     def set_entity_cache(self, entity):
   486         """Add `entity` to the connection entity cache"""
   487         """Add `entity` to the connection entity cache"""
   487         # XXX not using _open_only because before at creation time. _set_user
   488         # XXX not using _open_only because before at creation time. _set_user
   488         # call this function to cache the Connection user.
   489         # call this function to cache the Connection user.
   489         if entity.cw_etype != 'CWUser' and not self._open:
   490         if entity.cw_etype != 'CWUser' and not self._open:
   490             raise ProgrammingError('Closed Connection: %s'
   491             raise ProgrammingError('Closed Connection: %s' % self.connectionid)
   491                                     % self.connectionid)
       
   492         ecache = self.transaction_data.setdefault('ecache', {})
   492         ecache = self.transaction_data.setdefault('ecache', {})
   493         ecache.setdefault(entity.eid, entity)
   493         ecache.setdefault(entity.eid, entity)
   494 
   494 
   495     @_open_only
   495     @_open_only
   496     def entity_cache(self, eid):
   496     def entity_cache(self, eid):
   524 
   524 
   525         without read security check but also all the burden of rql execution.
   525         without read security check but also all the burden of rql execution.
   526         You may use this in hooks when you know both eids of the relation you
   526         You may use this in hooks when you know both eids of the relation you
   527         want to add.
   527         want to add.
   528         """
   528         """
   529         self.add_relations([(rtype, [(fromeid,  toeid)])])
   529         self.add_relations([(rtype, [(fromeid, toeid)])])
   530 
   530 
   531     @_open_only
   531     @_open_only
   532     def add_relations(self, relations):
   532     def add_relations(self, relations):
   533         '''set many relation using a shortcut similar to the one in add_relation
   533         '''set many relation using a shortcut similar to the one in add_relation
   534 
   534 
   553                     relations_dict[rtype] = eids
   553                     relations_dict[rtype] = eids
   554             self.repo.glob_add_relations(self, relations_dict)
   554             self.repo.glob_add_relations(self, relations_dict)
   555             for edited in edited_entities.values():
   555             for edited in edited_entities.values():
   556                 self.repo.glob_update_entity(self, edited)
   556                 self.repo.glob_update_entity(self, edited)
   557 
   557 
   558 
       
   559     @_open_only
   558     @_open_only
   560     def delete_relation(self, fromeid, rtype, toeid):
   559     def delete_relation(self, fromeid, rtype, toeid):
   561         """provide direct access to the repository method to delete a relation.
   560         """provide direct access to the repository method to delete a relation.
   562 
   561 
   563         This is equivalent to the following rql query:
   562         This is equivalent to the following rql query:
   604         if rcache is not None:
   603         if rcache is not None:
   605             rset, entities = rcache
   604             rset, entities = rcache
   606             rset = rset.copy()
   605             rset = rset.copy()
   607             entities = list(entities)
   606             entities = list(entities)
   608             rset.rows.append([targeteid])
   607             rset.rows.append([targeteid])
   609             if not isinstance(rset.description, list): # else description not set
   608             if not isinstance(rset.description, list):  # else description not set
   610                 rset.description = list(rset.description)
   609                 rset.description = list(rset.description)
   611             rset.description.append([self.entity_metas(targeteid)['type']])
   610             rset.description.append([self.entity_metas(targeteid)['type']])
   612             targetentity = self.entity_from_eid(targeteid)
   611             targetentity = self.entity_from_eid(targeteid)
   613             if targetentity.cw_rset is None:
   612             if targetentity.cw_rset is None:
   614                 targetentity.cw_rset = rset
   613                 targetentity.cw_rset = rset
   638                            role, targeteid)
   637                            role, targeteid)
   639                 return
   638                 return
   640             rset = rset.copy()
   639             rset = rset.copy()
   641             entities = list(entities)
   640             entities = list(entities)
   642             del rset.rows[idx]
   641             del rset.rows[idx]
   643             if isinstance(rset.description, list): # else description not set
   642             if isinstance(rset.description, list):  # else description not set
   644                 del rset.description[idx]
   643                 del rset.description[idx]
   645             del entities[idx]
   644             del entities[idx]
   646             rset.rowcount -= 1
   645             rset.rowcount -= 1
   647             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
   646             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
   648                 rset, tuple(entities))
   647                 rset, tuple(entities))
   694         self.pruned_hooks_cache.clear()
   693         self.pruned_hooks_cache.clear()
   695         categories = set(categories)
   694         categories = set(categories)
   696         if self.hooks_mode is HOOKS_DENY_ALL:
   695         if self.hooks_mode is HOOKS_DENY_ALL:
   697             enabledcats = self.enabled_hook_cats
   696             enabledcats = self.enabled_hook_cats
   698             changes = enabledcats & categories
   697             changes = enabledcats & categories
   699             enabledcats -= changes # changes is small hence faster
   698             enabledcats -= changes  # changes is small hence faster
   700         else:
   699         else:
   701             disabledcats = self.disabled_hook_cats
   700             disabledcats = self.disabled_hook_cats
   702             changes = categories - disabledcats
   701             changes = categories - disabledcats
   703             disabledcats |= changes # changes is small hence faster
   702             disabledcats |= changes  # changes is small hence faster
   704         return tuple(changes)
   703         return tuple(changes)
   705 
   704 
   706     @_open_only
   705     @_open_only
   707     def enable_hook_categories(self, *categories):
   706     def enable_hook_categories(self, *categories):
   708         """enable the given hook categories:
   707         """enable the given hook categories:
   714         self.pruned_hooks_cache.clear()
   713         self.pruned_hooks_cache.clear()
   715         categories = set(categories)
   714         categories = set(categories)
   716         if self.hooks_mode is HOOKS_DENY_ALL:
   715         if self.hooks_mode is HOOKS_DENY_ALL:
   717             enabledcats = self.enabled_hook_cats
   716             enabledcats = self.enabled_hook_cats
   718             changes = categories - enabledcats
   717             changes = categories - enabledcats
   719             enabledcats |= changes # changes is small hence faster
   718             enabledcats |= changes  # changes is small hence faster
   720         else:
   719         else:
   721             disabledcats = self.disabled_hook_cats
   720             disabledcats = self.disabled_hook_cats
   722             changes = disabledcats & categories
   721             changes = disabledcats & categories
   723             disabledcats -= changes # changes is small hence faster
   722             disabledcats -= changes  # changes is small hence faster
   724         return tuple(changes)
   723         return tuple(changes)
   725 
   724 
   726     @_open_only
   725     @_open_only
   727     def is_hook_category_activated(self, category):
   726     def is_hook_category_activated(self, category):
   728         """return a boolean telling if the given category is currently activated
   727         """return a boolean telling if the given category is currently activated
   786     def describe(self, eid, asdict=False):
   785     def describe(self, eid, asdict=False):
   787         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
   786         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
   788         etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
   787         etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
   789         metas = {'type': etype, 'source': source, 'extid': extid}
   788         metas = {'type': etype, 'source': source, 'extid': extid}
   790         if asdict:
   789         if asdict:
   791             metas['asource'] = metas['source'] # XXX pre 3.19 client compat
   790             metas['asource'] = metas['source']  # XXX pre 3.19 client compat
   792             return metas
   791             return metas
   793         return etype, source, extid
   792         return etype, source, extid
   794 
   793 
   795     @_open_only
   794     @_open_only
   796     def entity_metas(self, eid):
   795     def entity_metas(self, eid):
   964 def cnx_attr(attr_name, writable=False):
   963 def cnx_attr(attr_name, writable=False):
   965     """return a property to forward attribute access to connection.
   964     """return a property to forward attribute access to connection.
   966 
   965 
   967     This is to be used by session"""
   966     This is to be used by session"""
   968     args = {}
   967     args = {}
       
   968 
   969     @deprecated('[3.19] use a Connection object instead')
   969     @deprecated('[3.19] use a Connection object instead')
   970     def attr_from_cnx(session):
   970     def attr_from_cnx(session):
   971         return getattr(session._cnx, attr_name)
   971         return getattr(session._cnx, attr_name)
       
   972 
   972     args['fget'] = attr_from_cnx
   973     args['fget'] = attr_from_cnx
   973     if writable:
   974     if writable:
   974         @deprecated('[3.19] use a Connection object instead')
   975         @deprecated('[3.19] use a Connection object instead')
   975         def write_attr(session, value):
   976         def write_attr(session, value):
   976             return setattr(session._cnx, attr_name, value)
   977             return setattr(session._cnx, attr_name, value)
   999      * other session data.
  1000      * other session data.
  1000     """
  1001     """
  1001 
  1002 
  1002     def __init__(self, user, repo, _id=None):
  1003     def __init__(self, user, repo, _id=None):
  1003         self.sessionid = _id or make_uid(unormalize(user.login))
  1004         self.sessionid = _id or make_uid(unormalize(user.login))
  1004         self.user = user # XXX repoapi: deprecated and store only a login.
  1005         self.user = user  # XXX repoapi: deprecated and store only a login.
  1005         self.repo = repo
  1006         self.repo = repo
  1006         self._timestamp = Timestamp()
  1007         self._timestamp = Timestamp()
  1007         self.data = {}
  1008         self.data = {}
  1008         self.closed = False
  1009         self.closed = False
  1009 
  1010 
  1052     def _touch(self):
  1053     def _touch(self):
  1053         """update latest session usage timestamp and reset mode to read"""
  1054         """update latest session usage timestamp and reset mode to read"""
  1054         self._timestamp.touch()
  1055         self._timestamp.touch()
  1055 
  1056 
  1056     local_perm_cache = cnx_attr('local_perm_cache')
  1057     local_perm_cache = cnx_attr('local_perm_cache')
       
  1058 
  1057     @local_perm_cache.setter
  1059     @local_perm_cache.setter
  1058     def local_perm_cache(self, value):
  1060     def local_perm_cache(self, value):
  1059         #base class assign an empty dict:-(
  1061         # base class assign an empty dict:-(
  1060         assert value == {}
  1062         assert value == {}
  1061         pass
  1063         pass
  1062 
  1064 
  1063     # deprecated ###############################################################
  1065     # deprecated ###############################################################
  1064 
  1066 
  1076     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
  1078     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
  1077         return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
  1079         return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
  1078 
  1080 
  1079     # these are overridden by set_log_methods below
  1081     # these are overridden by set_log_methods below
  1080     # only defining here to prevent pylint from complaining
  1082     # only defining here to prevent pylint from complaining
  1081     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
  1083     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
  1082 
       
  1083 
  1084 
  1084 
  1085 
  1085 class InternalManager(object):
  1086 class InternalManager(object):
  1086     """a manager user with all access rights used internally for task such as
  1087     """a manager user with all access rights used internally for task such as
  1087     bootstrapping the repository or creating regular users according to
  1088     bootstrapping the repository or creating regular users according to
  1126     def cw_adapt_to(self, iface):
  1127     def cw_adapt_to(self, iface):
  1127         if iface == 'IEmailable':
  1128         if iface == 'IEmailable':
  1128             return self._IEmailable
  1129             return self._IEmailable
  1129         return None
  1130         return None
  1130 
  1131 
  1131 from logging import getLogger
  1132 
  1132 from cubicweb import set_log_methods
       
  1133 set_log_methods(Session, getLogger('cubicweb.session'))
  1133 set_log_methods(Session, getLogger('cubicweb.session'))
  1134 set_log_methods(Connection, getLogger('cubicweb.session'))
  1134 set_log_methods(Connection, getLogger('cubicweb.session'))