diff roundup/rest.py @ 6090:e097ff5064b8

Allow transitive properties in @fields in REST API These transitive properties may not cross Multilinks, e.g., when querying 'issue' the property 'messages.author' is not allowed (because 'messages' is a multilink). A multilink at the end (e.g. messages in the example) is fine.
author Ralf Schlatterbeck <rsc@runtux.com>
date Thu, 13 Feb 2020 08:51:20 +0100
parents 00a24243887c
children 53c4668a6600
line wrap: on
line diff
--- a/roundup/rest.py	Wed Feb 12 17:25:39 2020 +0100
+++ b/roundup/rest.py	Thu Feb 13 08:51:20 2020 +0100
@@ -470,6 +470,45 @@
 
         return prop
 
+    def transitive_props (self, class_name, props):
+        """Construct a list of transitive properties from the given
+        argument, and return it after permission check. Raises
+        Unauthorised if no permission. Permission is checked by checking
+        search-permission on the path (delimited by '.') excluding the
+        last component and then checking View permission on the last
+        component. We do not allow to traverse multilinks -- the last
+        item of an expansion *may* be a multilink but in the middle of a
+        transitive prop.
+        """
+        checked_props = []
+        uid = self.db.getuid()
+        for p in props:
+            pn = p
+            cn = class_name
+            if '.' in p:
+                path, lc = p.rsplit('.', 1)
+                if not self.db.security.hasSearchPermission(uid, class_name, p):
+                    raise (Unauthorised
+                        ('User does not have permission on "%s.%s"'
+                        % (class_name, p)))
+                prev = prop = None
+                # This shouldn't raise any errors, otherwise the search
+                # permission check above would have failed
+                for pn in path.split('.'):
+                    cls = self.db.getclass(cn)
+                    prop = cls.getprops(protected=True)[pn]
+                    if isinstance(prop, hyperdb.Multilink):
+                        raise UsageError(
+                            'Multilink Traversal not allowed: %s' % p)
+                    cn = prop.classname
+                cls = self.db.getclass(cn)
+            # Now we have the classname in cn and the prop name in pn.
+            if not self.db.security.hasPermission('View', uid, cn, pn):
+                raise(Unauthorised
+                    ('User does not have permission on "%s.%s"' % (cn, pn)))
+            checked_props.append (p)
+        return checked_props
+
     def error_obj(self, status, msg, source=None):
         """Return an error object"""
         self.client.response_code = status
@@ -567,12 +606,24 @@
         try:
             # pn = propname
             for pn in sorted(props):
-                prop = props[pn]
-                if not self.db.security.hasPermission(
-                        'View', uid, class_name, pn, item_id
-                ):
+                ok = False
+                id = item_id
+                nd = node
+                cn = class_name
+                for p in pn.split('.'):
+                    if not self.db.security.hasPermission(
+                            'View', uid, cn, p, id
+                    ):
+                        break
+                    cl = self.db.getclass(cn)
+                    nd = cl.getnode(id)
+                    id = v = getattr(nd, p)
+                    prop = cl.getprops(protected=True)[p]
+                    cn = getattr(prop, 'classname', None)
+                else:
+                    ok = True
+                if not ok:
                     continue
-                v = getattr(node, pn)
                 if isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
                     linkcls = self.db.getclass(prop.classname)
                     cp = '%s/%s/' % (self.data_path, prop.classname)
@@ -654,7 +705,7 @@
             'index': 1   # setting just size starts at page 1
         }
         verbose = 1
-        display_props = {}
+        display_props = set()
         sort = []
         for form_field in input.value:
             key = form_field.name
@@ -670,12 +721,7 @@
                 if len(f) == 1:
                     f = value.split(":")
                 allprops = class_obj.getprops(protected=True)
-                for i in f:
-                    try:
-                        display_props[i] = allprops[i]
-                    except KeyError:
-                        raise UsageError("Failed to find property '%s' "
-                                         "for class %s." % (i, class_name))
+                display_props.update(self.transitive_props(class_name, f))
             elif key == "@sort":
                 f = value.split(",")
                 allprops = class_obj.getprops(protected=True)
@@ -780,8 +826,7 @@
         # add verbose elements. 2 and above get identifying label.
         if verbose > 1:
             lp = class_obj.labelprop()
-            # Label prop may be a protected property like activity
-            display_props[lp] = class_obj.getprops(protected=True)[lp]
+            display_props.add(lp)
 
         # extract result from data
         result = {}
@@ -891,17 +936,13 @@
             value = form_field.value
             if key == "@fields" or key == "@attrs":
                 if props is None:
-                    props = {}
+                    props = set()
                 # support , or : separated elements
                 f = value.split(",")
                 if len(f) == 1:
                     f = value.split(":")
                 allprops = class_obj.getprops(protected=True)
-                for i in f:
-                    try:
-                        props[i] = allprops[i]
-                    except KeyError:
-                        raise UsageError("Failed to find property '%s' for class %s." % (i, class_name))
+                props.update(self.transitive_props(class_name, f))
             elif key == "@protected":
                 # allow client to request read only
                 # properties like creator, activity etc.
@@ -912,11 +953,11 @@
 
         result = {}
         if props is None:
-            props = class_obj.getprops(protected=protected)
+            props = set(class_obj.getprops(protected=protected))
         else:
             if verbose > 1:
                 lp = class_obj.labelprop()
-                props[lp] = class_obj.properties[lp]
+                props.add(lp)
 
         result = {
             'id': itemid,

Roundup Issue Tracker: http://roundup-tracker.org/