diff --git a/base_user_role_import/README.rst b/base_user_role_import/README.rst new file mode 100644 index 0000000..c35ac3e --- /dev/null +++ b/base_user_role_import/README.rst @@ -0,0 +1,116 @@ +===================== +Base User Role Import +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d96a7c73bcab66dc75f18a87fad00f29534febbf874f042d4fca3f49f647a884 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_import + :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_import + :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_import_manager`` and +``base_user_role_extended`` to enable role-based import control. + +1. It integrates the "Import Access" permission field on model access + rights (``ir.model.access``) with user roles. +2. It dynamically enforces the import permission restriction based on + the user's active Roles. +3. Supports role policy bypass for superusers. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure role-based import access: + +1. Go to **Settings > Users & Companies > Roles** and select the Role + you want to configure. +2. Identify the implied Groups that grant access to the relevant models. +3. Edit the Group's **Access Rights** tab. +4. Set the **Import Access** field for the model access rules under that + Group. + +All permitted import permissions from the groups implied by a user's +active Roles will be automatically aggregated and dynamically enforced. + +Usage +===== + +Once role-based import access is configured: + +1. Log in as a user who has active Roles where none of the implied + groups grant "Import Access" for a specific model. +2. Go to the list or kanban view for that model. +3. The "Import" option will not be displayed or accessible in the user + interface. + +Known issues / Roadmap +====================== + +- None. + +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 `__ + + - Prayag + +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_import/__init__.py b/base_user_role_import/__init__.py new file mode 100644 index 0000000..368196f --- /dev/null +++ b/base_user_role_import/__init__.py @@ -0,0 +1,4 @@ +# 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_import/__manifest__.py b/base_user_role_import/__manifest__.py new file mode 100644 index 0000000..1c92143 --- /dev/null +++ b/base_user_role_import/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Base User Role Import", + "summary": "Bridge base_import_manager with base_user_role_extended", + "category": "Tools", + "version": "18.0.1.0.0", + "depends": ["base_import_manager", "base_user_role_extended"], + "website": "https://github.com/OCA/server-backend", + "author": "CIT Services, Odoo Community Association (OCA)", + "data": [], + "installable": True, + "application": False, + "license": "AGPL-3", +} diff --git a/base_user_role_import/models/__init__.py b/base_user_role_import/models/__init__.py new file mode 100644 index 0000000..6206110 --- /dev/null +++ b/base_user_role_import/models/__init__.py @@ -0,0 +1,5 @@ +# 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_role diff --git a/base_user_role_import/models/ir_ui_view.py b/base_user_role_import/models/ir_ui_view.py new file mode 100644 index 0000000..d2c6b8c --- /dev/null +++ b/base_user_role_import/models/ir_ui_view.py @@ -0,0 +1,22 @@ +# 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 _get_import_group_ids(self): + """Get group IDs representing active user roles for import check.""" + user = self.env.user + if user.bypass_role_policy: + return super()._get_import_group_ids() + + active_roles = user.role_line_ids.filtered(lambda r: r.is_enabled).mapped( + "role_id" + ) + if active_roles: + return active_roles.mapped("group_id").ids + + return super()._get_import_group_ids() diff --git a/base_user_role_import/models/res_users_role.py b/base_user_role_import/models/res_users_role.py new file mode 100644 index 0000000..cc320d6 --- /dev/null +++ b/base_user_role_import/models/res_users_role.py @@ -0,0 +1,14 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ResUsersRole(models.Model): + _inherit = "res.users.role" + + def collect_all_perm_fields(self, perm_fields=None): + """Include `perm_import` in the synchronized permission fields.""" + perm_fields = perm_fields or {} + perm_fields.setdefault("perm_import", False) + return super().collect_all_perm_fields(perm_fields=perm_fields) diff --git a/base_user_role_import/pyproject.toml b/base_user_role_import/pyproject.toml new file mode 100644 index 0000000..f759fc0 --- /dev/null +++ b/base_user_role_import/pyproject.toml @@ -0,0 +1,4 @@ + +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_user_role_import/readme/CONFIGURE.md b/base_user_role_import/readme/CONFIGURE.md new file mode 100644 index 0000000..16d4e45 --- /dev/null +++ b/base_user_role_import/readme/CONFIGURE.md @@ -0,0 +1,8 @@ +To configure role-based import access: + +1. Go to **Settings > Users & Companies > Roles** and select the Role you want to configure. +2. Identify the implied Groups that grant access to the relevant models. +3. Edit the Group's **Access Rights** tab. +4. Set the **Import Access** field for the model access rules under that Group. + +All permitted import permissions from the groups implied by a user's active Roles will be automatically aggregated and dynamically enforced. diff --git a/base_user_role_import/readme/CONTRIBUTORS.md b/base_user_role_import/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..4b54f31 --- /dev/null +++ b/base_user_role_import/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [CIT-Services](cit-services.eu) + - Prayag \<\> diff --git a/base_user_role_import/readme/DESCRIPTION.md b/base_user_role_import/readme/DESCRIPTION.md new file mode 100644 index 0000000..888751a --- /dev/null +++ b/base_user_role_import/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module bridges `base_import_manager` and `base_user_role_extended` to enable role-based import control. + +1. It integrates the "Import Access" permission field on model access rights (`ir.model.access`) with user roles. +2. It dynamically enforces the import permission restriction based on the user's active Roles. +3. Supports role policy bypass for superusers. diff --git a/base_user_role_import/readme/ROADMAP.md b/base_user_role_import/readme/ROADMAP.md new file mode 100644 index 0000000..1ac5b36 --- /dev/null +++ b/base_user_role_import/readme/ROADMAP.md @@ -0,0 +1 @@ +- None. diff --git a/base_user_role_import/readme/USAGE.md b/base_user_role_import/readme/USAGE.md new file mode 100644 index 0000000..77d38dc --- /dev/null +++ b/base_user_role_import/readme/USAGE.md @@ -0,0 +1,5 @@ +Once role-based import access is configured: + +1. Log in as a user who has active Roles where none of the implied groups grant "Import Access" for a specific model. +2. Go to the list or kanban view for that model. +3. The "Import" option will not be displayed or accessible in the user interface. diff --git a/base_user_role_import/static/description/index.html b/base_user_role_import/static/description/index.html new file mode 100644 index 0000000..9d944a4 --- /dev/null +++ b/base_user_role_import/static/description/index.html @@ -0,0 +1,468 @@ + + + + + +Base User Role Import + + + +
+

