From 352381a027d825fce29526c2ce3ecab016198a7d Mon Sep 17 00:00:00 2001 From: prayag Date: Wed, 2 Sep 2026 10:13:37 +0530 Subject: [PATCH] [ADD] base_user_role_export --- base_user_role_export/README.rst | 136 +++++ base_user_role_export/__init__.py | 3 + base_user_role_export/__manifest__.py | 15 + base_user_role_export/models/__init__.py | 4 + base_user_role_export/models/ir_ui_view.py | 38 ++ base_user_role_export/models/res_users.py | 46 ++ base_user_role_export/pyproject.toml | 3 + base_user_role_export/readme/CONTRIBUTORS.md | 2 + base_user_role_export/readme/DESCRIPTION.md | 18 + base_user_role_export/readme/HISTORY.md | 4 + base_user_role_export/readme/INSTALL.md | 4 + base_user_role_export/readme/USAGE.md | 16 + .../static/description/index.html | 488 ++++++++++++++++++ base_user_role_export/tests/__init__.py | 2 + .../tests/test_ir_ui_view.py | 110 ++++ base_user_role_export/tests/test_res_users.py | 106 ++++ 16 files changed, 995 insertions(+) create mode 100644 base_user_role_export/README.rst create mode 100644 base_user_role_export/__init__.py create mode 100644 base_user_role_export/__manifest__.py create mode 100644 base_user_role_export/models/__init__.py create mode 100644 base_user_role_export/models/ir_ui_view.py create mode 100644 base_user_role_export/models/res_users.py create mode 100644 base_user_role_export/pyproject.toml create mode 100644 base_user_role_export/readme/CONTRIBUTORS.md create mode 100644 base_user_role_export/readme/DESCRIPTION.md create mode 100644 base_user_role_export/readme/HISTORY.md create mode 100644 base_user_role_export/readme/INSTALL.md create mode 100644 base_user_role_export/readme/USAGE.md create mode 100644 base_user_role_export/static/description/index.html create mode 100644 base_user_role_export/tests/__init__.py create mode 100644 base_user_role_export/tests/test_ir_ui_view.py create mode 100644 base_user_role_export/tests/test_res_users.py diff --git a/base_user_role_export/README.rst b/base_user_role_export/README.rst new file mode 100644 index 0000000..4c3a6c3 --- /dev/null +++ b/base_user_role_export/README.rst @@ -0,0 +1,136 @@ +===================== +Base User Role Export +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2c495e8ebb65363964917c0340e1297f635bffb5bd59d65360c66f9f702ac249 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--backend-lightgray.png?logo=github + :target: https://github.com/OCA/server-backend/tree/18.0/base_user_role_export + :alt: OCA/server-backend +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-backend-18-0/server-backend-18-0-base_user_role_export + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-backend&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module bridges **Base User Role** (``base_user_role``) and **Base +Export Manager** (``base_export_manager``) to enforce role-based export +access control. + +When users are governed by roles, export permissions are determined +exclusively by the ``perm_export`` flag on ``ir.model.access`` records +that belong to the user's active role groups. Any model not covered by +such a record will be hidden from export, regardless of other group +memberships. + +Additionally, the export action (``export_xlsx`` button) in list and +kanban views is dynamically disabled for models where the active role +does not explicitly grant export access. + + **Important:** The Odoo group **"Access to export feature"** + (``base.group_allow_export``) **must be added to a role's implied + groups** for the export feature to be available to users assigned + that role. This group is pre-populated automatically when creating a + new role, but can be removed when export access is explicitly + unwanted. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +To install this module, you need to: + +1. Ensure the dependencies ``base_user_role_extended`` and + ``base_export_manager`` are available in your addons path. +2. Install this module ``base_user_role_export``. + +Usage +===== + +To use this module: + +1. Install this module (depends on ``base_user_role``, + ``base_user_role_extended``, and ``base_export_manager``). +2. Go to **Settings › Users & Companies › Roles**. +3. Create or open a role. +4. **Verify that the group "Access to export feature" is present in the + role's implied groups.** This group is added automatically when + creating a new role; remove it only when export should be explicitly + denied. +5. On the **Model Access** tab, set ``perm_export = True`` on each model + line that role should be allowed to export. +6. Assign the role to the relevant users and enable it. + +Users governed by an active role will only be able to export models for +which their role's model access record has ``perm_export`` checked +**and** the "Access to export feature" group is implied by their role. + +Changelog +========= + +18.0.1.0.0 +---------- + +- Initial release. Includes dynamic restriction of the ``export_xlsx`` + action in list and kanban views based on user role permissions. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* CIT Services + +Contributors +------------ + +- `CIT-Services `__ + + - Solomon Prabu s.prabu@cit-services.eu + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-backend `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_user_role_export/__init__.py b/base_user_role_export/__init__.py new file mode 100644 index 0000000..1c88ef8 --- /dev/null +++ b/base_user_role_export/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import models diff --git a/base_user_role_export/__manifest__.py b/base_user_role_export/__manifest__.py new file mode 100644 index 0000000..485e4a0 --- /dev/null +++ b/base_user_role_export/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Base User Role Export", + "version": "18.0.1.0.0", + "category": "Tools", + "summary": "Restrict export access via role-based groups using base_export_manager", + "author": "CIT Services, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-backend", + "license": "AGPL-3", + "depends": ["base_user_role_extended", "base_export_manager"], + "data": [], + "installable": True, +} diff --git a/base_user_role_export/models/__init__.py b/base_user_role_export/models/__init__.py new file mode 100644 index 0000000..d5bc914 --- /dev/null +++ b/base_user_role_export/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import ir_ui_view +from . import res_users diff --git a/base_user_role_export/models/ir_ui_view.py b/base_user_role_export/models/ir_ui_view.py new file mode 100644 index 0000000..9735f4a --- /dev/null +++ b/base_user_role_export/models/ir_ui_view.py @@ -0,0 +1,38 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class IrUiView(models.Model): + _inherit = "ir.ui.view" + + def _postprocess_access_rights(self, tree): + """Disable the 'export all' action based on the user's + effective model access rights.""" + target_model = tree.get("model_access_rights") + tree = super()._postprocess_access_rights(tree) + + if not target_model or tree.tag not in ("list", "kanban"): + return tree + + group_ids = tuple(self.env.user.with_context(role=True)._get_group_ids()) + has_export = bool( + self.env["ir.model.access"] + .sudo() + .search( + [ + ("model_id.model", "=", target_model), + ("perm_export", "=", True), + "|", + ("group_id", "=", False), + ("group_id", "in", group_ids), + ], + limit=1, + ) + ) + + if not has_export: + tree.set("export_xlsx", "0") + + return tree diff --git a/base_user_role_export/models/res_users.py b/base_user_role_export/models/res_users.py new file mode 100644 index 0000000..210b51c --- /dev/null +++ b/base_user_role_export/models/res_users.py @@ -0,0 +1,46 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + @api.model + def fetch_export_models(self): + """Return the list of models the current user is allowed to export. + + Enforces exclusive role-based access: only models whose + ``ir.model.access`` record has ``perm_export = True`` for one of the + current user's active role groups are returned. + + Falls back to the standard implementation when: + - the parent method does not exist (``base_export_manager`` not + installed), + - the user has the ``bypass_role_policy`` flag set, or + - the user has no enabled roles. + """ + if self.env.user.bypass_role_policy: + return super().fetch_export_models() + + user = self.env.user.sudo() + roles = user.role_line_ids.filtered(lambda line: line.is_enabled).mapped( + "role_id" + ) + + if not roles: + return super().fetch_export_models() + + role_group_ids = tuple(roles.mapped("group_id").ids) + accessobj = self.env["ir.model.access"].sudo() + + accessobj_ids = accessobj.search( + [ + ("perm_export", "=", True), + "|", + ("group_id", "=", False), + ("group_id", "in", role_group_ids), + ] + ) + return list(set(accessobj_ids.mapped("model_id.model"))) diff --git a/base_user_role_export/pyproject.toml b/base_user_role_export/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/base_user_role_export/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_user_role_export/readme/CONTRIBUTORS.md b/base_user_role_export/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..59b132f --- /dev/null +++ b/base_user_role_export/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [CIT-Services](cit-services.eu) + - Solomon Prabu diff --git a/base_user_role_export/readme/DESCRIPTION.md b/base_user_role_export/readme/DESCRIPTION.md new file mode 100644 index 0000000..295464d --- /dev/null +++ b/base_user_role_export/readme/DESCRIPTION.md @@ -0,0 +1,18 @@ +This module bridges **Base User Role** (`base_user_role`) and +**Base Export Manager** (`base_export_manager`) to enforce role-based export +access control. + +When users are governed by roles, export permissions are determined +exclusively by the ``perm_export`` flag on ``ir.model.access`` records that +belong to the user's active role groups. Any model not covered by such a +record will be hidden from export, regardless of other group memberships. + +Additionally, the export action (`export_xlsx` button) in list and kanban views +is dynamically disabled for models where the active role does not explicitly +grant export access. + +> **Important:** The Odoo group **"Access to export feature"** +> (``base.group_allow_export``) **must be added to a role's implied groups** +> for the export feature to be available to users assigned that role. +> This group is pre-populated automatically when creating a new role, but +> can be removed when export access is explicitly unwanted. diff --git a/base_user_role_export/readme/HISTORY.md b/base_user_role_export/readme/HISTORY.md new file mode 100644 index 0000000..f2c0ddc --- /dev/null +++ b/base_user_role_export/readme/HISTORY.md @@ -0,0 +1,4 @@ +18.0.1.0.0 +---------- + +* Initial release. Includes dynamic restriction of the `export_xlsx` action in list and kanban views based on user role permissions. diff --git a/base_user_role_export/readme/INSTALL.md b/base_user_role_export/readme/INSTALL.md new file mode 100644 index 0000000..d9eec1e --- /dev/null +++ b/base_user_role_export/readme/INSTALL.md @@ -0,0 +1,4 @@ +To install this module, you need to: + +1. Ensure the dependencies `base_user_role_extended` and `base_export_manager` are available in your addons path. +2. Install this module `base_user_role_export`. diff --git a/base_user_role_export/readme/USAGE.md b/base_user_role_export/readme/USAGE.md new file mode 100644 index 0000000..422cde0 --- /dev/null +++ b/base_user_role_export/readme/USAGE.md @@ -0,0 +1,16 @@ +To use this module: + +1. Install this module (depends on `base_user_role`, `base_user_role_extended`, + and `base_export_manager`). +2. Go to **Settings › Users & Companies › Roles**. +3. Create or open a role. +4. **Verify that the group "Access to export feature" is present in the + role's implied groups.** This group is added automatically when creating + a new role; remove it only when export should be explicitly denied. +5. On the **Model Access** tab, set `perm_export = True` on each model line + that role should be allowed to export. +6. Assign the role to the relevant users and enable it. + +Users governed by an active role will only be able to export models for which +their role's model access record has `perm_export` checked **and** the +"Access to export feature" group is implied by their role. diff --git a/base_user_role_export/static/description/index.html b/base_user_role_export/static/description/index.html new file mode 100644 index 0000000..8b2fc66 --- /dev/null +++ b/base_user_role_export/static/description/index.html @@ -0,0 +1,488 @@ + + + + + +Base User Role Export + + + +
+

