[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.
--- 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()
--- 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()
--- 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):
--- 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)
--- 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()
--- 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')
--- 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_'<relation> 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 ###################################################
--- 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:
--- 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',
--- 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')
--- 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')
--- 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):
--- 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
--- 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):
--- 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
--- 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)
--- 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):
--- 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)
--- 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:
--- 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()
--- 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:
--- 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:
--- 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)
--- 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)
--- 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
--- 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)
--- 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())
--- 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 <eid> 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>_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>_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):
--- 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
--- 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)
--- 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)
--- 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)
--- 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()
--- 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')