server/session.py
changeset 0 b97547f5f1fa
child 716 03ffbe9c5183
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """Repository users' and internal' sessions.
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 import sys
       
    10 import threading
       
    11 from time import time
       
    12 from types import NoneType
       
    13 from decimal import Decimal
       
    14 
       
    15 from mx.DateTime import DateTimeType, DateTimeDeltaType
       
    16 from rql.nodes import VariableRef, Function
       
    17 from yams import BASE_TYPES
       
    18 
       
    19 from cubicweb import RequestSessionMixIn, Binary
       
    20 from cubicweb.dbapi import ConnectionProperties
       
    21 from cubicweb.common.utils import make_uid
       
    22 from cubicweb.server.rqlrewrite import RQLRewriter
       
    23 
       
    24 def etype_from_pyobj(value):
       
    25     """guess yams type from python value"""
       
    26     # note:
       
    27     # * Password is not selectable so no problem)
       
    28     # * use type(value) and not value.__class__ since mx instances have no
       
    29     #   __class__ attribute
       
    30     # * XXX Date, Time
       
    31     return {bool: 'Boolean',
       
    32             int: 'Int',
       
    33             long: 'Int',
       
    34             float: 'Float',
       
    35             Decimal: 'Decimal',
       
    36             unicode: 'String',
       
    37             NoneType: None,
       
    38             Binary: 'Bytes',
       
    39             DateTimeType: 'Datetime',
       
    40             DateTimeDeltaType: 'Interval',
       
    41             }[type(value)]
       
    42 
       
    43 def is_final(rqlst, variable, args):
       
    44     # try to find if this is a final var or not
       
    45     for select in rqlst.children:
       
    46         for sol in select.solutions:
       
    47             etype = variable.get_type(sol, args)
       
    48             if etype is None:
       
    49                 continue
       
    50             if etype in BASE_TYPES:
       
    51                 return True
       
    52             return False   
       
    53 
       
    54 def _make_description(selected, args, solution):
       
    55     """return a description for a result set"""
       
    56     description = []
       
    57     for term in selected:
       
    58         description.append(term.get_type(solution, args))
       
    59     return description
       
    60 
       
    61 #XXX rql <= 0.18.3 bw compat
       
    62 from rql import stmts
       
    63 if not hasattr(stmts.Union, 'get_variable_variables'):
       
    64     def _union_get_variable_variables(self):
       
    65         """return the set of variable names which take different type according to
       
    66         the solution
       
    67         """
       
    68         change = set()
       
    69         values = {}
       
    70         for select in self.children:
       
    71             change.update(select.get_variable_variables(values))
       
    72         return change
       
    73     stmts.Union.get_variable_variables = _union_get_variable_variables
       
    74                         
       
    75     def _select_get_variable_variables(self, _values=None):
       
    76         """return the set of variable names which take different type according to
       
    77         the solution
       
    78         """
       
    79         change = set()
       
    80         if _values is None:
       
    81             _values = {}
       
    82         for solution in self.solutions:
       
    83             for vname, etype in solution.iteritems():
       
    84                 if not vname in _values:
       
    85                     _values[vname] = etype
       
    86                 elif _values[vname] != etype:
       
    87                     change.add(vname)
       
    88         return change
       
    89     stmts.Select.get_variable_variables = _select_get_variable_variables
       
    90 
       
    91 class Session(RequestSessionMixIn):
       
    92     """tie session id, user, connections pool and other session data all
       
    93     together
       
    94     """
       
    95     
       
    96     def __init__(self, user, repo, cnxprops=None, _id=None):
       
    97         super(Session, self).__init__(repo.vreg)
       
    98         self.id = _id or make_uid(user.login.encode('UTF8'))
       
    99         cnxprops = cnxprops or ConnectionProperties('inmemory')
       
   100         self.user = user
       
   101         self.repo = repo
       
   102         self.cnxtype = cnxprops.cnxtype
       
   103         self.creation = time()
       
   104         self.timestamp = self.creation
       
   105         self.is_internal_session = False
       
   106         self.is_super_session = False
       
   107         # short cut to querier .execute method
       
   108         self._execute = repo.querier.execute
       
   109         # shared data, used to communicate extra information between the client
       
   110         # and the rql server
       
   111         self.data = {}
       
   112         # i18n initialization
       
   113         self.set_language(cnxprops.lang)
       
   114         self._threaddata = threading.local()
       
   115         
       
   116     def get_mode(self):
       
   117         return getattr(self._threaddata, 'mode', 'read')
       
   118     def set_mode(self, value):
       
   119         self._threaddata.mode = value
       
   120     # transaction mode (read/write), resetted to read on commit / rollback
       
   121     mode = property(get_mode, set_mode)
       
   122 
       
   123     def get_commit_state(self):
       
   124         return getattr(self._threaddata, 'commit_state', None)
       
   125     def set_commit_state(self, value):
       
   126         self._threaddata.commit_state = value
       
   127     commit_state = property(get_commit_state, set_commit_state)
       
   128     
       
   129     # set according to transaction mode for each query
       
   130     @property
       
   131     def pool(self):
       
   132         return getattr(self._threaddata, 'pool', None)
       
   133     
       
   134     # pending transaction operations
       
   135     @property
       
   136     def pending_operations(self):
       
   137         try:
       
   138             return self._threaddata.pending_operations
       
   139         except AttributeError:
       
   140             self._threaddata.pending_operations = []
       
   141             return self._threaddata.pending_operations
       
   142     
       
   143     # rql rewriter
       
   144     @property
       
   145     def rql_rewriter(self):
       
   146         try:
       
   147             return self._threaddata._rewriter
       
   148         except AttributeError:
       
   149             self._threaddata._rewriter = RQLRewriter(self.repo.querier, self)
       
   150             return self._threaddata._rewriter
       
   151     
       
   152     # transaction queries data
       
   153     @property
       
   154     def _query_data(self):
       
   155         try:
       
   156             return self._threaddata._query_data
       
   157         except AttributeError:
       
   158             self._threaddata._query_data = {}
       
   159             return self._threaddata._query_data
       
   160     
       
   161     def set_language(self, language):
       
   162         """i18n configuration for translation"""
       
   163         vreg = self.vreg
       
   164         language = language or self.user.property_value('ui.language')
       
   165         try:
       
   166             self._ = self.__ = vreg.config.translations[language]
       
   167         except KeyError:
       
   168             language = vreg.property_value('ui.language')
       
   169             try:
       
   170                 self._ = self.__ = vreg.config.translations[language]
       
   171             except KeyError:
       
   172                 self._ = self.__ = unicode
       
   173         self.lang = language
       
   174         
       
   175     def change_property(self, prop, value):
       
   176         assert prop == 'lang' # this is the only one changeable property for now
       
   177         self.set_language(value)
       
   178 
       
   179     def __str__(self):
       
   180         return '<%ssession %s (%s 0x%x)>' % (self.cnxtype, self.user.login, 
       
   181                                              self.id, id(self))
       
   182 
       
   183     def etype_class(self, etype):
       
   184         """return an entity class for the given entity type"""
       
   185         return self.vreg.etype_class(etype)
       
   186     
       
   187     def entity(self, eid):
       
   188         """return a result set for the given eid"""
       
   189         return self.eid_rset(eid).get_entity(0, 0)
       
   190         
       
   191     def _touch(self):
       
   192         """update latest session usage timestamp and reset mode to read
       
   193         """
       
   194         self.timestamp = time()
       
   195         self.local_perm_cache.clear()
       
   196         self._threaddata.mode = 'read'
       
   197         
       
   198     def set_pool(self):
       
   199         """the session need a pool to execute some queries"""
       
   200         if self.pool is None:
       
   201             self._threaddata.pool = self.repo._get_pool()
       
   202             try:                
       
   203                 self._threaddata.pool.pool_set(self)
       
   204             except:
       
   205                 self.repo._free_pool(self.pool)
       
   206                 self._threaddata.pool = None
       
   207                 raise
       
   208         return self._threaddata.pool
       
   209             
       
   210     def reset_pool(self):
       
   211         """the session has no longer using its pool, at least for some time
       
   212         """
       
   213         # pool may be none if no operation has been done since last commit
       
   214         # or rollback
       
   215         if self.pool is not None and self.mode == 'read':
       
   216             # even in read mode, we must release the current transaction
       
   217             self.repo._free_pool(self.pool)
       
   218             self.pool.pool_reset(self)
       
   219             self._threaddata.pool = None
       
   220             
       
   221     def system_sql(self, sql, args=None):
       
   222         """return a sql cursor on the system database"""
       
   223         if not sql.split(None, 1)[0].upper() == 'SELECT':
       
   224             self.mode = 'write'
       
   225         cursor = self.pool['system']
       
   226         self.pool.source('system').doexec(cursor, sql, args)
       
   227         return cursor
       
   228 
       
   229     def actual_session(self):
       
   230         """return the original parent session if any, else self"""
       
   231         return self        
       
   232 
       
   233     # shared data handling ###################################################
       
   234     
       
   235     def get_shared_data(self, key, default=None, pop=False):
       
   236         """return value associated to `key` in session data"""
       
   237         if pop:
       
   238             return self.data.pop(key, default)
       
   239         else:
       
   240             return self.data.get(key, default)
       
   241         
       
   242     def set_shared_data(self, key, value, querydata=False):
       
   243         """set value associated to `key` in session data"""
       
   244         if querydata:
       
   245             self.set_query_data(key, value)
       
   246         else:
       
   247             self.data[key] = value
       
   248         
       
   249     # request interface #######################################################
       
   250     
       
   251     def set_entity_cache(self, entity):
       
   252         # no entity cache in the server, too high risk of inconsistency
       
   253         # between pre/post hooks
       
   254         pass
       
   255 
       
   256     def entity_cache(self, eid):
       
   257         raise KeyError(eid)
       
   258 
       
   259     def base_url(self):
       
   260         return self.repo.config['base-url'] or u''
       
   261         
       
   262     def from_controller(self):
       
   263         """return the id (string) of the controller issuing the request (no
       
   264         sense here, always return 'view')
       
   265         """
       
   266         return 'view'
       
   267     
       
   268     def source_defs(self):
       
   269         return self.repo.source_defs()
       
   270 
       
   271     def describe(self, eid):
       
   272         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   273         return self.repo.type_and_source_from_eid(eid, self)
       
   274     
       
   275     # db-api like interface ###################################################
       
   276 
       
   277     def source_from_eid(self, eid):
       
   278         """return the source where the entity with id <eid> is located"""
       
   279         return self.repo.source_from_eid(eid, self)
       
   280 
       
   281     def decorate_rset(self, rset, propagate=False):
       
   282         rset.vreg = self.vreg
       
   283         rset.req = propagate and self or self.actual_session()
       
   284         return rset
       
   285 
       
   286     @property
       
   287     def super_session(self):
       
   288         try:
       
   289             csession = self._threaddata.childsession
       
   290         except AttributeError:
       
   291             if self.is_super_session:
       
   292                 csession = self
       
   293             else:
       
   294                 csession = ChildSession(self)
       
   295             self._threaddata.childsession = csession
       
   296         # need shared pool set
       
   297         self.set_pool()
       
   298         return csession
       
   299         
       
   300     def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=False,
       
   301                        propagate=False):
       
   302         """like .execute but with security checking disabled (this method is
       
   303         internal to the server, it's not part of the db-api)
       
   304 
       
   305         if `propagate` is true, the super_session will be attached to the result
       
   306         set instead of the parent session, hence further query done through
       
   307         entities fetched from this result set will bypass security as well
       
   308         """
       
   309         return self.super_session.execute(rql, kwargs, eid_key, build_descr,
       
   310                                           propagate)
       
   311 
       
   312     @property
       
   313     def cursor(self):
       
   314         """return a rql cursor"""
       
   315         return self
       
   316     
       
   317     def execute(self, rql, kwargs=None, eid_key=None, build_descr=True,
       
   318                 propagate=False):
       
   319         """db-api like method directly linked to the querier execute method
       
   320 
       
   321         Becare that unlike actual cursor.execute, `build_descr` default to
       
   322         false
       
   323         """
       
   324         rset = self._execute(self, rql, kwargs, eid_key, build_descr)
       
   325         return self.decorate_rset(rset, propagate)
       
   326     
       
   327     def commit(self, reset_pool=True):
       
   328         """commit the current session's transaction"""
       
   329         if self.pool is None:
       
   330             assert not self.pending_operations
       
   331             self._query_data.clear()
       
   332             self._touch()
       
   333             return
       
   334         if self.commit_state:
       
   335             return
       
   336         # on rollback, an operation should have the following state
       
   337         # information:
       
   338         # - processed by the precommit/commit event or not
       
   339         # - if processed, is it the failed operation
       
   340         try:
       
   341             for trstate in ('precommit', 'commit'):
       
   342                 processed = []
       
   343                 self.commit_state = trstate
       
   344                 try:
       
   345                     while self.pending_operations:
       
   346                         operation = self.pending_operations.pop(0)
       
   347                         operation.processed = trstate
       
   348                         processed.append(operation)
       
   349                         operation.handle_event('%s_event' % trstate)
       
   350                     self.pending_operations[:] = processed
       
   351                     self.debug('%s session %s done', trstate, self.id)
       
   352                 except:
       
   353                     self.exception('error while %sing', trstate)
       
   354                     operation.failed = True
       
   355                     for operation in processed:
       
   356                         operation.handle_event('revert%s_event' % trstate)
       
   357                     self.rollback(reset_pool)
       
   358                     raise
       
   359             self.pool.commit()
       
   360         finally:
       
   361             self._touch()
       
   362             self.commit_state = None
       
   363             self.pending_operations[:] = []
       
   364             self._query_data.clear()
       
   365             if reset_pool:
       
   366                 self.reset_pool()
       
   367                         
       
   368     def rollback(self, reset_pool=True):
       
   369         """rollback the current session's transaction"""
       
   370         if self.pool is None:
       
   371             assert not self.pending_operations
       
   372             self._query_data.clear()
       
   373             self._touch()
       
   374             return
       
   375         try:
       
   376             while self.pending_operations:
       
   377                 try:
       
   378                     operation = self.pending_operations.pop(0)
       
   379                     operation.handle_event('rollback_event')
       
   380                 except:
       
   381                     self.critical('rollback error', exc_info=sys.exc_info())
       
   382                     continue
       
   383             self.pool.rollback()
       
   384         finally:
       
   385             self._touch()
       
   386             self.pending_operations[:] = []
       
   387             self._query_data.clear()
       
   388             if reset_pool:
       
   389                 self.reset_pool()
       
   390         
       
   391     def close(self):
       
   392         """do not close pool on session close, since they are shared now"""
       
   393         self.rollback()
       
   394         
       
   395     # transaction data/operations management ##################################
       
   396     
       
   397     def add_query_data(self, key, value):
       
   398         self._query_data.setdefault(key, []).append(value)
       
   399     
       
   400     def set_query_data(self, key, value):
       
   401         self._query_data[key] = value
       
   402         
       
   403     def query_data(self, key, default=None, setdefault=False, pop=False):
       
   404         if setdefault:
       
   405             assert not pop
       
   406             return self._query_data.setdefault(key, default)
       
   407         if pop:
       
   408             return self._query_data.pop(key, default)
       
   409         else:
       
   410             return self._query_data.get(key, default)
       
   411         
       
   412     def add_operation(self, operation, index=None):
       
   413         """add an observer"""
       
   414         assert self.commit_state != 'commit'
       
   415         if index is not None:
       
   416             self.pending_operations.insert(index, operation)
       
   417         else:
       
   418             self.pending_operations.append(operation)
       
   419             
       
   420     # querier helpers #########################################################
       
   421     
       
   422     def build_description(self, rqlst, args, result):
       
   423         """build a description for a given result"""
       
   424         if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1:
       
   425             # easy, all lines are identical
       
   426             selected = rqlst.children[0].selection
       
   427             solution = rqlst.children[0].solutions[0]
       
   428             description = _make_description(selected, args, solution)
       
   429             return [tuple(description)] * len(result)
       
   430         # hard, delegate the work :o)
       
   431         return self.manual_build_descr(rqlst, args, result)
       
   432 
       
   433     def manual_build_descr(self, rqlst, args, result):
       
   434         """build a description for a given result by analysing each row
       
   435         
       
   436         XXX could probably be done more efficiently during execution of query
       
   437         """
       
   438         # not so easy, looks for variable which changes from one solution
       
   439         # to another
       
   440         unstables = rqlst.get_variable_variables()
       
   441         basedescription = []
       
   442         todetermine = []
       
   443         selected = rqlst.children[0].selection # sample selection
       
   444         for i, term in enumerate(selected):
       
   445             if isinstance(term, Function) and term.descr().rtype is not None:
       
   446                 basedescription.append(term.get_type(term.descr().rtype, args))
       
   447                 continue
       
   448             for vref in term.get_nodes(VariableRef):
       
   449                 if vref.name in unstables:
       
   450                     basedescription.append(None)
       
   451                     todetermine.append( (i, is_final(rqlst, vref.variable, args)) )
       
   452                     break
       
   453             else:
       
   454                 # sample etype
       
   455                 etype = rqlst.children[0].solutions[0]
       
   456                 basedescription.append(term.get_type(etype, args))
       
   457         if not todetermine:
       
   458             return [tuple(basedescription)] * len(result)
       
   459         return self._build_descr(result, basedescription, todetermine)
       
   460     
       
   461     def _build_descr(self, result, basedescription, todetermine):
       
   462         description = []
       
   463         etype_from_eid = self.describe
       
   464         for row in result:
       
   465             row_descr = basedescription
       
   466             for index, isfinal in todetermine:
       
   467                 value = row[index]
       
   468                 if value is None:
       
   469                     # None value inserted by an outer join, no type
       
   470                     row_descr[index] = None
       
   471                     continue
       
   472                 if isfinal:
       
   473                     row_descr[index] = etype_from_pyobj(value)
       
   474                 else:
       
   475                     row_descr[index] = etype_from_eid(value)[0]
       
   476             description.append(tuple(row_descr))
       
   477         return description
       
   478 
       
   479     
       
   480 class ChildSession(Session):
       
   481     """child (or internal) session are used to hijack the security system
       
   482     """
       
   483     cnxtype = 'inmemory'
       
   484     
       
   485     def __init__(self, parent_session):
       
   486         self.id = None
       
   487         self.is_internal_session = False
       
   488         self.is_super_session = True
       
   489         # session which has created this one
       
   490         self.parent_session = parent_session
       
   491         self.user = InternalManager()
       
   492         self.repo = parent_session.repo
       
   493         self.vreg = parent_session.vreg
       
   494         self.data = parent_session.data
       
   495         self.encoding = parent_session.encoding
       
   496         self.lang = parent_session.lang
       
   497         self._ = self.__ = parent_session._
       
   498         # short cut to querier .execute method
       
   499         self._execute = self.repo.querier.execute
       
   500     
       
   501     @property
       
   502     def super_session(self):
       
   503         return self
       
   504 
       
   505     def get_mode(self):
       
   506         return self.parent_session.mode
       
   507     def set_mode(self, value):
       
   508         self.parent_session.set_mode(value)
       
   509     mode = property(get_mode, set_mode)
       
   510 
       
   511     def get_commit_state(self):
       
   512         return self.parent_session.commit_state
       
   513     def set_commit_state(self, value):
       
   514         self.parent_session.set_commit_state(value)
       
   515     commit_state = property(get_commit_state, set_commit_state)
       
   516     
       
   517     @property
       
   518     def pool(self):
       
   519         return self.parent_session.pool
       
   520     @property
       
   521     def pending_operations(self):
       
   522         return self.parent_session.pending_operations
       
   523     @property
       
   524     def _query_data(self):
       
   525         return self.parent_session._query_data
       
   526         
       
   527     def set_pool(self):
       
   528         """the session need a pool to execute some queries"""
       
   529         self.parent_session.set_pool()
       
   530             
       
   531     def reset_pool(self):
       
   532         """the session has no longer using its pool, at least for some time
       
   533         """
       
   534         self.parent_session.reset_pool()
       
   535 
       
   536     def actual_session(self):
       
   537         """return the original parent session if any, else self"""
       
   538         return self.parent_session
       
   539         
       
   540     def commit(self, reset_pool=True):
       
   541         """commit the current session's transaction"""
       
   542         self.parent_session.commit(reset_pool)
       
   543         
       
   544     def rollback(self, reset_pool=True):
       
   545         """rollback the current session's transaction"""
       
   546         self.parent_session.rollback(reset_pool)
       
   547         
       
   548     def close(self):
       
   549         """do not close pool on session close, since they are shared now"""
       
   550         self.rollback()
       
   551         
       
   552     def user_data(self):
       
   553         """returns a dictionnary with this user's information"""
       
   554         return self.parent_session.user_data()
       
   555 
       
   556 
       
   557 class InternalSession(Session):
       
   558     """special session created internaly by the repository"""
       
   559     
       
   560     def __init__(self, repo, cnxprops=None):
       
   561         super(InternalSession, self).__init__(_IMANAGER, repo, cnxprops,
       
   562                                               _id='internal')
       
   563         self.cnxtype = 'inmemory'
       
   564         self.is_internal_session = True
       
   565         self.is_super_session = True
       
   566     
       
   567     @property
       
   568     def super_session(self):
       
   569         return self
       
   570 
       
   571 
       
   572 class InternalManager(object):
       
   573     """a manager user with all access rights used internally for task such as
       
   574     bootstrapping the repository or creating regular users according to
       
   575     repository content
       
   576     """
       
   577     def __init__(self):
       
   578         self.eid = -1
       
   579         self.login = u'__internal_manager__'
       
   580         self.properties = {}
       
   581 
       
   582     def matching_groups(self, groups):
       
   583         return 1
       
   584 
       
   585     def is_in_group(self, group):
       
   586         return True
       
   587 
       
   588     def owns(self, eid):
       
   589         return True
       
   590     
       
   591     def has_permission(self, pname, contexteid=None):
       
   592         return True
       
   593 
       
   594     def property_value(self, key):
       
   595         if key == 'ui.language':
       
   596             return 'en'
       
   597         return None
       
   598 
       
   599 _IMANAGER= InternalManager()
       
   600 
       
   601 from logging import getLogger
       
   602 from cubicweb import set_log_methods
       
   603 set_log_methods(Session, getLogger('cubicweb.session'))