Mercurial > p > roundup > code
diff roundup/rest.py @ 5705:457fc482e6b1
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.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 11 Apr 2019 20:54:39 -0400 |
| parents | 61874fd78ced |
| children | f9a762678af6 |
line wrap: on
line diff
--- a/roundup/rest.py Wed Apr 10 22:43:16 2019 -0400 +++ b/roundup/rest.py Thu Apr 11 20:54:39 2019 -0400 @@ -64,7 +64,7 @@ except Unauthorised as msg: code = 403 data = msg - except UsageError as msg: + except (UsageError, KeyError) as msg: code = 400 data = msg except (AttributeError, Reject) as msg: @@ -344,7 +344,7 @@ self.base_path = '%srest' % (self.db.config.TRACKER_WEB) self.data_path = self.base_path + '/data' - def props_from_args(self, cl, args, itemid=None): + def props_from_args(self, cl, args, itemid=None, skip_protected=True): """Construct a list of properties from the given arguments, and return them after validation. @@ -354,18 +354,46 @@ itemid (string, optional): itemid of the object Returns: - dict: dictionary of validated properties + dict: dictionary of validated properties excluding + protected properties if strip_protected=True. + + Raises: UsageError if property does not exist and is not + prefixed with @ indicating it's a meta variable. + """ - class_props = cl.properties.keys() + unprotected_class_props = cl.properties.keys() + protected_class_props = [ p for p in + list(cl.getprops(protected=True)) + if p not in unprotected_class_props ] props = {} # props = dict.fromkeys(class_props, None) for arg in args: key = arg.name value = arg.value - if key not in class_props: + if key.startswith('@'): + # meta setting, not db property setting/reference continue + if key in protected_class_props: + # Skip protected props as a convenience. + # Allows user to get object with all props, + # change one prop, submit entire object + # without having to remove any protected props + # FIXME: Enhancement: raise error if value of prop + # doesn't match db entry. In this case assume user + # is really trying to set value. Another possibility is + # they have an old copy of the data and it has been + # updated. In the update case, we want etag validation + # to generate the exception to reduce confusion. I think + # etag validation occurs before this function is called but + # I am not positive. + if skip_protected: + continue + elif key not in unprotected_class_props: + # report bad props as this is an error. + raise UsageError("Property %s not found in class %s"%(key, + cl.classname)) props[key] = self.prop_from_arg(cl, key, value, itemid) return props @@ -390,7 +418,7 @@ x = key.encode('ascii') except UnicodeEncodeError: raise UsageError( - 'argument %r is no valid ascii keyword' % key + 'argument %r is not a valid ascii keyword' % key ) if value: try: @@ -944,6 +972,10 @@ self.db.commit() except (TypeError, IndexError, ValueError) as message: raise ValueError(message) + except KeyError as message: + # key error returned for changing protected keys + # and changing invalid keys + raise UsageError(message) result = { 'id': item_id, @@ -995,6 +1027,10 @@ self.db.commit() except (TypeError, IndexError, ValueError) as message: raise ValueError(message) + except KeyError as message: + # key error returned for changing protected keys + # and changing invalid keys + raise UsageError(message) result = { 'id': item_id, @@ -1112,6 +1148,16 @@ ) class_obj = self.db.getclass(class_name) + if attr_name not in class_obj.getprops(protected=False): + if attr_name in class_obj.getprops(protected=True): + raise UsageError("Attribute '%s' can not be updated " \ + "for class %s."%(attr_name, class_name)) + else: + raise UsageError("Attribute '%s' not valid for class %s."%( + attr_name, class_name)) + if attr_name in class_obj.get_required_props(): + raise UsageError("Attribute '%s' is required by class %s and can not be deleted."%( + attr_name, class_name)) props = {} prop_obj = class_obj.get(item_id, attr_name) if isinstance(prop_obj, list): @@ -1125,6 +1171,10 @@ self.db.commit() except (TypeError, IndexError, ValueError) as message: raise ValueError(message) + except KeyError as message: + # key error returned for changing protected keys + # and changing invalid keys + raise UsageError(message) result = { 'status': 'ok' @@ -1196,8 +1246,10 @@ } else: # else patch operation is processing data - props = self.props_from_args(class_obj, input.value, item_id) + props = self.props_from_args(class_obj, input.value, item_id, + skip_protected=False) + required_props = class_obj.get_required_props() for prop in props: if not self.db.security.hasPermission( 'Edit', self.db.getuid(), class_name, prop, item_id @@ -1206,6 +1258,11 @@ 'Permission to edit %s of %s%s denied' % (prop, class_name, item_id) ) + if op == 'remove' and prop in required_props: + raise UsageError( + "Attribute '%s' is required by class %s " + "and can not be removed."%(prop, class_name) + ) props[prop] = self.patch_data( op, class_obj.get(item_id, prop), props[prop] @@ -1267,6 +1324,10 @@ prop = attr_name class_obj = self.db.getclass(class_name) + if attr_name not in class_obj.getprops(protected=False): + if attr_name in class_obj.getprops(protected=True): + raise UsageError("Attribute '%s' can not be updated " \ + "for class %s."%(attr_name, class_name)) self.raise_if_no_etag(class_name, item_id, input) @@ -1285,6 +1346,10 @@ self.db.commit() except (TypeError, IndexError, ValueError) as message: raise ValueError(message) + except KeyError as message: + # key error returned for changing protected keys + # and changing invalid keys + raise UsageError(message) result = { 'id': item_id, @@ -1608,6 +1673,9 @@ self.client.setHeader("Content-Type", "application/xml") output = b2s(dicttoxml(output, root=False)) else: + # FIXME?? consider moving this earlier. We should + # error out before doing any work if we can't + # display acceptable output. self.client.response_code = 406 output = "Content type is not accepted by client"
