Commits

Anonymous committed 9b3744c

[svn r6063] XmlRpcPlugin: Implemented full support for Trac 0.11 fine-grained permissions and security policies. Closes #5380.

This change makes some changes in the displayed set of methods, as the new answer to permissions is "it depends"... It depends on whatever security policies are running on the specific Trac installation. There is therefore new support for specifying `None` as method permission, but then each method is responsible for checking permissions per resource.

Most important/useful methods now use resource-level permissions, and the method listing and HTML display of API is therefore changed to always list all operative methods. HTML tables is also reformatted to be easier to browse.

Version bumped.

Comments (0)

Files changed (5)

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

trunk/tracrpc/api.py

         self.namespace_description = inspect.getdoc(provider)
 
     def __call__(self, req, args):
-        req.perm.assert_permission(self.permission)
+        if self.permission:
+            req.perm.assert_permission(self.permission)
         result = self.callable(req, *args)
         # If result is null, return a zero
         if result is None:
         for provider in self.method_handlers:
             for candidate in provider.xmlrpc_methods():
                 # Expand all fields of method description
-                c = Method(provider, *candidate)
-                if req.perm.has_permission(c.permission):
-                    yield c
+                yield Method(provider, *candidate)
 
     def multicall(self, req, signatures):
         """ Takes an array of XML-RPC calls encoded as structs of the form (in
         use of that method. If no such string is available, an empty string is
         returned. The documentation string may contain HTML markup. """
         p = self.get_method(method)
-        req.perm.assert_permission(p.permission)
         return '\n'.join((p.signature, '', p.description))
 
     def methodSignature(self, req, method):
         is an array of types. The first of these types is the return type of
         the method, the rest are parameters. """
         p = self.get_method(method)
-        req.perm.assert_permission(p.permission)
         return [','.join([RPC_TYPES[x] for x in sig]) for sig in p.xmlrpc_signatures()]
 
     def getAPIVersion(self, req):

trunk/tracrpc/templates/xmlrpclist.html

             <table class="listing tickets">
               <thead>
                 <tr>
-                  <th>Function</th>
-                  <th>Description</th>
-                  <th><nobr>Permission required</nobr></th>
+                  <th style="width:40%">Function</th>
+                  <th style="width:45%">Description</th>
+                  <th style="width:15%">Permission required</th>
                 </tr>
               </thead>
               <tbody py:for="idx, function in enumerate(namespace.methods)">
                 <tr class="${'color3-' + (idx % 2 == 0 and 'even' or 'odd')}">
-                  <td><nobr>${function[0]}</nobr></td>
+                  <td style="padding-left:4em;text-indent:-4em">${function[0]}</td>
                   <td>${function[1]}</td>
-                  <td>${function[2]}</td>
+                  <td>${function[2] or "By resource"}</td>
                 </tr>
               </tbody>
             </table>

trunk/tracrpc/ticket.py

 from trac.attachment import Attachment
 from trac.core import *
-from trac.perm import PermissionCache
+from trac.perm import PermissionError
+from trac.resource import Resource
 import trac.ticket.model as model
 import trac.ticket.query as query
 from trac.ticket.api import TicketSystem
         return 'ticket'
 
     def xmlrpc_methods(self):
-        yield ('TICKET_VIEW', ((list,), (list, str)), self.query)
-        yield ('TICKET_VIEW', ((list, xmlrpclib.DateTime),), self.getRecentChanges)
-        yield ('TICKET_VIEW', ((list, int),), self.getAvailableActions)
-        yield ('TICKET_VIEW', ((list, int),), self.getActions)
-        yield ('TICKET_VIEW', ((list, int),), self.get)
+        yield (None, ((list,), (list, str)), self.query)
+        yield (None, ((list, xmlrpclib.DateTime),), self.getRecentChanges)
+        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 ('TICKET_VIEW', ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update)
-        yield ('TICKET_ADMIN', ((None, int),), self.delete)
-        yield ('TICKET_VIEW', ((dict, int), (dict, int, int)), self.changeLog)
-        yield ('TICKET_VIEW', ((list, int),), self.listAttachments)
-        yield ('TICKET_VIEW', ((xmlrpclib.Binary, int, str),), self.getAttachment)
-        yield ('TICKET_APPEND',
+        yield (None, ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update)
+        yield (None, ((None, int),), self.delete)
+        yield (None, ((dict, int), (dict, int, int)), self.changeLog)
+        yield (None, ((list, int),), self.listAttachments)
+        yield (None, ((xmlrpclib.Binary, int, str),), self.getAttachment)
+        yield (None,
                ((str, int, str, str, xmlrpclib.Binary, bool),
                 (str, int, str, str, xmlrpclib.Binary)),
                self.putAttachment)
-        yield ('TICKET_ADMIN', ((bool, int, str),), self.deleteAttachment)
+        yield (None, ((bool, int, str),), self.deleteAttachment)
         yield ('TICKET_VIEW', ((list,),), self.getTicketFields)
 
     # Exported methods
     def query(self, req, qstr='status!=closed'):
         """ Perform a ticket query, returning a list of ticket ID's. """
         q = query.Query.from_string(self.env, qstr)
