# HG changeset patch # User Sylvain Thénault # Date 1268121583 -3600 # Node ID 13b0b96d7982788c0bf0e6c07229055c75e2ea37 # Parent b718626a0e60257dc27036a4798bd589e16d3604 [repo] enhanced security handling: deprecates unsafe_execute, in favor of explicit read/write security control using the `enabled_security` context manager. Also code executed on the repository side is now unsafe by default. diff -r b718626a0e60 -r 13b0b96d7982 devtools/fake.py --- a/devtools/fake.py Mon Mar 08 19:02:35 2010 +0100 +++ b/devtools/fake.py Tue Mar 09 08:59:43 2010 +0100 @@ -116,17 +116,6 @@ def validate_cache(self): pass - # session compatibility (in some test are using this class to test server - # side views...) - def actual_session(self): - """return the original parent session if any, else self""" - return self - - def unsafe_execute(self, *args, **kwargs): - """return the original parent session if any, else self""" - kwargs.pop('propagate', None) - return self.execute(*args, **kwargs) - class FakeUser(object): login = 'toto' @@ -136,18 +125,19 @@ class FakeSession(RequestSessionBase): + read_security = write_security = True + set_read_security = set_write_security = lambda *args, **kwargs: None + def __init__(self, repo=None, user=None): self.repo = repo self.vreg = getattr(self.repo, 'vreg', CubicWebVRegistry(FakeConfig(), initlog=False)) self.pool = FakePool() self.user = user or FakeUser() self.is_internal_session = False - self.is_super_session = self.user.eid == -1 self.transaction_data = {} - def execute(self, *args): + def execute(self, *args, **kwargs): pass - unsafe_execute = execute def commit(self, *args): self.transaction_data.clear() diff -r b718626a0e60 -r 13b0b96d7982 devtools/repotest.py --- a/devtools/repotest.py Mon Mar 08 19:02:35 2010 +0100 +++ b/devtools/repotest.py Tue Mar 09 08:59:43 2010 +0100 @@ -178,9 +178,10 @@ self._dumb_sessions = [] def get_max_eid(self): - return self.session.unsafe_execute('Any MAX(X)')[0][0] + return self.session.execute('Any MAX(X)')[0][0] def cleanup(self): - self.session.unsafe_execute('DELETE Any X WHERE X eid > %s' % self.maxeid) + self.session.set_pool() + self.session.execute('DELETE Any X WHERE X eid > %s' % self.maxeid) def tearDown(self): undo_monkey_patch() diff -r b718626a0e60 -r 13b0b96d7982 devtools/testlib.py --- a/devtools/testlib.py Mon Mar 08 19:02:35 2010 +0100 +++ b/devtools/testlib.py Tue Mar 09 08:59:43 2010 +0100 @@ -228,7 +228,9 @@ @property def session(self): """return current server side session (using default manager account)""" - return self.repo._sessions[self.cnx.sessionid] + session = self.repo._sessions[self.cnx.sessionid] + session.set_pool() + return session @property def adminsession(self): diff -r b718626a0e60 -r 13b0b96d7982 entities/authobjs.py --- a/entities/authobjs.py Mon Mar 08 19:02:35 2010 +0100 +++ b/entities/authobjs.py Tue Mar 09 08:59:43 2010 +0100 @@ -93,15 +93,10 @@ return self.groups == frozenset(('guests', )) def owns(self, eid): - if hasattr(self._cw, 'unsafe_execute'): - # use unsafe_execute on the repository side, in case - # session's user doesn't have access to CWUser - execute = self._cw.unsafe_execute - else: - execute = self._cw.execute try: - return execute('Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s', - {'x': eid, 'u': self.eid}, 'x') + return self._cw.execute( + 'Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s', + {'x': eid, 'u': self.eid}, 'x') except Unauthorized: return False owns = cached(owns, keyarg=1) diff -r b718626a0e60 -r 13b0b96d7982 entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Mon Mar 08 19:02:35 2010 +0100 +++ b/entities/test/unittest_wfobjs.py Tue Mar 09 08:59:43 2010 +0100 @@ -1,5 +1,7 @@ +from __future__ import with_statement from cubicweb.devtools.testlib import CubicWebTC from cubicweb import ValidationError +from cubicweb.server.session import security_enabled def add_wf(self, etype, name=None, default=False): if name is None: @@ -126,10 +128,11 @@ wf = add_wf(self, 'CWUser') s = wf.add_state(u'foo', initial=True) self.commit() - ex = self.assertRaises(ValidationError, self.session.unsafe_execute, + with security_enabled(self.session, write=False): + ex = self.assertRaises(ValidationError, self.session.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', {'x': self.user().eid, 's': s.eid}, 'x') - self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. " + self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. " "You may want to set a custom workflow for this entity first."}) def test_fire_transition(self): @@ -505,7 +508,7 @@ {'wf': self.wf.eid}) self.commit() - # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute + # XXX currently, we've to rely on hooks to set initial state, or to use execute # def test_initial_state(self): # cnx = self.login('stduser') # cu = cnx.cursor() diff -r b718626a0e60 -r 13b0b96d7982 entities/wfobjs.py --- a/entities/wfobjs.py Mon Mar 08 19:02:35 2010 +0100 +++ b/entities/wfobjs.py Tue Mar 09 08:59:43 2010 +0100 @@ -158,7 +158,7 @@ todelstate = self.state_by_name(todelstate) if not hasattr(replacement, 'eid'): replacement = self.state_by_name(replacement) - execute = self._cw.unsafe_execute + execute = self._cw.execute execute('SET X in_state S WHERE S eid %(s)s', {'s': todelstate.eid}, 's') execute('SET X from_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s', {'os': todelstate.eid, 'ns': replacement.eid}, 's') diff -r b718626a0e60 -r 13b0b96d7982 entity.py --- a/entity.py Mon Mar 08 19:02:35 2010 +0100 +++ b/entity.py Tue Mar 09 08:59:43 2010 +0100 @@ -20,6 +20,7 @@ from cubicweb.rset import ResultSet from cubicweb.selectors import yes from cubicweb.appobject import AppObject +from cubicweb.req import _check_cw_unsafe from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint from cubicweb.rqlrewrite import RQLRewriter @@ -531,8 +532,8 @@ # if some outer join are included to fetch inlined relations rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected), ','.join(rql)) - execute = getattr(self._cw, 'unsafe_execute', self._cw.execute) - rset = execute(rql, {'x': self.eid}, 'x', build_descr=False)[0] + rset = self._cw.execute(rql, {'x': self.eid}, 'x', + build_descr=False)[0] # handle attributes for i in xrange(1, lastattr): self[str(selected[i-1][0])] = rset[i] @@ -560,11 +561,8 @@ if not self.is_saved(): return None rql = "Any A WHERE X eid %%(x)s, X %s A" % name - # XXX should we really use unsafe_execute here? I think so (syt), - # see #344874 - execute = getattr(self._cw, 'unsafe_execute', self._cw.execute) try: - rset = execute(rql, {'x': self.eid}, 'x') + rset = self._cw.execute(rql, {'x': self.eid}, 'x') except Unauthorized: self[name] = value = None else: @@ -595,10 +593,7 @@ pass assert self.has_eid() rql = self.related_rql(rtype, role) - # XXX should we really use unsafe_execute here? I think so (syt), - # see #344874 - execute = getattr(self._cw, 'unsafe_execute', self._cw.execute) - rset = execute(rql, {'x': self.eid}, 'x') + rset = self._cw.execute(rql, {'x': self.eid}, 'x') self.set_related_cache(rtype, role, rset) return self.related(rtype, role, limit, entities) @@ -800,8 +795,9 @@ # raw edition utilities ################################################### - def set_attributes(self, _cw_unsafe=False, **kwargs): + def set_attributes(self, **kwargs): assert kwargs + _check_cw_unsafe(kwargs) relations = [] for key in kwargs: relations.append('X %s %%(%s)s' % (key, key)) @@ -809,25 +805,18 @@ self.update(kwargs) # and now update the database kwargs['x'] = self.eid - if _cw_unsafe: - self._cw.unsafe_execute( - 'SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x') - else: - self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), - kwargs, 'x') + self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), + kwargs, 'x') - def set_relations(self, _cw_unsafe=False, **kwargs): + def set_relations(self, **kwargs): """add relations to the given object. To set a relation where this entity is the object of the relation, use 'reverse_' as argument name. Values may be an entity, a list of entity, or None (meaning that all relations of the given type from or to this object should be deleted). """ - if _cw_unsafe: - execute = self._cw.unsafe_execute - else: - execute = self._cw.execute # XXX update cache + _check_cw_unsafe(kwargs) for attr, values in kwargs.iteritems(): if attr.startswith('reverse_'): restr = 'Y %s X' % attr[len('reverse_'):] @@ -839,14 +828,14 @@ continue if not isinstance(values, (tuple, list, set, frozenset)): values = (values,) - execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( + self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( restr, ','.join(str(r.eid) for r in values)), - {'x': self.eid}, 'x') + {'x': self.eid}, 'x') - def delete(self): + def delete(self, **kwargs): assert self.has_eid(), self.eid self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema, - {'x': self.eid}) + {'x': self.eid}, **kwargs) # server side utilities ################################################### diff -r b718626a0e60 -r 13b0b96d7982 goa/appobjects/dbmgmt.py --- a/goa/appobjects/dbmgmt.py Mon Mar 08 19:02:35 2010 +0100 +++ b/goa/appobjects/dbmgmt.py Tue Mar 09 08:59:43 2010 +0100 @@ -172,7 +172,7 @@ skip_etypes = ('CWGroup', 'CWUser') def call(self): - # XXX should use unsafe_execute with all hooks deactivated + # XXX should use unsafe execute with all hooks deactivated # XXX step by catching datastore errors? for eschema in self.schema.entities(): if eschema.final or eschema in self.skip_etypes: diff -r b718626a0e60 -r 13b0b96d7982 goa/dbinit.py --- a/goa/dbinit.py Mon Mar 08 19:02:35 2010 +0100 +++ b/goa/dbinit.py Tue Mar 09 08:59:43 2010 +0100 @@ -84,7 +84,7 @@ Put(gaeentity) def init_persistent_schema(ssession, schema): - execute = ssession.unsafe_execute + execute = ssession.execute rql = ('INSERT CWEType X: X name %(name)s, X description %(descr)s,' 'X final FALSE') eschema = schema.eschema('CWEType') @@ -96,7 +96,7 @@ 'descr': unicode(eschema.description)}) def insert_versions(ssession, config): - execute = ssession.unsafe_execute + execute = ssession.execute # insert versions execute('INSERT CWProperty X: X pkey %(pk)s, X value%(v)s', {'pk': u'system.version.cubicweb', diff -r b718626a0e60 -r 13b0b96d7982 hooks/email.py --- a/hooks/email.py Mon Mar 08 19:02:35 2010 +0100 +++ b/hooks/email.py Tue Mar 09 08:59:43 2010 +0100 @@ -26,7 +26,7 @@ def precommit_event(self): if self.condition(): - self.session.unsafe_execute( + self.session.execute( 'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype, {'x': self.entity.eid, 'y': self.email.eid}, 'x') diff -r b718626a0e60 -r 13b0b96d7982 hooks/integrity.py --- a/hooks/integrity.py Mon Mar 08 19:02:35 2010 +0100 +++ b/hooks/integrity.py Tue Mar 09 08:59:43 2010 +0100 @@ -35,13 +35,12 @@ RQLUniqueConstraint in two different transactions, as explained in http://intranet.logilab.fr/jpl/ticket/36564 """ - asession = session.actual_session() - if 'uniquecstrholder' in asession.transaction_data: + if 'uniquecstrholder' in session.transaction_data: return _UNIQUE_CONSTRAINTS_LOCK.acquire() - asession.transaction_data['uniquecstrholder'] = True + session.transaction_data['uniquecstrholder'] = True # register operation responsible to release the lock on commit/rollback - _ReleaseUniqueConstraintsOperation(asession) + _ReleaseUniqueConstraintsOperation(session) def _release_unique_cstr_lock(session): if 'uniquecstrholder' in session.transaction_data: @@ -69,7 +68,7 @@ return if self.rtype in self.session.transaction_data.get('pendingrtypes', ()): return - if self.session.unsafe_execute(*self._rql()).rowcount < 1: + if self.session.execute(*self._rql()).rowcount < 1: etype = self.session.describe(self.eid)[0] _ = self.session._ msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)') @@ -99,12 +98,8 @@ __abstract__ = True category = 'integrity' -class UserIntegrityHook(IntegrityHook): - __abstract__ = True - __select__ = IntegrityHook.__select__ & hook.regular_session() - -class CheckCardinalityHook(UserIntegrityHook): +class CheckCardinalityHook(IntegrityHook): """check cardinalities are satisfied""" __regid__ = 'checkcard' events = ('after_add_entity', 'before_delete_relation') @@ -176,7 +171,7 @@ pass -class CheckConstraintHook(UserIntegrityHook): +class CheckConstraintHook(IntegrityHook): """check the relation satisfy its constraints this is delayed to a precommit time operation since other relation which @@ -194,7 +189,7 @@ rdef=(self.eidfrom, self.rtype, self.eidto)) -class CheckAttributeConstraintHook(UserIntegrityHook): +class CheckAttributeConstraintHook(IntegrityHook): """check the attribute relation satisfy its constraints this is delayed to a precommit time operation since other relation which @@ -214,7 +209,7 @@ rdef=(self.entity.eid, attr, None)) -class CheckUniqueHook(UserIntegrityHook): +class CheckUniqueHook(IntegrityHook): __regid__ = 'checkunique' events = ('before_add_entity', 'before_update_entity') @@ -227,7 +222,7 @@ if val is None: continue rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr) - rset = self._cw.unsafe_execute(rql, {'val': val}) + rset = self._cw.execute(rql, {'val': val}) if rset and rset[0][0] != entity.eid: msg = self._cw._('the value "%s" is already used, use another one') raise ValidationError(entity.eid, {attr: msg % val}) @@ -244,9 +239,9 @@ if not (session.deleted_in_transaction(self.eid) or session.added_in_transaction(self.eid)): etype = session.describe(self.eid)[0] - session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s' - % (etype, self.relation), - {'x': self.eid}, 'x') + session.execute('DELETE %s X WHERE X eid %%(x)s, NOT %s' + % (etype, self.relation), + {'x': self.eid}, 'x') class DeleteCompositeOrphanHook(IntegrityHook): @@ -290,7 +285,7 @@ self.entity['name'] = newname -class TidyHtmlFields(UserIntegrityHook): +class TidyHtmlFields(IntegrityHook): """tidy HTML in rich text strings""" __regid__ = 'htmltidy' events = ('before_add_entity', 'before_update_entity') diff -r b718626a0e60 -r 13b0b96d7982 hooks/metadata.py --- a/hooks/metadata.py Mon Mar 08 19:02:35 2010 +0100 +++ b/hooks/metadata.py Tue Mar 09 08:59:43 2010 +0100 @@ -19,7 +19,7 @@ # eschema.eid is None if schema has been readen from the filesystem, not # from the database (eg during tests) if eschema.eid is None: - eschema.eid = session.unsafe_execute( + eschema.eid = session.execute( 'Any X WHERE X is CWEType, X name %(name)s', {'name': str(eschema)})[0][0] return eschema.eid @@ -103,18 +103,17 @@ events = ('after_add_entity',) def __call__(self): - asession = self._cw.actual_session() - if not asession.is_internal_session: - self._cw.add_relation(self.entity.eid, 'owned_by', asession.user.eid) - _SetCreatorOp(asession, entity=self.entity) + if not self._cw.is_internal_session: + self._cw.add_relation(self.entity.eid, 'owned_by', self._cw.user.eid) + _SetCreatorOp(self._cw, entity=self.entity) class _SyncOwnersOp(hook.Operation): def precommit_event(self): - self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,' - 'NOT EXISTS(X owned_by U, X eid %(x)s)', - {'c': self.compositeeid, 'x': self.composedeid}, - ('c', 'x')) + self.session.execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,' + 'NOT EXISTS(X owned_by U, X eid %(x)s)', + {'c': self.compositeeid, 'x': self.composedeid}, + ('c', 'x')) class SyncCompositeOwner(MetaDataHook): diff -r b718626a0e60 -r 13b0b96d7982 hooks/notification.py --- a/hooks/notification.py Mon Mar 08 19:02:35 2010 +0100 +++ b/hooks/notification.py Tue Mar 09 08:59:43 2010 +0100 @@ -103,7 +103,7 @@ class EntityUpdateHook(NotificationHook): __regid__ = 'notifentityupdated' __abstract__ = True # do not register by default - + __select__ = NotificationHook.__select__ & hook.from_dbapi_query() events = ('before_update_entity',) skip_attrs = set() @@ -111,8 +111,6 @@ session = self._cw if self.entity.eid in session.transaction_data.get('neweids', ()): return # entity is being created - if session.is_super_session: - return # ignore changes triggered by hooks # then compute changes changes = session.transaction_data.setdefault('changes', {}) thisentitychanges = changes.setdefault(self.entity.eid, set()) @@ -125,7 +123,7 @@ rqlsel.append(var) rqlrestr.append('X %s %s' % (attr, var)) rql = 'Any %s WHERE %s' % (','.join(rqlsel), ','.join(rqlrestr)) - rset = session.unsafe_execute(rql, {'x': self.entity.eid}, 'x') + rset = session.execute(rql, {'x': self.entity.eid}, 'x') for i, attr in enumerate(attrs): oldvalue = rset[0][i] newvalue = self.entity[attr] @@ -139,13 +137,11 @@ class SomethingChangedHook(NotificationHook): __regid__ = 'supervising' + __select__ = NotificationHook.__select__ & hook.from_dbapi_query() events = ('before_add_relation', 'before_delete_relation', 'after_add_entity', 'before_update_entity') def __call__(self): - # XXX use proper selectors - if self._cw.is_super_session or self._cw.repo.config.repairing: - return # ignore changes triggered by hooks or maintainance shell dest = self._cw.vreg.config['supervising-addrs'] if not dest: # no supervisors, don't do this for nothing... return diff -r b718626a0e60 -r 13b0b96d7982 hooks/security.py --- a/hooks/security.py Mon Mar 08 19:02:35 2010 +0100 +++ b/hooks/security.py Tue Mar 09 08:59:43 2010 +0100 @@ -9,6 +9,7 @@ __docformat__ = "restructuredtext en" from cubicweb import Unauthorized +from cubicweb.selectors import objectify_selector, lltrace from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook @@ -53,10 +54,17 @@ pass +@objectify_selector +@lltrace +def write_security_enabled(cls, req, **kwargs): + if req is None or not req.write_security: + return 0 + return 1 + class SecurityHook(hook.Hook): __abstract__ = True category = 'security' - __select__ = hook.Hook.__select__ & hook.regular_session() + __select__ = hook.Hook.__select__ & write_security_enabled() class AfterAddEntitySecurityHook(SecurityHook): diff -r b718626a0e60 -r 13b0b96d7982 hooks/syncschema.py --- a/hooks/syncschema.py Mon Mar 08 19:02:35 2010 +0100 +++ b/hooks/syncschema.py Tue Mar 09 08:59:43 2010 +0100 @@ -801,7 +801,7 @@ if name in CORE_ETYPES: raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')}) # delete every entities of this type - self._cw.unsafe_execute('DELETE %s X' % name) + self._cw.execute('DELETE %s X' % name) DropTable(self._cw, table=SQL_PREFIX + name) MemSchemaCWETypeDel(self._cw, name) @@ -986,7 +986,7 @@ if not (subjschema.eid in pendings or objschema.eid in pendings): session.execute('DELETE X %s Y WHERE X is %s, Y is %s' % (rschema, subjschema, objschema)) - execute = session.unsafe_execute + execute = session.execute rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,' 'R eid %%(x)s' % rdeftype, {'x': self.eidto}) lastrel = rset[0][0] == 0 diff -r b718626a0e60 -r 13b0b96d7982 hooks/workflow.py --- a/hooks/workflow.py Mon Mar 08 19:02:35 2010 +0100 +++ b/hooks/workflow.py Tue Mar 09 08:59:43 2010 +0100 @@ -19,8 +19,8 @@ nocheck = session.transaction_data.setdefault('skip-security', set()) nocheck.add((x, 'in_state', oldstate)) nocheck.add((x, 'in_state', newstate)) - # delete previous state first in case we're using a super session, - # unless in_state isn't stored in the system source + # delete previous state first unless in_state isn't stored in the system + # source fromsource = session.describe(x)[1] if fromsource == 'system' or \ not session.repo.sources_by_uri[fromsource].support_relation('in_state'): @@ -42,9 +42,7 @@ and entity.current_workflow: state = entity.current_workflow.initial if state: - # use super session to by-pass security checks - session.super_session.add_relation(entity.eid, 'in_state', - state.eid) + session.add_relation(entity.eid, 'in_state', state.eid) class _FireAutotransitionOp(hook.Operation): @@ -122,14 +120,7 @@ msg = session._('exiting from subworkflow %s') msg %= session._(forentity.current_workflow.name) session.transaction_data[(forentity.eid, 'subwfentrytr')] = True - # XXX iirk - req = forentity._cw - forentity._cw = session.super_session - try: - trinfo = forentity.change_state(tostate, msg, u'text/plain', - tr=wftr) - finally: - forentity._cw = req + forentity.change_state(tostate, msg, u'text/plain', tr=wftr) # hooks ######################################################################## @@ -195,7 +186,8 @@ raise ValidationError(entity.eid, {None: msg}) # True if we are coming back from subworkflow swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) - cowpowers = session.is_super_session or 'managers' in session.user.groups + cowpowers = ('managers' in session.user.groups + or not session.write_security) # no investigate the requested state change... try: treid = entity['by_transition'] @@ -266,7 +258,7 @@ class CheckInStateChangeAllowed(WorkflowHook): - """check state apply, in case of direct in_state change using unsafe_execute + """check state apply, in case of direct in_state change using unsafe execute """ __regid__ = 'wfcheckinstate' __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state') @@ -307,8 +299,7 @@ return entity = self._cw.entity_from_eid(self.eidfrom) try: - entity.set_attributes(modification_date=datetime.now(), - _cw_unsafe=True) + entity.set_attributes(modification_date=datetime.now()) except RepositoryError, ex: # usually occurs if entity is coming from a read-only source # (eg ldap user) diff -r b718626a0e60 -r 13b0b96d7982 mail.py --- a/mail.py Mon Mar 08 19:02:35 2010 +0100 +++ b/mail.py Tue Mar 09 08:59:43 2010 +0100 @@ -215,16 +215,9 @@ """return a list of either 2-uple (email, language) or user entity to who this email should be sent """ - # use super_session when available, we don't want to consider security - # when selecting recipients_finder - try: - req = self._cw.super_session - except AttributeError: - req = self._cw - finder = self._cw.vreg['components'].select('recipients_finder', req, - rset=self.cw_rset, - row=self.cw_row or 0, - col=self.cw_col or 0) + finder = self._cw.vreg['components'].select( + 'recipients_finder', self._cw, rset=self.cw_rset, + row=self.cw_row or 0, col=self.cw_col or 0) return finder.recipients() def send_now(self, recipients, msg): diff -r b718626a0e60 -r 13b0b96d7982 misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Mon Mar 08 19:02:35 2010 +0100 +++ b/misc/migration/bootstrapmigration_repository.py Tue Mar 09 08:59:43 2010 +0100 @@ -20,8 +20,7 @@ if applcubicwebversion == (3, 6, 0) and cubicwebversion >= (3, 6, 0): _add_relation_definition_no_perms('CWAttribute', 'update_permission', 'CWGroup') _add_relation_definition_no_perms('CWAttribute', 'update_permission', 'RQLExpression') - session.set_pool() - session.unsafe_execute('SET X update_permission Y WHERE X is CWAttribute, X add_permission Y') + rql('SET X update_permission Y WHERE X is CWAttribute, X add_permission Y') drop_relation_definition('CWAttribute', 'add_permission', 'CWGroup') drop_relation_definition('CWAttribute', 'add_permission', 'RQLExpression') drop_relation_definition('CWAttribute', 'delete_permission', 'CWGroup') @@ -29,7 +28,6 @@ elif applcubicwebversion < (3, 6, 0) and cubicwebversion >= (3, 6, 0): session.set_pool() - session.execute = session.unsafe_execute permsdict = ss.deserialize_ertype_permissions(session) changes = session.disable_hooks_category.add('integrity') @@ -81,13 +79,11 @@ deactivate_verification_hooks() add_relation_type('cwuri') base_url = session.base_url() - # use an internal session since some entity might forbid modifications to admin - isession = repo.internal_session() for eid, in rql('Any X', ask_confirm=False): type, source, extid = session.describe(eid) if source == 'system': - isession.execute('SET X cwuri %(u)s WHERE X eid %(x)s', - {'x': eid, 'u': base_url + u'eid/%s' % eid}) + rql('SET X cwuri %(u)s WHERE X eid %(x)s', + {'x': eid, 'u': base_url + u'eid/%s' % eid}) isession.commit() reactivate_verification_hooks() session.set_shared_data('do-not-insert-cwuri', False) diff -r b718626a0e60 -r 13b0b96d7982 misc/migration/postcreate.py --- a/misc/migration/postcreate.py Mon Mar 08 19:02:35 2010 +0100 +++ b/misc/migration/postcreate.py Tue Mar 09 08:59:43 2010 +0100 @@ -42,8 +42,8 @@ # need this since we already have at least one user in the database (the default admin) for user in rql('Any X WHERE X is CWUser').entities(): - session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': user.eid, 's': activated.eid}, 'x') + rql('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', + {'x': user.eid, 's': activated.eid}, 'x') # on interactive mode, ask for level 0 persistent options if interactive_mode: diff -r b718626a0e60 -r 13b0b96d7982 req.py --- a/req.py Mon Mar 08 19:02:35 2010 +0100 +++ b/req.py Tue Mar 09 08:59:43 2010 +0100 @@ -22,6 +22,12 @@ CACHE_REGISTRY = {} +def _check_cw_unsafe(kwargs): + if kwargs.pop('_cw_unsafe', False): + warn('[3.7] _cw_unsafe argument is deprecated, now unsafe by ' + 'default, control it using cw_[read|write]_security.', + DeprecationWarning, stacklevel=3) + class Cache(dict): def __init__(self): super(Cache, self).__init__() @@ -110,19 +116,18 @@ # XXX move to CWEntityManager or even better as factory method (unclear # where yet...) - def create_entity(self, etype, _cw_unsafe=False, **kwargs): + def create_entity(self, etype, **kwargs): """add a new entity of the given type Example (in a shell session): - c = create_entity('Company', name=u'Logilab') - create_entity('Person', works_for=c, firstname=u'John', lastname=u'Doe') + >>> c = create_entity('Company', name=u'Logilab') + >>> create_entity('Person', firstname=u'John', lastname=u'Doe', + ... works_for=c) """ - if _cw_unsafe: - execute = self.unsafe_execute - else: - execute = self.execute + _check_cw_unsafe(kwargs) + execute = self.execute rql = 'INSERT %s X' % etype relations = [] restrictions = set() @@ -162,7 +167,7 @@ restr = 'X %s Y' % attr execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( restr, ','.join(str(r.eid) for r in values)), - {'x': created.eid}, 'x') + {'x': created.eid}, 'x', build_descr=False) return created def ensure_ro_rql(self, rql): @@ -281,7 +286,7 @@ userinfo['name'] = "cubicweb" userinfo['email'] = "" return userinfo - user = self.actual_session().user + user = self.user userinfo['login'] = user.login userinfo['name'] = user.name() userinfo['email'] = user.get_email() diff -r b718626a0e60 -r 13b0b96d7982 schema.py --- a/schema.py Mon Mar 08 19:02:35 2010 +0100 +++ b/schema.py Tue Mar 09 08:59:43 2010 +0100 @@ -704,7 +704,7 @@ rql = 'Any %s WHERE %s' % (self.mainvars, restriction) if self.distinct_query: rql = 'DISTINCT ' + rql - return session.unsafe_execute(rql, args, ck, build_descr=False) + return session.execute(rql, args, ck, build_descr=False) class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint): @@ -830,13 +830,10 @@ return True return False if keyarg is None: - # on the server side, use unsafe_execute, but this is not available - # on the client side (session is actually a request) - execute = getattr(session, 'unsafe_execute', session.execute) kwargs.setdefault('u', session.user.eid) cachekey = kwargs.keys() try: - rset = execute(rql, kwargs, cachekey, build_descr=True) + rset = session.execute(rql, kwargs, cachekey, build_descr=True) except NotImplementedError: self.critical('cant check rql expression, unsupported rql %s', rql) if self.eid is not None: @@ -1084,9 +1081,9 @@ elif form is not None: cw = form._cw if cw is not None: - if hasattr(cw, 'is_super_session'): + if hasattr(cw, 'write_security'): # test it's a session and not a request # cw is a server session - hasperm = cw.is_super_session or \ + hasperm = not cw.write_security or \ not cw.is_hooks_category_activated('integrity') or \ cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT) else: diff -r b718626a0e60 -r 13b0b96d7982 server/checkintegrity.py --- a/server/checkintegrity.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/checkintegrity.py Tue Mar 09 08:59:43 2010 +0100 @@ -6,6 +6,8 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from __future__ import with_statement + __docformat__ = "restructuredtext en" import sys @@ -15,6 +17,7 @@ from cubicweb.schema import PURE_VIRTUAL_RTYPES from cubicweb.server.sqlutils import SQL_PREFIX +from cubicweb.server.session import security_enabled def has_eid(sqlcursor, eid, eids): """return true if the eid is a valid eid""" @@ -277,9 +280,10 @@ # yo, launch checks if checks: eids_cache = {} - for check in checks: - check_func = globals()['check_%s' % check] - check_func(repo.schema, session, eids_cache, fix=fix) + with security_enabled(session, read=False): # ensure no read security + for check in checks: + check_func = globals()['check_%s' % check] + check_func(repo.schema, session, eids_cache, fix=fix) if fix: cnx.commit() else: diff -r b718626a0e60 -r 13b0b96d7982 server/hook.py --- a/server/hook.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/hook.py Tue Mar 09 08:59:43 2010 +0100 @@ -33,6 +33,8 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from __future__ import with_statement + __docformat__ = "restructuredtext en" from warnings import warn @@ -47,7 +49,7 @@ from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector, implements) from cubicweb.appobject import AppObject - +from cubicweb.server.session import security_enabled ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity', 'before_update_entity', 'after_update_entity', @@ -76,13 +78,20 @@ event, obj.__module__, obj.__name__)) super(HooksRegistry, self).register(obj, **kwargs) - def call_hooks(self, event, req=None, **kwargs): + def call_hooks(self, event, session=None, **kwargs): kwargs['event'] = event - for hook in sorted(self.possible_objects(req, **kwargs), key=lambda x: x.order): - if hook.enabled: + if session is None: + for hook in sorted(self.possible_objects(session, **kwargs), + key=lambda x: x.order): hook() - else: - warn('[3.6] %s: enabled is deprecated' % hook.__class__) + else: + # by default, hooks are executed with security turned off + with security_enabled(session, read=False): + hooks = sorted(self.possible_objects(session, **kwargs), + key=lambda x: x.order) + with security_enabled(session, write=False): + for hook in hooks: + hook() VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry @@ -104,6 +113,14 @@ @objectify_selector @lltrace +def _bw_is_enabled(cls, req, **kwargs): + if cls.enabled: + return 1 + warn('[3.6] %s: enabled is deprecated' % cls) + return 0 + +@objectify_selector +@lltrace def match_event(cls, req, **kwargs): if kwargs.get('event') in cls.events: return 1 @@ -118,11 +135,10 @@ @objectify_selector @lltrace -def regular_session(cls, req, **kwargs): - if req is None or req.is_super_session: - return 0 - return 1 - +def from_dbapi_query(cls, req, **kwargs): + if req.running_dbapi_query: + return 1 + return 0 class rechain(object): def __init__(self, *iterators): @@ -175,7 +191,7 @@ class Hook(AppObject): __registry__ = 'hooks' - __select__ = match_event() & enabled_category() + __select__ = match_event() & enabled_category() & _bw_is_enabled() # set this in derivated classes events = None category = None @@ -260,7 +276,7 @@ else: assert self.rtype in self.object_relations meid, seid = self.eidto, self.eidfrom - self._cw.unsafe_execute( + self._cw.execute( 'SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'\ % (self.main_rtype, self.main_rtype, self.main_rtype), {'x': meid, 'e': seid}, ('x', 'e')) @@ -278,7 +294,7 @@ def __call__(self): eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0]) - execute = self._cw.unsafe_execute + execute = self._cw.execute for rel in self.subject_relations: if rel in eschema.subjrels: execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' @@ -303,7 +319,7 @@ def __call__(self): eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0]) - execute = self._cw.unsafe_execute + execute = self._cw.execute for rel in self.subject_relations: if rel in eschema.subjrels: execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' @@ -507,6 +523,6 @@ class RQLPrecommitOperation(Operation): def precommit_event(self): - execute = self.session.unsafe_execute + execute = self.session.execute for rql in self.rqls: execute(*rql) diff -r b718626a0e60 -r 13b0b96d7982 server/migractions.py --- a/server/migractions.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/migractions.py Tue Mar 09 08:59:43 2010 +0100 @@ -243,17 +243,26 @@ @property def session(self): if self.config is not None: - return self.repo._get_session(self.cnx.sessionid) + session = self.repo._get_session(self.cnx.sessionid) + if session.pool is None: + session.set_read_security(False) + session.set_write_security(False) + session.set_pool() + return session # no access to session on remote instance return None def commit(self): if hasattr(self, '_cnx'): self._cnx.commit() + if self.session: + self.session.set_pool() def rollback(self): if hasattr(self, '_cnx'): self._cnx.rollback() + if self.session: + self.session.set_pool() def rqlexecall(self, rqliter, cachekey=None, ask_confirm=True): for rql, kwargs in rqliter: @@ -313,7 +322,6 @@ self.cmd_reactivate_verification_hooks() def install_custom_sql_scripts(self, directory, driver): - self.session.set_pool() # ensure pool is set for fpath in glob(osp.join(directory, '*.sql.%s' % driver)): newname = osp.basename(fpath).replace('.sql.%s' % driver, '.%s.sql' % driver) @@ -698,10 +706,7 @@ groupmap = self.group_mapping() cstrtypemap = self.cstrtype_mapping() # register the entity into CWEType - try: - execute = self._cw.unsafe_execute - except AttributeError: - execute = self._cw.execute + execute = self._cw.execute ss.execschemarql(execute, eschema, ss.eschema2rql(eschema, groupmap)) # add specializes relation if needed self.rqlexecall(ss.eschemaspecialize2rql(eschema), ask_confirm=confirm) @@ -842,10 +847,7 @@ """ reposchema = self.repo.schema rschema = self.fs_schema.rschema(rtype) - try: - execute = self._cw.unsafe_execute - except AttributeError: - execute = self._cw.execute + execute = self._cw.execute # register the relation into CWRType and insert necessary relation # definitions ss.execschemarql(execute, rschema, ss.rschema2rql(rschema, addrdef=False)) @@ -905,10 +907,7 @@ rschema = self.fs_schema.rschema(rtype) if not rtype in self.repo.schema: self.cmd_add_relation_type(rtype, addrdef=False, commit=True) - try: - execute = self._cw.unsafe_execute - except AttributeError: - execute = self._cw.execute + execute = self._cw.execute rdef = self._get_rdef(rschema, subjtype, objtype) ss.execschemarql(execute, rdef, ss.rdef2rql(rdef, self.cstrtype_mapping(), @@ -1184,7 +1183,6 @@ level actions """ if not ask_confirm or self.confirm('Execute sql: %s ?' % sql): - self.session.set_pool() # ensure pool is set try: cu = self.session.system_sql(sql, args) except: @@ -1203,10 +1201,7 @@ if not isinstance(rql, (tuple, list)): rql = ( (rql, kwargs), ) res = None - try: - execute = self._cw.unsafe_execute - except AttributeError: - execute = self._cw.execute + execute = self._cw.execute for rql, kwargs in rql: if kwargs: msg = '%s (%s)' % (rql, kwargs) diff -r b718626a0e60 -r 13b0b96d7982 server/querier.py --- a/server/querier.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/querier.py Tue Mar 09 08:59:43 2010 +0100 @@ -6,6 +6,8 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from __future__ import with_statement + __docformat__ = "restructuredtext en" from itertools import repeat @@ -23,7 +25,7 @@ from cubicweb.server.utils import cleanup_solutions from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction - +from cubicweb.server.session import security_enabled def empty_rset(rql, args, rqlst=None): """build an empty result set object""" @@ -200,8 +202,11 @@ return rqlst to actually execute """ noinvariant = set() - if security and not self.session.is_super_session: - self._insert_security(union, noinvariant) + if security and self.session.read_security: + # ensure security is turned of when security is inserted, + # else we may loop for ever... + with security_enabled(self.session, read=False): + self._insert_security(union, noinvariant) self.rqlhelper.simplify(union) self.sqlannotate(union) set_qdata(self.schema.rschema, union, noinvariant) @@ -299,7 +304,6 @@ note: rqlst should not have been simplified at this point """ - assert not self.session.is_super_session user = self.session.user schema = self.schema msgs = [] @@ -601,14 +605,14 @@ self._rql_cache[cachekey] = rqlst orig_rqlst = rqlst if not rqlst.TYPE == 'select': - if not session.is_super_session: + if session.read_security: check_no_password_selected(rqlst) # write query, ensure session's mode is 'write' so connections # won't be released until commit/rollback session.mode = 'write' cachekey = None else: - if not session.is_super_session: + if session.read_security: for select in rqlst.children: check_no_password_selected(select) # on select query, always copy the cached rqlst so we don't have to diff -r b718626a0e60 -r 13b0b96d7982 server/repository.py --- a/server/repository.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/repository.py Tue Mar 09 08:59:43 2010 +0100 @@ -15,6 +15,8 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from __future__ import with_statement + __docformat__ = "restructuredtext en" import sys @@ -36,7 +38,7 @@ typed_eid) from cubicweb import cwvreg, schema, server from cubicweb.server import utils, hook, pool, querier, sources -from cubicweb.server.session import Session, InternalSession +from cubicweb.server.session import Session, InternalSession, security_enabled class CleanupEidTypeCacheOp(hook.SingleLastOperation): @@ -80,10 +82,10 @@ this kind of behaviour has to be done in the repository so we don't have hooks order hazardness """ - # XXX now that rql in migraction default to unsafe_execute we don't want to - # skip that for super session (though we can still skip it for internal - # sessions). Also we should imo rely on the orm to first fetch existing - # entity if any then delete it. + # skip that for internal session or if integrity explicitly disabled + # + # XXX we should imo rely on the orm to first fetch existing entity if any + # then delete it. if session.is_internal_session \ or not session.is_hooks_category_activated('integrity'): return @@ -100,23 +102,15 @@ rschema = session.repo.schema.rschema(rtype) if card[0] in '1?': if not rschema.inlined: # inlined relations will be implicitly deleted - rset = session.unsafe_execute('Any X,Y WHERE X %s Y, X eid %%(x)s, ' - 'NOT Y eid %%(y)s' % rtype, - {'x': eidfrom, 'y': eidto}, 'x') - if rset: - safe_delete_relation(session, rschema, *rset[0]) + with security_enabled(session, read=False): + session.execute('DELETE X %s Y WHERE X eid %%(x)s, ' + 'NOT Y eid %%(y)s' % rtype, + {'x': eidfrom, 'y': eidto}, 'x') if card[1] in '1?': - rset = session.unsafe_execute('Any X,Y WHERE X %s Y, Y eid %%(y)s, ' - 'NOT X eid %%(x)s' % rtype, - {'x': eidfrom, 'y': eidto}, 'y') - if rset: - safe_delete_relation(session, rschema, *rset[0]) - - -def safe_delete_relation(session, rschema, subject, object): - if not rschema.has_perm(session, 'delete', fromeid=subject, toeid=object): - raise Unauthorized() - session.repo.glob_delete_relation(session, subject, rschema.type, object) + with security_enabled(session, read=False): + session.execute('DELETE X %sY WHERE Y eid %%(y)s, ' + 'NOT X eid %%(x)s' % rtype, + {'x': eidfrom, 'y': eidto}, 'y') class Repository(object): @@ -918,21 +912,22 @@ rql = [] eschema = self.schema.eschema(etype) pendingrtypes = session.transaction_data.get('pendingrtypes', ()) - for rschema, targetschemas, x in eschema.relation_definitions(): - rtype = rschema.type - if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes: - continue - var = '%s%s' % (rtype.upper(), x.upper()) - if x == 'subject': - # don't skip inlined relation so they are regularly - # deleted and so hooks are correctly called - selection = 'X %s %s' % (rtype, var) - else: - selection = '%s %s X' % (var, rtype) - rql = 'DELETE %s WHERE X eid %%(x)s' % selection - # unsafe_execute since we suppose that if user can delete the entity, - # he can delete all its relations without security checking - session.unsafe_execute(rql, {'x': eid}, 'x', build_descr=False) + with security_enabled(session, read=False, write=False): + for rschema, targetschemas, x in eschema.relation_definitions(): + rtype = rschema.type + if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes: + continue + var = '%s%s' % (rtype.upper(), x.upper()) + if x == 'subject': + # don't skip inlined relation so they are regularly + # deleted and so hooks are correctly called + selection = 'X %s %s' % (rtype, var) + else: + selection = '%s %s X' % (var, rtype) + rql = 'DELETE %s WHERE X eid %%(x)s' % selection + # if user can delete the entity, he can delete all its relations + # without security checking + session.execute(rql, {'x': eid}, 'x', build_descr=False) def locate_relation_source(self, session, subject, rtype, object): subjsource = self.source_from_eid(subject, session) diff -r b718626a0e60 -r 13b0b96d7982 server/schemaserial.py --- a/server/schemaserial.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/schemaserial.py Tue Mar 09 08:59:43 2010 +0100 @@ -218,7 +218,7 @@ if not quiet: _title = '-> storing the schema in the database ' print _title, - execute = cursor.unsafe_execute + execute = cursor.execute eschemas = schema.entities() if not quiet: pb_size = (len(eschemas + schema.relations()) diff -r b718626a0e60 -r 13b0b96d7982 server/session.py --- a/server/session.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/session.py Tue Mar 09 08:59:43 2010 +0100 @@ -5,6 +5,8 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from __future__ import with_statement + __docformat__ = "restructuredtext en" import sys @@ -71,6 +73,37 @@ self.session.enable_hooks_category(*self.changes) self.session.set_hooks_mode(self.oldmode) +INDENT = '' +class security_enabled(object): + """context manager to control security w/ session.execute, since by + default security is disabled on queries executed on the repository + side. + """ + def __init__(self, session, read=None, write=None): + self.session = session + self.read = read + self.write = write + + def __enter__(self): +# global INDENT + if self.read is not None: + self.oldread = self.session.set_read_security(self.read) +# print INDENT + 'read', self.read, self.oldread + if self.write is not None: + self.oldwrite = self.session.set_write_security(self.write) +# print INDENT + 'write', self.write, self.oldwrite +# INDENT += ' ' + + def __exit__(self, exctype, exc, traceback): +# global INDENT +# INDENT = INDENT[:-2] + if self.read is not None: + self.session.set_read_security(self.oldread) +# print INDENT + 'reset read to', self.oldread + if self.write is not None: + self.session.set_write_security(self.oldwrite) +# print INDENT + 'reset write to', self.oldwrite + class Session(RequestSessionBase): @@ -88,7 +121,6 @@ self.creation = time() self.timestamp = self.creation self.is_internal_session = False - self.is_super_session = False self.default_mode = 'read' # short cut to querier .execute method self._execute = repo.querier.execute @@ -109,19 +141,9 @@ def hijack_user(self, user): """return a fake request/session using specified user""" session = Session(user, self.repo) - session._threaddata = self.actual_session()._threaddata + session._threaddata.pool = pool return session - def _super_call(self, __cb, *args, **kwargs): - if self.is_super_session: - __cb(self, *args, **kwargs) - return - self.is_super_session = True - try: - __cb(self, *args, **kwargs) - finally: - self.is_super_session = False - def add_relation(self, fromeid, rtype, toeid): """provide direct access to the repository method to add a relation. @@ -133,14 +155,13 @@ You may use this in hooks when you know both eids of the relation you want to add. """ - if self.vreg.schema[rtype].inlined: - entity = self.entity_from_eid(fromeid) - entity[rtype] = toeid - self._super_call(self.repo.glob_update_entity, - entity, set((rtype,))) - else: - self._super_call(self.repo.glob_add_relation, - fromeid, rtype, toeid) + with security_enabled(self, False, False): + if self.vreg.schema[rtype].inlined: + entity = self.entity_from_eid(fromeid) + entity[rtype] = toeid + self.repo.glob_update_entity(self, entity, set((rtype,))) + else: + self.repo.glob_add_relation(self, fromeid, rtype, toeid) def delete_relation(self, fromeid, rtype, toeid): """provide direct access to the repository method to delete a relation. @@ -153,14 +174,13 @@ You may use this in hooks when you know both eids of the relation you want to delete. """ - if self.vreg.schema[rtype].inlined: - entity = self.entity_from_eid(fromeid) - entity[rtype] = None - self._super_call(self.repo.glob_update_entity, - entity, set((rtype,))) - else: - self._super_call(self.repo.glob_delete_relation, - fromeid, rtype, toeid) + with security_enabled(self, False, False): + if self.vreg.schema[rtype].inlined: + entity = self.entity_from_eid(fromeid) + entity[rtype] = None + self.repo.glob_update_entity(self, entity, set((rtype,))) + else: + self.repo.glob_delete_relation(self, fromeid, rtype, toeid) # relations cache handling ################################################# @@ -229,10 +249,6 @@ # resource accessors ###################################################### - def actual_session(self): - """return the original parent session if any, else self""" - return self - def system_sql(self, sql, args=None, rollback_on_failure=True): """return a sql cursor on the system database""" if not sql.split(None, 1)[0].upper() == 'SELECT': @@ -276,6 +292,74 @@ rdef = rschema.rdef(subjtype, objtype) return rdef.get(rprop) + # security control ######################################################### + + DEFAULT_SECURITY = object() # evaluated to true by design + + @property + def read_security(self): + """return a boolean telling if read security is activated or not""" + try: + return self._threaddata.read_security + except AttributeError: + self._threaddata.read_security = self.DEFAULT_SECURITY + return self._threaddata.read_security + + def set_read_security(self, activated): + """[de]activate read security, returning the previous value set for + later restoration. + + you should usually use the `security_enabled` context manager instead + of this to change security settings. + """ + oldmode = self.read_security + self._threaddata.read_security = activated + # dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not + # issued on the session). This is tricky since we the execution model of + # a (write) user query is: + # + # repository.execute (security enabled) + # \-> querier.execute + # \-> repo.glob_xxx (add/update/delete entity/relation) + # \-> deactivate security before calling hooks + # \-> WE WANT TO CHECK QUERY NATURE HERE + # \-> potentially, other calls to querier.execute + # + # so we can't rely on simply checking session.read_security, but + # recalling the first transition from DEFAULT_SECURITY to something + # else (False actually) is not perfect but should be enough + self._threaddata.dbapi_query = oldmode is self.DEFAULT_SECURITY + return oldmode + + @property + def write_security(self): + """return a boolean telling if write security is activated or not""" + try: + return self._threaddata.write_security + except: + self._threaddata.write_security = self.DEFAULT_SECURITY + return self._threaddata.write_security + + def set_write_security(self, activated): + """[de]activate write security, returning the previous value set for + later restoration. + + you should usually use the `security_enabled` context manager instead + of this to change security settings. + """ + oldmode = self.write_security + self._threaddata.write_security = activated + return oldmode + + @property + def running_dbapi_query(self): + """return a boolean telling if it's triggered by a db-api query or by + a session query. + + To be used in hooks, else may have a wrong value. + """ + return getattr(self._threaddata, 'dbapi_query', True) + # hooks activation control ################################################# # all hooks should be activated during normal execution @@ -505,46 +589,15 @@ """return the source where the entity with id is located""" return self.repo.source_from_eid(eid, self) - def decorate_rset(self, rset, propagate=False): + def decorate_rset(self, rset): rset.vreg = self.vreg - rset.req = propagate and self or self.actual_session() + rset.req = self return rset - @property - def super_session(self): - try: - csession = self.childsession - except AttributeError: - if isinstance(self, (ChildSession, InternalSession)): - csession = self - else: - csession = ChildSession(self) - self.childsession = csession - # need shared pool set - self.set_pool(checkclosed=False) - return csession - - def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True, - propagate=False): - """like .execute but with security checking disabled (this method is - internal to the server, it's not part of the db-api) - - if `propagate` is true, the super_session will be attached to the result - set instead of the parent session, hence further query done through - entities fetched from this result set will bypass security as well - """ - return self.super_session.execute(rql, kwargs, eid_key, build_descr, - propagate) - - def execute(self, rql, kwargs=None, eid_key=None, build_descr=True, - propagate=False): - """db-api like method directly linked to the querier execute method - - Becare that unlike actual cursor.execute, `build_descr` default to - false - """ + def execute(self, rql, kwargs=None, eid_key=None, build_descr=True): + """db-api like method directly linked to the querier execute method""" rset = self._execute(self, rql, kwargs, eid_key, build_descr) - return self.decorate_rset(rset, propagate) + return self.decorate_rset(rset) def _clear_thread_data(self): """remove everything from the thread local storage, except pool @@ -569,58 +622,60 @@ return if self.commit_state: return - # on rollback, an operation should have the following state - # information: - # - processed by the precommit/commit event or not - # - if processed, is it the failed operation - try: - for trstate in ('precommit', 'commit'): - processed = [] - self.commit_state = trstate - try: - while self.pending_operations: - operation = self.pending_operations.pop(0) - operation.processed = trstate - processed.append(operation) + # by default, operations are executed with security turned off + with security_enabled(self, False, False): + # on rollback, an operation should have the following state + # information: + # - processed by the precommit/commit event or not + # - if processed, is it the failed operation + try: + for trstate in ('precommit', 'commit'): + processed = [] + self.commit_state = trstate + try: + while self.pending_operations: + operation = self.pending_operations.pop(0) + operation.processed = trstate + processed.append(operation) + operation.handle_event('%s_event' % trstate) + self.pending_operations[:] = processed + self.debug('%s session %s done', trstate, self.id) + except: + self.exception('error while %sing', trstate) + # if error on [pre]commit: + # + # * set .failed = True on the operation causing the failure + # * call revert_event on processed operations + # * call rollback_event on *all* operations + # + # that seems more natural than not calling rollback_event + # for processed operations, and allow generic rollback + # instead of having to implements rollback, revertprecommit + # and revertcommit, that will be enough in mont case. + operation.failed = True + for operation in processed: + operation.handle_event('revert%s_event' % trstate) + # XXX use slice notation since self.pending_operations is a + # read-only property. + self.pending_operations[:] = processed + self.pending_operations + self.rollback(reset_pool) + raise + self.pool.commit() + self.commit_state = trstate = 'postcommit' + while self.pending_operations: + operation = self.pending_operations.pop(0) + operation.processed = trstate + try: operation.handle_event('%s_event' % trstate) - self.pending_operations[:] = processed - self.debug('%s session %s done', trstate, self.id) - except: - self.exception('error while %sing', trstate) - # if error on [pre]commit: - # - # * set .failed = True on the operation causing the failure - # * call revert_event on processed operations - # * call rollback_event on *all* operations - # - # that seems more natural than not calling rollback_event - # for processed operations, and allow generic rollback - # instead of having to implements rollback, revertprecommit - # and revertcommit, that will be enough in mont case. - operation.failed = True - for operation in processed: - operation.handle_event('revert%s_event' % trstate) - # XXX use slice notation since self.pending_operations is a - # read-only property. - self.pending_operations[:] = processed + self.pending_operations - self.rollback(reset_pool) - raise - self.pool.commit() - self.commit_state = trstate = 'postcommit' - while self.pending_operations: - operation = self.pending_operations.pop(0) - operation.processed = trstate - try: - operation.handle_event('%s_event' % trstate) - except: - self.critical('error while %sing', trstate, - exc_info=sys.exc_info()) - self.info('%s session %s done', trstate, self.id) - finally: - self._clear_thread_data() - self._touch() - if reset_pool: - self.reset_pool(ignoremode=True) + except: + self.critical('error while %sing', trstate, + exc_info=sys.exc_info()) + self.info('%s session %s done', trstate, self.id) + finally: + self._clear_thread_data() + self._touch() + if reset_pool: + self.reset_pool(ignoremode=True) def rollback(self, reset_pool=True): """rollback the current session's transaction""" @@ -630,21 +685,23 @@ self._touch() self.debug('rollback session %s done (no db activity)', self.id) return - try: - while self.pending_operations: - try: - operation = self.pending_operations.pop(0) - operation.handle_event('rollback_event') - except: - self.critical('rollback error', exc_info=sys.exc_info()) - continue - self.pool.rollback() - self.debug('rollback for session %s done', self.id) - finally: - self._clear_thread_data() - self._touch() - if reset_pool: - self.reset_pool(ignoremode=True) + # by default, operations are executed with security turned off + with security_enabled(self, False, False): + try: + while self.pending_operations: + try: + operation = self.pending_operations.pop(0) + operation.handle_event('rollback_event') + except: + self.critical('rollback error', exc_info=sys.exc_info()) + continue + self.pool.rollback() + self.debug('rollback for session %s done', self.id) + finally: + self._clear_thread_data() + self._touch() + if reset_pool: + self.reset_pool(ignoremode=True) def close(self): """do not close pool on session close, since they are shared now""" @@ -767,6 +824,25 @@ # deprecated ############################################################### + @deprecated("[3.7] control security with session.[read|write]_security") + def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True, + propagate=False): + """like .execute but with security checking disabled (this method is + internal to the server, it's not part of the db-api) + """ + return self.execute(rql, kwargs, eid_key, build_descr) + + @property + @deprecated("[3.7] is_super_session is deprecated, test " + "session.read_security and or session.write_security") + def is_super_session(self): + return not self.read_security or not self.write_security + + @deprecated("[3.7] session is actual session") + def actual_session(self): + """return the original parent session if any, else self""" + return self + @property @deprecated("[3.6] use session.vreg.schema") def schema(self): @@ -793,99 +869,6 @@ return self.entity_from_eid(eid) -class ChildSession(Session): - """child (or internal) session are used to hijack the security system - """ - cnxtype = 'inmemory' - - def __init__(self, parent_session): - self.id = None - self.is_internal_session = False - self.is_super_session = True - # session which has created this one - self.parent_session = parent_session - self.user = InternalManager() - self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone - self.repo = parent_session.repo - self.vreg = parent_session.vreg - self.data = parent_session.data - self.encoding = parent_session.encoding - self.lang = parent_session.lang - self._ = self.__ = parent_session._ - # short cut to querier .execute method - self._execute = self.repo.querier.execute - - @property - def super_session(self): - return self - - @property - def hooks_mode(self): - return self.parent_session.hooks_mode - def set_hooks_mode(self, mode): - return self.parent_session.set_hooks_mode(mode) - - @property - def disabled_hooks_categories(self): - return self.parent_session.disabled_hooks_categories - - @property - def enabled_hooks_categories(self): - return self.parent_session.enabled_hooks_categories - - - def get_mode(self): - return self.parent_session.mode - def set_mode(self, value): - self.parent_session.set_mode(value) - mode = property(get_mode, set_mode) - - def get_commit_state(self): - return self.parent_session.commit_state - def set_commit_state(self, value): - self.parent_session.set_commit_state(value) - commit_state = property(get_commit_state, set_commit_state) - - @property - def pool(self): - return self.parent_session.pool - @property - def pending_operations(self): - return self.parent_session.pending_operations - @property - def transaction_data(self): - return self.parent_session.transaction_data - - def set_pool(self): - """the session need a pool to execute some queries""" - self.parent_session.set_pool() - - def reset_pool(self): - """the session has no longer using its pool, at least for some time - """ - self.parent_session.reset_pool() - - def actual_session(self): - """return the original parent session if any, else self""" - return self.parent_session - - def commit(self, reset_pool=True): - """commit the current session's transaction""" - self.parent_session.commit(reset_pool) - - def rollback(self, reset_pool=True): - """rollback the current session's transaction""" - self.parent_session.rollback(reset_pool) - - def close(self): - """do not close pool on session close, since they are shared now""" - self.rollback() - - def user_data(self): - """returns a dictionnary with this user's information""" - return self.parent_session.user_data() - - class InternalSession(Session): """special session created internaly by the repository""" @@ -895,11 +878,7 @@ self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone self.cnxtype = 'inmemory' self.is_internal_session = True - self.is_super_session = True - - @property - def super_session(self): - return self + self.disable_hooks_category('integrity') class InternalManager(object): diff -r b718626a0e60 -r 13b0b96d7982 server/ssplanner.py --- a/server/ssplanner.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/ssplanner.py Tue Mar 09 08:59:43 2010 +0100 @@ -63,6 +63,7 @@ return {} eidconsts = {} neweids = session.transaction_data.get('neweids', ()) + checkread = session.read_security for rel in rqlst.where.get_nodes(Relation): if rel.r_type == 'eid' and not rel.neged(strict=True): lhs, rhs = rel.get_variable_parts() @@ -71,7 +72,7 @@ # check read permission here since it may not be done by # the generated select substep if not emited (eg nothing # to be selected) - if eid not in neweids: + if checkread and eid not in neweids: eschema(session.describe(eid)[0]).check_perm(session, 'read') eidconsts[lhs.variable] = eid return eidconsts diff -r b718626a0e60 -r 13b0b96d7982 server/test/unittest_hook.py --- a/server/test/unittest_hook.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/test/unittest_hook.py Tue Mar 09 08:59:43 2010 +0100 @@ -110,6 +110,8 @@ self.o.register(AddAnyHook) dis = set() cw = mock_object(vreg=self.vreg, + set_read_security=lambda *a,**k: None, + set_write_security=lambda *a,**k: None, is_hook_activated=lambda x, cls: cls.category not in dis) self.assertRaises(HookCalled, self.o.call_hooks, 'before_add_entity', cw) diff -r b718626a0e60 -r 13b0b96d7982 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Mon Mar 08 19:02:35 2010 +0100 +++ b/server/test/unittest_repository.py Tue Mar 09 08:59:43 2010 +0100 @@ -184,7 +184,9 @@ repo = self.repo cnxid = repo.connect(self.admlogin, password=self.admpassword) # rollback state change which trigger TrInfo insertion - user = repo._get_session(cnxid).user + session = repo._get_session(cnxid) + session.set_pool() + user = session.user user.fire_transition('deactivate') rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) self.assertEquals(len(rset), 1) diff -r b718626a0e60 -r 13b0b96d7982 sobjects/notification.py --- a/sobjects/notification.py Mon Mar 08 19:02:35 2010 +0100 +++ b/sobjects/notification.py Tue Mar 09 08:59:43 2010 +0100 @@ -33,11 +33,9 @@ def recipients(self): mode = self._cw.vreg.config['default-recipients-mode'] if mode == 'users': - # use unsafe execute else we may don't have the right to see users - # to notify... - execute = self._cw.unsafe_execute + execute = self._cw.execute dests = [(u.get_email(), u.property_value('ui.language')) - for u in execute(self.user_rql, build_descr=True, propagate=True).entities()] + for u in execute(self.user_rql, build_descr=True).entities()] elif mode == 'default-dest-addrs': lang = self._cw.vreg.property_value('ui.language') dests = zip(self._cw.vreg.config['default-dest-addrs'], repeat(lang)) @@ -158,7 +156,8 @@ if not rdef.has_perm(self._cw, 'read', eid=self.cw_rset[0][0]): continue # XXX suppose it's a subject relation... - elif not rschema.has_perm(self._cw, 'read', fromeid=self.cw_rset[0][0]): # XXX toeid + elif not rschema.has_perm(self._cw, 'read', + fromeid=self.cw_rset[0][0]): continue if attr in self.no_detailed_change_attrs: msg = _('%s updated') % _(attr) diff -r b718626a0e60 -r 13b0b96d7982 sobjects/supervising.py --- a/sobjects/supervising.py Mon Mar 08 19:02:35 2010 +0100 +++ b/sobjects/supervising.py Tue Mar 09 08:59:43 2010 +0100 @@ -92,7 +92,7 @@ return self._cw._('[%s supervision] changes summary') % self._cw.vreg.config.appid def call(self, changes): - user = self._cw.actual_session().user + user = self._cw.user self.w(self._cw._('user %s has made the following change(s):\n\n') % user.login) for event, changedescr in filter_changes(changes): @@ -129,8 +129,8 @@ self.w(u' %s' % entity.absolute_url()) def _relation_context(self, changedescr): - _ = self._cw._ - session = self._cw.actual_session() + session = self._cw + _ = session._cw._ def describe(eid): try: return _(session.describe(eid)[0]).lower() diff -r b718626a0e60 -r 13b0b96d7982 test/unittest_entity.py --- a/test/unittest_entity.py Mon Mar 08 19:02:35 2010 +0100 +++ b/test/unittest_entity.py Tue Mar 09 08:59:43 2010 +0100 @@ -436,7 +436,7 @@ def test_complete_relation(self): session = self.session - eid = session.unsafe_execute( + eid = session.execute( 'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 ' 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x')