Commits

osimons  committed 7c38495

[svn r9911] XmlRpcPlugin: Add ability to create and update tickets from author other than the user making request, and at a timestamp other than 'now'. Added extra permission checks for both changes (admin only).

Thanks to all those that have contributed patches and feedback for these issues.

Closes #3988, #5445

  • Participants
  • Parent commits d3e15ed

Comments (0)

Files changed (3)

File trunk/setup.py

 
 setup(
     name='TracXMLRPC',
-    version='1.1.0',
+    version='1.1.1',
     license='BSD',
     author='Alec Thomas',
     author_email='alec@swapoff.org',

File trunk/tracrpc/tests/ticket.py

             for tid in tids:
                 self.admin.ticket.delete(tid)
 
+    def test_update_author(self):
+        tid = self.admin.ticket.create("ticket_update_author", "one", {})
+        self.admin.ticket.update(tid, 'comment1', {})
+        self.admin.ticket.update(tid, 'comment2', {}, False, 'foo')
+        self.user.ticket.update(tid, 'comment3', {}, False, 'should_be_rejected')
+        changes = self.admin.ticket.changeLog(tid)
+        self.assertEquals(3, len(changes))
+        for when, who, what, cnum, comment, _tid in changes:
+            self.assertTrue(comment in ('comment1', 'comment2', 'comment3'))
+            if comment == 'comment1':
+                self.assertEquals('admin', who)
+            if comment == 'comment2':
+                self.assertEquals('foo', who)
+            if comment == 'comment3':
+                self.assertEquals('user', who)
+        self.admin.ticket.delete(tid)
+
+    def test_create_at_time(self):
+        from trac.util.datefmt import to_datetime, utc
+        now = to_datetime(None, utc)
+        minus1 = now - datetime.timedelta(days=1)
+        # create the tickets (user ticket will not be permitted to change time)
+        one = self.admin.ticket.create("create_at_time1", "ok", {}, False,
+                                        xmlrpclib.DateTime(minus1))
+        two = self.user.ticket.create("create_at_time3", "ok", {}, False,
+                                        xmlrpclib.DateTime(minus1))
+        # get the tickets
+        t1 = self.admin.ticket.get(one)
+        t2 = self.admin.ticket.get(two)
+        # check timestamps
+        self.assertTrue(t1[1] < t2[1])
+        self.admin.ticket.delete(one)
+        self.admin.ticket.delete(two)
+
+    def test_update_at_time(self):
+        from trac.util.datefmt import to_datetime, utc
+        now = to_datetime(None, utc)
+        minus1 = now - datetime.timedelta(hours=1)
+        minus2 = now - datetime.timedelta(hours=2)
+        tid = self.admin.ticket.create("ticket_update_at_time", "ok", {})
+        self.admin.ticket.update(tid, 'one', {}, False, '', xmlrpclib.DateTime(minus2))
+        self.admin.ticket.update(tid, 'two', {}, False, '', xmlrpclib.DateTime(minus1))
+        self.user.ticket.update(tid, 'three', {}, False, '', xmlrpclib.DateTime(minus1))
+        self.user.ticket.update(tid, 'four', {})
+        changes = self.admin.ticket.changeLog(tid)
+        self.assertEquals(4, len(changes))
+        # quick test to make sure each is older than previous
+        self.assertTrue(changes[0][0] < changes[1][0] < changes[2][0])
+        # margin of 1 second for tests (calls take time...)
+        justnow = xmlrpclib.DateTime(now - datetime.timedelta(seconds=1))
+        self.assertTrue(justnow <= changes[2][0])
+        self.assertTrue(justnow <= changes[3][0])
+        self.admin.ticket.delete(tid)
+
+
 class RpcTicketVersionTestCase(TracRpcTestCase):
     
     def setUp(self):

File trunk/tracrpc/ticket.py

         yield (None, ((list, int),), self.getAvailableActions)
         yield (None, ((list, int),), self.getActions)
         yield (None, ((list, int),), self.get)
-        yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool)), self.create)
-        yield (None, ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update)
+        yield ('TICKET_CREATE', ((int, str, str),
+                                 (int, str, str, dict),
+                                 (int, str, str, dict, bool),
+                                 (int, str, str, dict, bool, datetime)),
+                      self.create)
+        yield (None, ((list, int, str),
+                      (list, int, str, dict),
+                      (list, int, str, dict, bool),
+                      (list, int, str, dict, bool, str),
+                      (list, int, str, dict, bool, str, datetime)),
+                      self.update)
         yield (None, ((None, int),), self.delete)
         yield (None, ((dict, int), (dict, int, int)), self.changeLog)
         yield (None, ((list, int),), self.listAttachments)
         req.perm(t.resource).require('TICKET_VIEW')
         return (t.id, t.time_created, t.time_changed, t.values)
 
