2525from roundup .exceptions import *
2626from roundup .cgi .exceptions import *
2727
28+ from hashlib import md5
29+
2830# Py3 compatible basestring
2931try :
3032 basestring
@@ -59,6 +61,9 @@ def format_object(self, *args, **kwargs):
5961 except ValueError as msg :
6062 code = 409
6163 data = msg
64+ except PreconditionFailed as msg :
65+ code = 412
66+ data = msg
6267 except NotImplementedError :
6368 code = 402 # nothing to pay, just a mark for debugging purpose
6469 data = 'Method under development'
@@ -91,6 +96,66 @@ def format_object(self, *args, **kwargs):
9196 return result
9297 return format_object
9398
99+ def calculate_etag (node , classname = "Missing" , id = "0" ):
100+ '''given a hyperdb node generate a hashed representation of it to be
101+ used as an etag.
102+
103+ This code needs a __repr__ function in the Password class. This
104+ replaces the repr(items) which would be:
105+
106+ <roundup.password.Password instance at 0x7f3442406170>
107+
108+ with the string representation:
109+
110+ {PBKDF2}10000$k4d74EDgxlbH...A
111+
112+ This makes the representation repeatable as the location of the
113+ password instance is not static and we need a constant value to
114+ calculate the etag.
115+
116+ Note that repr() is chosen for the node rather than str() since
117+ repr is meant to be an unambiguous representation.
118+
119+ classname and id are used for logging only.
120+ '''
121+
122+ items = node .items (protected = True ) # include every item
123+ etag = md5 (repr (items )).hexdigest ()
124+ logger .debug ("object=%s%s; tag=%s; repr=%s" , classname , id ,
125+ etag , repr (node .items (protected = True )))
126+ return etag
127+
128+ def check_etag (node , etags , classname = "Missing" , id = "0" ):
129+ '''Take a list of etags and compare to the etag for the given node.
130+
131+ Iterate over all supplied etags,
132+ If a tag fails to match, return False.
133+ If at least one etag matches, return True.
134+ If all etags are None, return False.
135+
136+ '''
137+ have_etag_match = False
138+
139+ node_etag = calculate_etag (node , classname , id )
140+
141+ for etag in etags :
142+ if etag != None :
143+ if etag != node_etag :
144+ return False
145+ have_etag_match = True
146+
147+ if have_etag_match :
148+ return True
149+ else :
150+ return False
151+
152+ def obtain_etags (headers ,input ):
153+ '''Get ETags value from headers or payload data'''
154+ etags = []
155+ if '@etag' in input :
156+ etags .append (input ['@etag' ].value );
157+ etags .append (headers .getheader ("ETag" , None ))
158+ return etags
94159
95160def parse_accept_header (accept ):
96161 """
@@ -484,6 +549,8 @@ def get_element(self, class_name, item_id, input):
484549 )
485550
486551 class_obj = self .db .getclass (class_name )
552+ node = class_obj .getnode (item_id )
553+ etag = calculate_etag (node , class_name , item_id )
487554 props = None
488555 for form_field in input .value :
489556 key = form_field .name
@@ -496,7 +563,7 @@ def get_element(self, class_name, item_id, input):
496563
497564 try :
498565 result = [
499- (prop_name , class_obj . get ( item_id , prop_name ))
566+ (prop_name , node . __getattr__ ( prop_name ))
500567 for prop_name in props
501568 if self .db .security .hasPermission (
502569 'View' , self .db .getuid (), class_name , prop_name ,
@@ -508,9 +575,11 @@ def get_element(self, class_name, item_id, input):
508575 'id' : item_id ,
509576 'type' : class_name ,
510577 'link' : '%s/%s/%s' % (self .data_path , class_name , item_id ),
511- 'attributes' : dict (result )
578+ 'attributes' : dict (result ),
579+ '@etag' : etag
512580 }
513581
582+ self .client .setHeader ("ETag" , '"%s"' % etag )
514583 return 200 , result
515584
516585 @Routing .route ("/data/<:class_name>/<:item_id>/<:attr_name>" , 'GET' )
@@ -546,15 +615,19 @@ def get_attribute(self, class_name, item_id, attr_name, input):
546615 )
547616
548617 class_obj = self .db .getclass (class_name )
549- data = class_obj .get (item_id , attr_name )
618+ node = class_obj .getnode (item_id )
619+ etag = calculate_etag (node , class_name , item_id )
620+ data = node .__getattr__ (attr_name )
550621 result = {
551622 'id' : item_id ,
552623 'type' : type (data ),
553624 'link' : "%s/%s/%s/%s" %
554625 (self .data_path , class_name , item_id , attr_name ),
555- 'data' : data
626+ 'data' : data ,
627+ '@etag' : etag
556628 }
557629
630+ self .client .setHeader ("ETag" , '"%s"' % etag )
558631 return 200 , result
559632
560633 @Routing .route ("/data/<:class_name>" , 'POST' )
@@ -655,6 +728,12 @@ def put_element(self, class_name, item_id, input):
655728 (p , class_name , item_id )
656729 )
657730 try :
731+ if not check_etag (class_obj .getnode (item_id ),
732+ obtain_etags (self .client .request .headers , input ),
733+ class_name ,
734+ item_id ):
735+ raise PreconditionFailed ("Etag is missing or does not match."
736+ "Retreive asset and retry modification if valid." )
658737 result = class_obj .set (item_id , ** props )
659738 self .db .commit ()
660739 except (TypeError , IndexError , ValueError ) as message :
@@ -697,7 +776,6 @@ def put_attribute(self, class_name, item_id, attr_name, input):
697776 'Permission to edit %s%s %s denied' %
698777 (class_name , item_id , attr_name )
699778 )
700-
701779 class_obj = self .db .getclass (class_name )
702780 props = {
703781 attr_name : self .prop_from_arg (
@@ -706,6 +784,11 @@ def put_attribute(self, class_name, item_id, attr_name, input):
706784 }
707785
708786 try :
787+ if not check_etag (class_obj .getnode (item_id ),
788+ obtain_etags (self .client .request .headers , input ),
789+ class_name , item_id ):
790+ raise PreconditionFailed ("Etag is missing or does not match."
791+ "Retreive asset and retry modification if valid." )
709792 result = class_obj .set (item_id , ** props )
710793 self .db .commit ()
711794 except (TypeError , IndexError , ValueError ) as message :
@@ -791,6 +874,13 @@ def delete_element(self, class_name, item_id, input):
791874 'Permission to retire %s %s denied' % (class_name , item_id )
792875 )
793876
877+ if not check_etag (class_obj .getnode (item_id ),
878+ obtain_etags (self .client .request .headers , input ),
879+ class_name ,
880+ item_id ):
881+ raise PreconditionFailed ("Etag is missing or does not match."
882+ "Retreive asset and retry modification if valid." )
883+
794884 class_obj .retire (item_id )
795885 self .db .commit ()
796886 result = {
@@ -834,6 +924,13 @@ def delete_attribute(self, class_name, item_id, attr_name, input):
834924 props [attr_name ] = None
835925
836926 try :
927+ if not check_etag (class_obj .getnode (item_id ),
928+ obtain_etags (self .client .request .headers , input ),
929+ class_name ,
930+ item_id ):
931+ raise PreconditionFailed ("Etag is missing or does not match."
932+ "Retreive asset and retry modification if valid." )
933+
837934 class_obj .set (item_id , ** props )
838935 self .db .commit ()
839936 except (TypeError , IndexError , ValueError ) as message :
@@ -877,6 +974,13 @@ def patch_element(self, class_name, item_id, input):
877974 op = self .__default_patch_op
878975 class_obj = self .db .getclass (class_name )
879976
977+ if not check_etag (class_obj .getnode (item_id ),
978+ obtain_etags (self .client .request .headers , input ),
979+ class_name ,
980+ item_id ):
981+ raise PreconditionFailed ("Etag is missing or does not match."
982+ "Retreive asset and retry modification if valid." )
983+
880984 # if patch operation is action, call the action handler
881985 action_args = [class_name + item_id ]
882986 if op == 'action' :
@@ -978,6 +1082,14 @@ def patch_attribute(self, class_name, item_id, attr_name, input):
9781082
9791083 prop = attr_name
9801084 class_obj = self .db .getclass (class_name )
1085+
1086+ if not check_etag (class_obj .getnode (item_id ),
1087+ obtain_etags (self .client .request .headers , input ),
1088+ class_name ,
1089+ item_id ):
1090+ raise PreconditionFailed ("Etag is missing or does not match."
1091+ "Retreive asset and retry modification if valid." )
1092+
9811093 props = {
9821094 prop : self .prop_from_arg (
9831095 class_obj , prop , input ['data' ].value , item_id
0 commit comments