backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 24 Mar 2010 17:58:05 +0100
changeset 5003 cb27485ef5ae
parent 4984 6cb91be7707f (current diff)
parent 5002 0ce27e435b3a (diff)
child 5004 4cc020ee70e2
backport stable
--- a/cwvreg.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/cwvreg.py	Wed Mar 24 17:58:05 2010 +0100
@@ -22,12 +22,6 @@
 from cubicweb.rtags import RTAGS
 
 
-@onevent('before-registry-reload')
-def clear_rtag_objects():
-    for rtag in RTAGS:
-        rtag.clear()
-
-
 def use_interfaces(obj):
     """return interfaces used by the given object by searching for implements
     selectors, with a bw compat fallback to accepts_interfaces attribute
@@ -265,6 +259,13 @@
         self.schema = None
         self.initialized = False
         self.reset()
+        if self.config.mode != 'test':
+            # don't clear rtags during test, this may cause breakage with
+            # manually imported appobject modules
+            @onevent('before-registry-reload')
+            def clear_rtag_objects():
+                for rtag in RTAGS:
+                    rtag.clear()
 
     def setdefault(self, regid):
         try:
--- a/doc/book/en/admin/additional-tips.rst	Wed Mar 24 08:42:49 2010 +0100
+++ b/doc/book/en/admin/additional-tips.rst	Wed Mar 24 17:58:05 2010 +0100
@@ -9,12 +9,10 @@
 Backup, backup, backup
 ``````````````````````
 
-It is always a good idea to backup. If your system does not do that,
-you should set it up. Note that whenever you do an upgrade,
-`cubicweb-ctl` offers you to backup your database.
-
-There are a number of ways for doing backups. Before you go ahead,
-make sure the following permissions are correct ::
+It is always a good idea to backup. If your system does not do that, you should
+set it up. Note that whenever you do an upgrade, `cubicweb-ctl` offers you to
+backup your database.  There are a number of ways for doing backups. Before you
+go ahead, make sure the following permissions are correct ::
 
    # chgrp postgres /var/lib/cubicweb/backup
 
@@ -24,31 +22,36 @@
 
    # chmod g+r /etc/cubicweb.d/*<instance>*/sources
 
-**Classic way**
+**Classic way on PostgreSQL server**
 
-Simply use the pg_dump in a cron ::
+Simply use the pg_dump in a cron installed for `postgres` user on the database server::
 
-    su -c "pg_dump -Fc --username=cubicweb --no-owner" postgres > <your-instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
+    # m h  dom mon dow   command
+    0 2 * * * pg_dump -Fc --username=cubicweb --no-owner <instance> > /var/backups/<instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
 
 **CubicWeb way**
 
-The CubicWeb way is to use the `db-dump` command. For that, you have to put your passwords in a user-only-readable file at the
-root of the postgres user. The file is `.pgpass` (`chmod 0600`), in this case for a socket run connection to postgres ::
+The CubicWeb way is to use the `db-dump` command. For that, you have to put
+your passwords in a user-only-readable file at the home directory of root user.
+The file is `.pgpass` (`chmod 0600`), in this case for a socket run connection
+to PostgreSQL ::
 
-    /var/run/postgresql:5432:<instance>:cubicweb:<password>
+    /var/run/postgresql:5432:<instance>:<database user>:<database password>
 
 The postgres documentation for the `.pgpass` format can be found `here`_
 
-Then add the following command to the crontab of the postgres user (`su posgres 'crontab -e'`)::
+Then add the following command to the crontab of the user (`crontab -e`)::
 
     # m h  dom mon dow   command
     0 2 * * * cubicweb-ctl db-dump <instance>
 
 **The automated sysadmin way**
 
-You can use a combination `backup-ninja`_ (which has a postgres script in the example directory), `backuppc`)_ (for versionning).
+You can use a combination `backup-ninja`_ (which has a postgres script in the
+example directory), `backuppc`)_ (for versionning).
 
-Please note that in the *CubicWeb way* it adds a second location for your password which is error-prone.
+Please note that in the *CubicWeb way* it adds a second location for your
+password which is error-prone.
 
 .. _`here` : http://www.postgresql.org/docs/current/static/libpq-pgpass.html
 .. _`backup-ninja` : https://labs.riseup.net/code/projects/show/backupninja/
--- a/entity.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/entity.py	Wed Mar 24 17:58:05 2010 +0100
@@ -235,11 +235,13 @@
     def __setitem__(self, attr, value):
         """override __setitem__ to update self.edited_attributes.
 
