From c248594ee5255b232e42438783b9061d235153cb Mon Sep 17 00:00:00 2001
From: Mitali Patel <mpa@odoo.com>
Date: Wed, 12 Feb 2020 14:09:12 +0000
Subject: [PATCH] [IMP] base: allow restricting / removing exports right

Add a new group allowing administrators to remove the ability for
users to bulk-export data from the database. It's pretty minor as
technically the user can still access the underlying object through
the basic methods but it's still a bit of a roadblock.

Users can export by default.

Task 2170900

closes odoo/odoo#45400

Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
---
 addons/test_xlsx_export/tests/test_export.py  |  2 +-
 .../src/js/views/list/list_controller.js      | 14 ++++--
 addons/web/static/tests/views/list_tests.js   | 46 +++++++++++++++----
 odoo/addons/base/data/res_users_demo.xml      |  2 +-
 odoo/addons/base/security/base_groups.xml     |  7 ++-
 odoo/addons/base/security/ir.model.access.csv |  2 +-
 odoo/models.py                                |  2 +
 7 files changed, 58 insertions(+), 17 deletions(-)

diff --git a/addons/test_xlsx_export/tests/test_export.py b/addons/test_xlsx_export/tests/test_export.py
index 7a2e50a80e3c..9ba0960be5ab 100644
--- a/addons/test_xlsx_export/tests/test_export.py
+++ b/addons/test_xlsx_export/tests/test_export.py
@@ -22,7 +22,7 @@ class XlsxCreatorCase(common.HttpCase):
         super().setUp()
         self.model = self.env[self.model_name]
 