+        ticket_realm = Resource('ticket')
         out = []
         for t in q.execute(req):
-            out.append(t['id'])
+            tid = t['id']
+            if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)):
+                out.append(tid)
         return out
 
     def getRecentChanges(self, req, since):
         cursor.execute('SELECT id FROM ticket'
                        ' WHERE changetime >= %s', (since,))
         result = []
+        ticket_realm = Resource('ticket')
         for row in cursor:
-            result.append(int(row[0]))
+            tid = int(row[0])
+            if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)):
+                result.append(tid)
         return result
 
     def getAvailableActions(self, req, id):
                     controls.append((elem.attrib.get('name'),
                                     value, options))
             actions.append((action, first_label, ". ".join(hints) + '.', controls))
-        self.log.debug('Rpc ticket.getActions for ticket %d, user %s: %s' % (
-                    id, req.authname, repr(actions)))
         return actions
 
     def get(self, req, id):
         """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """
         t = model.Ticket(self.env, id)
+        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):
             # FIXME: Old, non-restricted update - remove soon!
             self.log.warning("Rpc ticket.update for ticket %d by user %s " \
                     "has no workflow 'action'." % (id, req.authname))
+            req.perm(t.resource).require('TICKET_MODIFY')
             for k, v in attributes.iteritems():
                 t[k] = v
             t.save_changes(req.authname, comment, when=now)
     def delete(self, req, id):
         """ Delete ticket with the given id. """
         t = model.Ticket(self.env, id)
+        req.perm(t.resource).require('TICKET_ADMIN')
         t.delete()
         ts = TicketSystem(self.env)
         # Call ticket change listeners
 
     def changeLog(self, req, id, when=0):
         t = model.Ticket(self.env, id)
+        req.perm(t.resource).require('TICKET_VIEW')
         for date, author, field, old, new, permanent in t.get_changelog(when):
             yield (date, author, field, old, new, permanent)
     # Use existing documentation from Ticket model
     def listAttachments(self, req, ticket):
         """ Lists attachments for a given ticket. Returns (filename,
         description, size, time, author) for each attachment."""
-        for t in Attachment.select(self.env, 'ticket', ticket):
-            yield (t.filename, t.description, t.size, 
-                   t.date, t.author)
+        attachments = []
+        for a in Attachment.select(self.env, 'ticket', ticket):
+            if 'ATTACHMENT_VIEW' in req.perm(a.resource):
+                yield (a.filename, a.description, a.size, a.date, a.author)
 
     def getAttachment(self, req, ticket, filename):
         """ returns the content of an attachment. """
         attachment = Attachment(self.env, 'ticket', ticket, filename)
+        req.perm(attachment.resource).require('ATTACHMENT_VIEW')
         return xmlrpclib.Binary(attachment.open().read())
 
     def putAttachment(self, req, ticket, filename, description, data, replace=True):
         """ Add an attachment, optionally (and defaulting to) overwriting an
         existing one. Returns filename."""
         if not model.Ticket(self.env, ticket).exists:
-            raise TracError, 'Ticket "%s" does not exist' % ticket
+            raise TracError('Ticket "%s" does not exist' % ticket)
         if replace:
             try:
                 attachment = Attachment(self.env, 'ticket', ticket, filename)
+                req.perm(attachment.resource).require('ATTACHMENT_DELETE')
                 attachment.delete()
             except TracError:
                 pass
         attachment = Attachment(self.env, 'ticket', ticket)
