comparison roundup/rest.py @ 5729:9ea2ce9d10cf

A few internet references report that etags for the same underlying resource but with different representation (xml, json ...) should have different etags. That is currently not the case. Added code to allow incorporation of representation info into the etag. By default the representation is "json", but future patches can pass the representation down and modify flow to match requested representation.
author John Rouillard <rouilj@ieee.org>
date Sat, 25 May 2019 14:23:16 -0400
parents 8b5171f353eb
children 4aa26a9f3b47
comparison
equal deleted inserted replaced
5728:bfd28644fe43 5729:9ea2ce9d10cf
116 'data': data 116 'data': data
117 } 117 }
118 return result 118 return result
119 return format_object 119 return format_object
120 120
121 def calculate_etag (node, key, classname="Missing", id="0"): 121 def calculate_etag (node, key, classname="Missing", id="0",
122 repr_format="json"):
122 '''given a hyperdb node generate a hashed representation of it to be 123 '''given a hyperdb node generate a hashed representation of it to be
123 used as an etag. 124 used as an etag.
124 125
125 This code needs a __repr__ function in the Password class. This 126 This code needs a __repr__ function in the Password class. This
126 replaces the repr(items) which would be: 127 replaces the repr(items) which would be:
140 141
141 classname and id are used for logging only. 142 classname and id are used for logging only.
142 ''' 143 '''
143 144
144 items = node.items(protected=True) # include every item 145 items = node.items(protected=True) # include every item
145 etag = hmac.new(bs2b(key),bs2b(repr(sorted(items)))).hexdigest() 146 etag = hmac.new(bs2b(key),bs2b(repr_format +
147 repr(sorted(items)))).hexdigest()
146 logger.debug("object=%s%s; tag=%s; repr=%s", classname, id, 148 logger.debug("object=%s%s; tag=%s; repr=%s", classname, id,
147 etag, repr(node.items(protected=True))) 149 etag, repr(node.items(protected=True)))
148 # Quotes are part of ETag spec, normal headers don't have quotes 150 # Quotes are part of ETag spec, normal headers don't have quotes
149 return '"%s"' % etag 151 return '"%s"' % etag
150 152
151 def check_etag (node, key, etags, classname="Missing", id="0"): 153 def check_etag (node, key, etags, classname="Missing", id="0",
154 repr_format="json"):
152 '''Take a list of etags and compare to the etag for the given node. 155 '''Take a list of etags and compare to the etag for the given node.
153 156
154 Iterate over all supplied etags, 157 Iterate over all supplied etags,
155 If a tag fails to match, return False. 158 If a tag fails to match, return False.
156 If at least one etag matches, return True. 159 If at least one etag matches, return True.
157 If all etags are None, return False. 160 If all etags are None, return False.
158 161
159 ''' 162 '''
160 have_etag_match=False 163 have_etag_match=False
161 164
162 node_etag = calculate_etag(node, key, classname, id) 165 node_etag = calculate_etag(node, key, classname, id,
166 repr_format=repr_format)
163 167
164 for etag in etags: 168 for etag in etags:
165 if etag is not None: 169 if etag is not None:
166 if etag != node_etag: 170 if etag != node_etag:
167 return False 171 return False
506 else: 510 else:
507 raise UsageError('PATCH Operation %s is not allowed' % op) 511 raise UsageError('PATCH Operation %s is not allowed' % op)
508 512
509 return result 513 return result
510 514
511 def raise_if_no_etag(self, class_name, item_id, input): 515 def raise_if_no_etag(self, class_name, item_id, input, repr_format="json"):
512 class_obj = self.db.getclass(class_name) 516 class_obj = self.db.getclass(class_name)
513 if not check_etag(class_obj.getnode(item_id), 517 if not check_etag(class_obj.getnode(item_id),
514 self.db.config.WEB_SECRET_KEY, 518 self.db.config.WEB_SECRET_KEY,
515 obtain_etags(self.client.request.headers, input), 519 obtain_etags(self.client.request.headers, input),
516 class_name, 520 class_name,
517 item_id): 521 item_id, repr_format=repr_format):
518 raise PreconditionFailed( 522 raise PreconditionFailed(
519 "If-Match is missing or does not match." 523 "If-Match is missing or does not match."
520 " Retrieve asset and retry modification if valid.") 524 " Retrieve asset and retry modification if valid.")
521 525
522 def format_item(self, node, item_id, props=None, verbose=1): 526 def format_item(self, node, item_id, props=None, verbose=1):
787 'Permission to view %s%s denied' % (class_name, itemid) 791 'Permission to view %s%s denied' % (class_name, itemid)
788 ) 792 )
789 793
790 node = class_obj.getnode(itemid) 794 node = class_obj.getnode(itemid)
791 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY, 795 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY,
792 class_name, itemid) 796 class_name, itemid, repr_format="json")
793 props = None 797 props = None
794 protected=False 798 protected=False
795 verbose=1 799 verbose=1
796 for form_field in input.value: 800 for form_field in input.value:
797 key = form_field.name 801 key = form_field.name
869 ) 873 )
870 874
871 class_obj = self.db.getclass(class_name) 875 class_obj = self.db.getclass(class_name)
872 node = class_obj.getnode(item_id) 876 node = class_obj.getnode(item_id)
873 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY, 877 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY,
874 class_name, item_id) 878 class_name, item_id, repr_format="json")
875 data = node.__getattr__(attr_name) 879 data = node.__getattr__(attr_name)
876 result = { 880 result = {
877 'id': item_id, 881 'id': item_id,
878 'type': str(type(data)), 882 'type': str(type(data)),
879 'link': "%s/%s/%s/%s" % 883 'link': "%s/%s/%s/%s" %

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