Base User Role Export

+ + +

Beta License: AGPL-3 OCA/server-backend Translate me on Weblate Try me on Runboat

+

This module bridges Base User Role (base_user_role) and Base +Export Manager (base_export_manager) to enforce role-based export +access control.

+

When users are governed by roles, export permissions are determined +exclusively by the perm_export flag on ir.model.access records +that belong to the user’s active role groups. Any model not covered by +such a record will be hidden from export, regardless of other group +memberships.

+

Additionally, the export action (export_xlsx button) in list and +kanban views is dynamically disabled for models where the active role +does not explicitly grant export access.

+
+Important: The Odoo group “Access to export feature” +(base.group_allow_export) must be added to a role’s implied +groups for the export feature to be available to users assigned +that role. This group is pre-populated automatically when creating a +new role, but can be removed when export access is explicitly +unwanted.
+

Table of contents

+ +
+

Installation

+

To install this module, you need to:

+
    +
  1. Ensure the dependencies base_user_role_extended and +base_export_manager are available in your addons path.
  2. +
  3. Install this module base_user_role_export.
  4. +
+
+
+

Usage

+

To use this module:

+
    +
  1. Install this module (depends on base_user_role, +base_user_role_extended, and base_export_manager).
  2. +
  3. Go to Settings › Users & Companies › Roles.
  4. +
  5. Create or open a role.
  6. +
  7. Verify that the group “Access to export feature” is present in the +role’s implied groups. This group is added automatically when +creating a new role; remove it only when export should be explicitly +denied.
  8. +
  9. On the Model Access tab, set perm_export = True on each model +line that role should be allowed to export.
  10. +
  11. Assign the role to the relevant users and enable it.
  12. +
