From 9b41f14108b3a976d74c54c6a8ef2f68d5471642 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 11 Jan 2022 14:30:47 +0100
Subject: [PATCH 1/4] api: raise bad request on unknown filter (#59224)

---
 tests/api/test_formdata.py   | 35 +++++++++++++++++++++++++++++++++++
 wcs/backoffice/management.py | 28 ++++++++++++++++++++++++++++
 2 files changed, 63 insertions(+)

diff --git a/tests/api/test_formdata.py b/tests/api/test_formdata.py
index 77338372..a2d77ad2 100644
--- a/tests/api/test_formdata.py
+++ b/tests/api/test_formdata.py
@@ -766,6 +766,41 @@ def test_api_list_formdata(pub, local_user):
     get_app(pub).get(sign_uri('/api/forms/test/list?filter=all&limit=plop', user=local_user), status=400)
 
 
+def test_api_list_formdata_unknown_filter(pub, local_user):
+    pub.role_class.wipe()
+    role = pub.role_class(name='test')
+    role.store()
+
+    local_user.roles = [role.id]
+    local_user.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_roles = {'_receiver': role.id}
+    formdef.fields = []
+    formdef.store()
+
+    data_class = formdef.data_class()
+    data_class.wipe()
+    for i in range(10):
+        formdata = data_class()
+        formdata.just_created()
+        formdata.jump_status('new')
+        formdata.store()
+
+    resp = get_app(pub).get(sign_uri('/api/forms/test/list', user=local_user))
+    assert len(resp.json) == 10
+
+    resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-foobar=42', user=local_user), status=400)
+    assert resp.json['err_desc'] == 'Invalid filter "foobar"'
+
+    resp = get_app(pub).get(
+        sign_uri('/api/forms/test/list?filter-foobar=42&filter-baz=35', user=local_user), status=400
+    )
+    assert resp.json['err_desc'] == 'Invalid filters "baz", "foobar"'
+
+
 def test_api_list_formdata_date_filter(pub, local_user):
     if not pub.is_using_postgresql():
         pytest.skip('this requires SQL')
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 1c673608..7037120d 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1663,6 +1663,19 @@ class FormPage(Directory):
         else:
             request_form = {}
 
+        fake_fields_ids = [f.id for f in fake_fields]
+        filters_in_request = {
+            k.replace('filter-', '')
+            for k in filters_dict
+            if k.startswith('filter-') and not k.endswith('-value')
+        }
+        filters_in_request = {
+            f
+            for f in filters_in_request
+            if f not in fake_fields_ids + ['status', 'user-uuid', 'submission-agent-uuid']
+        }
+        known_filters = set()
+
         for filter_field in fake_fields + list(self.get_formdef_fields()):
             if filter_field.type not in self.get_filterable_field_types():
                 continue
@@ -1728,6 +1741,9 @@ class FormPage(Directory):
                 # if there's a filter-%(id)s, it is used to enable the actual
                 # filter, and the value will be found in filter-%s-value.
                 filter_field_key = 'filter-%s-value' % filter_field.contextual_id
+                known_filters.add(filter_field.contextual_id)
+            else:
+                known_filters.add(filter_field.contextual_varname)
 
             if not filter_field_key:
                 # if there's not known filter key, skip.
@@ -1808,6 +1824,18 @@ class FormPage(Directory):
             elif filter_field.type == 'date':
                 criterias.append(Equal('f%s' % filter_field.id, filter_field_value))
 
+        unknown_filters = sorted(filters_in_request - known_filters)
+        if unknown_filters:
+            error_message = (
+                ngettext(
+                    'Invalid filter "%(filters)s"',
+                    'Invalid filters "%(filters)s"',
+                    len(unknown_filters),
+                )
+                % {'filters': '", "'.join(f for f in unknown_filters)}
+            )
+            raise RequestError(error_message)
+
         return criterias
 
     def listing_top_actions(self):
-- 
2.34.1

