comparison 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
comparison
equal deleted inserted replaced
6089:15d1ce536c73 6090:e097ff5064b8
468 except hyperdb.HyperdbValueError as msg: 468 except hyperdb.HyperdbValueError as msg:
469 raise UsageError(msg) 469 raise UsageError(msg)
470 470
471 return prop 471 return prop
472 472
473 def transitive_props (self, class_name, props):
474 """Construct a list of transitive properties from the given
475 argument, and return it after permission check. Raises
476 Unauthorised if no permission. Permission is checked by checking
477 search-permission on the path (delimited by '.') excluding the
478 last component and then checking View permission on the last
479 component. We do not allow to traverse multilinks -- the last
480 item of an expansion *may* be a multilink but in the middle of a
481 transitive prop.
482 """
483 checked_props = []
484 uid = self.db.getuid()
485 for p in props:
486 pn = p
487 cn = class_name
488 if '.' in p:
489 path, lc = p.rsplit('.', 1)
490 if not self.db.security.hasSearchPermission(uid, class_name, p):
491 raise (Unauthorised
492 ('User does not have permission on "%s.%s"'
493 % (class_name, p)))
494 prev = prop = None
495 # This shouldn't raise any errors, otherwise the search
496 # permission check above would have failed
497 for pn in path.split('.'):
498 cls = self.db.getclass(cn)
499 prop = cls.getprops(protected=True)[pn]
500 if isinstance(prop, hyperdb.Multilink):
501 raise UsageError(
502 'Multilink Traversal not allowed: %s' % p)
503 cn = prop.classname
504 cls = self.db.getclass(cn)
505 # Now we have the classname in cn and the prop name in pn.
506 if not self.db.security.hasPermission('View', uid, cn, pn):
507 raise(Unauthorised
508 ('User does not have permission on "%s.%s"' % (cn, pn)))
509 checked_props.append (p)
510 return checked_props
511
473 def error_obj(self, status, msg, source=None): 512 def error_obj(self, status, msg, source=None):
474 """Return an error object""" 513 """Return an error object"""
475 self.client.response_code = status 514 self.client.response_code = status
476 result = { 515 result = {
477 'error': { 516 'error': {
565 604
566 result = {} 605 result = {}
567 try: 606 try:
568 # pn = propname 607 # pn = propname
569 for pn in sorted(props): 608 for pn in sorted(props):
570 prop = props[pn] 609 ok = False
571 if not self.db.security.hasPermission( 610 id = item_id
572 'View', uid, class_name, pn, item_id 611 nd = node
573 ): 612 cn = class_name
613 for p in pn.split('.'):
614 if not self.db.security.hasPermission(
615 'View', uid, cn, p, id
616 ):
617 break
618 cl = self.db.getclass(cn)
619 nd = cl.getnode(id)
620 id = v = getattr(nd, p)
621 prop = cl.getprops(protected=True)[p]
622 cn = getattr(prop, 'classname', None)
623 else:
624 ok = True
625 if not ok:
574 continue 626 continue
575 v = getattr(node, pn)
576 if isinstance(prop, (hyperdb.Link, hyperdb.Multilink)): 627 if isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
577 linkcls = self.db.getclass(prop.classname) 628 linkcls = self.db.getclass(prop.classname)
578 cp = '%s/%s/' % (self.data_path, prop.classname) 629 cp = '%s/%s/' % (self.data_path, prop.classname)
579 if verbose and v: 630 if verbose and v:
580 if isinstance(v, type([])): 631 if isinstance(v, type([])):
652 page = { 703 page = {
653 'size': None, 704 'size': None,
654 'index': 1 # setting just size starts at page 1 705 'index': 1 # setting just size starts at page 1
655 } 706 }
656 verbose = 1 707 verbose = 1
657 display_props = {} 708 display_props = set()
658 sort = [] 709 sort = []
659 for form_field in input.value: 710 for form_field in input.value:
660 key = form_field.name 711 key = form_field.name
661 value = form_field.value 712 value = form_field.value
662 if key.startswith("@page_"): # serve the paging purpose 713 if key.startswith("@page_"): # serve the paging purpose
668 elif key == "@fields" or key == "@attrs": 719 elif key == "@fields" or key == "@attrs":
669 f = value.split(",") 720 f = value.split(",")
670 if len(f) == 1: 721 if len(f) == 1:
671 f = value.split(":") 722 f = value.split(":")
672 allprops = class_obj.getprops(protected=True) 723 allprops = class_obj.getprops(protected=True)
673 for i in f: 724 display_props.update(self.transitive_props(class_name, f))
674 try:
675 display_props[i] = allprops[i]
676 except KeyError:
677 raise UsageError("Failed to find property '%s' "
678 "for class %s." % (i, class_name))
679 elif key == "@sort": 725 elif key == "@sort":
680 f = value.split(",") 726 f = value.split(",")
681 allprops = class_obj.getprops(protected=True) 727 allprops = class_obj.getprops(protected=True)
682 for p in f: 728 for p in f:
683 if not p: 729 if not p:
778 # of the DB already sorts by ID if no sort option was given. 824 # of the DB already sorts by ID if no sort option was given.
779 825
780 # add verbose elements. 2 and above get identifying label. 826 # add verbose elements. 2 and above get identifying label.
781 if verbose > 1: 827 if verbose > 1:
782 lp = class_obj.labelprop() 828 lp = class_obj.labelprop()
783 # Label prop may be a protected property like activity 829 display_props.add(lp)
784 display_props[lp] = class_obj.getprops(protected=True)[lp]
785 830
786 # extract result from data 831 # extract result from data
787 result = {} 832 result = {}
788 result['collection'] = [] 833 result['collection'] = []
789 for item_id in obj_list: 834 for item_id in obj_list:
889 for form_field in input.value: 934 for form_field in input.value:
890 key = form_field.name 935 key = form_field.name
891 value = form_field.value 936 value = form_field.value
892 if key == "@fields" or key == "@attrs": 937 if key == "@fields" or key == "@attrs":
893 if props is None: 938 if props is None:
894 props = {} 939 props = set()
895 # support , or : separated elements 940 # support , or : separated elements
896 f = value.split(",") 941 f = value.split(",")
897 if len(f) == 1: 942 if len(f) == 1:
898 f = value.split(":") 943 f = value.split(":")
899 allprops = class_obj.getprops(protected=True) 944 allprops = class_obj.getprops(protected=True)
900 for i in f: 945 props.update(self.transitive_props(class_name, f))
901 try:
902 props[i] = allprops[i]
903 except KeyError:
904 raise UsageError("Failed to find property '%s' for class %s." % (i, class_name))
905 elif key == "@protected": 946 elif key == "@protected":
906 # allow client to request read only 947 # allow client to request read only
907 # properties like creator, activity etc. 948 # properties like creator, activity etc.
908 # used only if no @fields/@attrs 949 # used only if no @fields/@attrs
909 protected = value.lower() == "true" 950 protected = value.lower() == "true"
910 elif key == "@verbose": 951 elif key == "@verbose":
911 verbose = int(value) 952 verbose = int(value)
912 953
913 result = {} 954 result = {}
914 if props is None: 955 if props is None:
915 props = class_obj.getprops(protected=protected) 956 props = set(class_obj.getprops(protected=protected))
916 else: 957 else:
917 if verbose > 1: 958 if verbose > 1:
918 lp = class_obj.labelprop() 959 lp = class_obj.labelprop()
919 props[lp] = class_obj.properties[lp] 960 props.add(lp)
920 961
921 result = { 962 result = {
922 'id': itemid, 963 'id': itemid,
923 'type': class_name, 964 'type': class_name,
924 'link': '%s/%s/%s' % (self.data_path, class_name, item_id), 965 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),

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