diff --git a/base_rule_override/README.rst b/base_rule_override/README.rst new file mode 100644 index 0000000000..4409530733 --- /dev/null +++ b/base_rule_override/README.rst @@ -0,0 +1,117 @@ +==================== +Record Rule Override +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f3ef453ceddc10e3dbbfc8c25430e92b9471f84a597ac1e63ef0988a9e2c69aa + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/14.0/base_rule_override + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-14-0/server-ux-14-0-base_rule_override + :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-ux&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a new boolean on Record rules that changes +the way they are evaluated. + +If a Record Rule is of type "override", then it must pass +in order for the record to be visible. + +In other words, "override" record rules are in AND with other +rules, not in OR as they normally are. + +Use with caution! + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +In debug mode, navigate to Settings > Technical > Record Rules. + +Create a new rule, or edit an existing one, and check the +"Override" checkbox. + +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 +~~~~~~~ + +* PyTech SRL +* Ooops404 + +Contributors +~~~~~~~~~~~~ + +* PyTech SRL : + + * Alessandro Uffreduzzi + +* Ooops404 : + + * Francesco Foresti + +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. + +.. |maintainer-ilyasProgrammer| image:: https://github.com/ilyasProgrammer.png?size=40px + :target: https://github.com/ilyasProgrammer + :alt: ilyasProgrammer +.. |maintainer-aleuffre| image:: https://github.com/aleuffre.png?size=40px + :target: https://github.com/aleuffre + :alt: aleuffre +.. |maintainer-renda-dev| image:: https://github.com/renda-dev.png?size=40px + :target: https://github.com/renda-dev + :alt: renda-dev +.. |maintainer-PicchiSeba| image:: https://github.com/PicchiSeba.png?size=40px + :target: https://github.com/PicchiSeba + :alt: PicchiSeba + +Current `maintainers `__: + +|maintainer-ilyasProgrammer| |maintainer-aleuffre| |maintainer-renda-dev| |maintainer-PicchiSeba| + +This module is part of the `OCA/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_rule_override/__init__.py b/base_rule_override/__init__.py new file mode 100644 index 0000000000..071962a357 --- /dev/null +++ b/base_rule_override/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import uninstall_hook diff --git a/base_rule_override/__manifest__.py b/base_rule_override/__manifest__.py new file mode 100644 index 0000000000..23fae8e8b9 --- /dev/null +++ b/base_rule_override/__manifest__.py @@ -0,0 +1,13 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Record Rule Override", + "version": "14.0.1.0.0", + "author": "PyTech SRL, Ooops404, Odoo Community Association (OCA)", + "maintainers": ["ilyasProgrammer", "aleuffre", "renda-dev", "PicchiSeba"], + "depends": ["base"], + "data": ["views/ir_rule_views.xml"], + "website": "https://github.com/OCA/server-ux", + "license": "AGPL-3", + "installable": True, + "uninstall_hook": "uninstall_hook", +} diff --git a/base_rule_override/hooks.py b/base_rule_override/hooks.py new file mode 100644 index 0000000000..c5deb316dc --- /dev/null +++ b/base_rule_override/hooks.py @@ -0,0 +1,9 @@ +# Copyright 2024 Ooops404 +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import SUPERUSER_ID, api + + +def uninstall_hook(cr, registry): + """Set all "is_override" rules to inactive before uninstalling.""" + env = api.Environment(cr, SUPERUSER_ID, {}) + env["ir.rule"].search([("is_override", "=", True)]).write({"active": False}) diff --git a/base_rule_override/models/__init__.py b/base_rule_override/models/__init__.py new file mode 100644 index 0000000000..fc1cd7bf66 --- /dev/null +++ b/base_rule_override/models/__init__.py @@ -0,0 +1 @@ +from . import ir_rule diff --git a/base_rule_override/models/ir_rule.py b/base_rule_override/models/ir_rule.py new file mode 100644 index 0000000000..5deff506f6 --- /dev/null +++ b/base_rule_override/models/ir_rule.py @@ -0,0 +1,81 @@ +# Copyright 2024 Ooops404 +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models, tools +from odoo.osv import expression +from odoo.tools import config +from odoo.tools.safe_eval import safe_eval + + +class IrRule(models.Model): + _inherit = "ir.rule" + + is_override = fields.Boolean( + string="Override", + help="Unlike other rules, override rules " + "must be passed in order to allow visibility to the record", + ) + + def _get_failing(self, for_records, mode="read"): + res = super()._get_failing(for_records, mode) + if res: + return res + Model = for_records.browse(()).sudo() + eval_context = self._eval_context() + override_rules = self._get_rules(Model._name, mode=mode).sudo() + + def is_failing(r, ids=for_records.ids): + dom = safe_eval(r.domain_force, eval_context) if r.domain_force else [] + return Model.search_count( + expression.AND([[("id", "in", ids)], expression.normalize_domain(dom)]) + ) < len(ids) + + return override_rules.filtered(lambda r: is_failing(r)).with_user(self.env.user) + + def _get_rules(self, model_name, mode="read"): + """ + Return non-override rules by default + """ + res = super()._get_rules(model_name, mode).sudo() + if self.env.context.get("get_rule_override"): + res = res.filtered(lambda x: x.is_override) + else: + res = res.filtered(lambda x: not x.is_override) + return res.with_user(self.env.user) + + @api.model + @tools.conditional( + "xml" not in config["dev_mode"], + tools.ormcache( + "self.env.uid", + "self.env.su", + "model_name", + "mode", + "tuple(self._compute_domain_context_values())", + ), + ) + def _compute_domain(self, model_name, mode="read"): + """ + Override domains are in AND with other domains + regardless of groups + """ + res = super()._compute_domain(model_name, mode) + override_rules = self.with_context(get_rule_override=True)._get_rules( + model_name, mode=mode + ) + if not override_rules: + return res + + # browse user and rules as SUPERUSER_ID to avoid access errors! + eval_context = self._eval_context() + override_domains = [] + for rule in override_rules.sudo(): + # evaluate the domain for the current user + dom = ( + safe_eval(rule.domain_force, eval_context) if rule.domain_force else [] + ) + dom = expression.normalize_domain(dom) + override_domains.append(dom) + if res is None: + return expression.AND(override_domains) + return expression.AND(override_domains + [res]) diff --git a/base_rule_override/readme/CONTRIBUTORS.rst b/base_rule_override/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..d31a7b6ce6 --- /dev/null +++ b/base_rule_override/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* PyTech SRL : + + * Alessandro Uffreduzzi + +* Ooops404 : + + * Francesco Foresti diff --git a/base_rule_override/readme/DESCRIPTION.rst b/base_rule_override/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..f1ef4cfffb --- /dev/null +++ b/base_rule_override/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +This module adds a new boolean on Record rules that changes +the way they are evaluated. + +If a Record Rule is of type "override", then it must pass +in order for the record to be visible. + +In other words, "override" record rules are in AND with other +rules, not in OR as they normally are. + +Use with caution! diff --git a/base_rule_override/readme/USAGE.rst b/base_rule_override/readme/USAGE.rst new file mode 100644 index 0000000000..c4882fd969 --- /dev/null +++ b/base_rule_override/readme/USAGE.rst @@ -0,0 +1,4 @@ +In debug mode, navigate to Settings > Technical > Record Rules. + +Create a new rule, or edit an existing one, and check the +"Override" checkbox. diff --git a/base_rule_override/static/description/index.html b/base_rule_override/static/description/index.html new file mode 100644 index 0000000000..70486497ce --- /dev/null +++ b/base_rule_override/static/description/index.html @@ -0,0 +1,450 @@ + + + + + + +Record Rule Override + + + +
+