-    def create(self, req, summary, description, attributes = {}, notify=False):
-        """ Create a new ticket, returning the ticket ID. """
+    def create(self, req, summary, description, attributes={}, notify=False, when=None):
+        """ Create a new ticket, returning the ticket ID.
+        Overriding 'when' requires admin permission. """
         t = model.Ticket(self.env)
         t['summary'] = summary
         t['description'] = description
             t[k] = v
         t['status'] = 'new'
         t['resolution'] = ''
-        t.insert()
+        # custom create timestamp?
+        if when and not 'TICKET_ADMIN' in req.perm:
+            self.log.warn("RPC ticket.create: %r not allowed to create with "
+                    "non-current timestamp (%r)", req.authname, when)
+            when = None
+        t.insert(when=when)
         # Call ticket change listeners
         ts = TicketSystem(self.env)
         for listener in ts.change_listeners:
                                    "of ticket #%s: %s" % (t.id, e))
         return t.id
 
-    def update(self, req, id, comment, attributes = {}, notify=False):
+    def update(self, req, id, comment, attributes={}, notify=False, author='', when=None):
         """ Update a ticket, returning the new ticket in the same form as
-        getTicket(). Requires a valid 'action' in attributes to support workflow. """
-        now = to_datetime(None, utc)
+        getTicket(). Requires a valid 'action' in attributes to support workflow.
+        Overriding 'author' and 'when' requires admin permission. """
         t = model.Ticket(self.env, id)
+        # custom author?
+        if author and not (req.authname == 'anonymous' \
+                            or 'TICKET_ADMIN' in req.perm(t.resource)):
+            # only allow custom author if anonymous is permitted or user is admin
+            self.log.warn("RPC ticket.update: %r not allowed to change author "
+                    "to %r for comment on #%d", req.authname, author, id)
+            author = ''
+        author = author or req.authname
+        # custom change timestamp?
+        if when and not 'TICKET_ADMIN' in req.perm(t.resource):
+            self.log.warn("RPC ticket.update: %r not allowed to update #%d with "
+                    "non-current timestamp (%r)", author, id, when)
+            when = None
+        when = when or to_datetime(None, utc)
+        # and action...
         if not 'action' in attributes:
             # FIXME: Old, non-restricted update - remove soon!
             self.log.warning("Rpc ticket.update for ticket %d by user %s " \
             req.perm(t.resource).require('TICKET_MODIFY')
             for k, v in attributes.iteritems():
                 t[k] = v
-            t.save_changes(req.authname, comment, when=now)
+            t.save_changes(author, comment, when=when)
         else:
             ts = TicketSystem(self.env)
             tm = TicketModule(self.env)
             else:
                 tm._apply_ticket_changes(t, changes)
                 self.log.debug("Rpc ticket.update save: %s" % repr(t.values))
-                t.save_changes(req.authname, comment, when=now)
+                t.save_changes(author, comment, when=when)
                 # Apply workflow side-effects
                 for controller in controllers:
                     controller.apply_action_side_effects(req, t, action)
                 # Call ticket change listeners
                 for listener in ts.change_listeners:
-                    listener.ticket_changed(t, comment, req.authname, t._old)
+                    listener.ticket_changed(t, comment, author, t._old)
         if notify:
             try:
                 tn = TicketNotifyEmail(self.env)
-                tn.notify(t, newticket=False, modtime=now)
+                tn.notify(t, newticket=False, modtime=when)
             except Exception, e:
                 self.log.exception("Failure sending notification on change of "
                                    "ticket #%s: %s" % (t.id, e))