author | Denis Laxalde <denis.laxalde@logilab.fr> |
Mon, 19 Jun 2017 18:15:28 +0200 | |
changeset 12188 | fea018b2e056 |
parent 12023 | 0d2b889c85d3 |
child 12567 | 26744ad37953 |
permissions | -rw-r--r-- |
# copyright 2003-2016 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 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.appobject import AppObject # 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): 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 and 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) repo.bootstrap() 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) failed = sqlexec(schemasql, execute, pbtitle=_title) 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) # replace previous schema by the new repo's one. This is necessary so that we give the proper # schema to `initialize_schema` above since it will initialize .eid attribute of schema elements schema = repo.schema 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 # noqa # 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'), }