Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1123 sub document validate filters #1125

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Patches and Contributions
- Kurt Bonne
- Kurt Doherty
- Luca Di Gaspero
- Luca Moretto
- Luis Fernando Gomes
- Magdas Adrian
- Mandar Vaze
Expand Down
16 changes: 16 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ uppercase.
``/v1/<endpoint>``). Defaults to ``''``.

``ALLOWED_FILTERS`` List of fields on which filtering is allowed.
Entries in this list work in a hierarchical
way. This means that, for instance, filtering
on ``'dict.sub_dict.foo'`` is allowed if
``ALLOWED_FILTERS`` contains any of
``'dict.sub_dict.foo``, ``'dict.sub_dict'``
or ``'dict'``. Instead filtering on
``'dict'`` is allowed if ``ALLOWED_FILTERS``
contains ``'dict'``.
Can be set to ``[]`` (no filters allowed)
or ``['*']`` (filters allowed on every
field). Unless your API is comprised of
Expand Down Expand Up @@ -798,6 +806,14 @@ always lowercase.
:ref:`subresources`.

``allowed_filters`` List of fields on which filtering is allowed.
Entries in this list work in a hierarchical
way. This means that, for instance, filtering
on ``'dict.sub_dict.foo'`` is allowed if
``allowed_filters`` contains any of
``'dict.sub_dict.foo``, ``'dict.sub_dict'``
or ``'dict'``. Instead filtering on
``'dict'`` is allowed if ``allowed_filters``
contains ``'dict'``.
Can be set to ``[]`` (no filters allowed), or
``['*']`` (fields allowed on every field).
Defaults to ``['*']``.
Expand Down
55 changes: 55 additions & 0 deletions eve/tests/methods/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,31 @@ def test_get_where_allowed_filters(self):
'?where=%s' % where))
self.assert200(r.status_code)

# `allowed_filters` contains "rows" --> filter key "rows.price"
# must be allowed
self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \
['rows']
where = '{"rows.price": 10}'
r = self.test_client.get('%s%s' % (self.known_resource_url,
'?where=%s' % where))
self.assert200(r.status_code)

# `allowed_filters` contains "rows.price" --> filter key "rows.price"
# must be allowed
self.app.config['DOMAIN'][self.known_resource]['allowed_filters'] = \
['rows.price']
r = self.test_client.get('%s%s' % (self.known_resource_url,
'?where=%s' % where))
self.assert200(r.status_code)

# `allowed_filters` contains "rows.price" --> filter key "rows"
# must NOT be allowed
where = '{"rows": {"sku": "value", "price": 10}}'
r = self.test_client.get('%s%s' % (self.known_resource_url,
'?where=%s' % where))
self.assert400(r.status_code)
self.assertTrue(b"'rows' not allowed" in r.get_data())

def test_get_with_post_override(self):
# POST request with GET override turns into a GET
headers = [('X-HTTP-Method-Override', 'GET')]
Expand Down Expand Up @@ -1099,6 +1124,36 @@ def test_get_invalid_where_fields(self):
response, status = self.get(self.known_resource, where)
self.assert200(status)

# test for nested resource field validating correctly
# (location is dict)
where = '?where={"location.address": "str 1"}'
response, status = self.get(self.known_resource, where)
self.assert200(status)

# test for nested resource field validating correctly
# (rows is list of dicts)
where = '?where={"rows.price": 10}'
response, status = self.get(self.known_resource, where)
self.assert200(status)

# test for nested resource field validating correctly
# (dict_list_fixed_len is a fixed-size list of dicts)
where = '?where={"dict_list_fixed_len.key2": 1}'
response, status = self.get(self.known_resource, where)
self.assert200(status)

# test for nested resource field not validating correctly
# (bad_base_key doesn't exist in the base resource schema)
where = '?where={"bad_base_key.sub": 1}'
response, status = self.get(self.known_resource, where)
self.assert400(status)

# test for nested resource field not validating correctly
# (bad_sub_key doesn't exist in the dict_list_fixed_len schema)
where = '?where={"dict_list_fixed_len.bad_sub_key": 1}'
response, status = self.get(self.known_resource, where)
self.assert400(status)

def test_get_lookup_field_as_string(self):
# Test that a resource where 'item_lookup_field' is set to a field
# of string type and which value is castable to a ObjectId is still
Expand Down
13 changes: 13 additions & 0 deletions eve/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@
'type': 'list',
'items': [{'type': 'objectid'}]
},
'dict_list_fixed_len': {
'type': 'list',
'items': [
{
'type': 'dict',
'schema': {'key1': {'type': 'string'}}
},
{
'type': 'dict',
'schema': {'key2': {'type': 'integer'}}
}
]
},
'dependency_field1': {
'type': 'string',
'default': 'default'
Expand Down
71 changes: 61 additions & 10 deletions eve/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,17 @@ def validate_filters(where, resource):

def validate_filter(filter):
for key, value in filter.items():
if '*' not in allowed and key not in allowed:
return "filter on '%s' not allowed" % key
if '*' not in allowed:
def recursive_check_allowed(filter_key, allowed_filters):
if filter_key not in allowed_filters:
base_composed_key, _, _ = filter_key.rpartition('.')
return base_composed_key and recursive_check_allowed(
base_composed_key, allowed_filters)

return True

if not recursive_check_allowed(key, allowed):
return "filter on '%s' not allowed" % key

if key in ('$or', '$and', '$nor'):
if not isinstance(value, list):
Expand All @@ -401,16 +410,58 @@ def validate_filter(filter):
return r
else:
if config.VALIDATE_FILTERS:
def get_sub_schemas(base_schema):
def dict_sub_schema(base):
if base.get('type') == 'dict':
return base.get('schema')

return None

if base_schema.get('type') == 'list':
if 'schema' in base_schema:
# Try to get dict sub-schema for arbitrary
# sized list
sub = dict_sub_schema(base_schema['schema'])
return [sub] if sub is not None else []
elif 'items' in base_schema:
# Try to get dict sub-schema(s) for
# fixed-size list
items = base_schema['items']
sub_schemas = []
for item in items:
sub = dict_sub_schema(item)
if sub is not None:
sub_schemas.append(sub)

return sub_schemas
else:
sub = dict_sub_schema(base_schema)
return [sub] if sub is not None else []

def recursive_validate_filter(key, value, schema):
if key not in schema:
base_key, _, sub_keys = key.partition('.')
if sub_keys and base_key in schema:
# key is the composition of base field and
# sub-fields
sub_schemas = get_sub_schemas(schema[base_key])
for sub_schema in sub_schemas:
if recursive_validate_filter(sub_keys,
value,
sub_schema):
return True

return False
else:
field_schema = schema.get(key)
v = Validator({key: field_schema})
return v.validate({key: value})

res_schema = config.DOMAIN[resource]['schema']
if key not in res_schema:
if not recursive_validate_filter(key, value, res_schema):
return "filter on '%s' is invalid"
else:
field_schema = res_schema.get(key)
v = Validator({key: field_schema})
if not v.validate({key: value}):
return "filter on '%s' is invalid"
else:
return None

return None

if '*' in allowed and not config.VALIDATE_FILTERS:
return None
Expand Down