Skip to content

Commit 43040dd

Browse files
committed
Method PUT: ignore specification of protected properties which can not
be set. Filtering them out of the payload list. This lets the result of a get using: class/id?@Protected=true&@verbose=0 be used as input to a PUT operation without having to strip the protected properties. Note this does not raise an error if the PUT protected property is different from the value in the db. If the property is different but the etag/if-match passes, the user attempted to set the protected property and this should result in an error, but will not with this patch. Method DELETE class/id/attribute: raise error when trying to delete protected or required attribute/property. Raise UsageError when attribute doesn't exist. Method PATCH class/id: raise error when trying to replace/remove protected attribute/property raise error when trying to remove required attribute/property Catch KeyError at top level and turn into 400 error. If payload has an attribute/property that does not exist, raise UsageError which becomes a 400 error.
1 parent 5d5a300 commit 43040dd

File tree

2 files changed

+247
-12
lines changed

2 files changed

+247
-12
lines changed

roundup/rest.py

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def format_object(self, *args, **kwargs):
6464
except Unauthorised as msg:
6565
code = 403
6666
data = msg
67-
except UsageError as msg:
67+
except (UsageError, KeyError) as msg:
6868
code = 400
6969
data = msg
7070
except (AttributeError, Reject) as msg:
@@ -344,7 +344,7 @@ def __init__(self, client, db):
344344
self.base_path = '%srest' % (self.db.config.TRACKER_WEB)
345345
self.data_path = self.base_path + '/data'
346346

347-
def props_from_args(self, cl, args, itemid=None):
347+
def props_from_args(self, cl, args, itemid=None, skip_protected=True):
348348
"""Construct a list of properties from the given arguments,
349349
and return them after validation.
350350
@@ -354,18 +354,46 @@ def props_from_args(self, cl, args, itemid=None):
354354
itemid (string, optional): itemid of the object
355355
356356
Returns:
357-
dict: dictionary of validated properties
357+
dict: dictionary of validated properties excluding
358+
protected properties if strip_protected=True.
359+
360+
Raises: UsageError if property does not exist and is not
361+
prefixed with @ indicating it's a meta variable.
362+
358363
359364
"""
360-
class_props = cl.properties.keys()
365+
unprotected_class_props = cl.properties.keys()
366+
protected_class_props = [ p for p in
367+
list(cl.getprops(protected=True))
368+
if p not in unprotected_class_props ]
361369
props = {}
362370
# props = dict.fromkeys(class_props, None)
363371

364372
for arg in args:
365373
key = arg.name
366374
value = arg.value
367-
if key not in class_props:
375+
if key.startswith('@'):
376+
# meta setting, not db property setting/reference
368377
continue
378+
if key in protected_class_props:
379+
# Skip protected props as a convenience.
380+
# Allows user to get object with all props,
381+
# change one prop, submit entire object
382+
# without having to remove any protected props
383+
# FIXME: Enhancement: raise error if value of prop
384+
# doesn't match db entry. In this case assume user
385+
# is really trying to set value. Another possibility is
386+
# they have an old copy of the data and it has been
387+
# updated. In the update case, we want etag validation
388+
# to generate the exception to reduce confusion. I think
389+
# etag validation occurs before this function is called but
390+
# I am not positive.
391+
if skip_protected:
392+
continue
393+
elif key not in unprotected_class_props:
394+
# report bad props as this is an error.
395+
raise UsageError("Property %s not found in class %s"%(key,
396+
cl.classname))
369397
props[key] = self.prop_from_arg(cl, key, value, itemid)
370398

371399
return props
@@ -390,7 +418,7 @@ def prop_from_arg(self, cl, key, value, itemid=None):
390418
x = key.encode('ascii')
391419
except UnicodeEncodeError:
392420
raise UsageError(
393-
'argument %r is no valid ascii keyword' % key
421+
'argument %r is not a valid ascii keyword' % key
394422
)
395423
if value:
396424
try:
@@ -944,6 +972,10 @@ def put_element(self, class_name, item_id, input):
944972
self.db.commit()
945973
except (TypeError, IndexError, ValueError) as message:
946974
raise ValueError(message)
975+
except KeyError as message:
976+
# key error returned for changing protected keys
977+
# and changing invalid keys
978+
raise UsageError(message)
947979

