diff --git a/base_extended_security/__manifest__.py b/base_extended_security/__manifest__.py index cc54c107..46aababe 100644 --- a/base_extended_security/__manifest__.py +++ b/base_extended_security/__manifest__.py @@ -3,13 +3,13 @@ { "name": "Base Extended Security", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "author": "Numigi", "maintainer": "Numigi", "license": "LGPL-3", "category": "Other", "summary": "Securize access to records", - "depends": ["web"], + "depends": ["web", "account"], "data": [ "security/ir.model.access.csv", "views/extended_security_rule.xml", diff --git a/base_extended_security/tests/__init__.py b/base_extended_security/tests/__init__.py index 8055b126..83bd3303 100644 --- a/base_extended_security/tests/__init__.py +++ b/base_extended_security/tests/__init__.py @@ -1,7 +1,5 @@ # Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from . import test_crud from . import test_search from . import test_security_rules -from . import test_web_export diff --git a/base_extended_security/tests/common.py b/base_extended_security/tests/common.py deleted file mode 100644 index c0103602..00000000 --- a/base_extended_security/tests/common.py +++ /dev/null @@ -1,102 +0,0 @@ -from odoo import models, api -from odoo.exceptions import AccessError -from odoo.osv.expression import AND -from odoo.tests.common import TransactionCase - - -EMPLOYEE_ACCESS_MESSAGE = "You are not authorized to access employees." -NON_CUSTOMER_READ_MESSAGE = "You are not authorized to read non-customers." -NON_CUSTOMER_WRITE_MESSAGE = "You are not authorized to edit non-customers." -NON_CUSTOMER_CREATE_MESSAGE = "You are not authorized to create non-customers." -NON_CUSTOMER_UNLINK_MESSAGE = "You are not authorized to delete non-customers." - - -class ResPartner(models.Model): - - _inherit = "res.partner" - - def get_extended_security_domain(self): - domain = super().get_extended_security_domain() - return AND((domain, [("customer_rank", ">", 0)])) - - def check_extended_security_all(self): - super().check_extended_security_all() - for partner in self: - if partner.employee: - raise AccessError(EMPLOYEE_ACCESS_MESSAGE) - - def check_extended_security_read(self): - super().check_extended_security_read() - for partner in self: - if partner.customer_rank < 1: - raise AccessError(NON_CUSTOMER_READ_MESSAGE) - - def check_extended_security_write(self): - super().check_extended_security_write() - for partner in self: - if partner.customer_rank < 1: - raise AccessError(NON_CUSTOMER_WRITE_MESSAGE) - - def check_extended_security_create(self): - super().check_extended_security_create() - for partner in self: - if partner.customer_rank < 1: - raise AccessError(NON_CUSTOMER_CREATE_MESSAGE) - - def check_extended_security_unlink(self): - super().check_extended_security_unlink() - for partner in self: - if partner.customer_rank < 1: - raise AccessError(NON_CUSTOMER_UNLINK_MESSAGE) - - @api.model - def get_read_access_actions(self): - res = super().get_read_access_actions() - res.append("create_company") - return res - - -class ControllerCase(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.customer = cls.env["res.partner"].create( - { - "name": "My Partner Customer", - "supplier_rank": 0, - "customer_rank": 1, - } - ) - cls.supplier = cls.env["res.partner"].create( - { - "name": "My Partner Supplier", - "supplier_rank": 1, - "customer_rank": 0, - } - ) - cls.supplier_customer = cls.env["res.partner"].create( - { - "name": "My Partner Customer Supplier", - "supplier_rank": 1, - "customer_rank": 1, - } - ) - cls.employee = cls.env["res.partner"].create( - { - "name": "My Partner Customer Supplier", - "supplier_rank": 1, - "customer_rank": 1, - "employee": True, - } - ) - - cls.customer_count = cls.env["res.partner"].search_count( - [("customer_rank", ">", 0)] - ) - cls.supplier_customer_count = cls.env["res.partner"].search_count( - [ - "&", - ("customer_rank", ">", 0), - ("supplier_rank", ">", 0), - ] - ) diff --git a/base_extended_security/tests/test_crud.py b/base_extended_security/tests/test_crud.py deleted file mode 100644 index ec8b44e2..00000000 --- a/base_extended_security/tests/test_crud.py +++ /dev/null @@ -1,233 +0,0 @@ -# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import pytest -from odoo.addons.test_http_request.common import mock_odoo_request -from odoo.exceptions import AccessError -from .common import ( - ControllerCase, - EMPLOYEE_ACCESS_MESSAGE, - NON_CUSTOMER_READ_MESSAGE, - NON_CUSTOMER_WRITE_MESSAGE, - NON_CUSTOMER_CREATE_MESSAGE, - NON_CUSTOMER_UNLINK_MESSAGE, -) -from ..controllers.crud import DataSetWithExtendedSecurity - - -class TestControllers(ControllerCase): - def setUp(self): - super().setUp() - self.controller = DataSetWithExtendedSecurity() - - def _read(self, records): - with mock_odoo_request(self.env): - return self.controller.call_kw( - "res.partner", - "read", - [records.ids, ["name", "customer_rank", "supplier_rank"]], - {}, - ) - - def test_on_read_with_employee__access_error_raised(self): - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._read(self.employee) - - def test_on_read_with_non_customer__access_error_raised(self): - with pytest.raises(AccessError, match=NON_CUSTOMER_READ_MESSAGE): - self._read(self.supplier) - - def test_on_read_with_customer__access_error_not_raised(self): - self._read(self.customer | self.supplier_customer) - - def _write(self, records, values): - with mock_odoo_request(self.env): - return self.controller.call_kw( - "res.partner", "write", [records.ids, values], {} - ) - - def test_on_write_with_employee__access_error_raised(self): - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._write(self.employee, {"name": "My Employee"}) - - def test_on_write_with_non_customer__access_error_raised(self): - with pytest.raises(AccessError, match=NON_CUSTOMER_WRITE_MESSAGE): - self._write(self.supplier, {"name": "My Supplier"}) - - def test_on_write__if_not_authorized_after_write__access_error_raised(self): - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._write(self.customer, {"employee": True}) - - def test_on_write_with_customer__access_error_not_raised(self): - self._write(self.customer | self.supplier_customer, {"name": "My Customer"}) - - def _create(self, values): - with mock_odoo_request(self.env): - return self.controller.call_kw("res.partner", "create", [values], {}) - - def test_on_create_with_employee__access_error_raised(self): - values = [ - { - "name": "My Employee", - "supplier_rank": 1, - "customer_rank": 1, - "employee": True, - } - ] - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._create(values) - - def test_on_create_with_non_customer__access_error_raised(self): - values = [ - { - "name": "My Supplier", - "customer_rank": 0, - "supplier_rank": 1, - } - ] - with pytest.raises(AccessError, match=NON_CUSTOMER_CREATE_MESSAGE): - self._create(values) - - def test_on_create_with_customer__access_error_not_raised(self): - values = [ - { - "name": "My Customer", - "customer_rank": 1, - "supplier_rank": 0, - }, - { - "name": "My Supplier Customer", - "customer_rank": 1, - "supplier_rank": 1, - }, - ] - self._create(values) - - def _unlink(self, records): - with mock_odoo_request(self.env): - return self.controller.call_kw("res.partner", "unlink", [records.ids], {}) - - def test_on_unlink_with_employee__access_error_raised(self): - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._unlink(self.employee) - - def test_on_unlink_with_non_customer__access_error_raised(self): - with pytest.raises(AccessError, match=NON_CUSTOMER_UNLINK_MESSAGE): - self._unlink(self.supplier) - - def test_on_unlink_with_customer__access_error_not_raised(self): - self._unlink(self.customer | self.supplier_customer) - - def _x2many_unlink(self, parent, child): - self._write(parent, {"child_ids": [(2, child.id)]}) - - def test_on_x2many_unlink_with_employee__access_error_raised(self): - self.customer.child_ids |= self.employee - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._x2many_unlink(self.customer, self.employee) - - def test_on_x2many_unlink_with_non_customer__access_error_raised(self): - self.customer.child_ids |= self.supplier - with pytest.raises(AccessError, match=NON_CUSTOMER_UNLINK_MESSAGE): - self._x2many_unlink(self.customer, self.supplier) - - def test_on_x2many_unlink_with_customer__access_error_not_raised(self): - self.customer.child_ids |= self.supplier_customer - self._x2many_unlink(self.customer, self.supplier_customer) - - def _x2many_write(self, parent, child): - vals = {"name": "Some Value"} - self._write(parent, {"child_ids": [(1, child.id, vals)]}) - - def test_on_x2many_write_with_employee__access_error_raised(self): - self.customer.child_ids |= self.employee - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._x2many_write(self.customer, self.employee) - - def test_on_x2many_write_with_non_customer__access_error_raised(self): - self.customer.child_ids |= self.supplier - with pytest.raises(AccessError, match=NON_CUSTOMER_WRITE_MESSAGE): - self._x2many_write(self.customer, self.supplier) - - def test_on_x2many_write_with_customer__access_error_not_raised(self): - self.customer.child_ids |= self.supplier_customer - self._x2many_write(self.customer, self.supplier_customer) - - def _x2many_create(self, parent, vals): - self._write(parent, {"child_ids": [(0, 0, vals)]}) - - def test_on_x2many_create_with_non_customer__access_error_raised(self): - with pytest.raises(AccessError, match=NON_CUSTOMER_WRITE_MESSAGE): - self._x2many_create( - self.customer, - { - "name": "Some Contact", - "customer_rank": 0, - }, - ) - - def test_on_x2many_create_with_customer__access_error_not_raised(self): - self._x2many_create( - self.customer, - { - "name": "Some Contact", - "customer_rank": 1, - }, - ) - - def _name_create(self, name): - with mock_odoo_request(self.env): - return self.controller.call_kw("res.partner", "name_create", [name], {}) - - def _set_default_value(self, field, value): - self.env["ir.default"].set("res.partner", field, value, user_id=self.env.uid) - - def test_on_name_create_with_customer__access_error_not_raised(self): - self._set_default_value("customer_rank", 1) - self._name_create("My Partner") - - def test_on_name_create_with_non_customer__access_error_raised(self): - self._set_default_value("customer_rank", 0) - with pytest.raises(AccessError, match=NON_CUSTOMER_CREATE_MESSAGE): - self._name_create("My Partner") - - def test_on_name_create_with_employee__access_error_not_raised(self): - with pytest.raises(AccessError, match=NON_CUSTOMER_CREATE_MESSAGE): - self._name_create("My Partner") - - def test_on_many2many_tags_read__access_error_not_raised(self): - fields = ["display_name", "color"] - self._read_many2many_tags(self.employee, fields) - - def test_on_many2many_tags_read__with_all_fields_requested(self): - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._read_many2many_tags(self.employee, None) - - def _read_many2many_tags(self, records, fields): - with mock_odoo_request(self.env): - return self.controller.call_kw( - "res.partner", "read", [records.ids, fields], {} - ) - - def _call_button(self, records, action_name): - with mock_odoo_request(self.env): - return self.controller.call_button( - "res.partner", action_name, [records.ids], {} - ) - - def test_toggle_active_with_employee__access_error_raised(self): - with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - self._call_button(self.employee, "toggle_active") - - def test_toggle_active_with_customer__access_error_not_raised(self): - self._call_button(self.customer, "toggle_active") - - # def test_on_x2many_create_with_employee__access_error_raised(self): - # TODO: Check why we commented this test on v14 - # otherwise we should know the reason - # with pytest.raises(AccessError, match=EMPLOYEE_ACCESS_MESSAGE): - # self._x2many_create(self.customer, { - # 'name': 'Some Contact', - # 'customer_rank': 1, - # 'supplier_rank': 1, - # }) diff --git a/base_extended_security/tests/test_search.py b/base_extended_security/tests/test_search.py index 09e41992..85a375ed 100644 --- a/base_extended_security/tests/test_search.py +++ b/base_extended_security/tests/test_search.py @@ -1,14 +1,14 @@ # Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from ddt import ddt, data, unpack +from ddt import ddt +from odoo.tests.common import TransactionCase from odoo.addons.test_http_request.common import mock_odoo_request -from .common import ControllerCase from ..controllers.search import DataSetWithExtendedSearchSecurity @ddt -class TestControllers(ControllerCase): +class TestControllers(TransactionCase): def setUp(self): super().setUp() self.controller = DataSetWithExtendedSearchSecurity() @@ -29,38 +29,12 @@ def _read_group(self, domain, fields, groupby, domain_kwarg): return self.controller.call_kw("res.partner", "read_group", args, kwargs) - @data(True, False) - def test_read_group_with_supplier_domain(self, domain_kwarg): - domain = [("supplier_rank", ">", 0)] - groups = self._read_group( - domain, - fields=["customer_rank"], - groupby="customer_rank", - domain_kwarg=domain_kwarg, - ) - assert len(groups) == 1 - assert groups[0]["customer_rank_count"] == self.supplier_customer_count - def _search(self, domain, domain_kwarg): with mock_odoo_request(self.env): args = [] if domain_kwarg else [domain] kwargs = {"domain": domain} if domain_kwarg else {} return self.controller.call_kw("res.partner", "search", args, kwargs) - @data(True, False) - def test_search_with_empty_domain(self, domain_kwarg): - ids = self._search([], domain_kwarg) - assert self.customer.id in ids - assert self.supplier.id not in ids - assert self.supplier_customer.id in ids - - @data(True, False) - def test_search_with_supplier_domain(self, domain_kwarg): - ids = self._search([("supplier_rank", ">", 0)], domain_kwarg) - assert self.customer.id not in ids - assert self.supplier.id not in ids - assert self.supplier_customer.id in ids - def _name_search(self, name, domain, name_kwarg, domain_kwarg): with mock_odoo_request(self.env): args = [] @@ -81,48 +55,12 @@ def _name_search(self, name, domain, name_kwarg, domain_kwarg): ) return [r[0] for r in name_get] - @data( - (True, True), - (False, False), - (False, True), - ) - @unpack - def _test_name_search_with_empty_domain(self, name_kwarg, domain_kwarg): - ids = self._name_search("My Partner", [], name_kwarg, domain_kwarg) - assert self.customer.id in ids - assert self.supplier.id not in ids - assert self.supplier_customer.id in ids - - @data( - (True, True), - (False, False), - (False, True), - ) - @unpack - def test_name_search_with_supplier_domain(self, name_kwarg, domain_kwarg): - ids = self._name_search( - "My Partner", [("supplier_rank", ">", 0)], name_kwarg, domain_kwarg - ) - assert self.customer.id not in ids - assert self.supplier.id not in ids - assert self.supplier_customer.id in ids - def _search_count(self, domain, domain_kwarg): with mock_odoo_request(self.env): args = [] if domain_kwarg else [domain] kwargs = {"domain": domain} if domain_kwarg else {} return self.controller.call_kw("res.partner", "search_count", args, kwargs) - @data(True, False) - def test_search_count_with_empty_domain(self, domain_kwarg): - count = self._search_count([], domain_kwarg) - assert count == self.customer_count - - @data(True, False) - def test_search_count_with_supplier_domain(self, domain_kwarg): - count = self._search_count([("supplier_rank", ">", 0)], domain_kwarg) - assert count == self.supplier_customer_count - def _search_read(self, domain, use_search_read_route, domain_kwarg): with mock_odoo_request(self.env): if use_search_read_route: @@ -140,42 +78,3 @@ def _search_read(self, domain, use_search_read_route, domain_kwarg): ) return [r["id"] for r in records] - - @data( - (True, False), - (False, False), - (False, True), - ) - @unpack - def test_search_read_with_empty_domain(self, use_search_read_route, domain_kwarg): - ids = self._search_read([], use_search_read_route, domain_kwarg) - assert self.customer.id in ids - assert self.supplier.id not in ids - assert self.supplier_customer.id in ids - - @data( - (True, False), - (False, False), - (False, True), - ) - @unpack - def test_search_read_with_supplier_domain( - self, use_search_read_route, domain_kwarg - ): - ids = self._search_read( - [("supplier_rank", ">", 0)], use_search_read_route, domain_kwarg - ) - assert self.customer.id not in ids - assert self.supplier.id not in ids - assert self.supplier_customer.id in ids - - @data(True, False) - def test_read_group_with_empty_domain(self, domain_kwarg): - groups = self._read_group( - [], - fields=["customer_rank"], - groupby="customer_rank", - domain_kwarg=domain_kwarg, - ) - assert len(groups) == 2 - assert groups[0]["customer_rank_count"] + 1 == self.customer_count diff --git a/base_extended_security/tests/test_security_rules.py b/base_extended_security/tests/test_security_rules.py index 0f217984..c0ffd797 100644 --- a/base_extended_security/tests/test_security_rules.py +++ b/base_extended_security/tests/test_security_rules.py @@ -206,7 +206,7 @@ def test_read_access_action(self): self.rule.model_id = self.env.ref("base.model_res_partner") self.rule.perm_write = True form_view = self._get_partner_form_view_arch() - assert form_view.xpath("//button[@name='create_company']") + assert form_view.xpath("//field[@name='vat']") @data( ("write", "edit"), diff --git a/base_extended_security/tests/test_web_export.py b/base_extended_security/tests/test_web_export.py deleted file mode 100644 index a51fc248..00000000 --- a/base_extended_security/tests/test_web_export.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import json -import pytest -from odoo.addons.test_http_request.common import mock_odoo_request -from odoo.exceptions import AccessError -from .common import ControllerCase -from ..controllers.web_export import CSVControllerWithSecurity - - -class TestWebExport(ControllerCase): - def setUp(self): - super().setUp() - self.controller = CSVControllerWithSecurity() - - def _export(self, ids, domain): - params = { - "model": "res.partner", - "ids": ids, - "domain": domain, - "import_compat": True, - "fields": [{"name": "name"}], - } - data_ = json.dumps(params) - with mock_odoo_request(self.env): - response = self.controller.base(data_) - return response.data.decode("utf-8") - - def test_if_given_record_ids__and_not_access_all__raise_access_error(self): - with pytest.raises(AccessError): - self._export(ids=[self.employee.id], domain=[]) - - def test_if_given_record_ids__and_not_access_read__raise_access_error(self): - with pytest.raises(AccessError): - self._export(ids=[self.supplier.id], domain=[]) - - import unittest.mock as mock - - def test_if_given_record_ids__and_has_access_to_record__data_returned(self): - ids = [self.customer.id, self.supplier_customer.id] - data_ = self._export(ids=ids, domain=[]) - assert self.customer.name in data_ - assert self.supplier_customer.name in data_ - - def test_if_given_domain__domain_filter_applied_to_data(self): - data_ = self._export(ids=[], domain=[]) - assert self.customer.name in data_ - assert self.supplier.name not in data_ - assert self.supplier_customer.name in data_