diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/server/__init__.py --- /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 . +"""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 script + mhandler.cmd_exec_event_script('pre%s' % event) + # execute cubes pre script if any + for cube in reversed(cubes): + mhandler.cmd_exec_event_script('pre%s' % event, cube) + # execute instance's pre 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 script + mhandler.cmd_exec_event_script('post%s' % event) + # execute cubes'post script if any + for cube in reversed(cubes): + mhandler.cmd_exec_event_script('post%s' % event, cube) + # execute instance's post 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'), + }