948980
result = {
949981
'id': item_id,
@@ -995,6 +1027,10 @@ def put_attribute(self, class_name, item_id, attr_name, input):
9951027
self.db.commit()
9961028
except (TypeError, IndexError, ValueError) as message:
9971029
raise ValueError(message)
1030+
except KeyError as message:
1031+
# key error returned for changing protected keys
1032+
# and changing invalid keys
1033+
raise UsageError(message)
9981034

9991035
result = {
10001036
'id': item_id,
@@ -1112,6 +1148,16 @@ def delete_attribute(self, class_name, item_id, attr_name, input):
11121148
)
11131149

11141150
class_obj = self.db.getclass(class_name)
1151+
if attr_name not in class_obj.getprops(protected=False):
1152+
if attr_name in class_obj.getprops(protected=True):
1153+
raise UsageError("Attribute '%s' can not be updated " \
1154+
"for class %s."%(attr_name, class_name))
1155+
else:
1156+
raise UsageError("Attribute '%s' not valid for class %s."%(
1157+
attr_name, class_name))
1158+
if attr_name in class_obj.get_required_props():
1159+
raise UsageError("Attribute '%s' is required by class %s and can not be deleted."%(
1160+
attr_name, class_name))
11151161
props = {}
11161162
prop_obj = class_obj.get(item_id, attr_name)
11171163
if isinstance(prop_obj, list):
@@ -1125,6 +1171,10 @@ def delete_attribute(self, class_name, item_id, attr_name, input):
11251171
self.db.commit()
11261172
except (TypeError, IndexError, ValueError) as message:
11271173
raise ValueError(message)
1174+
except KeyError as message:
1175+
# key error returned for changing protected keys
1176+
# and changing invalid keys
1177+
raise UsageError(message)
11281178

11291179
result = {
11301180
'status': 'ok'
@@ -1196,8 +1246,10 @@ def patch_element(self, class_name, item_id, input):
11961246
}
11971247
else:
11981248
# else patch operation is processing data
1199-
props = self.props_from_args(class_obj, input.value, item_id)
1249+
props = self.props_from_args(class_obj, input.value, item_id,
1250+
skip_protected=False)
12001251

1252+
required_props = class_obj.get_required_props()
12011253
for prop in props:
12021254
if not self.db.security.hasPermission(
12031255
'Edit', self.db.getuid(), class_name, prop, item_id
@@ -1206,6 +1258,11 @@ def patch_element(self, class_name, item_id, input):
12061258
'Permission to edit %s of %s%s denied' %
12071259
(prop, class_name, item_id)
12081260
)
1261+
if op == 'remove' and prop in required_props:
1262+
raise UsageError(
1263+
"Attribute '%s' is required by class %s "
1264+
"and can not be removed."%(prop, class_name)
1265+
)
12091266

12101267
props[prop] = self.patch_data(
12111268
op, class_obj.get(item_id, prop), props[prop]
@@ -1267,6 +1324,10 @@ def patch_attribute(self, class_name, item_id, attr_name, input):
12671324

12681325
prop = attr_name
12691326
class_obj = self.db.getclass(class_name)
1327+
if attr_name not in class_obj.getprops(protected=False):
1328+
if attr_name in class_obj.getprops(protected=True):
1329+
raise UsageError("Attribute '%s' can not be updated " \
1330+
"for class %s."%(attr_name, class_name))
12701331

12711332
self.raise_if_no_etag(class_name, item_id, input)
12721333

@@ -1285,6 +1346,10 @@ def patch_attribute(self, class_name, item_id, attr_name, input):
12851346
self.db.commit()
12861347
except (TypeError, IndexError, ValueError) as message:
12871348
raise ValueError(message)
1349+
except KeyError as message:
1350+
# key error returned for changing protected keys
1351+
# and changing invalid keys
1352+
raise UsageError(message)
12881353

12891354
result = {
12901355
'id': item_id,
@@ -1608,6 +1673,9 @@ def dispatch(self, method, uri, input):
16081673
self.client.setHeader("Content-Type", "application/xml")
16091674
output = b2s(dicttoxml(output, root=False))
16101675
else:
1676+
# FIXME?? consider moving this earlier. We should
1677+
# error out before doing any work if we can't
1678+
# display acceptable output.
16111679
self.client.response_code = 406
16121680
output = "Content type is not accepted by client"
16131681

0 commit comments

Comments
 (0)