-        mail_new_test_user(self.env, login='fof', password='123456789')
+        u = mail_new_test_user(self.env, login='fof', password='123456789', groups='base.group_user,base.group_allow_export')
         self.authenticate('fof', '123456789')
 
         self.worksheet = {}  # mock worksheet
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 4a3972d50eb0..988291fdb1e2 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -58,6 +58,9 @@ var ListController = BasicController.extend({
         this.fieldChangedPrevented = false;
         this.isPageSelected = false; // true iff all records of the page are selected
         this.isDomainSelected = false; // true iff the user selected all records matching the domain
+        session.user_has_group('base.group_allow_export').then(has_group => {
+            this.isExportEnable = has_group;
+        });
         Object.defineProperty(this, 'mode', {
             get: () => this.renderer.isEditable() ? 'edit' : 'readonly',
             set: () => {},
@@ -372,10 +375,13 @@ var ListController = BasicController.extend({
             return null;
         }
         const props = this._super(...arguments);
-        const otherActionItems = [{
-            description: _t("Export"),
-            callback: () => this._onExportData(),
-        }];
+        const otherActionItems = [];
+        if (this.isExportEnable) {
+            otherActionItems.push({
+                description: _t("Export"),
+                callback: () => this._onExportData()
+            });
+        }
         if (this.archiveEnabled) {
             otherActionItems.push({
                 description: _t("Archive"),
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 94a7a3bb1569..b2340f370652 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -177,7 +177,7 @@ QUnit.module('Views', {
     });
 
     QUnit.test('list with delete="0"', async function (assert) {
-        assert.expect(4);
+        assert.expect(3);
 
         const list = await createView({
             View: ListView,
@@ -192,13 +192,7 @@ QUnit.module('Views', {
         assert.ok(list.$('tbody td.o_list_record_selector').length, 'should have at least one record');
 
         await testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input'));
-        assert.containsOnce(list.el, 'div.o_control_panel .o_cp_action_menus');
-        await cpHelpers.toggleActionMenu(list);
-        assert.deepEqual(
-            cpHelpers.getMenuItemTexts(list),
-            ['Export'],
-            'action menu should not have Delete button'
-        );
+        assert.containsNone(list.el, 'div.o_control_panel .o_cp_action_menus .o_dropdown_menu');
 
         list.destroy();
     });
@@ -222,6 +216,40 @@ QUnit.module('Views', {
         list.destroy();
     });
 
+    QUnit.test('list with export button', async function (assert) {
+        assert.expect(4);
+
+        const list = await createView({
+            View: ListView,
+            model: 'foo',
+            data: this.data,
+            viewOptions: {hasActionMenus: true},
+            arch: '<tree><field name="foo"/></tree>',
+            session: {
+                async user_has_group(group) {
+                    if (group === 'base.group_allow_export') {
+                        return true;
+                    }
+                    return this._super(...arguments);
+                },
+            },
+        });
+
+        assert.containsNone(list.el, 'div.o_control_panel .o_cp_action_menus');
+        assert.ok(list.$('tbody td.o_list_record_selector').length, 'should have at least one record');
+
+        await testUtils.dom.click(list.$('tbody td.o_list_record_selector:first input'));
+        assert.containsOnce(list.el, 'div.o_control_panel .o_cp_action_menus');
+        await cpHelpers.toggleActionMenu(list);
+        assert.deepEqual(
+            cpHelpers.getMenuItemTexts(list),
+            ['Export', 'Delete'],
+            'action menu should have Export button'
+        );
+
+        list.destroy();
+    });
+
     QUnit.test('simple editable rendering', async function (assert) {
         assert.expect(15);
 
@@ -4538,7 +4566,7 @@ QUnit.module('Views', {
         await testUtils.dom.click(list.$('.o_list_record_selector:first input'));
 
         await cpHelpers.toggleActionMenu(list);
-        assert.deepEqual(cpHelpers.getMenuItemTexts(list), ['Export', 'Delete', 'Action event']);
+        assert.deepEqual(cpHelpers.getMenuItemTexts(list), ['Delete', 'Action event']);
 
         list.destroy();
     });
diff --git a/odoo/addons/base/data/res_users_demo.xml b/odoo/addons/base/data/res_users_demo.xml
index 980c4d2b3bc0..4d3a65721deb 100644
--- a/odoo/addons/base/data/res_users_demo.xml
+++ b/odoo/addons/base/data/res_users_demo.xml
@@ -35,7 +35,7 @@
             <field name="password">demo</field>
             <field name="signature" type="xml"><span>-- <br/>+Mr Demo</span></field>
             <field name="company_id" ref="main_company"/>
-            <field name="groups_id" eval="[(6,0,[ref('base.group_user'), ref('base.group_partner_manager')])]"/>
+            <field name="groups_id" eval="[(6,0,[ref('base.group_user'), ref('base.group_partner_manager'), ref('base.group_allow_export')])]"/>
             <field name="image_1920" type="base64" file="base/static/img/user_demo-image.jpg"/>
         </record>
 
diff --git a/odoo/addons/base/security/base_groups.xml b/odoo/addons/base/security/base_groups.xml
index 3bdf8ee39282..5d184c0564dc 100644
--- a/odoo/addons/base/security/base_groups.xml
+++ b/odoo/addons/base/security/base_groups.xml
@@ -36,6 +36,11 @@
         <record model="res.groups" id="group_no_one">
             <field name="name">Technical Features</field>
         </record>
+        <record id="group_allow_export" model="res.groups">
+            <field name="name">Access to export feature</field>
+            <field name="category_id" ref="base.module_category_hidden"/>
+            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
+        </record>
         <record model="res.groups" id="group_user">
             <field name="implied_ids" eval="[(4, ref('group_no_one'))]"/>
             <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
@@ -47,7 +52,7 @@
         </record>
 
         <record id="default_user" model="res.users">
-            <field name="groups_id" eval="[(4,ref('base.group_partner_manager'))]"/>
+            <field name="groups_id" eval="[(4, ref('base.group_partner_manager')), (4, ref('base.group_allow_export'))]"/>
         </record>
 
         <!--
diff --git a/odoo/addons/base/security/ir.model.access.csv b/odoo/addons/base/security/ir.model.access.csv
index 861ebbf5aac8..a3405311a5fa 100644
--- a/odoo/addons/base/security/ir.model.access.csv
+++ b/odoo/addons/base/security/ir.model.access.csv
@@ -3,7 +3,7 @@
 "access_ir_attachment_group_user","ir_attachment group_user","model_ir_attachment","group_user",1,1,1,1
 "access_ir_attachment_group_portal_public","ir_attachment group_portal_public","model_ir_attachment",,0,0,0,0
 "access_ir_cron_group_cron","ir_cron group_cron","model_ir_cron","group_system",1,1,1,1
-"access_ir_exports_group_system","ir_exports group_system","model_ir_exports","base.group_user",1,1,1,1
+"access_ir_exports_group_system","ir_exports group_system","model_ir_exports","base.group_allow_export",1,1,1,1
 "access_ir_exports_line_group_system","ir_exports_line group_system","model_ir_exports_line","base.group_user",1,1,1,1
 "access_ir_model_group_erp_manager","ir_model group_erp_manager","model_ir_model","group_erp_manager",1,1,1,1
 "access_ir_model_constraint_group_erp_manager","ir_model_constraint group_erp_manager","model_ir_model_constraint","group_erp_manager",1,1,1,1
diff --git a/odoo/models.py b/odoo/models.py
index 52dadd4b041a..8a46be32f1f8 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -912,6 +912,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
 
             This method is used when exporting data via client menu
         """
+        if not (self.env.user._is_admin() or self.env.user.has_group('base.group_allow_export')):
+            raise UserError(_("You don't have the rights to export data. Please contact an Administrator."))
         fields_to_export = [fix_import_export_id_paths(f) for f in fields_to_export]
         return {'datas': self._export_rows(fields_to_export)}
 
-- 
GitLab