-        Typically, a before_update_hook could do::
+        Typically, a before_[update|add]_hook could do::
 
             entity['generated_attr'] = generated_value
 
-        and this way, edited_attributes will be updated accordingly
+        and this way, edited_attributes will be updated accordingly. Also, add
+        the attribute to skip_security since we don't want to check security
+        for such attributes set by hooks.
         """
         if attr == 'eid':
             warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
@@ -247,14 +249,40 @@
             self.eid = value
         else:
             super(Entity, self).__setitem__(attr, value)
-            if hasattr(self, 'edited_attributes'):
+            # don't add attribute into skip_security if already in edited
+            # attributes, else we may accidentaly skip a desired security check
+            if hasattr(self, 'edited_attributes') and \
+                   attr not in self.edited_attributes:
                 self.edited_attributes.add(attr)
                 self.skip_security_attributes.add(attr)
 
+    def __delitem__(self, attr):
+        """override __delitem__ to update self.edited_attributes on cleanup of
+        undesired changes introduced in the entity's dict. For example, see the
+        code snippet below from the `forge` cube:
+
+        .. sourcecode:: python
+
+            edited = self.entity.edited_attributes
+            has_load_left = 'load_left' in edited
+            if 'load' in edited and self.entity.load_left is None:
+                self.entity.load_left = self.entity['load']
+            elif not has_load_left and edited:
+                # cleanup, this may cause undesired changes
+                del self.entity['load_left']
+
+        """
+        super(Entity, self).__delitem__(attr)
+        if hasattr(self, 'edited_attributes'):
+            self.edited_attributes.remove(attr)
+
     def setdefault(self, attr, default):
         """override setdefault to update self.edited_attributes"""
         super(Entity, self).setdefault(attr, default)
-        if hasattr(self, 'edited_attributes'):
+        # don't add attribute into skip_security if already in edited
+        # attributes, else we may accidentaly skip a desired security check
+        if hasattr(self, 'edited_attributes') and \
+               attr not in self.edited_attributes:
             self.edited_attributes.add(attr)
             self.skip_security_attributes.add(attr)
 
@@ -861,8 +889,8 @@
             else:
                 restr = 'X %s Y' % attr
             if values is None:
-                execute('DELETE %s WHERE X eid %%(x)s' % restr,
-                        {'x': self.eid}, 'x')
+                self._cw.execute('DELETE %s WHERE X eid %%(x)s' % restr,
+                                 {'x': self.eid}, 'x')
                 continue
             if not isinstance(values, (tuple, list, set, frozenset)):
                 values = (values,)
@@ -901,13 +929,16 @@
             _ = unicode
         else:
             _ = self._cw._
-        if creation or not hasattr(self, 'edited_attributes'):
+        if creation:
             # on creations, we want to check all relations, especially
             # required attributes
-            relations = None
-        else:
+            relations = [rschema for rschema in self.e_schema.subject_relations()
+                         if rschema.final and rschema.type != 'eid']
+        elif hasattr(self, 'edited_attributes'):
             relations = [self._cw.vreg.schema.rschema(rtype)
                          for rtype in self.edited_attributes]
+        else:
+            relations = None
         self.e_schema.check(self, creation=creation, _=_,
                             relations=relations)
 
--- a/hooks/notification.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/hooks/notification.py	Wed Mar 24 17:58:05 2010 +0100
@@ -112,12 +112,12 @@
         if session.added_in_transaction(self.entity.eid):
             return # entity is being created
         # then compute changes
-        changes = session.transaction_data.setdefault('changes', {})
-        thisentitychanges = changes.setdefault(self.entity.eid, set())
         attrs = [k for k in self.entity.edited_attributes
                  if not k in self.skip_attrs]
         if not attrs:
             return
+        changes = session.transaction_data.setdefault('changes', {})
+        thisentitychanges = changes.setdefault(self.entity.eid, set())
         rqlsel, rqlrestr = [], ['X eid %(x)s']
         for i, attr in enumerate(attrs):
             var = chr(65+i)
--- a/hooks/security.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/hooks/security.py	Wed Mar 24 17:58:05 2010 +0100
@@ -25,15 +25,16 @@
         except AttributeError:
             editedattrs = entity # XXX unexpected
     for attr in editedattrs:
-        try:
-            dontcheck.remove(attr)
+        if attr in dontcheck:
             continue
-        except KeyError:
-            pass
         rdef = eschema.rdef(attr)
         if rdef.final: # non final relation are checked by other hooks
             # add/delete should be equivalent (XXX: unify them into 'update' ?)
             rdef.check_perm(session, 'update', eid=eid)
+    # don't update dontcheck until everything went fine: see usage in
+    # after_update_entity, where if we got an Unauthorized at hook time, we will
+    # retry and commit time
+    dontcheck |= frozenset(editedattrs)
 
 
 class _CheckEntityPermissionOp(hook.LateOperation):
--- a/server/session.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/server/session.py	Wed Mar 24 17:58:05 2010 +0100
@@ -155,6 +155,9 @@
         session = Session(user, self.repo)
         threaddata = session._threaddata
         threaddata.pool = self.pool
+        # share pending_operations, else operation added in the hi-jacked
+        # session such as SendMailOp won't ever be processed
+        threaddata.pending_operations = self.pending_operations
         # everything in transaction_data should be copied back but the entity
         # type cache we don't want to avoid security pb
         threaddata.transaction_data = self.transaction_data.copy()
@@ -885,13 +888,15 @@
 
     # deprecated ###############################################################
 
-    @deprecated("[3.7] control security with session.[read|write]_security")
+    @deprecated("[3.7] execute is now unsafe by default in hooks/operation. You"
+                " can also 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)
+        with security_enabled(self, read=False, write=False):
+            return self.execute(rql, kwargs, eid_key, build_descr)
 
     @property
     @deprecated("[3.7] is_super_session is deprecated, test "
--- a/server/test/unittest_rql2sql.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/server/test/unittest_rql2sql.py	Wed Mar 24 17:58:05 2010 +0100
@@ -543,13 +543,10 @@
 ORDER BY 4 DESC'''),
 
 
-    ("Any X WHERE X eid 0, X eid 0",
-     '''SELECT 0'''),
-
-    ("Any X WHERE X eid 0, X eid 0, X test TRUE",
+    ("Any X WHERE X eid 0, X test TRUE",
      '''SELECT _X.cw_eid
 FROM cw_Personne AS _X
-WHERE _X.cw_eid=0 AND _X.cw_eid=0 AND _X.cw_test=TRUE'''),
+WHERE _X.cw_eid=0 AND _X.cw_test=TRUE'''),
 
     ("Any X,GROUP_CONCAT(TN) GROUPBY X ORDERBY XN WHERE T tags X, X name XN, T name TN, X is CWGroup",
      '''SELECT _X.cw_eid, GROUP_CONCAT(_T.cw_name)
--- a/web/application.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/web/application.py	Wed Mar 24 17:58:05 2010 +0100
@@ -123,7 +123,11 @@
         SESSION_MANAGER = self.session_manager
         if not 'last_login_time' in self.vreg.schema:
             self._update_last_login_time = lambda x: None
-        CW_EVENT_MANAGER.bind('after-registry-reload', self.reset_session_manager)
+        if self.vreg.config.mode != 'test':
+            # don't try to reset session manager during test, this leads to
+            # weird failures when running multiple tests
+            CW_EVENT_MANAGER.bind('after-registry-reload',
+                                  self.reset_session_manager)
 
     def reset_session_manager(self):
         data = self.session_manager.dump_data()
--- a/web/test/unittest_uicfg.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/web/test/unittest_uicfg.py	Wed Mar 24 17:58:05 2010 +0100
@@ -1,10 +1,12 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web import uicfg
 
+abaa = uicfg.actionbox_appearsin_addmenu
+
 class UICFGTC(CubicWebTC):
 
-    def test(self):
-        self.skip('write some tests')
+    def test_default_actionbox_appearsin_addmenu_config(self):
+        self.failIf(abaa.etype_get('TrInfo', 'wf_info_for', 'object', 'CWUser'))
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/web/views/actions.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/web/views/actions.py	Wed Mar 24 17:58:05 2010 +0100
@@ -404,16 +404,8 @@
 
 addmenu = uicfg.actionbox_appearsin_addmenu
 addmenu.tag_subject_of(('*', 'require_permission', '*'), False)
-addmenu.tag_subject_of(('*', 'wf_info_for', '*'), False)
-addmenu.tag_object_of(('*', 'wf_info_for', '*'), False)
-addmenu.tag_object_of(('*', 'state_of', 'CWEType'), True)
-addmenu.tag_object_of(('*', 'transition_of', 'CWEType'), True)
 addmenu.tag_object_of(('*', 'relation_type', 'CWRType'), True)
 addmenu.tag_object_of(('*', 'from_entity', 'CWEType'), False)
 addmenu.tag_object_of(('*', 'to_entity', 'CWEType'), False)
 addmenu.tag_object_of(('*', 'in_group', 'CWGroup'), True)
 addmenu.tag_object_of(('*', 'bookmarked_by', 'CWUser'), True)
-addmenu.tag_subject_of(('Transition', 'destination_state', '*'), True)
-addmenu.tag_object_of(('*', 'allowed_transition', 'Transition'), True)
-addmenu.tag_object_of(('*', 'destination_state', 'State'), True)
-addmenu.tag_subject_of(('State', 'allowed_transition', '*'), True)
--- a/web/views/workflow.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/web/views/workflow.py	Wed Mar 24 17:58:05 2010 +0100
@@ -35,6 +35,15 @@
 _abaa.tag_subject_of(('State', 'allowed_transition', 'BaseTransition'), False)
 _abaa.tag_object_of(('SubWorkflowExitPoint', 'destination_state', 'State'),
                     False)
+_abaa.tag_subject_of(('*', 'wf_info_for', '*'), False)
+_abaa.tag_object_of(('*', 'wf_info_for', '*'), False)
+
+_abaa.tag_object_of(('*', 'state_of', 'CWEType'), True)
+_abaa.tag_object_of(('*', 'transition_of', 'CWEType'), True)
+_abaa.tag_subject_of(('Transition', 'destination_state', '*'), True)
+_abaa.tag_object_of(('*', 'allowed_transition', 'Transition'), True)
+_abaa.tag_object_of(('*', 'destination_state', 'State'), True)
+_abaa.tag_subject_of(('State', 'allowed_transition', '*'), True)
 _abaa.tag_object_of(('State', 'state_of', 'Workflow'), True)
 _abaa.tag_object_of(('Transition', 'transition_of', 'Workflow'), True)
 _abaa.tag_object_of(('WorkflowTransition', 'transition_of', 'Workflow'), True)
--- a/web/views/xmlrss.py	Wed Mar 24 08:42:49 2010 +0100
+++ b/web/views/xmlrss.py	Wed Mar 24 17:58:05 2010 +0100
@@ -50,11 +50,14 @@
         self.w(u'<%s>\n' % (entity.e_schema))
         for rschema, attrschema in entity.e_schema.attribute_definitions():
             attr = rschema.type
-            try:
-                value = entity[attr]
-            except KeyError:
-                # Bytes
-                continue
+            if attr == 'eid':
+                value = entity.eid
+            else:
+                try:
+                    value = entity[attr]
+                except KeyError:
+                    # Bytes
+                    continue
             if value is not None:
                 if attrschema == 'Bytes':
                     from base64 import b64encode