cubicweb/server/__init__.py
changeset 11057 0b59724cb3f2
parent 10894 c8c6ad8adbdb
child 11129 97095348b3ee
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/__init__.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,364 @@
+# copyright 2003-2014 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/>.
+"""Server subcube of cubicweb : defines objects used only on the server
+(repository) side
+
+The server module contains functions to initialize a new repository.
+"""
+from __future__ import print_function
+
+__docformat__ = "restructuredtext en"
+
+import sys
+from os.path import join, exists
+from glob import glob
+from contextlib import contextmanager
+
+from six import text_type, string_types
+from six.moves import filter
+
+from logilab.common.modutils import LazyObject
+from logilab.common.textutils import splitstrip
+from logilab.common.registry import yes
+from logilab import database
+
+from yams import BASE_GROUPS
+
+from cubicweb import CW_SOFTWARE_ROOT
+from cubicweb.appobject import AppObject
+
+class ShuttingDown(BaseException):
+    """raised when trying to access some resources while the repository is
+    shutting down. Inherit from BaseException so that `except Exception` won't
+    catch it.
+    """
+
+# server-side services #########################################################
+
+class Service(AppObject):
+    """Base class for services.
+
+    A service is a selectable object that performs an action server-side.
+    Use :class:`cubicweb.dbapi.Connection.call_service` to call them from
+    the web-side.
+
+    When inheriting this class, do not forget to define at least the __regid__
+    attribute (and probably __select__ too).
+    """
+    __registry__ = 'services'
+    __select__ = yes()
+
+    def call(self, **kwargs):
+        raise NotImplementedError
+
+
+# server-side debugging ########################################################
+
+# server debugging flags. They may be combined using binary operators.
+
+#:no debug information
+DBG_NONE = 0  #: no debug information
+#: rql execution information
+DBG_RQL  = 1
+#: executed sql
+DBG_SQL  = 2
+#: repository events
+DBG_REPO = 4
+#: multi-sources
+DBG_MS   = 8
+#: hooks
+DBG_HOOKS = 16
+#: operations
+DBG_OPS = 32
+#: security
+DBG_SEC = 64
+#: more verbosity
+DBG_MORE = 128
+#: all level enabled
+DBG_ALL  = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE
+
+_SECURITY_ITEMS = []
+_SECURITY_CAPS = ['read', 'add', 'update', 'delete', 'transition']
+
+#: current debug mode
+DEBUG = 0
+
+@contextmanager
+def tunesecurity(items=(), capabilities=()):
+    """Context manager to use in conjunction with DBG_SEC.
+
+    This allows some tuning of:
+    * the monitored capabilities ('read', 'add', ....)
+    * the object being checked by the security checkers
+
+    When no item is given, all of them will be watched.
+    By default all capabilities are monitored, unless specified.
+
+    Example use::
+
+      from cubicweb.server import debugged, DBG_SEC, tunesecurity
+      with debugged(DBG_SEC):
+          with tunesecurity(items=('Elephant', 'trumps'),
+                            capabilities=('update', 'delete')):
+              babar.cw_set(trumps=celeste)
+              flore.cw_delete()
+
+      ==>
+
+      check_perm: 'update' 'relation Elephant.trumps.Elephant'
+       [(ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s),
+       {'eid': 2167}, True)]
+      check_perm: 'delete' 'Elephant'
+       [(ERQLExpression(Any X WHERE U has_delete_permission X, X eid %(x)s, U eid %(u)s),
+       {'eid': 2168}, True)]
+
+    """
+    olditems = _SECURITY_ITEMS[:]
+    _SECURITY_ITEMS.extend(list(items))
+    oldactions = _SECURITY_CAPS[:]
+    _SECURITY_CAPS[:] = capabilities
+    yield
+    _SECURITY_ITEMS[:] = olditems
+    _SECURITY_CAPS[:] = oldactions
+
+def set_debug(debugmode):
+    """change the repository debugging mode"""
+    global DEBUG
+    if not debugmode:
+        DEBUG = 0
+        return
+    if isinstance(debugmode, string_types):
+        for mode in splitstrip(debugmode, sep='|'):
+            DEBUG |= globals()[mode]
+    else:
+        DEBUG |= debugmode
+
+class debugged(object):
+    """Context manager and decorator to help debug the repository.
+
+    It can be used either as a context manager:
+
+    >>> with debugged('DBG_RQL | DBG_REPO'):
+    ...     # some code in which you want to debug repository activity,
+    ...     # seing information about RQL being executed an repository events.
+
+    or as a function decorator:
+
+    >>> @debugged('DBG_RQL | DBG_REPO')
+    ... def some_function():
+    ...     # some code in which you want to debug repository activity,
+    ...     # seing information about RQL being executed an repository events
+
+    The debug mode will be reset to its original value when leaving the "with"
+    block or the decorated function.
+    """
+    def __init__(self, debugmode):
+        self.debugmode = debugmode
+        self._clevel = None
+
+    def __enter__(self):
+        """enter with block"""
+        self._clevel = DEBUG
+        set_debug(self.debugmode)
+
+    def __exit__(self, exctype, exc, traceback):
+        """leave with block"""
+        set_debug(self._clevel)
+        return traceback is None
+
+    def __call__(self, func):
+        """decorate function"""
+        def wrapped(*args, **kwargs):
+            _clevel = DEBUG
+            set_debug(self.debugmode)
+            try:
+                return func(*args, **kwargs)
+            finally:
+                set_debug(self._clevel)
+        return wrapped
+
+# database initialization ######################################################
+
+def create_user(session, login, pwd, *groups):
+    # monkey patch this method if you want to customize admin/anon creation
+    # (that maybe necessary if you change CWUser's schema)
+    user = session.create_entity('CWUser', login=login, upassword=pwd)
+    for group in groups:
+        session.execute('SET U in_group G WHERE U eid %(u)s, G name %(group)s',
+                        {'u': user.eid, 'group': text_type(group)})
+    return user
+
+def init_repository(config, interactive=True, drop=False, vreg=None,
+                    init_config=None):
+    """initialise a repository database by creating tables add filling them
+    with the minimal set of entities (ie at least the schema, base groups and
+    a initial user)
+    """
+    from cubicweb.repoapi import get_repository, connect
+    from cubicweb.server.repository import Repository
+    from cubicweb.server.utils import manager_userpasswd
+    from cubicweb.server.sqlutils import sqlexec, sqlschema, sql_drop_all_user_tables
+    from cubicweb.server.sqlutils import _SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION as drop_filter
+    # configuration to avoid db schema loading and user'state checking
+    # on connection
+    config.creating = True
+    config.consider_user_state = False
+    config.cubicweb_appobject_path = set(('hooks', 'entities'))
+    config.cube_appobject_path = set(('hooks', 'entities'))
+    # only enable the system source at initialization time
+    repo = Repository(config, vreg=vreg)
+    if init_config is not None:
+        # further config initialization once it has been bootstrapped
+        init_config(config)
+    schema = repo.schema
+    sourcescfg = config.read_sources_file()
+    source = sourcescfg['system']
+    driver = source['db-driver']
+    with repo.internal_cnx() as cnx:
+        sqlcnx = cnx.cnxset.cnx
+        sqlcursor = cnx.cnxset.cu
+        execute = sqlcursor.execute
+        if drop:
+            helper = database.get_db_helper(driver)
+            dropsql = sql_drop_all_user_tables(helper, sqlcursor)
+            # We may fail dropping some tables because of table dependencies, in a first pass.
+            # So, we try a second drop sequence to drop remaining tables if needed.
+            # Note that 2 passes is an arbitrary choice as it seems enough for our usecases
+            # (looping may induce infinite recursion when user have no rights for example).
+            # Here we try to keep code simple and backend independent. That's why we don't try to
+            # distinguish remaining tables (missing privileges, dependencies, ...).
+            failed = sqlexec(dropsql, execute, cnx=sqlcnx,
+                             pbtitle='-> dropping tables (first pass)')
+            if failed:
+                failed = sqlexec(failed, execute, cnx=sqlcnx,
+                                 pbtitle='-> dropping tables (second pass)')
+                remainings = list(filter(drop_filter, helper.list_tables(sqlcursor)))
+                assert not remainings, 'Remaining tables: %s' % ', '.join(remainings)
+        handler = config.migration_handler(schema, interactive=False, repo=repo, cnx=cnx)
+        # install additional driver specific sql files
+        handler.cmd_install_custom_sql_scripts()
+        for cube in reversed(config.cubes()):
+            handler.cmd_install_custom_sql_scripts(cube)
+        _title = '-> creating tables '
+        print(_title, end=' ')
+        # schema entities and relations tables
+        # can't skip entities table even if system source doesn't support them,
+        # they are used sometimes by generated sql. Keeping them empty is much
+        # simpler than fixing this...
+        schemasql = sqlschema(schema, driver)
+        #skip_entities=[str(e) for e in schema.entities()
+        #               if not repo.system_source.support_entity(str(e))])
+        failed = sqlexec(schemasql, execute, pbtitle=_title, delimiter=';;')
+        if failed:
+            print('The following SQL statements failed. You should check your schema.')
+            print(failed)
+            raise Exception('execution of the sql schema failed, you should check your schema')
+        sqlcursor.close()
+        sqlcnx.commit()
+    with repo.internal_cnx() as cnx:
+        # insert entity representing the system source
+        ssource = cnx.create_entity('CWSource', type=u'native', name=u'system')
+        repo.system_source.eid = ssource.eid
+        cnx.execute('SET X cw_source X WHERE X eid %(x)s', {'x': ssource.eid})
+        # insert base groups and default admin
+        print('-> inserting default user and default groups.')
+        try:
+            login = text_type(sourcescfg['admin']['login'])
+            pwd = sourcescfg['admin']['password']
+        except KeyError:
+            if interactive:
+                msg = 'enter login and password of the initial manager account'
+                login, pwd = manager_userpasswd(msg=msg, confirm=True)
+            else:
+                login, pwd = text_type(source['db-user']), source['db-password']
+        # sort for eid predicatability as expected in some server tests
+        for group in sorted(BASE_GROUPS):
+            cnx.create_entity('CWGroup', name=text_type(group))
+        admin = create_user(cnx, login, pwd, u'managers')
+        cnx.execute('SET X owned_by U WHERE X is IN (CWGroup,CWSource), U eid %(u)s',
+                        {'u': admin.eid})
+        cnx.commit()
+    repo.shutdown()
+    # re-login using the admin user
+    config._cubes = None # avoid assertion error
+    repo = get_repository(config=config)
+    with connect(repo, login, password=pwd) as cnx:
+        with cnx.security_enabled(False, False):
+            repo.system_source.eid = ssource.eid # redo this manually
+            handler = config.migration_handler(schema, interactive=False,
+                                               cnx=cnx, repo=repo)
+            # serialize the schema
+            initialize_schema(config, schema, handler)
+            # yoo !
+            cnx.commit()
+            repo.system_source.init_creating()
+            cnx.commit()
+    repo.shutdown()
+    # restore initial configuration
+    config.creating = False
+    config.consider_user_state = True
+    # (drop instance attribute to get back to class attribute)
+    del config.cubicweb_appobject_path
+    del config.cube_appobject_path
+    print('-> database for instance %s initialized.' % config.appid)
+
+
+def initialize_schema(config, schema, mhandler, event='create'):
+    from cubicweb.server.schemaserial import serialize_schema
+    cnx = mhandler.cnx
+    cubes = config.cubes()
+    # deactivate every hooks but those responsible to set metadata
+    # so, NO INTEGRITY CHECKS are done, to have quicker db creation.
+    # Active integrity is kept else we may pb such as two default
+    # workflows for one entity type.
+    with cnx.deny_all_hooks_but('metadata', 'activeintegrity'):
+        # execute cubicweb's pre<event> script
+        mhandler.cmd_exec_event_script('pre%s' % event)
+        # execute cubes pre<event> script if any
+        for cube in reversed(cubes):
+            mhandler.cmd_exec_event_script('pre%s' % event, cube)
+        # execute instance's pre<event> script (useful in tests)
+        mhandler.cmd_exec_event_script('pre%s' % event, apphome=True)
+        # enter instance'schema into the database
+        serialize_schema(cnx, schema)
+        cnx.commit()
+        # execute cubicweb's post<event> script
+        mhandler.cmd_exec_event_script('post%s' % event)
+        # execute cubes'post<event> script if any
+        for cube in reversed(cubes):
+            mhandler.cmd_exec_event_script('post%s' % event, cube)
+        # execute instance's post<event> script (useful in tests)
+        mhandler.cmd_exec_event_script('post%s' % event, apphome=True)
+
+
+# sqlite'stored procedures have to be registered at connection opening time
+from logilab.database import SQL_CONNECT_HOOKS
+
+# add to this set relations which should have their add security checking done
+# *BEFORE* adding the actual relation (done after by default)
+BEFORE_ADD_RELATIONS = set(('owned_by',))
+
+# add to this set relations which should have their add security checking done
+# *at COMMIT TIME* (done after by default)
+ON_COMMIT_ADD_RELATIONS = set(())
+
+# available sources registry
+SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'),
+                'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
+                'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'),
+                }