+        req.perm(attachment.resource).require('ATTACHMENT_CREATE')
         attachment.author = req.authname
         attachment.description = description
         attachment.insert(filename, StringIO(data.data), len(data.data))
         if not model.Ticket(self.env, ticket).exists:
             raise TracError('Ticket "%s" does not exists' % ticket)
         attachment = Attachment(self.env, 'ticket', ticket, filename)
+        req.perm(attachment.resource).require('ATTACHMENT_DELETE')
         attachment.delete()
         return True
 

trunk/tracrpc/wiki.py

 import posixpath
 
 from trac.core import *
-from trac.perm import IPermissionRequestor
+from trac.resource import Resource
 from trac.util.datefmt import to_timestamp, to_datetime, utc
 from trac.wiki.api import WikiSystem
 from trac.wiki.model import WikiPage
         return 'wiki'
 
     def xmlrpc_methods(self):
-        yield ('WIKI_VIEW', ((dict, xmlrpclib.DateTime),), self.getRecentChanges)
+        yield (None, ((dict, xmlrpclib.DateTime),), self.getRecentChanges)
         yield ('WIKI_VIEW', ((int,),), self.getRPCVersionSupported)
-        yield ('WIKI_VIEW', ((str, str), (str, str, int),), self.getPage)
-        yield ('WIKI_VIEW', ((str, str, int),), self.getPage, 'getPageVersion')
-        yield ('WIKI_VIEW', ((str, str), (str, str, int)), self.getPageHTML)
-        yield ('WIKI_VIEW', ((str, str), (str, str, int)), self.getPageHTML, 'getPageHTMLVersion')
-        yield ('WIKI_VIEW', ((list,),), self.getAllPages)
-        yield ('WIKI_VIEW', ((dict, str), (dict, str, int)), self.getPageInfo)
-        yield ('WIKI_VIEW', ((dict, str, int),), self.getPageInfo, 'getPageInfoVersion')
-        yield ('WIKI_CREATE', ((bool, str, str, dict),), self.putPage)
-        yield ('WIKI_VIEW', ((list, str),), self.listAttachments)
-        yield ('WIKI_VIEW', ((xmlrpclib.Binary, str),), self.getAttachment)
-        yield ('WIKI_MODIFY', ((bool, str, xmlrpclib.Binary),), self.putAttachment)
-        yield ('WIKI_MODIFY', ((bool, str, str, str, xmlrpclib.Binary),
+        yield (None, ((str, str), (str, str, int),), self.getPage)
+        yield (None, ((str, str, int),), self.getPage, 'getPageVersion')
+        yield (None, ((str, str), (str, str, int)), self.getPageHTML)
+        yield (None, ((str, str), (str, str, int)), self.getPageHTML, 'getPageHTMLVersion')
+        yield (None, ((list,),), self.getAllPages)
+        yield (None, ((dict, str), (dict, str, int)), self.getPageInfo)
+        yield (None, ((dict, str, int),), self.getPageInfo, 'getPageInfoVersion')
+        yield (None, ((bool, str, str, dict),), self.putPage)
+        yield (None, ((list, str),), self.listAttachments)
+        yield (None, ((xmlrpclib.Binary, str),), self.getAttachment)
+        yield (None, ((bool, str, xmlrpclib.Binary),), self.putAttachment)
+        yield (None, ((bool, str, str, str, xmlrpclib.Binary),
                                (bool, str, str, str, xmlrpclib.Binary, bool)),
                                self.putAttachmentEx)
-        yield ('WIKI_DELETE', ((bool, str),(bool, str, int)), self.deletePage)
-        yield ('WIKI_DELETE', ((bool, str),), self.deleteAttachment)
+        yield (None, ((bool, str),(bool, str, int)), self.deletePage)
+        yield (None, ((bool, str),), self.deleteAttachment)
         yield ('WIKI_VIEW', ((list, str),), self.listLinks)
         yield ('WIKI_VIEW', ((str, str),), self.wikiToHtml)
 
     def getRecentChanges(self, req, since):
         """ Get list of changed pages since timestamp """
         since = to_timestamp(since)
+        wiki_realm = Resource('wiki')
         db = self.env.get_db_cnx()
         cursor = db.cursor()
         cursor.execute('SELECT name, max(time), author, version, comment FROM wiki'
                        ' WHERE time >= %s GROUP BY name ORDER BY max(time) DESC', (since,))
         result = []
         for name, when, author, version, comment in cursor:
-            result.append(self._page_info(name, when, author, version, comment))
+            if 'WIKI_VIEW' in req.perm(wiki_realm(id=name, version=version)):
+                result.append(
+                    self._page_info(name, when, author, version, comment))
         return result
 
     def getRPCVersionSupported(self, req):
     def getPage(self, req, pagename, version=None):
         """ Get the raw Wiki text of page, latest version. """
         page = WikiPage(self.env, pagename, version)
+        req.perm(page.resource).require('WIKI_VIEW')
         if page.exists:
             return page.text
         else:
 
     def getAllPages(self, req):
         """ Returns a list of all pages. The result is an array of utf8 pagenames. """
-        return list(self.wiki.get_pages())
+        pages = []
+        for page in self.wiki.get_pages():
+            if 'WIKI_VIEW' in req.perm(Resource('wiki', page)):
+                pages.append(page)
+        return pages
 
     def getPageInfo(self, req, pagename, version=None):
         """ Returns information about the given page. """
         page = WikiPage(self.env, pagename, version)
+        req.perm(page.resource).require('WIKI_VIEW')
         if page.exists:
             last_update = page.get_history().next()
             return self._page_info(page.name, last_update[1],
         """ writes the content of the page. """
         page = WikiPage(self.env, pagename)
         if page.readonly:
-            req.perm.assert_permission('WIKI_ADMIN')
+            req.perm(page.resource).require('WIKI_ADMIN')
         elif not page.exists:
-            req.perm.assert_permission('WIKI_CREATE')
+            req.perm(page.resource).require('WIKI_CREATE')
         else:
-            req.perm.assert_permission('WIKI_MODIFY')
+            req.perm(page.resource).require('WIKI_MODIFY')
 
         page.text = content
-        if req.perm.has_permission('WIKI_ADMIN'):
+        if req.perm(page.resource).has_permission('WIKI_ADMIN'):
             page.readonly = attributes.get('readonly') and 1 or 0
 
         page.save(attributes.get('author', req.authname),
-                  attributes.get('comment'),
-                  req.remote_addr)
+                  attributes.get('comment'), req.remote_addr)
         return True
 
     def deletePage(self, req, name, version=None):
         """Delete a Wiki page (all versions) or a specific version by
         including an optional version number. Attachments will also be
         deleted if page no longer exists. Returns True for success."""
+        wp = WikiPage(self.env, name, version)
+        req.perm(wp.resource).require('WIKI_DELETE')
         try:
-            wp = WikiPage(self.env, name, version)
             wp.delete(version)
             return True
         except:
 
     def listAttachments(self, req, pagename):
         """ Lists attachments on a given page. """
-        return [pagename + '/' + a.filename for a in Attachment.select(self.env, 'wiki', pagename)]
+        for a in Attachment.select(self.env, 'wiki', pagename):
+            if 'ATTACHMENT_VIEW' in req.perm(a.resource):
+                yield pagename + '/' + a.filename
 
     def getAttachment(self, req, path):
         """ returns the content of an attachment. """
         pagename, filename = posixpath.split(path)
         attachment = Attachment(self.env, 'wiki', pagename, filename)
+        req.perm(attachment.resource).require('ATTACHMENT_VIEW')
         return xmlrpclib.Binary(attachment.open().read())
 
     def putAttachment(self, req, path, data):
         if replace:
             try:
                 attachment = Attachment(self.env, 'wiki', pagename, filename)
+                req.perm(attachment.resource).require('ATTACHMENT_DELETE')
                 attachment.delete()
             except TracError:
                 pass
         attachment = Attachment(self.env, 'wiki', pagename)
+        req.perm(attachment.resource).require('ATTACHMENT_CREATE')
         attachment.author = req.authname
         attachment.description = description
         attachment.insert(filename, StringIO(data.data), len(data.data))
         if not WikiPage(self.env, pagename).exists:
             raise TracError, 'Wiki page "%s" does not exist' % pagename
         attachment = Attachment(self.env, 'wiki', pagename, filename)
+        req.perm(attachment.resource).require('ATTACHMENT_DELETE')
         attachment.delete()
         return True
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.