Base User Role Import

+ + +

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

+

This module bridges base_import_manager and +base_user_role_extended to enable role-based import control.

+
    +
  1. It integrates the “Import Access” permission field on model access +rights (ir.model.access) with user roles.
  2. +
  3. It dynamically enforces the import permission restriction based on +the user’s active Roles.
  4. +
  5. Supports role policy bypass for superusers.
  6. +
+

Table of contents

+ +
+

Configuration

+

To configure role-based import access:

+
    +
  1. Go to Settings > Users & Companies > Roles and select the Role +you want to configure.
  2. +
  3. Identify the implied Groups that grant access to the relevant models.
  4. +
  5. Edit the Group’s Access Rights tab.
  6. +
  7. Set the Import Access field for the model access rules under that +Group.
  8. +
+

All permitted import permissions from the groups implied by a user’s +active Roles will be automatically aggregated and dynamically enforced.

+
+
+

Usage

+

Once role-based import access is configured:

+
    +
  1. Log in as a user who has active Roles where none of the implied +groups grant “Import Access” for a specific model.
  2. +
  3. Go to the list or kanban view for that model.
  4. +
  5. The “Import” option will not be displayed or accessible in the user +interface.
  6. +
+
+
+

Known issues / Roadmap

+
    +
  • None.
  • +
