diff --git a/addons/cetmix_tower_server/tests/test_jet_action_access.py b/addons/cetmix_tower_server/tests/test_jet_action_access.py new file mode 100644 index 0000000..87942c6 --- /dev/null +++ b/addons/cetmix_tower_server/tests/test_jet_action_access.py @@ -0,0 +1,647 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import AccessError + +from .common_jets import TestTowerJetsCommon + + +class TestTowerJetActionAccess(TestTowerJetsCommon): + """ + Test access rules for Jet Action model (cx.tower.jet.action) + """ + + # ====================== + # User Read Access + # ====================== + + def test_user_read_access_level_user_and_template_user(self): + """ + User: can read when action access_level is User + (1) AND template access_level is User (1) + """ + template = self.JetTemplate.create( + { + "name": "User Level Template", + "reference": "user_level_template", + "access_level": "1", # User level + "user_ids": False, + "manager_ids": False, + } + ) + action = self.JetAction.create( + { + "name": "Action U", + "reference": "action_u", + "access_level": "1", # User level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.user).search([("id", "=", action.id)]) + self.assertEqual( + len(records), + 1, + "User should read when action and template access_level are User", + ) + + def test_user_read_when_in_template_users(self): + """ + User: can read when action access_level is User (1) + AND user is added to template Users + """ + template = self.JetTemplate.create( + { + "name": "Manager Level Template (user granted)", + "reference": "manager_level_template_user", + "access_level": "2", # Manager level + "user_ids": [(4, self.user.id)], + "manager_ids": False, + } + ) + action = self.JetAction.create( + { + "name": "Action TU", + "reference": "action_tu", + "access_level": "1", # User level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.user).search([("id", "=", action.id)]) + self.assertEqual( + len(records), + 1, + "User should read when action access_level is" + " User and user in template Users", + ) + + def test_user_read_when_in_jet_users(self): + """ + User: can read when action access_level is + User (1) AND user is added to Jet Users + """ + template = self.JetTemplate.create( + { + "name": "Manager Level Template", + "reference": "manager_level_template_jet", + "access_level": "2", # Manager level + "user_ids": False, + "manager_ids": False, + } + ) + # Add server to template's server_ids for jet creation + template.write({"server_ids": [(4, self.server_test_1.id)]}) + self._create_jet( + name="Test Jet from Template", + reference="test_jet_from_template", + template=template, + server=self.server_test_1, + user_ids=[(4, self.user.id)], # Add user to Jet's user_ids + server_user_ids=[(4, self.user.id)], # Also add to server for jet access + ) + action = self.JetAction.create( + { + "name": "Action JU", + "reference": "action_ju", + "access_level": "1", # User level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.user).search([("id", "=", action.id)]) + self.assertEqual( + len(records), + 1, + "User should read when action access_level is User and user in Jet Users", + ) + + def test_user_read_no_access_action_not_user_level(self): + """User: cannot read when action access_level is NOT User (1)""" + template = self.JetTemplate.create( + { + "name": "User Level Template", + "reference": "user_level_template_no_access", + "access_level": "1", # User level + "user_ids": False, + "manager_ids": False, + } + ) + action = self.JetAction.create( + { + "name": "Action M", + "reference": "action_m", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.user).search([("id", "=", action.id)]) + self.assertEqual( + len(records), + 0, + "User should not read when action access_level is not User", + ) + + def test_user_read_no_access_template_conditions_not_met(self): + """ + User: cannot read when action access_level is User (1) + and template conditions not met + """ + template = self.JetTemplate.create( + { + "name": "Manager Level Template", + "reference": "manager_level_template_no_access", + "access_level": "2", # Manager level + "user_ids": False, # User not in template Users + "manager_ids": False, + } + ) + # Don't create any jets with user in user_ids + action = self.JetAction.create( + { + "name": "Action NA", + "reference": "action_na", + "access_level": "1", # User level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.user).search([("id", "=", action.id)]) + self.assertEqual( + len(records), + 0, + "User should not read when action is User level" + " and template conditions not met", + ) + + def test_user_write_forbidden(self): + """User: cannot write/create/delete records""" + template = self.JetTemplate.create( + { + "name": "User Level Template", + "reference": "user_level_template_write", + "access_level": "1", + "user_ids": [(4, self.user.id)], + } + ) + action = self.JetAction.create( + { + "name": "Action W", + "reference": "action_w_user", + "access_level": "1", + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + # Write forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.user).browse(action.id).write({"priority": 5}) + + # Create forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.user).create( + { + "name": "Action Created", + "reference": "action_created_user", + "access_level": "1", + "jet_template_id": template.id, + "state_from_id": self.state_stopped.id, + "state_to_id": self.state_running.id, + "state_transit_id": self.state_starting.id, + } + ) + + # Delete forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.user).browse(action.id).unlink() + + # ====================== + # Manager Read Access + # ====================== + + def test_manager_read_access_level_manager_or_less(self): + """ + Manager: can read when action access_level <= Manager (2) + AND template access_level <= Manager (2) + """ + template = self.JetTemplate.create( + { + "name": "Manager Level Template", + "reference": "manager_level_template", + "access_level": "2", + } + ) + action = self.JetAction.create( + { + "name": "Action R", + "reference": "action_r", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.manager).search( + [("id", "=", action.id)] + ) + self.assertEqual( + len(records), + 1, + "Manager should read when action and template level <= Manager", + ) + + def test_manager_read_when_in_template_users(self): + """ + Manager: can read when action access_level <= Manager (2) + AND user is added to template Users + even if template access_level is Root (3) + """ + template = self.JetTemplate.create( + { + "name": "Root Level Template (user granted)", + "reference": "root_level_template_user", + "access_level": "3", + "user_ids": [(4, self.manager.id)], + } + ) + action = self.JetAction.create( + { + "name": "Action RU", + "reference": "action_ru", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.manager).search( + [("id", "=", action.id)] + ) + self.assertEqual( + len(records), + 1, + "Manager should read when action level <= Manager and in template Users", + ) + + def test_manager_read_when_in_template_managers(self): + """ + Manager: can read when action access_level <= Manager (2) + AND user is added to template Managers + even if template access_level is Root (3) + """ + template = self.JetTemplate.create( + { + "name": "Root Level Template (manager)", + "reference": "root_level_template_manager", + "access_level": "3", + "manager_ids": [(4, self.manager.id)], + } + ) + action = self.JetAction.create( + { + "name": "Action RM", + "reference": "action_rm", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.manager).search( + [("id", "=", action.id)] + ) + self.assertEqual( + len(records), + 1, + "Manager should read when action level <= Manager and in template Managers", + ) + + def test_manager_read_no_access_action_root_level(self): + """ + Manager: cannot read when action access_level is Root (3) + even if template conditions are met + """ + template = self.JetTemplate.create( + { + "name": "Manager Level Template", + "reference": "manager_level_template_no_access", + "access_level": "2", + "manager_ids": [(4, self.manager.id)], + } + ) + action = self.JetAction.create( + { + "name": "Action Root", + "reference": "action_root", + "access_level": "3", # Root level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + records = self.JetAction.with_user(self.manager).search( + [("id", "=", action.id)] + ) + self.assertEqual( + len(records), + 0, + "Manager should not read when action access_level is Root", + ) + + # ====================== + # Manager Write/Create/Delete + # ====================== + + def test_manager_write_when_in_template_managers(self): + """ + Manager: can write when action access_level <= Manager (2) + AND user is in template Managers + """ + template = self.JetTemplate.create( + { + "name": "Template For Write", + "reference": "template_for_write", + "manager_ids": [(4, self.manager.id)], + } + ) + action = self.JetAction.create( + { + "name": "Action W", + "reference": "action_w", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + # Write + self.JetAction.with_user(self.manager).browse(action.id).write({"priority": 99}) + action.invalidate_recordset() + self.assertEqual( + action.priority, + 99, + "Manager should be able to write when action level" + " <= Manager and in Managers", + ) + + # Create + created = self.JetAction.with_user(self.manager).create( + { + "name": "Action W Created", + "reference": "action_w_created", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_stopped.id, + "state_to_id": self.state_running.id, + "state_transit_id": self.state_starting.id, + } + ) + self.assertTrue( + created, + "Manager should be able to create when action level " + "<= Manager and in Managers", + ) + + # Delete + self.JetAction.with_user(self.manager).browse(created.id).unlink() + after = self.JetAction.search([("id", "=", created.id)]) + self.assertEqual( + len(after), + 0, + "Manager should be able to delete when action level " + "<= Manager and in Managers", + ) + + def test_manager_write_forbidden_when_not_in_template_managers(self): + """ + Manager: cannot write/create/delete if NOT in template Managers + even if action access_level <= Manager (2) + """ + template = self.JetTemplate.create( + { + "name": "Template No Write", + "reference": "template_no_write", + } + ) + action = self.JetAction.create( + { + "name": "Action NW", + "reference": "action_nw", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + # Write forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.manager).browse(action.id).write( + {"priority": 5} + ) + + # Create forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.manager).create( + { + "name": "Action NW Created", + "reference": "action_nw_created", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_stopped.id, + "state_to_id": self.state_running.id, + "state_transit_id": self.state_starting.id, + } + ) + + # Delete forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.manager).browse(action.id).unlink() + + def test_manager_write_forbidden_when_action_root_level(self): + """ + Manager: cannot write/create/delete when action access_level is Root (3) + even if user is in template Managers + """ + template = self.JetTemplate.create( + { + "name": "Template For Write", + "reference": "template_for_write_root", + "manager_ids": [(4, self.manager.id)], + } + ) + action = self.JetAction.create( + { + "name": "Action Root W", + "reference": "action_root_w", + "access_level": "3", # Root level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + # Write forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.manager).browse(action.id).write( + {"priority": 5} + ) + + # Create forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.manager).create( + { + "name": "Action Root Created", + "reference": "action_root_created", + "access_level": "3", # Root level + "jet_template_id": template.id, + "state_from_id": self.state_stopped.id, + "state_to_id": self.state_running.id, + "state_transit_id": self.state_starting.id, + } + ) + + # Delete forbidden + with self.assertRaises(AccessError): + self.JetAction.with_user(self.manager).browse(action.id).unlink() + + def test_manager_write_on_root_level_template_when_in_managers(self): + """ + Manager: can write/create/delete on Root-level template + when action access_level <= Manager (2) AND user is in Managers + """ + template = self.JetTemplate.create( + { + "name": "Root Level Template For Write", + "reference": "root_level_template_for_write", + "access_level": "3", + "manager_ids": [(4, self.manager.id)], + } + ) + action = self.JetAction.create( + { + "name": "Action RW", + "reference": "action_rw", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_running.id, + "state_to_id": self.state_stopped.id, + "state_transit_id": self.state_stopping.id, + } + ) + + # Write + self.JetAction.with_user(self.manager).browse(action.id).write({"priority": 42}) + action.invalidate_recordset() + self.assertEqual( + action.priority, + 42, + "Manager should write on Root-level template when action level " + "<= Manager and in Managers", + ) + + # Create + created = self.JetAction.with_user(self.manager).create( + { + "name": "Action RW Created", + "reference": "action_rw_created", + "access_level": "2", # Manager level + "jet_template_id": template.id, + "state_from_id": self.state_stopped.id, + "state_to_id": self.state_running.id, + "state_transit_id": self.state_starting.id, + } + ) + self.assertTrue( + created, + "Manager should create on Root-level template when action level " + "<= Manager and in Managers", + ) + + # Delete + self.JetAction.with_user(self.manager).browse(created.id).unlink() + after = self.JetAction.search([("id", "=", created.id)]) + self.assertEqual( + len(after), + 0, + "Manager should delete on Root-level template when action level " + "<= Manager and in Managers", + ) + + # ====================== + # Root Access + # ====================== + + def test_root_full_access(self): + """Root: full CRUD access for any record""" + template = self.JetTemplate.with_user(self.root).create( + { + "name": "Root Template", + "reference": "root_template", + "access_level": "3", + } + ) + + # Create + action = self.JetAction.with_user(self.root).create( + { + "name": "Root Action", + "reference": "root_action", + "jet_template_id": template.id, + "state_from_id": self.state_initial.id, + "state_to_id": self.state_running.id, + "state_transit_id": self.state_starting.id, + } + ) + + # Read + records = self.JetAction.with_user(self.root).search([("id", "=", action.id)]) + self.assertEqual(len(records), 1, "Root should read any record") + + # Write + action.with_user(self.root).write({"priority": 7}) + action.invalidate_recordset() + self.assertEqual(action.priority, 7, "Root should update any record") + + # Delete + action.with_user(self.root).unlink() + self.assertEqual( + len( + self.JetAction.with_user(self.root).search( + [("reference", "=", "root_action")] + ) + ), + 0, + "Root should delete any record", + )