comparison 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
comparison
equal deleted inserted replaced
5679:df9eb574b717 5680:f77209ddd579
469 item_id): 469 item_id):
470 raise PreconditionFailed( 470 raise PreconditionFailed(
471 "If-Match is missing or does not match." 471 "If-Match is missing or does not match."
472 " Retrieve asset and retry modification if valid.") 472 " Retrieve asset and retry modification if valid.")
473 473
474 @Routing.route("/data/<:class_name>", 'GET') 474 def format_item(self, node, item_id, props=None, verbose=1):
475 @_data_decorator 475 ''' display class obj as requested by verbose and
476 def get_collection(self, class_name, input): 476 props.
477 """GET resource from class URI. 477 '''
478
479 This function returns only items have View permission
480 class_name should be valid already
481
482 Args:
483 class_name (string): class name of the resource (Ex: issue, msg)
484 input (list): the submitted form of the user
485
486 Returns:
487 int: http status code 200 (OK)
488 list: list of reference item in the class
489 id: id of the object
490 link: path to the object
491 """
492 if class_name not in self.db.classes:
493 raise NotFound('Class %s not found' % class_name)
494
495 uid = self.db.getuid() 478 uid = self.db.getuid()
496 479 class_name = node.cl.classname
497 if not self.db.security.hasPermission(
498 'View', uid, class_name
499 ):
500 raise Unauthorised('Permission to view %s denied' % class_name)
501
502 class_obj = self.db.getclass(class_name)
503 class_path = '%s/%s/' % (self.data_path, class_name)
504
505 # Handle filtering and pagination
506 filter_props = {}
507 page = {
508 'size': None,
509 'index': 1 # setting just size starts at page 1
510 }
511 verbose = 1
512 for form_field in input.value:
513 key = form_field.name
514 value = form_field.value
515 if key.startswith("@page_"): # serve the paging purpose
516 key = key[6:]
517 value = int(value)
518 page[key] = value
519 elif key == "@verbose":
520 verbose = int (value)
521 else: # serve the filter purpose
522 prop = class_obj.getprops()[key]
523 # We drop properties without search permission silently
524 # This reflects the current behavior of other roundup
525 # interfaces
526 if not self.db.security.hasSearchPermission(
527 uid, class_name, key
528 ):
529 continue
530 if isinstance (prop, (hyperdb.Link, hyperdb.Multilink)):
531 vals = []
532 linkcls = self.db.getclass (prop.classname)
533 for p in value.split(","):
534 if prop.try_id_parsing and p.isdigit():
535 vals.append(p)
536 else:
537 vals.append(linkcls.lookup(p))
538 filter_props[key] = vals
539 else:
540 filter_props[key] = value
541 if not filter_props:
542 obj_list = class_obj.list()
543 else:
544 obj_list = class_obj.filter(None, filter_props)
545
546 # extract result from data
547 result={}
548 result['collection'] = [
549 {'id': item_id, 'link': class_path + item_id}
550 for item_id in obj_list
551 if self.db.security.hasPermission(
552 'View', uid, class_name, itemid=item_id
553 )
554 ]
555
556 # add verbose elements. First identifying label.
557 if verbose > 1:
558 label = class_obj.labelprop()
559 for obj in result['collection']:
560 id = obj['id']
561 if self.db.security.hasPermission(
562 'View', uid, class_name, property=label,
563 itemid=id
564 ):
565 obj[label] = class_obj.get(id, label)
566
567 result_len = len(result['collection'])
568
569 # pagination - page_index from 1...N
570 if page['size'] is not None:
571 page_start = max((page['index']-1) * page['size'], 0)
572 page_end = min(page_start + page['size'], result_len)
573 result['collection'] = result['collection'][page_start:page_end]
574 result['@links'] = {}
575 for rel in ('next', 'prev', 'self'):
576 if rel == 'next':
577 # if current index includes all data, continue
578 if page['index']*page['size'] > result_len: continue
579 index=page['index']+1
580 if rel == 'prev':
581 if page['index'] <= 1: continue
582 index=page['index']-1
583 if rel == 'self': index=page['index']
584
585 result['@links'][rel] = []
586 result['@links'][rel].append({
587 'rel': rel,
588 'uri': "%s/%s?@page_index=%s&"%(self.data_path,
589 class_name,index) \
590 + '&'.join([ "%s=%s"%(field.name,field.value) \
591 for field in input.value \
592 if field.name != "@page_index"]) })
593
594 result['@total_size'] = result_len
595 self.client.setHeader("X-Count-Total", str(result_len))
596 return 200, result
597
598 @Routing.route("/data/<:class_name>/<:item_id>", 'GET')
599 @_data_decorator
600 def get_element(self, class_name, item_id, input):
601 """GET resource from object URI.
602
603 This function returns only properties have View permission
604 class_name and item_id should be valid already
605
606 Args:
607 class_name (string): class name of the resource (Ex: issue, msg)
608 item_id (string): id of the resource (Ex: 12, 15)
609 or (if the class has a key property) this can also be
610 the key name, e.g. class_name = status, item_id = 'open'
611 input (list): the submitted form of the user
612
613 Returns:
614 int: http status code 200 (OK)
615 dict: a dictionary represents the object
616 id: id of the object
617 type: class name of the object
618 link: link to the object
619 attributes: a dictionary represent the attributes of the object
620 """
621 if class_name not in self.db.classes:
622 raise NotFound('Class %s not found' % class_name)
623 class_obj = self.db.getclass(class_name)
624 uid = self.db.getuid()
625 # If it's not numeric it is a key
626 if item_id.isdigit():
627 itemid = item_id
628 else:
629 keyprop = class_obj.getkey()
630 try:
631 k, v = item_id.split('=', 1)
632 if k != keyprop:
633 raise UsageError ("Not key property")
634 except ValueError:
635 v = item_id
636 pass
637 if not self.db.security.hasPermission(
638 'View', uid, class_name, itemid=item_id, property=keyprop
639 ):
640 raise Unauthorised(
641 'Permission to view %s%s.%s denied'
642 % (class_name, item_id, keyprop)
643 )
644 itemid = class_obj.lookup(v)
645 if not self.db.security.hasPermission(
646 'View', uid, class_name, itemid=itemid
647 ):
648 raise Unauthorised(
649 'Permission to view %s%s denied' % (class_name, itemid)
650 )
651
652 node = class_obj.getnode(itemid)
653 etag = calculate_etag(node, class_name, itemid)
654 props = None
655 protected=False
656 verbose=1
657 for form_field in input.value:
658 key = form_field.name
659 value = form_field.value
660 if key == "@fields":
661 props = value.split(",")
662 if key == "@protected":
663 # allow client to request read only
664 # properties like creator, activity etc.
665 protected = value.lower() == "true"
666 if key == "@verbose":
667 verbose = int (value)
668
669 result = {} 480 result = {}
670 if props is None:
671 props = class_obj.getprops(protected=protected)
672
673 try: 481 try:
482 # pn = propname
674 for pn in sorted(props): 483 for pn in sorted(props):
675 prop = props[pn] 484 prop = props[pn]
676 if not self.db.security.hasPermission( 485 if not self.db.security.hasPermission(
677 'View', uid, class_name, pn, itemid 486 'View', uid, class_name, pn, item_id
678 ): 487 ):
679 continue 488 continue
680 v = getattr(node, pn) 489 v = getattr(node, pn)
681 if isinstance (prop, (hyperdb.Link, hyperdb.Multilink)): 490 if isinstance (prop, (hyperdb.Link, hyperdb.Multilink)):
682 linkcls = self.db.getclass (prop.classname) 491 linkcls = self.db.getclass (prop.classname)
706 u = self.db.config.TRACKER_WEB 515 u = self.db.config.TRACKER_WEB
707 p = u + '%s%s/' % (class_name, node.id) 516 p = u + '%s%s/' % (class_name, node.id)
708 result[pn] = dict(link = p) 517 result[pn] = dict(link = p)
709 else: 518 else:
710 result[pn] = v 519 result[pn] = v
520 elif isinstance(prop, hyperdb.Password):
521 if v != None: # locked users like anonymous have None
522 result[pn] = "[password hidden scheme %s]"%v.scheme
523 else:
524 # Don't divulge it's a locked account. Choose most
525 # secure as default.
526 result[pn] = "[password hidden scheme PBKDF2]"
711 else: 527 else:
712 result[pn] = v 528 result[pn] = v
713 except KeyError as msg: 529 except KeyError as msg:
714 raise UsageError("%s field not valid" % msg) 530 raise UsageError("%s field not valid" % msg)
531
532 return result
533
534
535 @Routing.route("/data/<:class_name>", 'GET')
536 @_data_decorator
537 def get_collection(self, class_name, input):
538 """GET resource from class URI.
539
540 This function returns only items have View permission
541 class_name should be valid already
542
543 Args:
544 class_name (string): class name of the resource (Ex: issue, msg)
545 input (list): the submitted form of the user
546
547 Returns:
548 int: http status code 200 (OK)
549 list: list of reference item in the class
550 id: id of the object
551 link: path to the object
552 """
553 if class_name not in self.db.classes:
554 raise NotFound('Class %s not found' % class_name)
555
556 uid = self.db.getuid()
557
558 if not self.db.security.hasPermission(
559 'View', uid, class_name
560 ):
561 raise Unauthorised('Permission to view %s denied' % class_name)
562
563 class_obj = self.db.getclass(class_name)
564 class_path = '%s/%s/' % (self.data_path, class_name)
565
566 # Handle filtering and pagination
567 filter_props = {}
568 page = {
569 'size': None,
570 'index': 1 # setting just size starts at page 1
571 }
572 verbose = 1
573 display_props = {}
574 for form_field in input.value:
575 key = form_field.name
576 value = form_field.value
577 if key.startswith("@page_"): # serve the paging purpose
578 key = key[6:]
579 value = int(value)
580 page[key] = value
581 elif key == "@verbose":
582 verbose = int (value)
583 elif key == "@fields" or key == "@attrs":
584 f = value.split(",")
585 if len(f) == 1:
586 f=value.split(",")
587 for i in f:
588 display_props[i] = class_obj.properties[i]
589 else: # serve the filter purpose
590 prop = class_obj.getprops()[key]
591 # We drop properties without search permission silently
592 # This reflects the current behavior of other roundup
593 # interfaces
594 if not self.db.security.hasSearchPermission(
595 uid, class_name, key
596 ):
597 continue
598 if isinstance (prop, (hyperdb.Link, hyperdb.Multilink)):
599 vals = []
600 linkcls = self.db.getclass (prop.classname)
601 for p in value.split(","):
602 if prop.try_id_parsing and p.isdigit():
603 vals.append(p)
604 else:
605 vals.append(linkcls.lookup(p))
606 filter_props[key] = vals
607 else:
608 filter_props[key] = value
609 if not filter_props:
610 obj_list = class_obj.list()
611 else:
612 obj_list = class_obj.filter(None, filter_props)
613
614 # Sort list as specified by sortorder
615 # This is more useful for things where there is an
616 # explicit order. E.G. status has an order that is
617 # roughly the progression of the issue through
618 # the states so open is before closed.
619 obj_list.sort()
620
621 # add verbose elements. 2 and above get identifying label.
622 if verbose > 1:
623 lp = class_obj.labelprop()
624 display_props[lp] = class_obj.properties[lp]
625
626 # extract result from data
627 result={}
628 result['collection']=[]
629 for item_id in obj_list:
630 if self.db.security.hasPermission(
631 'View', uid, class_name, itemid=item_id):
632 r = {'id': item_id, 'link': class_path + item_id}
633 if display_props:
634 r.update(self.format_item(class_obj.getnode(item_id),
635 item_id,
636 props=display_props,
637 verbose=verbose))
638 result['collection'].append(r)
639
640 result_len = len(result['collection'])
641
642 # pagination - page_index from 1...N
643 if page['size'] is not None:
644 page_start = max((page['index']-1) * page['size'], 0)
645 page_end = min(page_start + page['size'], result_len)
646 result['collection'] = result['collection'][page_start:page_end]
647 result['@links'] = {}
648 for rel in ('next', 'prev', 'self'):
649 if rel == 'next':
650 # if current index includes all data, continue
651 if page['index']*page['size'] > result_len: continue
652 index=page['index']+1
653 if rel == 'prev':
654 if page['index'] <= 1: continue
655 index=page['index']-1
656 if rel == 'self': index=page['index']
657
658 result['@links'][rel] = []
659 result['@links'][rel].append({
660 'rel': rel,
661 'uri': "%s/%s?@page_index=%s&"%(self.data_path,
662 class_name,index) \
663 + '&'.join([ "%s=%s"%(field.name,field.value) \
664 for field in input.value \
665 if field.name != "@page_index"]) })
666
667 result['@total_size'] = result_len
668 self.client.setHeader("X-Count-Total", str(result_len))
669 return 200, result
670
671 @Routing.route("/data/<:class_name>/<:item_id>", 'GET')
672 @_data_decorator
673 def get_element(self, class_name, item_id, input):
674 """GET resource from object URI.
675
676 This function returns only properties have View permission
677 class_name and item_id should be valid already
678
679 Args:
680 class_name (string): class name of the resource (Ex: issue, msg)
681 item_id (string): id of the resource (Ex: 12, 15)
682 or (if the class has a key property) this can also be
683 the key name, e.g. class_name = status, item_id = 'open'
684 input (list): the submitted form of the user
685
686 Returns:
687 int: http status code 200 (OK)
688 dict: a dictionary represents the object
689 id: id of the object
690 type: class name of the object
691 link: link to the object
692 attributes: a dictionary represent the attributes of the object
693 """
694 if class_name not in self.db.classes:
695 raise NotFound('Class %s not found' % class_name)
696 class_obj = self.db.getclass(class_name)
697 uid = self.db.getuid()
698 # If it's not numeric it is a key
699 if item_id.isdigit():
700 itemid = item_id
701 else:
702 keyprop = class_obj.getkey()
703 try:
704 k, v = item_id.split('=', 1)
705 if k != keyprop:
706 raise UsageError ("Not key property")
707 except ValueError:
708 v = item_id
709 pass
710 if not self.db.security.hasPermission(
711 'View', uid, class_name, itemid=item_id, property=keyprop
712 ):
713 raise Unauthorised(
714 'Permission to view %s%s.%s denied'
715 % (class_name, item_id, keyprop)
716 )
717 itemid = class_obj.lookup(v)
718 if not self.db.security.hasPermission(
719 'View', uid, class_name, itemid=itemid
720 ):
721 raise Unauthorised(
722 'Permission to view %s%s denied' % (class_name, itemid)
723 )
724
725 node = class_obj.getnode(itemid)
726 etag = calculate_etag(node, class_name, itemid)
727 props = None
728 protected=False
729 verbose=1
730 for form_field in input.value:
731 key = form_field.name
732 value = form_field.value
733 if key == "@fields" or key == "@attrs":
734 if props is None:
735 props = {}
736 # support , or : separated elements
737 f=value.split(",")
738 if len(f) == 1:
739 f=value.split(":")
740 for i in f:
741 props[i] = class_obj.properties[i]
742 elif key == "@protected":
743 # allow client to request read only
744 # properties like creator, activity etc.
745 # used only if no @fields/@attrs
746 protected = value.lower() == "true"
747 elif key == "@verbose":
748 verbose = int (value)
749
750 result = {}
751 if props is None:
752 props = class_obj.getprops(protected=protected)
753 else:
754 if verbose > 1:
755 lp = class_obj.labelprop()
756 props[lp] = class_obj.properties[lp]
757
715 result = { 758 result = {
716 'id': itemid, 759 'id': itemid,
717 'type': class_name, 760 'type': class_name,
718 'link': '%s/%s/%s' % (self.data_path, class_name, item_id), 761 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
719 'attributes': dict(result), 762 'attributes': self.format_item(node, itemid, props=props,
763 verbose=verbose),
720 '@etag': etag 764 '@etag': etag
721 } 765 }
722 766
723 self.client.setHeader("ETag", etag) 767 self.client.setHeader("ETag", etag)
724 return 200, result 768 return 200, result

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