+
+
+

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_import/tests/__init__.py b/base_user_role_import/tests/__init__.py new file mode 100644 index 0000000..bbd5ebe --- /dev/null +++ b/base_user_role_import/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_base_user_role_import diff --git a/base_user_role_import/tests/test_base_user_role_import.py b/base_user_role_import/tests/test_base_user_role_import.py new file mode 100644 index 0000000..f60b6f2 --- /dev/null +++ b/base_user_role_import/tests/test_base_user_role_import.py @@ -0,0 +1,161 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from lxml import etree + +from odoo import Command +from odoo.tests.common import TransactionCase + + +class TestBaseUserRoleImport(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.test_group_1 = cls.env["res.groups"].create({"name": "Test Group 1"}) + cls.test_group_2 = cls.env["res.groups"].create({"name": "Test Group 2"}) + + user_vals = { + "name": "Test User", + "login": "test_user_import_role", + "groups_id": [ + Command.set([cls.test_group_1.id, cls.env.ref("base.group_user").id]) + ], + "bypass_role_policy": False, + } + cls.test_user = cls.env["res.users"].create(user_vals) + + cls.model_res_partner = cls.env["ir.model"]._get_id("res.partner") + + cls.env["ir.model.access"].search( + [("model_id", "=", cls.model_res_partner)] + ).write({"perm_import": False}) + + cls.role1 = cls.env["res.users.role"].create( + { + "name": "Role 1", + "group_id": cls.test_group_1.id, + } + ) + cls.role2 = cls.env["res.users.role"].create( + { + "name": "Role 2", + "group_id": cls.test_group_2.id, + } + ) + + cls.access_group_1 = cls.env["ir.model.access"].create( + { + "name": "Access Group 1", + "model_id": cls.model_res_partner, + "group_id": cls.test_group_1.id, + "perm_read": True, + "perm_import": False, + } + ) + + cls.access_group_2 = cls.env["ir.model.access"].create( + { + "name": "Access Group 2", + "model_id": cls.model_res_partner, + "group_id": cls.test_group_2.id, + "perm_read": True, + "perm_import": True, + } + ) + + def _get_tree(self, tag="list", target_model="res.partner"): + tree = etree.Element(tag) + if target_model: + tree.set("model_access_rights", target_model) + return tree + + def test_ir_ui_view_bypass_role_policy_no_import(self): + self.test_user.bypass_role_policy = True + tree = self._get_tree() + processed_tree = ( + self.env["ir.ui.view"] + .with_user(self.test_user) + ._postprocess_access_rights(tree) + ) + self.assertEqual(processed_tree.get("import"), "0") + + def test_ir_ui_view_bypass_role_policy_with_import(self): + self.test_user.bypass_role_policy = True + self.test_user.write({"groups_id": [Command.link(self.test_group_2.id)]}) + tree = self._get_tree() + processed_tree = ( + self.env["ir.ui.view"] + .with_user(self.test_user) + ._postprocess_access_rights(tree) + ) + self.assertNotIn("import", processed_tree.attrib) + + def test_ir_ui_view_with_active_roles_no_import(self): + self.test_user.write( + { + "role_line_ids": [ + Command.create( + { + "role_id": self.role1.id, + "is_enabled": True, + } + ) + ] + } + ) + + tree = self._get_tree() + processed_tree = ( + self.env["ir.ui.view"] + .with_user(self.test_user) + ._postprocess_access_rights(tree) + ) + self.assertEqual(processed_tree.get("import"), "0") + + def test_ir_ui_view_with_active_roles_with_import(self): + self.test_user.write( + { + "role_line_ids": [ + Command.create( + { + "role_id": self.role2.id, + "is_enabled": True, + } + ) + ] + } + ) + tree = self._get_tree() + processed_tree = ( + self.env["ir.ui.view"] + .with_user(self.test_user) + ._postprocess_access_rights(tree) + ) + self.assertNotIn("import", processed_tree.attrib) + + def test_res_users_role_update_role_model_access(self): + role = self.env["res.users.role"].create( + { + "name": "Role Test Update", + } + ) + fields = {"perm_read": True} + role._update_role_model_access(perm_fields=fields) + self.assertIn("perm_import", fields)