Mercurial > p > roundup > code
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), |
