diff roundup/rest.py @ 5680:f77209ddd579

Refactored REST code that formats an item for display. A GET on /class_name or /class_name/item_id use the same back end code. So both now support use of @fields (or @attrs) as a comma (or colon) separated list of property names to display. @verbose is respected for all data for /class_name. This also obscures passwords so they don't leak even in encrypted form.
author John Rouillard <rouilj@ieee.org>
date Fri, 29 Mar 2019 23:57:13 -0400
parents df9eb574b717
children 6457fd696a43
line wrap: on
line diff
--- a/roundup/rest.py	Thu Mar 28 18:21:29 2019 +0100
+++ b/roundup/rest.py	Fri Mar 29 23:57:13 2019 -0400
@@ -471,6 +471,67 @@
                 "If-Match is missing or does not match."
                 " Retrieve asset and retry modification if valid.")
 
+    def format_item(self, node, item_id, props=None, verbose=1):
+        ''' display class obj as requested by verbose and
+            props.
+        '''
+        uid = self.db.getuid()
+        class_name = node.cl.classname
+        result = {}
+        try:
+            # pn = propname
+            for pn in sorted(props):
+                prop = props[pn]
+                if not self.db.security.hasPermission(
+                        'View', uid, class_name, pn, item_id
+                ):
+                    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)
+                    if verbose and v:
+                        if isinstance(v, type([])):
+                            r = []
+                            for id in v:
+                                d = dict(id = id, link = cp + id)
+                                if verbose > 1:
+                                    label = linkcls.labelprop()
+                                    d [label] = linkcls.get(id, label)
+                                r.append(d)
+                            result[pn] = r
+                        else:
+                            result[pn] = dict(id = v, link = cp + v)
+                            if verbose > 1:
+                                label = linkcls.labelprop()
+                                result[pn][label] = linkcls.get(v, label)
+                    else:
+                        result[pn] = v
+                elif isinstance (prop, hyperdb.String) and pn == 'content':
+                    # Do not show the (possibly HUGE) content prop
+                    # unless very verbose, we display the standard
+                    # download link instead
+                    if verbose < 3:
+                        u = self.db.config.TRACKER_WEB
+                        p = u + '%s%s/' % (class_name, node.id)
+                        result[pn] = dict(link = p)
+                    else:
+                        result[pn] = v
+                elif isinstance(prop, hyperdb.Password):
+                    if v != None: # locked users like anonymous have None
+                        result[pn] = "[password hidden scheme %s]"%v.scheme
+                    else:
+                        # Don't divulge it's a locked account. Choose most
+                        # secure as default.
+                        result[pn] = "[password hidden scheme PBKDF2]"
+                else:
+                    result[pn] = v
+        except KeyError as msg:
+            raise UsageError("%s field not valid" % msg)
+
+        return result
+
+
     @Routing.route("/data/<:class_name>", 'GET')
     @_data_decorator
     def get_collection(self, class_name, input):
@@ -509,6 +570,7 @@
             'index': 1   # setting just size starts at page 1
         }
         verbose = 1
+        display_props = {}
         for form_field in input.value:
             key = form_field.name
             value = form_field.value
@@ -518,6 +580,12 @@
                 page[key] = value
             elif key == "@verbose":
                 verbose = int (value)
+            elif key == "@fields" or key == "@attrs":
+                f = value.split(",")
+                if len(f) == 1:
+                    f=value.split(",")
+                for i in f:
+                    display_props[i] = class_obj.properties[i]
             else: # serve the filter purpose
                 prop = class_obj.getprops()[key]
                 # We drop properties without search permission silently
@@ -543,26 +611,31 @@
         else:
             obj_list = class_obj.filter(None, filter_props)
 
+        # Sort list as specified by sortorder
+        # This is more useful for things where there is an
+        # explicit order. E.G. status has an order that is
+        # roughly the progression of the issue through
+        # the states so open is before closed.
+        obj_list.sort()
+
+        # add verbose elements. 2 and above get identifying label.
+        if verbose > 1:
+            lp = class_obj.labelprop()
+            display_props[lp] = class_obj.properties[lp]
+
         # extract result from data
         result={}
-        result['collection'] = [
-            {'id': item_id, 'link': class_path + item_id}
-            for item_id in obj_list
+        result['collection']=[]
+        for item_id in obj_list:
             if self.db.security.hasPermission(
-                'View', uid, class_name, itemid=item_id
-            )
-        ]
-
-        # add verbose elements. First identifying label.
-        if verbose > 1:
-            label = class_obj.labelprop()
-            for obj in result['collection']:
-                id = obj['id']
-                if self.db.security.hasPermission(
-                        'View', uid, class_name, property=label,
-                        itemid=id
-                ):
-                    obj[label] = class_obj.get(id, label)
+                    'View', uid, class_name, itemid=item_id):
+                r = {'id': item_id, 'link': class_path + item_id}
+                if display_props:
+                    r.update(self.format_item(class_obj.getnode(item_id),
+                                               item_id,
+                                               props=display_props,
+                                               verbose=verbose))
+                result['collection'].append(r)
 
         result_len = len(result['collection'])
 
@@ -657,66 +730,37 @@
         for form_field in input.value:
             key = form_field.name
             value = form_field.value
-            if key == "@fields":
-                props = value.split(",")
-            if key == "@protected":
+            if key == "@fields" or key == "@attrs":
+                if props is None:
+                    props = {}
+                # support , or : separated elements
+                f=value.split(",")
+                if len(f) == 1:
+                    f=value.split(":")
+                for i in f:
+                    props[i] = class_obj.properties[i]
+            elif key == "@protected":
                 # allow client to request read only
                 # properties like creator, activity etc.
+                # used only if no @fields/@attrs
                 protected = value.lower() == "true"
-            if key == "@verbose":
+            elif key == "@verbose":
                 verbose = int (value)
 
         result = {}
         if props is None:
             props = class_obj.getprops(protected=protected)
+        else:
+            if verbose > 1:
+                lp = class_obj.labelprop()
+                props[lp] = class_obj.properties[lp]
 
-        try:
-            for pn in sorted(props):
-                prop = props[pn]
-                if not self.db.security.hasPermission(
-                    'View', uid, class_name, pn, itemid
-                ):
-                    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)
-                    if verbose and v:
-                        if isinstance(v, type([])):
-                            r = []
-                            for id in v:
-                                d = dict(id = id, link = cp + id)
-                                if verbose > 1:
-                                    label = linkcls.labelprop()
-                                    d [label] = linkcls.get(id, label)
-                                r.append(d)
-                            result[pn] = r
-                        else:
-                            result[pn] = dict(id = v, link = cp + v)
-                            if verbose > 1:
-                                label = linkcls.labelprop()
-                                result[pn][label] = linkcls.get(v, label)
-                    else:
-                        result[pn] = v
-                elif isinstance (prop, hyperdb.String) and pn == 'content':
-                    # Do not show the (possibly HUGE) content prop
-                    # unless very verbose, we display the standard
-                    # download link instead
-                    if verbose < 3:
-                        u = self.db.config.TRACKER_WEB
-                        p = u + '%s%s/' % (class_name, node.id)
-                        result[pn] = dict(link = p)
-                    else:
-                        result[pn] = v
-                else:
-                    result[pn] = v
-        except KeyError as msg:
-            raise UsageError("%s field not valid" % msg)
         result = {
             'id': itemid,
             'type': class_name,
             'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
-            'attributes': dict(result),
+            'attributes': self.format_item(node, itemid, props=props,
+                                 verbose=verbose),
             '@etag': etag
         }
 

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