From 9cb0c382ffc5e8094500ff7d1975517a372af6dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 14 May 2021 15:11:43 +0200
Subject: [PATCH 3/3] backoffice: check category permissions for
 export/statistics views (#53667)

---
 tests/backoffice_pages/test_all.py | 61 ++++++++++++++++++++++++++++++
 wcs/backoffice/management.py       | 30 +++++++++++----
 wcs/categories.py                  |  9 +++++
 3 files changed, 92 insertions(+), 8 deletions(-)

diff --git a/tests/backoffice_pages/test_all.py b/tests/backoffice_pages/test_all.py
index 96ed36d0c..f652e9e20 100644
--- a/tests/backoffice_pages/test_all.py
+++ b/tests/backoffice_pages/test_all.py
@@ -1500,6 +1500,67 @@ def test_backoffice_statistics(pub):
     assert 'End: 2013-01-01' in resp.text
 
 
+def test_backoffice_form_category_permissions(pub):
+    user = create_user(pub)
+    create_environment(pub)
+
+    formdef = FormDef.get_by_urlname('form-title')
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'Export a Spreadsheet' in resp.text
+    assert 'Statistics' in resp.text
+
+    cat1 = Category(name='cat1')
+    cat1.store()
+    formdef.category_id = cat1.id
+    formdef.store()
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'Export a Spreadsheet' in resp.text
+    assert 'Statistics' in resp.text
+
+    role = pub.role_class(name='limited perms')
+    role.store()
+    cat1.export_roles = [role]
+    cat1.store()
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'Export a Spreadsheet' not in resp.text
+    assert 'Statistics' in resp.text
+
+    cat1.statistics_roles = [role]
+    cat1.store()
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'Export a Spreadsheet' not in resp.text
+    assert 'Statistics' not in resp.text
+    app.get('/backoffice/management/form-title/stats', status=403)
+    app.get('/backoffice/management/form-title/export-spreadsheet', status=403)
+    app.get('/backoffice/management/form-title/csv', status=403)
+    app.get('/backoffice/management/form-title/ods', status=403)
+
+    # check it's ok for admins
+    user.is_admin = True
+    user.store()
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'Export a Spreadsheet' in resp.text
+    assert 'Statistics' in resp.text
+    app.get('/backoffice/management/form-title/stats', status=200)
+    app.get('/backoffice/management/form-title/export-spreadsheet', status=200)
+    app.get('/backoffice/management/form-title/csv', status=200)
+    app.get('/backoffice/management/form-title/ods', status=200)
+
+    # check it's ok for agents with roles
+    user.is_admin = False
+    user.roles.append(role.id)
+    user.store()
+    resp = app.get('/backoffice/management/form-title/')
+    assert 'Export a Spreadsheet' in resp.text
+    assert 'Statistics' in resp.text
+    app.get('/backoffice/management/form-title/stats', status=200)
+    app.get('/backoffice/management/form-title/export-spreadsheet', status=200)
+    app.get('/backoffice/management/form-title/csv', status=200)
+    app.get('/backoffice/management/form-title/ods', status=200)
+
+
 def test_backoffice_multi_actions(pub):
     create_superuser(pub)
     create_environment(pub)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index f5823b370..c5828fe8c 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1176,16 +1176,20 @@ class FormPage(Directory):
 
     def get_formdata_sidebar_actions(self, qs=''):
         r = TemplateIO(html=True)
-        r += htmltext(
-            ' <li><a rel="popup" data-base-href="export-spreadsheet" data-autoclose-dialog="true" '
-            'href="export-spreadsheet%s">%s</a></li>'
-        ) % (
-            qs,
-            _('Export a Spreadsheet'),
-        )
+        if not self.formdef.category or self.formdef.category.has_permission('export', get_request().user):
+            r += htmltext(
+                ' <li><a rel="popup" data-base-href="export-spreadsheet" data-autoclose-dialog="true" '
+                'href="export-spreadsheet%s">%s</a></li>'
+            ) % (
+                qs,
+                _('Export a Spreadsheet'),
+            )
         if self.formdef.geolocations:
             r += htmltext(' <li><a data-base-href="map" href="map%s">%s</a></li>') % (qs, _('Plot on a Map'))
-        if 'stats' in self._q_exports:
+        if 'stats' in self._q_exports and (
+            not self.formdef.category
+            or self.formdef.category.has_permission('statistics', get_request().user)
+        ):
             r += htmltext(' <li class="stats"><a href="stats">%s</a></li>') % _('Statistics')
         return r.getvalue()
 
@@ -2133,6 +2137,8 @@ class FormPage(Directory):
 
     def export_spreadsheet(self):
         self.check_access()
+        if self.formdef.category and not self.formdef.category.has_permission('export', get_request().user):
+            raise errors.AccessForbiddenError()
         form = Form()
         form.add_hidden('query_string', get_request().get_query())
         form.add(
@@ -2168,6 +2174,8 @@ class FormPage(Directory):
 
     def csv(self):
         self.check_access()
+        if self.formdef.category and not self.formdef.category.has_permission('export', get_request().user):
+            raise errors.AccessForbiddenError()
         fields = self.get_fields_from_query()
         selected_filter = self.get_filter_from_query()
         user = get_request().user
@@ -2220,6 +2228,8 @@ class FormPage(Directory):
         if get_request().has_anonymised_data_api_restriction():
             # api/ will let this pass but we don't want that.
             raise errors.AccessForbiddenError()
+        if self.formdef.category and not self.formdef.category.has_permission('export', get_request().user):
+            raise errors.AccessForbiddenError()
         fields = self.get_fields_from_query()
         selected_filter = self.get_filter_from_query()
         user = get_user_from_api_query_string() or get_request().user
@@ -2469,6 +2479,10 @@ class FormPage(Directory):
 
     def stats(self):
         self.check_access()
+        if self.formdef.category and not self.formdef.category.has_permission(
+            'statistics', get_request().user
+        ):
+            raise errors.AccessForbiddenError()
         get_logger().info('backoffice - form %s - stats' % self.formdef.name)
         html_top('management', '%s - %s' % (_('Form'), self.formdef.name))
         r = TemplateIO(html=True)
diff --git a/wcs/categories.py b/wcs/categories.py
index c9bc3001b..34a14c0d8 100644
--- a/wcs/categories.py
+++ b/wcs/categories.py
@@ -124,6 +124,15 @@ class Category(XmlStorableObject):
             text = '<p>%s</p>' % text
         return htmltext(text)
 
+    def has_permission(self, permission_name, user):
+        if user.is_admin:
+            return True
+        permission_roles = getattr(self, '%s_roles' % permission_name, None) or []
+        if not permission_roles:
+            return True
+        user_roles = set(user.get_roles()) if user else set()
+        return bool(user_roles.intersection([x.id for x in permission_roles]))
+
 
 class CardDefCategory(Category):
     _names = 'carddef_categories'
-- 
2.31.1