Record Rule Override

+ + +

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

+

This module adds a new boolean on Record rules that changes +the way they are evaluated.

+

If a Record Rule is of type “override”, then it must pass +in order for the record to be visible.

+

In other words, “override” record rules are in AND with other +rules, not in OR as they normally are.

+

Use with caution!

+

Table of contents

+ +
+

Usage

+

In debug mode, navigate to Settings > Technical > Record Rules.

+

Create a new rule, or edit an existing one, and check the +“Override” checkbox.

+
+
+

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

+
    +
  • PyTech SRL
  • +
  • Ooops404
  • +
+
+
+

Contributors

+ +
+
+

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.

+

Current maintainers:

+

ilyasProgrammer aleuffre renda-dev PicchiSeba

+

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

+

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

+
+
+
+ + diff --git a/base_rule_override/tests/__init__.py b/base_rule_override/tests/__init__.py new file mode 100644 index 0000000000..9284e8a3c8 --- /dev/null +++ b/base_rule_override/tests/__init__.py @@ -0,0 +1 @@ +from . import test_base_rule_override diff --git a/base_rule_override/tests/test_base_rule_override.py b/base_rule_override/tests/test_base_rule_override.py new file mode 100644 index 0000000000..18db716fc5 --- /dev/null +++ b/base_rule_override/tests/test_base_rule_override.py @@ -0,0 +1,95 @@ +from odoo.exceptions import AccessError +from odoo.tests import SavepointCase +from odoo.tools import mute_logger + +from ..hooks import uninstall_hook + + +class TestBaseRuleOverride(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.rule_model = cls.env["ir.rule"] + cls.partner_model = cls.env["res.partner"] + cls.group_user = cls.env.ref("base.group_user") + cls.partner_azure = cls.env.ref("base.res_partner_12") + cls.private_address = cls.partner_model.create( + { + "name": "Private Test", + "type": "private", + "parent_id": cls.partner_azure.id, + } + ) + cls.rule_public_contacts = cls.env.ref("base.res_partner_rule_private_employee") + cls.rule_private_contacts = cls.env.ref("base.res_partner_rule_private_group") + cls.group_private_addresses = cls.env.ref("base.group_private_addresses") + cls.user_demo = cls.env.ref("base.user_demo") + + @mute_logger("odoo.addons.base.models.ir_rule") + def test_rule_standard_behaviour(self): + """ + Test that standard behaviour is not influenced + """ + self.assertNotIn(self.group_private_addresses, self.user_demo.groups_id) + self.partner_azure.with_user(self.user_demo).read() + with self.assertRaises(AccessError): + self.private_address.with_user(self.user_demo).read() + + self.user_demo.groups_id += self.group_private_addresses + self.partner_azure.with_user(self.user_demo).read() + self.private_address.with_user(self.user_demo).read() + + @mute_logger("odoo.addons.base.models.ir_rule") + def test_rule_override(self): + """ + Test that an "override" rule denies access + to a record even if other rules pass + """ + rule = self.rule_model.create( + { + "name": "Test rule", + "model_id": self.env.ref("base.model_res_partner").id, + "groups": [(4, self.group_user.id)], + "domain_force": """[("name", "ilike", "Azure")]""", + "is_override": True, + } + ) + self.assertNotIn(self.group_private_addresses, self.user_demo.groups_id) + self.partner_azure.with_user(self.user_demo).read() + with self.assertRaises(AccessError): + self.private_address.with_user(self.user_demo).read() + + self.user_demo.groups_id += self.group_private_addresses + self.partner_azure.with_user(self.user_demo).read() + with self.assertRaises(AccessError): + self.private_address.with_user(self.user_demo).read() + + self.partner_azure.name = "Some other name" + with self.assertRaises(AccessError): + self.partner_azure.with_user(self.user_demo).read() + + rule.active = False + + self.partner_azure.with_user(self.user_demo).read() + self.private_address.with_user(self.user_demo).read() + + @mute_logger("odoo.addons.base.models.ir_rule") + def test_uninstall_hook(self): + rule = self.rule_model.create( + { + "name": "Test rule", + "model_id": self.env.ref("base.model_res_partner").id, + "groups": [(4, self.group_user.id)], + "domain_force": """[("name", "ilike", "Azure")]""", + "is_override": True, + } + ) + rule_2 = rule.copy() + all_rules = self.rule_model.search([("is_override", "=", True)]) + self.assertEqual(all_rules, rule + rule_2) + + uninstall_hook(self.env.cr, False) + + self.assertFalse(self.rule_model.search([("is_override", "=", True)])) + self.assertFalse(rule.active) + self.assertFalse(rule_2.active) diff --git a/base_rule_override/views/ir_rule_views.xml b/base_rule_override/views/ir_rule_views.xml new file mode 100644 index 0000000000..ae45b96b9a --- /dev/null +++ b/base_rule_override/views/ir_rule_views.xml @@ -0,0 +1,40 @@ + + + + view.rule.form + ir.rule + + + + + + + + + + view.rule.tree + ir.rule + + + + + + + + + + view.rule.search + ir.rule + + + + + + + + + diff --git a/setup/base_rule_override/odoo/addons/base_rule_override b/setup/base_rule_override/odoo/addons/base_rule_override new file mode 120000 index 0000000000..3fb1bd8237 --- /dev/null +++ b/setup/base_rule_override/odoo/addons/base_rule_override @@ -0,0 +1 @@ +../../../../base_rule_override \ No newline at end of file diff --git a/setup/base_rule_override/setup.py b/setup/base_rule_override/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/base_rule_override/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)