+

Users governed by an active role will only be able to export models for +which their role’s model access record has perm_export checked +and the “Access to export feature” group is implied by their role.

+
+
+

Changelog

+
+

18.0.1.0.0

+
    +
  • Initial release. Includes dynamic restriction of the export_xlsx +action in list and kanban views based on user role permissions.
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • CIT Services
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/server-backend project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_user_role_export/tests/__init__.py b/base_user_role_export/tests/__init__.py new file mode 100644 index 0000000..2b2f516 --- /dev/null +++ b/base_user_role_export/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_ir_ui_view +from . import test_res_users diff --git a/base_user_role_export/tests/test_ir_ui_view.py b/base_user_role_export/tests/test_ir_ui_view.py new file mode 100644 index 0000000..5e8666d --- /dev/null +++ b/base_user_role_export/tests/test_ir_ui_view.py @@ -0,0 +1,110 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from lxml import etree + +from odoo.tests import new_test_user +from odoo.tests.common import TransactionCase + + +class TestIrUiView(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + polluted_columns = [ + ("res_partner", "autopost_bills"), + ("res_users", "notification_type"), + ] + for table, column in polluted_columns: + cls.env.cr.execute( + f""" + SELECT column_name + FROM information_schema.columns + WHERE table_name='{table}' AND column_name='{column}' + """ + ) + if cls.env.cr.fetchone(): + cls.env.cr.execute( + f"ALTER TABLE {table} ALTER COLUMN {column} DROP NOT NULL" + ) + + cls.user_admin = cls.env.ref("base.user_admin") + cls.user_root = cls.env.ref("base.user_root") + cls.test_user = new_test_user( + cls.env, + login="test_role_user", + name="Test Role User", + groups="base.group_user,base.group_erp_manager", + ) + cls.model_res_partner = cls.env.ref("base.model_res_partner") + cls.model_res_users = cls.env.ref("base.model_res_users") + + cls.role_partner_manager = cls.env["res.users.role"].create( + {"name": "Partner Manager Role"} + ) + cls.env["res.users.role.line"].create( + { + "user_id": cls.test_user.id, + "role_id": cls.role_partner_manager.id, + } + ) + + cls.env["ir.model.access"].create( + { + "name": "partner manager export access", + "model_id": cls.model_res_partner.id, + "group_id": cls.role_partner_manager.group_id.id, + "perm_read": True, + "perm_write": True, + "perm_create": False, + "perm_unlink": False, + "perm_export": True, + } + ) + cls.env["ir.model.access"].create( + { + "name": "partner manager no export access", + "model_id": cls.model_res_users.id, + "group_id": cls.role_partner_manager.group_id.id, + "perm_read": True, + "perm_write": False, + "perm_create": False, + "perm_unlink": False, + "perm_export": False, + } + ) + + def test_postprocess_access_rights_allowed(self): + tree = etree.fromstring( + '' + ) + processed_tree = ( + self.env["ir.ui.view"] + .with_user(self.test_user) + ._postprocess_access_rights(tree) + ) + self.assertNotEqual(processed_tree.get("export_xlsx"), "0") + + def test_postprocess_access_rights_restricted(self): + tree = etree.fromstring( + '' + ) + processed_tree = ( + self.env["ir.ui.view"] + .with_user(self.test_user) + ._postprocess_access_rights(tree) + ) + self.assertEqual(processed_tree.get("export_xlsx"), "0") + + def test_postprocess_access_rights_non_list_kanban(self): + tree = etree.fromstring( + '
' + ) + processed_tree = ( + self.env["ir.ui.view"] + .with_user(self.test_user) + ._postprocess_access_rights(tree) + ) + self.assertIsNone(processed_tree.get("export_xlsx")) diff --git a/base_user_role_export/tests/test_res_users.py b/base_user_role_export/tests/test_res_users.py new file mode 100644 index 0000000..abcd26d --- /dev/null +++ b/base_user_role_export/tests/test_res_users.py @@ -0,0 +1,106 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import new_test_user +from odoo.tests.common import TransactionCase + + +class TestResUsers(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + polluted_columns = [ + ("res_partner", "autopost_bills"), + ("res_users", "notification_type"), + ] + for table, column in polluted_columns: + cls.env.cr.execute( + f""" + SELECT column_name + FROM information_schema.columns + WHERE table_name='{table}' AND column_name='{column}' + """ + ) + if cls.env.cr.fetchone(): + cls.env.cr.execute( + f"ALTER TABLE {table} ALTER COLUMN {column} DROP NOT NULL" + ) + + cls.model_res_partner = cls.env.ref("base.model_res_partner") + cls.model_res_users = cls.env.ref("base.model_res_users") + + cls.user_role = new_test_user( + cls.env, + login="export_role_user", + name="Export Role User", + groups="base.group_user,base.group_erp_manager", + ) + cls.user_bypass = new_test_user( + cls.env, + login="bypass_role_user", + name="Bypass Role User", + groups="base.group_user,base.group_erp_manager", + bypass_role_policy=True, + ) + cls.user_no_role = new_test_user( + cls.env, + login="no_role_user", + name="No Role User", + groups="base.group_user,base.group_erp_manager", + ) + + cls.role = cls.env["res.users.role"].create( + {"name": "Export Test Role"} + ) + cls.env["res.users.role.line"].create( + { + "user_id": cls.user_role.id, + "role_id": cls.role.id, + } + ) + + cls.env["ir.model.access"].create( + { + "name": "res.partner export access", + "model_id": cls.model_res_partner.id, + "group_id": cls.role.group_id.id, + "perm_read": True, + "perm_export": True, + } + ) + cls.env["ir.model.access"].create( + { + "name": "res.users no export access", + "model_id": cls.model_res_users.id, + "group_id": cls.role.group_id.id, + "perm_read": True, + "perm_export": False, + } + ) + + def test_fetch_export_models_role_user(self): + models = ( + self.env["res.users"] + .with_user(self.user_role) + .fetch_export_models() + ) + self.assertIn("res.partner", models) + self.assertNotIn("res.users", models) + + def test_fetch_export_models_bypass_user(self): + models = ( + self.env["res.users"] + .with_user(self.user_bypass) + .fetch_export_models() + ) + self.assertIsInstance(models, list) + + def test_fetch_export_models_no_role_user(self): + models = ( + self.env["res.users"] + .with_user(self.user_no_role) + .fetch_export_models() + ) + self.assertIsInstance(models, list)