cubicweb/server/__init__.py
changeset 11057 0b59724cb3f2
parent 10894 c8c6ad8adbdb
child 11129 97095348b3ee
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Server subcube of cubicweb : defines objects used only on the server
       
    19 (repository) side
       
    20 
       
    21 The server module contains functions to initialize a new repository.
       
    22 """
       
    23 from __future__ import print_function
       
    24 
       
    25 __docformat__ = "restructuredtext en"
       
    26 
       
    27 import sys
       
    28 from os.path import join, exists
       
    29 from glob import glob
       
    30 from contextlib import contextmanager
       
    31 
       
    32 from six import text_type, string_types
       
    33 from six.moves import filter
       
    34 
       
    35 from logilab.common.modutils import LazyObject
       
    36 from logilab.common.textutils import splitstrip
       
    37 from logilab.common.registry import yes
       
    38 from logilab import database
       
    39 
       
    40 from yams import BASE_GROUPS
       
    41 
       
    42 from cubicweb import CW_SOFTWARE_ROOT
       
    43 from cubicweb.appobject import AppObject
       
    44 
       
    45 class ShuttingDown(BaseException):
       
    46     """raised when trying to access some resources while the repository is
       
    47     shutting down. Inherit from BaseException so that `except Exception` won't
       
    48     catch it.
       
    49     """
       
    50 
       
    51 # server-side services #########################################################
       
    52 
       
    53 class Service(AppObject):
       
    54     """Base class for services.
       
    55 
       
    56     A service is a selectable object that performs an action server-side.
       
    57     Use :class:`cubicweb.dbapi.Connection.call_service` to call them from
       
    58     the web-side.
       
    59 
       
    60     When inheriting this class, do not forget to define at least the __regid__
       
    61     attribute (and probably __select__ too).
       
    62     """
       
    63     __registry__ = 'services'
       
    64     __select__ = yes()
       
    65 
       
    66     def call(self, **kwargs):
       
    67         raise NotImplementedError
       
    68 
       
    69 
       
    70 # server-side debugging ########################################################
       
    71 
       
    72 # server debugging flags. They may be combined using binary operators.
       
    73 
       
    74 #:no debug information
       
    75 DBG_NONE = 0  #: no debug information
       
    76 #: rql execution information
       
    77 DBG_RQL  = 1
       
    78 #: executed sql
       
    79 DBG_SQL  = 2
       
    80 #: repository events
       
    81 DBG_REPO = 4
       
    82 #: multi-sources
       
    83 DBG_MS   = 8
       
    84 #: hooks
       
    85 DBG_HOOKS = 16
       
    86 #: operations
       
    87 DBG_OPS = 32
       
    88 #: security
       
    89 DBG_SEC = 64
       
    90 #: more verbosity
       
    91 DBG_MORE = 128
       
    92 #: all level enabled
       
    93 DBG_ALL  = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE
       
    94 
       
    95 _SECURITY_ITEMS = []
       
    96 _SECURITY_CAPS = ['read', 'add', 'update', 'delete', 'transition']
       
    97 
       
    98 #: current debug mode
       
    99 DEBUG = 0
       
   100 
       
   101 @contextmanager
       
   102 def tunesecurity(items=(), capabilities=()):
       
   103     """Context manager to use in conjunction with DBG_SEC.
       
   104 
       
   105     This allows some tuning of:
       
   106     * the monitored capabilities ('read', 'add', ....)
       
   107     * the object being checked by the security checkers
       
   108 
       
   109     When no item is given, all of them will be watched.
       
   110     By default all capabilities are monitored, unless specified.
       
   111 
       
   112     Example use::
       
   113 
       
   114       from cubicweb.server import debugged, DBG_SEC, tunesecurity
       
   115       with debugged(DBG_SEC):
       
   116           with tunesecurity(items=('Elephant', 'trumps'),
       
   117                             capabilities=('update', 'delete')):
       
   118               babar.cw_set(trumps=celeste)
       
   119               flore.cw_delete()
       
   120 
       
   121       ==>
       
   122 
       
   123       check_perm: 'update' 'relation Elephant.trumps.Elephant'
       
   124        [(ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s),
       
   125        {'eid': 2167}, True)]
       
   126       check_perm: 'delete' 'Elephant'
       
   127        [(ERQLExpression(Any X WHERE U has_delete_permission X, X eid %(x)s, U eid %(u)s),
       
   128        {'eid': 2168}, True)]
       
   129 
       
   130     """
       
   131     olditems = _SECURITY_ITEMS[:]
       
   132     _SECURITY_ITEMS.extend(list(items))
       
   133     oldactions = _SECURITY_CAPS[:]
       
   134     _SECURITY_CAPS[:] = capabilities
       
   135     yield
       
   136     _SECURITY_ITEMS[:] = olditems
       
   137     _SECURITY_CAPS[:] = oldactions
       
   138 
       
   139 def set_debug(debugmode):
       
   140     """change the repository debugging mode"""
       
   141     global DEBUG
       
   142     if not debugmode:
       
   143         DEBUG = 0
       
   144         return
       
   145     if isinstance(debugmode, string_types):
       
   146         for mode in splitstrip(debugmode, sep='|'):
       
   147             DEBUG |= globals()[mode]
       
   148     else:
       
   149         DEBUG |= debugmode
       
   150 
       
   151 class debugged(object):
       
   152     """Context manager and decorator to help debug the repository.
       
   153 
       
   154     It can be used either as a context manager:
       
   155 
       
   156     >>> with debugged('DBG_RQL | DBG_REPO'):
       
   157     ...     # some code in which you want to debug repository activity,
       
   158     ...     # seing information about RQL being executed an repository events.
       
   159 
       
   160     or as a function decorator:
       
   161 
       
   162     >>> @debugged('DBG_RQL | DBG_REPO')
       
   163     ... def some_function():
       
   164     ...     # some code in which you want to debug repository activity,
       
   165     ...     # seing information about RQL being executed an repository events
       
   166 
       
   167     The debug mode will be reset to its original value when leaving the "with"
       
   168     block or the decorated function.
       
   169     """
       
   170     def __init__(self, debugmode):
       
   171         self.debugmode = debugmode
       
   172         self._clevel = None
       
   173 
       
   174     def __enter__(self):
       
   175         """enter with block"""
       
   176         self._clevel = DEBUG
       
   177         set_debug(self.debugmode)
       
   178 
       
   179     def __exit__(self, exctype, exc, traceback):
       
   180         """leave with block"""
       
   181         set_debug(self._clevel)
       
   182         return traceback is None
       
   183 
       
   184     def __call__(self, func):
       
   185         """decorate function"""
       
   186         def wrapped(*args, **kwargs):
       
   187             _clevel = DEBUG
       
   188             set_debug(self.debugmode)
       
   189             try:
       
   190                 return func(*args, **kwargs)
       
   191             finally:
       
   192                 set_debug(self._clevel)
       
   193         return wrapped
       
   194 
       
   195 # database initialization ######################################################
       
   196 
       
   197 def create_user(session, login, pwd, *groups):
       
   198     # monkey patch this method if you want to customize admin/anon creation
       
   199     # (that maybe necessary if you change CWUser's schema)
       
   200     user = session.create_entity('CWUser', login=login, upassword=pwd)
       
   201     for group in groups:
       
   202         session.execute('SET U in_group G WHERE U eid %(u)s, G name %(group)s',
       
   203                         {'u': user.eid, 'group': text_type(group)})
       
   204     return user
       
   205 
       
   206 def init_repository(config, interactive=True, drop=False, vreg=None,
       
   207                     init_config=None):
       
   208     """initialise a repository database by creating tables add filling them
       
   209     with the minimal set of entities (ie at least the schema, base groups and
       
   210     a initial user)
       
   211     """
       
   212     from cubicweb.repoapi import get_repository, connect
       
   213     from cubicweb.server.repository import Repository
       
   214     from cubicweb.server.utils import manager_userpasswd
       
   215     from cubicweb.server.sqlutils import sqlexec, sqlschema, sql_drop_all_user_tables
       
   216     from cubicweb.server.sqlutils import _SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION as drop_filter
       
   217     # configuration to avoid db schema loading and user'state checking
       
   218     # on connection
       
   219     config.creating = True
       
   220     config.consider_user_state = False
       
   221     config.cubicweb_appobject_path = set(('hooks', 'entities'))
       
   222     config.cube_appobject_path = set(('hooks', 'entities'))
       
   223     # only enable the system source at initialization time
       
   224     repo = Repository(config, vreg=vreg)
       
   225     if init_config is not None:
       
   226         # further config initialization once it has been bootstrapped
       
   227         init_config(config)
       
   228     schema = repo.schema
       
   229     sourcescfg = config.read_sources_file()
       
   230     source = sourcescfg['system']
       
   231     driver = source['db-driver']
       
   232     with repo.internal_cnx() as cnx:
       
   233         sqlcnx = cnx.cnxset.cnx
       
   234         sqlcursor = cnx.cnxset.cu
       
   235         execute = sqlcursor.execute
       
   236         if drop:
       
   237             helper = database.get_db_helper(driver)
       
   238             dropsql = sql_drop_all_user_tables(helper, sqlcursor)
       
   239             # We may fail dropping some tables because of table dependencies, in a first pass.
       
   240             # So, we try a second drop sequence to drop remaining tables if needed.
       
   241             # Note that 2 passes is an arbitrary choice as it seems enough for our usecases
       
   242             # (looping may induce infinite recursion when user have no rights for example).
       
   243             # Here we try to keep code simple and backend independent. That's why we don't try to
       
   244             # distinguish remaining tables (missing privileges, dependencies, ...).
       
   245             failed = sqlexec(dropsql, execute, cnx=sqlcnx,
       
   246                              pbtitle='-> dropping tables (first pass)')
       
   247             if failed:
       
   248                 failed = sqlexec(failed, execute, cnx=sqlcnx,
       
   249                                  pbtitle='-> dropping tables (second pass)')
       
   250                 remainings = list(filter(drop_filter, helper.list_tables(sqlcursor)))
       
   251                 assert not remainings, 'Remaining tables: %s' % ', '.join(remainings)
       
   252         handler = config.migration_handler(schema, interactive=False, repo=repo, cnx=cnx)
       
   253         # install additional driver specific sql files
       
   254         handler.cmd_install_custom_sql_scripts()
       
   255         for cube in reversed(config.cubes()):
       
   256             handler.cmd_install_custom_sql_scripts(cube)
       
   257         _title = '-> creating tables '
       
   258         print(_title, end=' ')
       
   259         # schema entities and relations tables
       
   260         # can't skip entities table even if system source doesn't support them,
       
   261         # they are used sometimes by generated sql. Keeping them empty is much
       
   262         # simpler than fixing this...
       
   263         schemasql = sqlschema(schema, driver)
       
   264         #skip_entities=[str(e) for e in schema.entities()
       
   265         #               if not repo.system_source.support_entity(str(e))])
       
   266         failed = sqlexec(schemasql, execute, pbtitle=_title, delimiter=';;')
       
   267         if failed:
       
   268             print('The following SQL statements failed. You should check your schema.')
       
   269             print(failed)
       
   270             raise Exception('execution of the sql schema failed, you should check your schema')
       
   271         sqlcursor.close()
       
   272         sqlcnx.commit()
       
   273     with repo.internal_cnx() as cnx:
       
   274         # insert entity representing the system source
       
   275         ssource = cnx.create_entity('CWSource', type=u'native', name=u'system')
       
   276         repo.system_source.eid = ssource.eid
       
   277         cnx.execute('SET X cw_source X WHERE X eid %(x)s', {'x': ssource.eid})
       
   278         # insert base groups and default admin
       
   279         print('-> inserting default user and default groups.')
       
   280         try:
       
   281             login = text_type(sourcescfg['admin']['login'])
       
   282             pwd = sourcescfg['admin']['password']
       
   283         except KeyError:
       
   284             if interactive:
       
   285                 msg = 'enter login and password of the initial manager account'
       
   286                 login, pwd = manager_userpasswd(msg=msg, confirm=True)
       
   287             else:
       
   288                 login, pwd = text_type(source['db-user']), source['db-password']
       
   289         # sort for eid predicatability as expected in some server tests
       
   290         for group in sorted(BASE_GROUPS):
       
   291             cnx.create_entity('CWGroup', name=text_type(group))
       
   292         admin = create_user(cnx, login, pwd, u'managers')
       
   293         cnx.execute('SET X owned_by U WHERE X is IN (CWGroup,CWSource), U eid %(u)s',
       
   294                         {'u': admin.eid})
       
   295         cnx.commit()
       
   296     repo.shutdown()
       
   297     # re-login using the admin user
       
   298     config._cubes = None # avoid assertion error
       
   299     repo = get_repository(config=config)
       
   300     with connect(repo, login, password=pwd) as cnx:
       
   301         with cnx.security_enabled(False, False):
       
   302             repo.system_source.eid = ssource.eid # redo this manually
       
   303             handler = config.migration_handler(schema, interactive=False,
       
   304                                                cnx=cnx, repo=repo)
       
   305             # serialize the schema
       
   306             initialize_schema(config, schema, handler)
       
   307             # yoo !
       
   308             cnx.commit()
       
   309             repo.system_source.init_creating()
       
   310             cnx.commit()
       
   311     repo.shutdown()
       
   312     # restore initial configuration
       
   313     config.creating = False
       
   314     config.consider_user_state = True
       
   315     # (drop instance attribute to get back to class attribute)
       
   316     del config.cubicweb_appobject_path
       
   317     del config.cube_appobject_path
       
   318     print('-> database for instance %s initialized.' % config.appid)
       
   319 
       
   320 
       
   321 def initialize_schema(config, schema, mhandler, event='create'):
       
   322     from cubicweb.server.schemaserial import serialize_schema
       
   323     cnx = mhandler.cnx
       
   324     cubes = config.cubes()
       
   325     # deactivate every hooks but those responsible to set metadata
       
   326     # so, NO INTEGRITY CHECKS are done, to have quicker db creation.
       
   327     # Active integrity is kept else we may pb such as two default
       
   328     # workflows for one entity type.
       
   329     with cnx.deny_all_hooks_but('metadata', 'activeintegrity'):
       
   330         # execute cubicweb's pre<event> script
       
   331         mhandler.cmd_exec_event_script('pre%s' % event)
       
   332         # execute cubes pre<event> script if any
       
   333         for cube in reversed(cubes):
       
   334             mhandler.cmd_exec_event_script('pre%s' % event, cube)
       
   335         # execute instance's pre<event> script (useful in tests)
       
   336         mhandler.cmd_exec_event_script('pre%s' % event, apphome=True)
       
   337         # enter instance'schema into the database
       
   338         serialize_schema(cnx, schema)
       
   339         cnx.commit()
       
   340         # execute cubicweb's post<event> script
       
   341         mhandler.cmd_exec_event_script('post%s' % event)
       
   342         # execute cubes'post<event> script if any
       
   343         for cube in reversed(cubes):
       
   344             mhandler.cmd_exec_event_script('post%s' % event, cube)
       
   345         # execute instance's post<event> script (useful in tests)
       
   346         mhandler.cmd_exec_event_script('post%s' % event, apphome=True)
       
   347 
       
   348 
       
   349 # sqlite'stored procedures have to be registered at connection opening time
       
   350 from logilab.database import SQL_CONNECT_HOOKS
       
   351 
       
   352 # add to this set relations which should have their add security checking done
       
   353 # *BEFORE* adding the actual relation (done after by default)
       
   354 BEFORE_ADD_RELATIONS = set(('owned_by',))
       
   355 
       
   356 # add to this set relations which should have their add security checking done
       
   357 # *at COMMIT TIME* (done after by default)
       
   358 ON_COMMIT_ADD_RELATIONS = set(())
       
   359 
       
   360 # available sources registry
       
   361 SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'),
       
   362                 'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
       
   363                 'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'),
       
   364                 }